diff --git a/src/export/pdf.rs b/src/export/pdf.rs index c050bfc5d..9c0e655f1 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -36,8 +36,9 @@ pub fn pdf(ctx: &Context, frames: &[Arc]) -> Vec { PdfExporter::new(ctx).export(frames) } -/// Identifies the sRGB color space definition. +/// Identifies the color space definitions. const SRGB: Name<'static> = Name(b"sRGB"); +const SRGB_GRAY: Name<'static> = Name(b"sRGBGray"); /// An exporter for a whole PDF document. struct PdfExporter<'a> { @@ -365,6 +366,11 @@ impl<'a> PdfExporter<'a> { let mut resources = pages.resources(); resources.color_spaces().insert(SRGB).start::().srgb(); + resources + .color_spaces() + .insert(SRGB_GRAY) + .start::() + .srgb_gray(); let mut fonts = resources.fonts(); for (font_ref, f) in self.face_map.pdf_indices(&self.face_refs) { @@ -437,9 +443,11 @@ struct Page { #[derive(Debug, Default, Clone)] struct State { transform: Transform, - fill: Option, - stroke: Option, font: Option<(FaceId, Length)>, + fill: Option, + fill_space: Option>, + stroke: Option, + stroke_space: Option>, } impl<'a> PageExporter<'a> { @@ -469,8 +477,6 @@ impl<'a> PageExporter<'a> { tx: Length::zero(), ty: frame.size.y, }); - 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, @@ -708,6 +714,7 @@ impl<'a> PageExporter<'a> { self.font_map.insert(face_id); let name = format_eco!("F{}", self.font_map.map(face_id)); self.content.set_font(Name(name.as_bytes()), size.to_f32()); + self.state.font = Some((face_id, size)); } } @@ -716,13 +723,26 @@ impl<'a> PageExporter<'a> { let f = |c| c as f32 / 255.0; let Paint::Solid(color) = fill; match color { + Color::Luma(c) => { + self.set_fill_color_space(SRGB_GRAY); + self.content.set_fill_gray(f(c.0)); + } Color::Rgba(c) => { + self.set_fill_color_space(SRGB); 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)); } } + self.state.fill = Some(fill); + } + } + + fn set_fill_color_space(&mut self, space: Name<'static>) { + if self.state.fill_space != Some(space) { + self.content.set_fill_color_space(ColorSpaceOperand::Named(space)); + self.state.fill_space = Some(space); } } @@ -731,7 +751,12 @@ impl<'a> PageExporter<'a> { let f = |c| c as f32 / 255.0; let Paint::Solid(color) = stroke.paint; match color { + Color::Luma(c) => { + self.set_stroke_color_space(SRGB_GRAY); + self.content.set_stroke_gray(f(c.0)); + } Color::Rgba(c) => { + self.set_stroke_color_space(SRGB); self.content.set_stroke_color([f(c.r), f(c.g), f(c.b)]); } Color::Cmyk(c) => { @@ -740,6 +765,14 @@ impl<'a> PageExporter<'a> { } self.content.set_line_width(stroke.thickness.to_f32()); + self.state.stroke = Some(stroke); + } + } + + fn set_stroke_color_space(&mut self, space: Name<'static>) { + if self.state.stroke_space != Some(space) { + self.content.set_stroke_color_space(ColorSpaceOperand::Named(space)); + self.state.stroke_space = Some(space); } } } diff --git a/src/geom/paint.rs b/src/geom/paint.rs index 9b310249d..a3a9b0be2 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -31,6 +31,8 @@ impl Debug for Paint { /// A color in a dynamic format. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum Color { + /// An 8-bit luma color. + Luma(LumaColor), /// An 8-bit RGBA color. Rgba(RgbaColor), /// An 8-bit CMYK color. @@ -60,6 +62,7 @@ impl Color { /// Convert this color to RGBA. pub fn to_rgba(self) -> RgbaColor { match self { + Self::Luma(luma) => luma.to_rgba(), Self::Rgba(rgba) => rgba, Self::Cmyk(cmyk) => cmyk.to_rgba(), } @@ -69,18 +72,48 @@ impl Color { impl Debug for Color { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { + Self::Luma(c) => Debug::fmt(c, f), Self::Rgba(c) => Debug::fmt(c, f), Self::Cmyk(c) => Debug::fmt(c, f), } } } -impl From for Color -where - T: Into, -{ - fn from(rgba: T) -> Self { - Self::Rgba(rgba.into()) +/// An 8-bit Luma color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct LumaColor(pub u8); + +impl LumaColor { + /// Construct a new luma color. + pub const fn new(luma: u8) -> Self { + Self(luma) + } + + /// Convert to an opque RGBA color. + pub const fn to_rgba(self) -> RgbaColor { + RgbaColor::new(self.0, self.0, self.0, 255) + } + + /// Convert to CMYK as a fraction of true black. + pub fn to_cmyk(self) -> CmykColor { + CmykColor::new( + (self.0 as f64 * 0.75) as u8, + (self.0 as f64 * 0.68) as u8, + (self.0 as f64 * 0.67) as u8, + (self.0 as f64 * 0.90) as u8, + ) + } +} + +impl Debug for LumaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "luma({})", self.0) + } +} + +impl From for Color { + fn from(luma: LumaColor) -> Self { + Self::Luma(luma) } } @@ -102,11 +135,6 @@ impl RgbaColor { 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 const fn gray(luma: u8) -> Self { - Self::new(luma, luma, luma, 255) - } } impl FromStr for RgbaColor { @@ -161,11 +189,7 @@ impl From for RgbaColor { impl Debug for RgbaColor { fn fmt(&self, f: &mut Formatter) -> fmt::Result { if f.alternate() { - write!( - f, - "rgba({:02}, {:02}, {:02}, {:02})", - self.r, self.g, self.b, self.a, - )?; + write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; } else { write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; if self.a != 255 { @@ -177,6 +201,15 @@ impl Debug for RgbaColor { } } +impl From for Color +where + T: Into, +{ + fn from(rgba: T) -> Self { + Self::Rgba(rgba.into()) + } +} + /// An 8-bit CMYK color. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct CmykColor { @@ -196,16 +229,6 @@ impl CmykColor { 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; diff --git a/src/library/mod.rs b/src/library/mod.rs index 27658189b..9708475b5 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -85,6 +85,7 @@ pub fn new() -> Scope { std.def_fn("odd", utility::odd); std.def_fn("mod", utility::mod_); std.def_fn("range", utility::range); + std.def_fn("luma", utility::luma); std.def_fn("rgb", utility::rgb); std.def_fn("cmyk", utility::cmyk); std.def_fn("repr", utility::repr); diff --git a/src/library/utility/color.rs b/src/library/utility/color.rs index 5857c4c7f..7c6ed873f 100644 --- a/src/library/utility/color.rs +++ b/src/library/utility/color.rs @@ -2,57 +2,65 @@ use std::str::FromStr; use crate::library::prelude::*; +/// Create a grayscale color. +pub fn luma(_: &mut Machine, args: &mut Args) -> TypResult { + let Component(luma) = args.expect("gray component")?; + Ok(Value::Color(LumaColor::new(luma).into())) +} + /// Create an RGB(A) color. pub fn rgb(_: &mut Machine, args: &mut Args) -> TypResult { - Ok(Value::from( + Ok(Value::Color( if let Some(string) = args.find::>()? { match RgbaColor::from_str(&string.v) { - Ok(color) => color, + Ok(color) => color.into(), Err(msg) => bail!(string.span, msg), } } else { - struct Component(u8); - - castable! { - Component, - Expected: "integer or ratio", - Value::Int(v) => match v { - 0 ..= 255 => Self(v as u8), - _ => Err("must be between 0 and 255")?, - }, - Value::Ratio(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(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) + RgbaColor::new(r, g, b, a).into() }, )) } /// Create a CMYK color. pub fn cmyk(_: &mut Machine, args: &mut Args) -> TypResult { - struct Component(u8); - - castable! { - Component, - Expected: "ratio", - Value::Ratio(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")?; + let RatioComponent(c) = args.expect("cyan component")?; + let RatioComponent(m) = args.expect("magenta component")?; + let RatioComponent(y) = args.expect("yellow component")?; + let RatioComponent(k) = args.expect("key component")?; Ok(Value::Color(CmykColor::new(c, m, y, k).into())) } + +/// An integer or ratio component. +struct Component(u8); + +castable! { + Component, + Expected: "integer or ratio", + Value::Int(v) => match v { + 0 ..= 255 => Self(v as u8), + _ => Err("must be between 0 and 255")?, + }, + Value::Ratio(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%")? + }, +} + +/// A component that must be a ratio. +struct RatioComponent(u8); + +castable! { + RatioComponent, + Expected: "ratio", + Value::Ratio(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%")? + }, +} diff --git a/tests/ref/utility/color.png b/tests/ref/utility/color.png index e7d87ba40..a03795f7f 100644 Binary files a/tests/ref/utility/color.png and b/tests/ref/utility/color.png differ diff --git a/tests/typ/utility/color.typ b/tests/typ/utility/color.typ index 81b67ae93..449655bf2 100644 --- a/tests/typ/utility/color.typ +++ b/tests/typ/utility/color.typ @@ -8,6 +8,12 @@ // Alpha channel. #test(rgb(255, 0, 0, 50%), rgb("ff000080")) +--- +// Test gray color conversion. +// Ref: true +#rect(fill: luma(0)) +#rect(fill: luma(80%)) + --- // Test CMYK color conversion. // Ref: true