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