From fe70db1f4ce078f7b41c163a1c0ead31fd04850a Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Tue, 8 Feb 2022 21:12:09 +0100 Subject: [PATCH] New color stuff - CMYK function - More default colors - Interpret RGB values as sRGB --- NOTICE | 28 +++++++++ src/eval/mod.rs | 6 +- src/export/pdf.rs | 44 +++++++++----- src/export/render.rs | 8 ++- src/geom/paint.rs | 108 ++++++++++++++++++++++++++++++++--- src/library/mod.rs | 25 ++++++-- src/library/shape.rs | 2 +- src/library/table.rs | 2 +- src/library/text.rs | 2 +- src/library/utility.rs | 51 +++++++++++++---- tests/ref/utility/color.png | Bin 0 -> 168 bytes tests/typ/layout/stack-1.typ | 4 +- tests/typ/utility/color.typ | 23 ++++---- tests/typeset.rs | 4 +- 14 files changed, 246 insertions(+), 61 deletions(-) create mode 100644 tests/ref/utility/color.png diff --git a/NOTICE b/NOTICE index d4b52a166..f678a3f73 100644 --- a/NOTICE +++ b/NOTICE @@ -104,6 +104,34 @@ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================================================ +================================================================================ +The MIT License applies to: + +* The default color set defined in `src/geom/color.rs` which is adapted from + the colors.css project + (https://clrs.cc/) + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +================================================================================ + ================================================================================ The Apache License Version 2.0 applies to: diff --git a/src/eval/mod.rs b/src/eval/mod.rs index ae680d95a..5129a41ff 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -39,7 +39,7 @@ use syntect::parsing::SyntaxSet; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult}; -use crate::geom::{Angle, Fractional, Length, Paint, Relative, RgbaColor}; +use crate::geom::{Angle, Color, Fractional, Length, Paint, Relative}; use crate::image::ImageStore; use crate::layout::RootNode; use crate::library::{self, DecoLine, TextNode}; @@ -278,8 +278,8 @@ impl RawNode { let foreground = THEME .settings .foreground - .map(RgbaColor::from) - .unwrap_or(RgbaColor::BLACK) + .map(Color::from) + .unwrap_or(Color::BLACK) .into(); match syntax { diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 19134f992..18bb5385c 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -7,8 +7,10 @@ use std::sync::Arc; use image::{DynamicImage, GenericImageView, ImageFormat, ImageResult, Rgba}; use pdf_writer::types::{ - ActionType, AnnotationType, CidFontType, FontFlags, SystemInfo, UnicodeCmap, + ActionType, AnnotationType, CidFontType, ColorSpaceOperand, FontFlags, SystemInfo, + UnicodeCmap, }; +use pdf_writer::writers::ColorSpace; use pdf_writer::{Content, Filter, Finish, Name, PdfWriter, Rect, Ref, Str, TextStr}; use ttf_parser::{name_id, GlyphId, Tag}; @@ -30,6 +32,9 @@ pub fn pdf(ctx: &Context, frames: &[Arc]) -> Vec { PdfExporter::new(ctx).export(frames) } +/// Identifies the sRGB color space definition. +pub const SRGB: Name<'static> = Name(b"sRGB"); + /// An exporter for a whole PDF document. struct PdfExporter<'a> { fonts: &'a FontStore, @@ -316,6 +321,8 @@ impl<'a> PdfExporter<'a> { pages.count(page_refs.len() as i32).kids(page_refs); let mut resources = pages.resources(); + resources.color_spaces().insert(SRGB).start::().srgb(); + let mut fonts = resources.fonts(); for (font_ref, f) in self.face_map.pdf_indices(&self.face_refs) { let name = format_eco!("F{}", f); @@ -390,6 +397,8 @@ impl<'a> PageExporter<'a> { // Make the coordinate system start at the top-left. self.bottom = frame.size.y.to_f32(); self.content.transform([1.0, 0.0, 0.0, -1.0, 0.0, self.bottom]); + self.content.set_fill_color_space(ColorSpaceOperand::Named(SRGB)); + self.content.set_stroke_color_space(ColorSpaceOperand::Named(SRGB)); self.write_frame(frame); Page { size: frame.size, @@ -624,23 +633,32 @@ impl<'a> PageExporter<'a> { fn set_fill(&mut self, fill: Paint) { if self.state.fill != Some(fill) { - let Paint::Solid(Color::Rgba(c)) = fill; - self.content.set_fill_rgb( - c.r as f32 / 255.0, - c.g as f32 / 255.0, - c.b as f32 / 255.0, - ); + let f = |c| c as f32 / 255.0; + let Paint::Solid(color) = fill; + match color { + Color::Rgba(c) => { + self.content.set_fill_color([f(c.r), f(c.g), f(c.b)]); + } + Color::Cmyk(c) => { + self.content.set_fill_cmyk(f(c.c), f(c.m), f(c.y), f(c.k)); + } + } } } fn set_stroke(&mut self, stroke: Stroke) { if self.state.stroke != Some(stroke) { - let Paint::Solid(Color::Rgba(c)) = stroke.paint; - self.content.set_stroke_rgb( - c.r as f32 / 255.0, - c.g as f32 / 255.0, - c.b as f32 / 255.0, - ); + let f = |c| c as f32 / 255.0; + let Paint::Solid(color) = stroke.paint; + match color { + Color::Rgba(c) => { + self.content.set_stroke_color([f(c.r), f(c.g), f(c.b)]); + } + Color::Cmyk(c) => { + self.content.set_stroke_cmyk(f(c.c), f(c.m), f(c.y), f(c.k)); + } + } + self.content.set_line_width(stroke.thickness.to_f32()); } } diff --git a/src/export/render.rs b/src/export/render.rs index c41bcbf2e..8b7aa46dd 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -10,7 +10,7 @@ use usvg::FitTo; use crate::font::{Face, FaceId}; use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; -use crate::geom::{self, Color, Length, Paint, PathElement, Size, Transform}; +use crate::geom::{self, Length, Paint, PathElement, Size, Transform}; use crate::image::{Image, RasterImage, Svg}; use crate::Context; @@ -279,7 +279,8 @@ fn render_outline_glyph( let bottom = top + mh; // Premultiply the text color. - let Paint::Solid(Color::Rgba(c)) = text.fill; + let Paint::Solid(color) = text.fill; + let c = color.to_rgba(); let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, 255).premultiply().get(); // Blend the glyph bitmap with the existing pixels on the canvas. @@ -453,7 +454,8 @@ impl From for sk::Transform { impl From for sk::Paint<'static> { fn from(paint: Paint) -> Self { let mut sk_paint = sk::Paint::default(); - let Paint::Solid(Color::Rgba(c)) = paint; + let Paint::Solid(color) = paint; + let c = color.to_rgba(); sk_paint.set_color_rgba8(c.r, c.g, c.b, c.a); sk_paint.anti_alias = true; sk_paint diff --git a/src/geom/paint.rs b/src/geom/paint.rs index f86386562..7342024c3 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -26,12 +26,44 @@ where pub enum Color { /// An 8-bit RGBA color. Rgba(RgbaColor), + /// An 8-bit CMYK color. + Cmyk(CmykColor), +} + +impl Color { + pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); + pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); + pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); + pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); + pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); + pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); + pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); + pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); + pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); + pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); + pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); + pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); + pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); + pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); + pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); + pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); + pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); + pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + match self { + Self::Rgba(rgba) => rgba, + Self::Cmyk(cmyk) => cmyk.to_rgba(), + } + } } impl Debug for Color { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Rgba(c) => Debug::fmt(c, f), + Self::Cmyk(c) => Debug::fmt(c, f), } } } @@ -59,19 +91,13 @@ pub struct RgbaColor { } impl RgbaColor { - /// Black color. - pub const BLACK: Self = Self { r: 0, g: 0, b: 0, a: 255 }; - - /// White color. - pub const WHITE: Self = Self { r: 255, g: 255, b: 255, a: 255 }; - /// Construct a new RGBA color. - pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { Self { r, g, b, a } } /// Construct a new, opaque gray color. - pub fn gray(luma: u8) -> Self { + pub const fn gray(luma: u8) -> Self { Self::new(luma, luma, luma, 255) } } @@ -155,6 +181,72 @@ impl Display for RgbaError { impl std::error::Error for RgbaError {} +/// An 8-bit CMYK color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct CmykColor { + /// The cyan component. + pub c: u8, + /// The magenta component. + pub m: u8, + /// The yellow component. + pub y: u8, + /// The key (black) component. + pub k: u8, +} + +impl CmykColor { + /// Construct a new CMYK color. + pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { + Self { c, m, y, k } + } + + /// Construct a new, opaque gray color as a fraction of true black. + pub fn gray(luma: u8) -> Self { + Self::new( + (luma as f64 * 0.75) as u8, + (luma as f64 * 0.68) as u8, + (luma as f64 * 0.67) as u8, + (luma as f64 * 0.90) as u8, + ) + } + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + let k = self.k as f32 / 255.0; + let f = |c| { + let c = c as f32 / 255.0; + (255.0 * (1.0 - c) * (1.0 - k)).round() as u8 + }; + + RgbaColor { + r: f(self.c), + g: f(self.m), + b: f(self.y), + a: 255, + } + } +} + +impl Debug for CmykColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let g = |c| c as f64 / 255.0; + write!( + f, + "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", + g(self.c), + g(self.m), + g(self.y), + g(self.k), + ) + } +} + +impl From for Color { + fn from(cmyk: CmykColor) -> Self { + Self::Cmyk(cmyk) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/library/mod.rs b/src/library/mod.rs index 550176457..33b327e92 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -142,6 +142,7 @@ pub fn new() -> Scope { std.def_func("mod", modulo); std.def_func("range", range); std.def_func("rgb", rgb); + std.def_func("cmyk", cmyk); std.def_func("lower", lower); std.def_func("upper", upper); std.def_func("roman", roman); @@ -150,12 +151,24 @@ pub fn new() -> Scope { std.def_func("sorted", sorted); // Predefined colors. - // TODO: More colors. - std.def_const("white", RgbaColor::WHITE); - std.def_const("black", RgbaColor::BLACK); - std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); - std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); - std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); + std.def_const("black", Color::BLACK); + std.def_const("gray", Color::GRAY); + std.def_const("silver", Color::SILVER); + std.def_const("white", Color::WHITE); + std.def_const("navy", Color::NAVY); + std.def_const("blue", Color::BLUE); + std.def_const("aqua", Color::AQUA); + std.def_const("teal", Color::TEAL); + std.def_const("eastern", Color::EASTERN); + std.def_const("purple", Color::PURPLE); + std.def_const("fuchsia", Color::FUCHSIA); + std.def_const("maroon", Color::MAROON); + std.def_const("red", Color::RED); + std.def_const("orange", Color::ORANGE); + std.def_const("yellow", Color::YELLOW); + std.def_const("olive", Color::OLIVE); + std.def_const("green", Color::GREEN); + std.def_const("lime", Color::LIME); // Other constants. std.def_const("ltr", Dir::LTR); diff --git a/src/library/shape.rs b/src/library/shape.rs index 5d31d570b..0dd75c085 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -125,7 +125,7 @@ impl Layout for ShapeNode { let thickness = styles.get(Self::THICKNESS); let stroke = styles .get(Self::STROKE) - .unwrap_or(fill.is_none().then(|| RgbaColor::BLACK.into())) + .unwrap_or(fill.is_none().then(|| Color::BLACK.into())) .map(|paint| Stroke { paint, thickness }); if fill.is_some() || stroke.is_some() { diff --git a/src/library/table.rs b/src/library/table.rs index d7aa61db0..b0f0fbf5c 100644 --- a/src/library/table.rs +++ b/src/library/table.rs @@ -21,7 +21,7 @@ impl TableNode { /// The secondary cell fill color. pub const SECONDARY: Option = None; /// How the stroke the cells. - pub const STROKE: Option = Some(RgbaColor::BLACK.into()); + pub const STROKE: Option = Some(Color::BLACK.into()); /// The stroke's thickness. pub const THICKNESS: Length = Length::pt(1.0); /// How much to pad the cells's content. diff --git a/src/library/text.rs b/src/library/text.rs index 00c20a9e3..019f8c35c 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -51,7 +51,7 @@ impl TextNode { /// Whether a monospace font should be preferred. pub const MONOSPACE: bool = false; /// The glyph fill color. - pub const FILL: Paint = RgbaColor::BLACK.into(); + pub const FILL: Paint = Color::BLACK.into(); /// Decorative lines. #[fold(|a, b| a.into_iter().chain(b).collect())] pub const LINES: Vec = vec![]; diff --git a/src/library/utility.rs b/src/library/utility.rs index 0f533be41..5b9831c52 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -94,21 +94,50 @@ pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult { Err(_) => bail!(string.span, "invalid hex string"), } } else { - let r = args.expect("red component")?; - let g = args.expect("green component")?; - let b = args.expect("blue component")?; - let a = args.eat()?.unwrap_or(Spanned::new(1.0, Span::detached())); - let f = |Spanned { v, span }: Spanned| { - if (0.0 ..= 1.0).contains(&v) { - Ok((v * 255.0).round() as u8) + struct Component(u8); + + castable! { + Component, + Expected: "integer or relative", + Value::Int(v) => match v { + 0 ..= 255 => Self(v as u8), + _ => Err("must be between 0 and 255")?, + }, + Value::Relative(v) => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) } else { - bail!(span, "value must be between 0.0 and 1.0"); - } - }; - RgbaColor::new(f(r)?, f(g)?, f(b)?, f(a)?) + Err("must be between 0% and 100%")? + }, + } + + let Component(r) = args.expect("red component")?; + let Component(g) = args.expect("green component")?; + let Component(b) = args.expect("blue component")?; + let Component(a) = args.eat()?.unwrap_or(Component(255)); + RgbaColor::new(r, g, b, a) }, )) } +/// Create an CMYK color. +pub fn cmyk(_: &mut EvalContext, args: &mut Args) -> TypResult { + struct Component(u8); + + castable! { + Component, + Expected: "relative", + Value::Relative(v) => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) + } else { + Err("must be between 0% and 100%")? + }, + } + + let Component(c) = args.expect("cyan component")?; + let Component(m) = args.expect("magenta component")?; + let Component(y) = args.expect("yellow component")?; + let Component(k) = args.expect("key component")?; + Ok(Value::Color(CmykColor::new(c, m, y, k).into())) +} /// The absolute value of a numeric value. pub fn abs(_: &mut EvalContext, args: &mut Args) -> TypResult { diff --git a/tests/ref/utility/color.png b/tests/ref/utility/color.png new file mode 100644 index 0000000000000000000000000000000000000000..e7d87ba4055b309bfaff442264a4a572be3aaade GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZE0~ypWah_|=|GAnz$e7@|NsAbZF*Tgn}7n= zbzQAMD%{h>F{I+w+nbKO4F(J>2Y-L*<^OX<*)djDgqc@y3cF`IYfr=OFE^bOYIL{k z(Y?vR!jvew@Jiy%^MUM*4hjN5KA37*-t-s5c*jyN_AvYq>-%PXL8rr8wgRnS@O1Ta JS?83{1OQYzHo*V@ literal 0 HcmV?d00001 diff --git a/tests/typ/layout/stack-1.typ b/tests/typ/layout/stack-1.typ index a4a0d6b85..d8075e9d4 100644 --- a/tests/typ/layout/stack-1.typ +++ b/tests/typ/layout/stack-1.typ @@ -8,8 +8,8 @@ ) #let shaded = { - let v = 0 - let next() = { v += 0.1; rgb(v, v, v) } + let v = 0% + let next() = { v += 10%; rgb(v, v, v) } w => rect(width: w, height: 10pt, fill: next()) } diff --git a/tests/typ/utility/color.typ b/tests/typ/utility/color.typ index 759672ff2..fa30a4f2d 100644 --- a/tests/typ/utility/color.typ +++ b/tests/typ/utility/color.typ @@ -3,20 +3,21 @@ --- // Compare both ways. -#test(rgb(0.0, 0.3, 0.7), rgb("004db3")) +#test(rgb(0%, 30%, 70%), rgb("004db3")) // Alpha channel. -#test(rgb(1.0, 0.0, 0.0, 0.5), rgb("ff000080")) +#test(rgb(255, 0, 0, 50%), rgb("ff000080")) + +--- +// Test CMYK color conversion. +// Ref: true +#rect(fill: cmyk(69%, 11%, 69%, 41%)) +#rect(fill: cmyk(50%, 64%, 16%, 17%)) --- // Error for values that are out of range. -// Error: 11-14 value must be between 0.0 and 1.0 -#test(rgb(-30, 15.5, 0.5)) - ---- -// Error for values that are out of range. -// Error: 26-30 value must be between 0.0 and 1.0 -#test(rgb(0.1, 0.2, 0.3, -0.1)) +// Error: 11-14 must be between 0 and 255 +#test(rgb(-30, 15, 50)) --- // Error: 6-11 invalid hex string @@ -31,5 +32,5 @@ #rgb(0, 1) --- -// Error: 21-26 expected float, found boolean -#rgb(0.1, 0.2, 0.3, false) +// Error: 21-26 expected integer or relative, found boolean +#rgb(10%, 20%, 30%, false) diff --git a/tests/typeset.rs b/tests/typeset.rs index f7f2eccfa..31610ffc1 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -11,7 +11,7 @@ use walkdir::WalkDir; use typst::diag::Error; use typst::eval::{Smart, StyleMap, Value}; use typst::frame::{Element, Frame}; -use typst::geom::Length; +use typst::geom::{Length, RgbaColor}; use typst::library::{PageNode, TextNode}; use typst::loading::FsLoader; use typst::parse::Scanner; @@ -77,6 +77,8 @@ fn main() { // Hook up an assert function into the global scope. let mut std = typst::library::new(); + std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); + std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); std.def_func("test", move |_, args| { let lhs = args.expect::("left-hand side")?; let rhs = args.expect::("right-hand side")?;