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 000000000..e7d87ba40
Binary files /dev/null and b/tests/ref/utility/color.png differ
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")?;