summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorSébastien d'Herbais de Thun <sebastien.d.herbais@gmail.com>2023-09-19 15:48:33 +0200
committerGitHub <noreply@github.com>2023-09-19 15:48:33 +0200
commit163c2e1aa27169c1eba946204096d3e8fdfd3c18 (patch)
tree0f4c58bf41239865a6cdcaac77a6314dbbb5b8c8 /crates
parentbb59f0e2b21c42a796b5eb8d8882a1d2b2a0c35f (diff)
Gradient Part 1 - Color rework (#2171)
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-library/src/lib.rs1
-rw-r--r--crates/typst-library/src/text/deco.rs2
-rw-r--r--crates/typst-library/src/text/raw.rs9
-rw-r--r--crates/typst/Cargo.toml2
-rw-r--r--crates/typst/src/doc.rs6
-rw-r--r--crates/typst/src/eval/func.rs9
-rw-r--r--crates/typst/src/eval/value.rs2
-rw-r--r--crates/typst/src/export/pdf/color.rs428
-rw-r--r--crates/typst/src/export/pdf/icc/sGrey-v4.iccbin0 -> 360 bytes
-rw-r--r--crates/typst/src/export/pdf/icc/sRGB-v4.iccbin0 -> 480 bytes
-rw-r--r--crates/typst/src/export/pdf/mod.rs8
-rw-r--r--crates/typst/src/export/pdf/page.rs77
-rw-r--r--crates/typst/src/export/pdf/postscript/hsl.ps63
-rw-r--r--crates/typst/src/export/pdf/postscript/hsv.ps62
-rw-r--r--crates/typst/src/export/pdf/postscript/oklab.ps78
-rw-r--r--crates/typst/src/export/render.rs26
-rw-r--r--crates/typst/src/export/svg.rs79
-rw-r--r--crates/typst/src/geom/color.rs1448
-rw-r--r--crates/typst/src/geom/mod.rs4
-rw-r--r--crates/typst/src/geom/scalar.rs2
20 files changed, 1833 insertions, 473 deletions
diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs
index bdb97f84..03e96441 100644
--- a/crates/typst-library/src/lib.rs
+++ b/crates/typst-library/src/lib.rs
@@ -63,6 +63,7 @@ fn prelude(global: &mut Scope) {
global.define("green", Color::GREEN);
global.define("lime", Color::LIME);
global.define("luma", Color::luma_data());
+ global.define("oklab", Color::oklab_data());
global.define("rgb", Color::rgb_data());
global.define("cmyk", Color::cmyk_data());
global.define("range", Array::range_data());
diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs
index 4f85abef..6fd56bb6 100644
--- a/crates/typst-library/src/text/deco.rs
+++ b/crates/typst-library/src/text/deco.rs
@@ -240,7 +240,7 @@ pub struct HighlightElem {
/// ```example
/// This is #highlight(fill: blue)[with blue].
/// ```
- #[default(Color::Rgba(RgbaColor::new(0xFF, 0xFF, 0x5F, 0xFF)).into())]
+ #[default(Color::from_u8(0xFF, 0xFF, 0x5F, 0xFF).into())]
pub fill: Paint,
/// The top end of the background rectangle.
diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs
index 1f46f94d..688e69d8 100644
--- a/crates/typst-library/src/text/raw.rs
+++ b/crates/typst-library/src/text/raw.rs
@@ -454,11 +454,12 @@ fn styled(piece: &str, foreground: Paint, style: synt::Style) -> Content {
body
}
-fn to_typst(synt::Color { r, g, b, a }: synt::Color) -> RgbaColor {
- RgbaColor { r, g, b, a }
+fn to_typst(synt::Color { r, g, b, a }: synt::Color) -> Color {
+ Color::from_u8(r, g, b, a)
}
-fn to_syn(RgbaColor { r, g, b, a }: RgbaColor) -> synt::Color {
+fn to_syn(color: Color) -> synt::Color {
+ let [r, g, b, a] = color.to_vec4_u8();
synt::Color { r, g, b, a }
}
@@ -628,7 +629,7 @@ fn item(
synt::ThemeItem {
scope: scope.parse().unwrap(),
style: synt::StyleModifier {
- foreground: color.map(|s| to_syn(s.parse::<RgbaColor>().unwrap())),
+ foreground: color.map(|s| to_syn(s.parse::<Color>().unwrap())),
background: None,
font_style,
},
diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml
index e34281e3..0775f67b 100644
--- a/crates/typst/Cargo.toml
+++ b/crates/typst/Cargo.toml
@@ -30,10 +30,10 @@ image = { version = "0.24", default-features = false, features = ["png", "jpeg",
indexmap = { version = "2", features = ["serde"] }
log = "0.4"
miniz_oxide = "0.7"
-oklab = "1"
once_cell = "1"
pdf-writer = "0.8.1"
pixglyph = "0.2"
+palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
regex = "1"
resvg = { version = "0.35.0", default-features = false, features = ["raster-images"] }
roxmltree = "0.18"
diff --git a/crates/typst/src/doc.rs b/crates/typst/src/doc.rs
index 1068fc20..8f7e080a 100644
--- a/crates/typst/src/doc.rs
+++ b/crates/typst/src/doc.rs
@@ -13,8 +13,7 @@ use crate::export::PdfPageLabel;
use crate::font::Font;
use crate::geom::{
self, rounded_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke,
- Geometry, Length, Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size,
- Transform,
+ Geometry, Length, Numeric, Paint, Point, Rel, Shape, Sides, Size, Transform,
};
use crate::image::Image;
use crate::model::{Content, Location, MetaElem, StyleChain};
@@ -352,8 +351,7 @@ impl Frame {
0,
Point::zero(),
FrameItem::Shape(
- Geometry::Rect(self.size)
- .filled(RgbaColor { a: 100, ..Color::TEAL.to_rgba() }.into()),
+ Geometry::Rect(self.size).filled(Color::TEAL.with_alpha(0.5).into()),
Span::detached(),
),
);
diff --git a/crates/typst/src/eval/func.rs b/crates/typst/src/eval/func.rs
index 87237352..d8a2c3e8 100644
--- a/crates/typst/src/eval/func.rs
+++ b/crates/typst/src/eval/func.rs
@@ -378,6 +378,15 @@ impl PartialEq for Func {
}
}
+impl PartialEq<&NativeFuncData> for Func {
+ fn eq(&self, other: &&NativeFuncData) -> bool {
+ match &self.repr {
+ Repr::Native(native) => native.function == other.function,
+ _ => false,
+ }
+ }
+}
+
impl From<Repr> for Func {
fn from(repr: Repr) -> Self {
Self { repr, span: Span::detached() }
diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs
index 539cb516..776608b3 100644
--- a/crates/typst/src/eval/value.rs
+++ b/crates/typst/src/eval/value.rs
@@ -604,7 +604,6 @@ primitive! { Plugin: "plugin", Plugin }
mod tests {
use super::*;
use crate::eval::{array, dict};
- use crate::geom::RgbaColor;
#[track_caller]
fn test(value: impl IntoValue, exp: &str) {
@@ -623,7 +622,6 @@ mod tests {
test(Ratio::one() / 2.0, "50%");
test(Ratio::new(0.3) + Length::from(Abs::cm(2.0)), "30% + 56.69pt");
test(Fr::one() * 7.55, "7.55fr");
- test(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "rgb(\"#010101\")");
// Collections.
test("hello", r#""hello""#);
diff --git a/crates/typst/src/export/pdf/color.rs b/crates/typst/src/export/pdf/color.rs
new file mode 100644
index 00000000..1ca0c3f7
--- /dev/null
+++ b/crates/typst/src/export/pdf/color.rs
@@ -0,0 +1,428 @@
+use std::sync::Arc;
+
+use pdf_writer::types::DeviceNSubtype;
+use pdf_writer::{writers, Dict, Filter, Name, PdfWriter, Ref};
+
+use super::page::PageContext;
+use super::RefExt;
+use crate::export::pdf::deflate;
+use crate::geom::{Color, ColorSpace, Paint};
+
+// The names of the color spaces.
+pub const SRGB: Name<'static> = Name(b"srgb");
+pub const D65_GRAY: Name<'static> = Name(b"d65gray");
+pub const OKLAB: Name<'static> = Name(b"oklab");
+pub const HSV: Name<'static> = Name(b"hsv");
+pub const HSL: Name<'static> = Name(b"hsl");
+pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb");
+
+// The names of the color components.
+const OKLAB_L: Name<'static> = Name(b"L");
+const OKLAB_A: Name<'static> = Name(b"A");
+const OKLAB_B: Name<'static> = Name(b"B");
+const HSV_H: Name<'static> = Name(b"H");
+const HSV_S: Name<'static> = Name(b"S");
+const HSV_V: Name<'static> = Name(b"V");
+const HSL_H: Name<'static> = Name(b"H");
+const HSL_S: Name<'static> = Name(b"S");
+const HSL_L: Name<'static> = Name(b"L");
+
+// The ICC profiles.
+const SRGB_ICC: &[u8] = include_bytes!("./icc/sRGB-v4.icc");
+const GRAY_ICC: &[u8] = include_bytes!("./icc/sGrey-v4.icc");
+
+// The PostScript functions for color spaces.
+const OKLAB_SOURCE: &str = include_str!("./postscript/oklab.ps");
+const HSL_SOURCE: &str = include_str!("./postscript/hsl.ps");
+const HSV_SOURCE: &str = include_str!("./postscript/hsv.ps");
+
+/// The color spaces present in the PDF document
+#[derive(Default)]
+pub struct ColorSpaces {
+ oklab: Option<Ref>,
+ srgb: Option<Ref>,
+ d65_gray: Option<Ref>,
+ hsv: Option<Ref>,
+ hsl: Option<Ref>,
+ use_linear_rgb: bool,
+}
+
+impl ColorSpaces {
+ /// Get a reference to the oklab color space.
+ ///
+ /// # Warning
+ /// The A and B components of the color must be offset by +0.4 before being
+ /// encoded into the PDF file.
+ pub fn oklab(&mut self, alloc: &mut Ref) -> Ref {
+ *self.oklab.get_or_insert_with(|| alloc.bump())
+ }
+
+ /// Get a reference to the srgb color space.
+ pub fn srgb(&mut self, alloc: &mut Ref) -> Ref {
+ *self.srgb.get_or_insert_with(|| alloc.bump())
+ }
+
+ /// Get a reference to the gray color space.
+ pub fn d65_gray(&mut self, alloc: &mut Ref) -> Ref {
+ *self.d65_gray.get_or_insert_with(|| alloc.bump())
+ }
+
+ /// Get a reference to the hsv color space.
+ ///
+ /// # Warning
+ /// The Hue component of the color must be in degrees and must be divided
+ /// by 360.0 before being encoded into the PDF file.
+ pub fn hsv(&mut self, alloc: &mut Ref) -> Ref {
+ *self.hsv.get_or_insert_with(|| alloc.bump())
+ }
+
+ /// Get a reference to the hsl color space.
+ ///
+ /// # Warning
+ /// The Hue component of the color must be in degrees and must be divided
+ /// by 360.0 before being encoded into the PDF file.
+ pub fn hsl(&mut self, alloc: &mut Ref) -> Ref {
+ *self.hsl.get_or_insert_with(|| alloc.bump())
+ }
+
+ /// Mark linear RGB as used.
+ pub fn linear_rgb(&mut self) {
+ self.use_linear_rgb = true;
+ }
+
+ /// Write the color space on usage.
+ pub fn write(
+ &mut self,
+ color_space: ColorSpace,
+ writer: writers::ColorSpace,
+ alloc: &mut Ref,
+ ) {
+ match color_space {
+ ColorSpace::Oklab => {
+ let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]);
+ self.write(ColorSpace::LinearRgb, oklab.alternate_color_space(), alloc);
+ oklab.tint_ref(self.oklab(alloc));
+ oklab.attrs().subtype(DeviceNSubtype::DeviceN);
+ }
+ ColorSpace::Srgb => writer.icc_based(self.srgb(alloc)),
+ ColorSpace::D65Gray => writer.icc_based(self.d65_gray(alloc)),
+ ColorSpace::LinearRgb => {
+ writer.cal_rgb(
+ [0.9505, 1.0, 1.0888],
+ None,
+ Some([1.0, 1.0, 1.0]),
+ Some([
+ 0.4124, 0.2126, 0.0193, 0.3576, 0.715, 0.1192, 0.1805, 0.0722,
+ 0.9505,
+ ]),
+ );
+ }
+ ColorSpace::Hsl => {
+ let mut hsl = writer.device_n([HSL_H, HSL_S, HSL_L]);
+ self.write(ColorSpace::Srgb, hsl.alternate_color_space(), alloc);
+ hsl.tint_ref(self.hsl(alloc));
+ hsl.attrs().subtype(DeviceNSubtype::DeviceN);
+ }
+ ColorSpace::Hsv => {
+ let mut hsv = writer.device_n([HSV_H, HSV_S, HSV_V]);
+ self.write(ColorSpace::Srgb, hsv.alternate_color_space(), alloc);
+ hsv.tint_ref(self.hsv(alloc));
+ hsv.attrs().subtype(DeviceNSubtype::DeviceN);
+ }
+ ColorSpace::Cmyk => writer.device_cmyk(),
+ }
+ }
+
+ // Write the color spaces to the PDF file.
+ pub fn write_color_spaces(&mut self, mut spaces: Dict, alloc: &mut Ref) {
+ if self.oklab.is_some() {
+ self.write(ColorSpace::Oklab, spaces.insert(OKLAB).start(), alloc);
+ }
+
+ if self.srgb.is_some() {
+ self.write(ColorSpace::Srgb, spaces.insert(SRGB).start(), alloc);
+ }
+
+ if self.d65_gray.is_some() {
+ self.write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), alloc);
+ }
+
+ if self.hsv.is_some() {
+ self.write(ColorSpace::Hsv, spaces.insert(HSV).start(), alloc);
+ }
+
+ if self.hsl.is_some() {
+ self.write(ColorSpace::Hsl, spaces.insert(HSL).start(), alloc);
+ }
+
+ if self.use_linear_rgb {
+ self.write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), alloc);
+ }
+ }
+
+ /// Write the necessary color spaces functions and ICC profiles to the
+ /// PDF file.
+ pub fn write_functions(&self, writer: &mut PdfWriter) {
+ // Write the Oklab function & color space
+ if let Some(oklab) = self.oklab {
+ let code = oklab_function();
+ writer
+ .post_script_function(oklab, &code)
+ .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
+ .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
+ .filter(Filter::FlateDecode);
+ }
+
+ // Write the HSV function & color space
+ if let Some(hsv) = self.hsv {
+ let code = hsv_function();
+ writer
+ .post_script_function(hsv, &code)
+ .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
+ .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
+ .filter(Filter::FlateDecode);
+ }
+
+ // Write the HSL function & color space
+ if let Some(hsl) = self.hsl {
+ let code = hsl_function();
+ writer
+ .post_script_function(hsl, &code)
+ .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
+ .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
+ .filter(Filter::FlateDecode);
+ }
+
+ // Write the sRGB color space
+ if let Some(srgb) = self.srgb {
+ let profile = srgb_icc();
+ writer
+ .icc_profile(srgb, &profile)
+ .n(3)
+ .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
+ .alternate()
+ .srgb();
+ }
+
+ // Write the gray color space
+ if let Some(gray) = self.d65_gray {
+ let profile = gray_icc();
+ writer
+ .icc_profile(gray, &profile)
+ .n(1)
+ .range([0.0, 1.0])
+ .alternate()
+ .d65_gray();
+ }
+ }
+}
+
+/// Deflated sRGB ICC profile
+#[comemo::memoize]
+fn srgb_icc() -> Arc<Vec<u8>> {
+ Arc::new(deflate(SRGB_ICC))
+}
+
+/// Deflated gray ICC profile
+#[comemo::memoize]
+fn gray_icc() -> Arc<Vec<u8>> {
+ Arc::new(deflate(GRAY_ICC))
+}
+
+/// Deflated Oklab PostScript function
+#[comemo::memoize]
+fn oklab_function() -> Arc<Vec<u8>> {
+ let code = minify(OKLAB_SOURCE);
+ Arc::new(deflate(code.as_bytes()))
+}
+
+/// Deflated HSV PostScript function
+#[comemo::memoize]
+fn hsv_function() -> Arc<Vec<u8>> {
+ let code = minify(HSV_SOURCE);
+ Arc::new(deflate(code.as_bytes()))
+}
+
+/// Deflated HSL PostScript function
+#[comemo::memoize]
+fn hsl_function() -> Arc<Vec<u8>> {
+ let code = minify(HSL_SOURCE);
+ Arc::new(deflate(code.as_bytes()))
+}
+
+/// This function removes comments, line spaces and carriage returns from a
+/// PostScript program. This is necessary to optimize the size of the PDF file.
+fn minify(source: &str) -> String {
+ let mut buf = String::with_capacity(source.len());
+ let mut s = unscanny::Scanner::new(source);
+ while let Some(c) = s.eat() {
+ match c {
+ '%' => {
+ s.eat_until('\n');
+ }
+ c if c.is_whitespace() => {
+ s.eat_whitespace();
+ if buf.ends_with(|c: char| !c.is_whitespace()) {
+ buf.push(' ');
+ }
+ }
+ _ => buf.push(c),
+ }
+ }
+ buf
+}
+
+/// Encodes the color into four f32s, which can be used in a PDF file.
+/// Ensures that the values are in the range [0.0, 1.0].
+///
+/// # Why?
+/// - Oklab: The a and b components are in the range [-0.4, 0.4] and the PDF
+/// specifies (and some readers enforce) that all color values be in the range
+/// [0.0, 1.0]. This means that the PostScript function and the encoded color
+/// must be offset by 0.4.
+/// - HSV/HSL: The hue component is in the range [0.0, 360.0] and the PDF format
+/// specifies that it must be in the range [0.0, 1.0]. This means that the
+/// PostScript function and the encoded color must be divided by 360.0.
+pub trait ColorEncode {
+ /// Performs the color to PDF f32 array conversion.
+ fn encode(&self, color: Color) -> [f32; 4];
+}
+
+impl ColorEncode for ColorSpace {
+ fn encode(&self, color: Color) -> [f32; 4] {
+ match self {
+ ColorSpace::Oklab => {
+ let [l, a, b, alpha] = color.to_oklab().to_vec4();
+ [l, (a + 0.4).clamp(0.0, 1.0), (b + 0.4).clamp(0.0, 1.0), alpha]
+ }
+ ColorSpace::Hsl => {
+ let [h, s, l, _] = color.to_hsl().to_vec4();
+ [h / 360.0, s, l, 0.0]
+ }
+ ColorSpace::Hsv => {
+ let [h, s, v, _] = color.to_hsv().to_vec4();
+ [h / 360.0, s, v, 0.0]
+ }
+ _ => color.to_vec4(),
+ }
+ }
+}
+
+/// Encodes a paint into either a fill or stroke color.
+pub trait PaintEncode {
+ /// Set the paint as the fill color.
+ fn set_as_fill(&self, page_context: &mut PageContext);
+
+ /// Set the paint as the stroke color.
+ fn set_as_stroke(&self, page_context: &mut PageContext);
+}
+
+impl PaintEncode for Paint {
+ fn set_as_fill(&self, ctx: &mut PageContext) {
+ let Paint::Solid(color) = self;
+ match color {
+ Color::Luma(_) => {
+ ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);
+ ctx.set_fill_color_space(D65_GRAY);
+
+ let [l, _, _, _] = ColorSpace::D65Gray.encode(*color);
+ ctx.content.set_fill_color([l]);
+ }
+ Color::Oklab(_) => {
+ ctx.parent.colors.oklab(&mut ctx.parent.alloc);
+ ctx.set_fill_color_space(OKLAB);
+
+ let [l, a, b, _] = ColorSpace::Oklab.encode(*color);
+ ctx.content.set_fill_color([l, a, b]);
+ }
+ Color::LinearRgb(_) => {
+ ctx.parent.colors.linear_rgb();
+ ctx.set_fill_color_space(LINEAR_SRGB);
+
+ let [r, g, b, _] = ColorSpace::LinearRgb.encode(*color);
+ ctx.content.set_fill_color([r, g, b]);
+ }
+ Color::Rgba(_) => {
+ ctx.parent.colors.srgb(&mut ctx.parent.alloc);
+ ctx.set_fill_color_space(SRGB);
+
+ let [r, g, b, _] = ColorSpace::Srgb.encode(*color);
+ ctx.content.set_fill_color([r, g, b]);
+ }
+ Color::Cmyk(_) => {
+ ctx.reset_fill_color_space();
+
+ let [c, m, y, k] = ColorSpace::Cmyk.encode(*color);
+ ctx.content.set_fill_cmyk(c, m, y, k);
+ }
+ Color::Hsl(_) => {
+ ctx.parent.colors.hsl(&mut ctx.parent.alloc);
+ ctx.set_fill_color_space(HSL);
+
+ let [h, s, l, _] = ColorSpace::Hsl.encode(*color);
+ ctx.content.set_fill_color([h, s, l]);
+ }
+ Color::Hsv(_) => {
+ ctx.parent.colors.hsv(&mut ctx.parent.alloc);
+ ctx.set_fill_color_space(HSV);
+
+ let [h, s, v, _] = ColorSpace::Hsv.encode(*color);
+ ctx.content.set_fill_color([h, s, v]);
+ }
+ }
+ }
+
+ fn set_as_stroke(&self, ctx: &mut PageContext) {
+ let Paint::Solid(color) = self;
+ match color {
+ Color::Luma(_) => {
+ ctx.parent.colors.d65_gray(&mut ctx.parent.alloc);
+ ctx.set_stroke_color_space(D65_GRAY);
+
+ let [l, _, _, _] = ColorSpace::D65Gray.encode(*color);
+ ctx.content.set_stroke_color([l]);
+ }
+ Color::Oklab(_) => {
+ ctx.parent.colors.oklab(&mut ctx.parent.alloc);
+ ctx.set_stroke_color_space(OKLAB);
+
+ let [l, a, b, _] = ColorSpace::Oklab.encode(*color);
+ ctx.content.set_stroke_color([l, a, b]);
+ }
+ Color::LinearRgb(_) => {
+ ctx.parent.colors.linear_rgb();
+ ctx.set_stroke_color_space(LINEAR_SRGB);
+
+ let [r, g, b, _] = ColorSpace::LinearRgb.encode(*color);
+ ctx.content.set_stroke_color([r, g, b]);
+ }
+ Color::Rgba(_) => {
+ ctx.parent.colors.srgb(&mut ctx.parent.alloc);
+ ctx.set_stroke_color_space(SRGB);
+
+ let [r, g, b, _] = ColorSpace::Srgb.encode(*color);
+ ctx.content.set_stroke_color([r, g, b]);
+ }
+ Color::Cmyk(_) => {
+ ctx.reset_stroke_color_space();
+
+ let [c, m, y, k] = ColorSpace::Cmyk.encode(*color);
+ ctx.content.set_stroke_cmyk(c, m, y, k);
+ }
+ Color::Hsl(_) => {
+ ctx.parent.colors.hsl(&mut ctx.parent.alloc);
+ ctx.set_stroke_color_space(HSL);
+
+ let [h, s, l, _] = ColorSpace::Hsl.encode(*color);
+ ctx.content.set_stroke_color([h, s, l]);
+ }
+ Color::Hsv(_) => {
+ ctx.parent.colors.hsv(&mut ctx.parent.alloc);
+ ctx.set_stroke_color_space(HSV);
+
+ let [h, s, v, _] = ColorSpace::Hsv.encode(*color);
+ ctx.content.set_stroke_color([h, s, v]);
+ }
+ }
+ }
+}
diff --git a/crates/typst/src/export/pdf/icc/sGrey-v4.icc b/crates/typst/src/export/pdf/icc/sGrey-v4.icc
new file mode 100644
index 00000000..2187b678
--- /dev/null
+++ b/crates/typst/src/export/pdf/icc/sGrey-v4.icc
Binary files differ
diff --git a/crates/typst/src/export/pdf/icc/sRGB-v4.icc b/crates/typst/src/export/pdf/icc/sRGB-v4.icc
new file mode 100644
index 00000000..d9f3c055
--- /dev/null
+++ b/crates/typst/src/export/pdf/icc/sRGB-v4.icc
Binary files differ
diff --git a/crates/typst/src/export/pdf/mod.rs b/crates/typst/src/export/pdf/mod.rs
index 7a831445..6a5aacf5 100644
--- a/crates/typst/src/export/pdf/mod.rs
+++ b/crates/typst/src/export/pdf/mod.rs
@@ -1,11 +1,13 @@
//! Exporting into PDF documents.
+mod color;
mod extg;
mod font;
mod image;
mod outline;
mod page;
+pub use self::color::{ColorEncode, ColorSpaces};
pub use self::page::{PdfPageLabel, PdfPageLabelStyle};
use std::cmp::Eq;
@@ -43,15 +45,12 @@ pub fn pdf(document: &Document) -> Vec<u8> {
ctx.writer.finish()
}
-/// Identifies the color space definitions.
-const SRGB: Name<'static> = Name(b"srgb");
-const D65_GRAY: Name<'static> = Name(b"d65gray");
-
/// Context for exporting a whole PDF document.
pub struct PdfContext<'a> {
document: &'a Document,
introspector: Introspector,
writer: PdfWriter,
+ colors: ColorSpaces,
pages: Vec<Page>,
page_heights: Vec<f32>,
alloc: Ref,
@@ -81,6 +80,7 @@ impl<'a> PdfContext<'a> {
document,
introspector: Introspector::new(&document.pages),
writer: PdfWriter::new(),
+ colors: ColorSpaces::default(),
pages: vec![],
page_heights: vec![],
alloc,
diff --git a/crates/typst/src/export/pdf/page.rs b/crates/typst/src/export/pdf/page.rs
index 12d05bcf..d9798f54 100644
--- a/crates/typst/src/export/pdf/page.rs
+++ b/crates/typst/src/export/pdf/page.rs
@@ -5,16 +5,16 @@ use pdf_writer::types::{
ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle,
NumberingStyle,
};
-use pdf_writer::writers::ColorSpace;
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
+use super::color::PaintEncode;
use super::extg::ExternalGraphicsState;
-use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB};
+use super::{deflate, AbsExt, EmExt, PdfContext, RefExt};
use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem};
use crate::font::Font;
use crate::geom::{
- self, Abs, Color, Em, FixedStroke, Geometry, LineCap, LineJoin, Numeric, Paint,
- Point, Ratio, Shape, Size, Transform,
+ self, Abs, Em, FixedStroke, Geometry, LineCap, LineJoin, Numeric, Paint, Point,
+ Ratio, Shape, Size, Transform,
};
use crate::image::Image;
@@ -86,10 +86,8 @@ pub fn write_page_tree(ctx: &mut PdfContext) {
.kids(ctx.page_refs.iter().copied());
let mut resources = pages.resources();
- let mut spaces = resources.color_spaces();
- spaces.insert(SRGB).start::<ColorSpace>().srgb();
- spaces.insert(D65_GRAY).start::<ColorSpace>().d65_gray();
- spaces.finish();
+ ctx.colors
+ .write_color_spaces(resources.color_spaces(), &mut ctx.alloc);
let mut fonts = resources.fonts();
for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) {
@@ -116,6 +114,9 @@ pub fn write_page_tree(ctx: &mut PdfContext) {
resources.finish();
pages.finish();
+
+ // Write all of the functions used by the document.
+ ctx.colors.write_functions(&mut ctx.writer);
}
/// Write a page tree node.
@@ -196,11 +197,11 @@ pub struct Page {
}
/// An exporter for the contents of a single PDF page.
-struct PageContext<'a, 'b> {
- parent: &'a mut PdfContext<'b>,
+pub struct PageContext<'a, 'b> {
+ pub parent: &'a mut PdfContext<'b>,
page_ref: Ref,
label: Option<PdfPageLabel>,
- content: Content,
+ pub content: Content,
state: State,
saves: Vec<State>,
bottom: f32,
@@ -249,21 +250,13 @@ impl PageContext<'_, '_> {
let stroke_opacity = stroke
.map(|stroke| {
let Paint::Solid(color) = stroke.paint;
- if let Color::Rgba(rgba_color) = color {
- rgba_color.a
- } else {
- 255
- }
+ color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
})
.unwrap_or(255);
let fill_opacity = fill
.map(|paint| {
let Paint::Solid(color) = paint;
- if let Color::Rgba(rgba_color) = color {
- rgba_color.a
- } else {
- 255
- }
+ color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
})
.unwrap_or(255);
self.set_external_graphics_state(&ExternalGraphicsState {
@@ -296,34 +289,19 @@ impl PageContext<'_, '_> {
fn set_fill(&mut self, fill: &Paint) {
if self.state.fill.as_ref() != Some(fill) {
- let f = |c| c as f32 / 255.0;
- let Paint::Solid(color) = fill;
- match color {
- Color::Luma(c) => {
- self.set_fill_color_space(D65_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.reset_fill_color_space();
- self.content.set_fill_cmyk(f(c.c), f(c.m), f(c.y), f(c.k));
- }
- }
+ fill.set_as_fill(self);
self.state.fill = Some(fill.clone());
}
}
- fn set_fill_color_space(&mut self, space: Name<'static>) {
+ pub 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);
}
}
- fn reset_fill_color_space(&mut self) {
+ pub fn reset_fill_color_space(&mut self) {
self.state.fill_space = None;
}
@@ -338,22 +316,7 @@ impl PageContext<'_, '_> {
miter_limit,
} = stroke;
- let f = |c| c as f32 / 255.0;
- let Paint::Solid(color) = paint;
- match color {
- Color::Luma(c) => {
- self.set_stroke_color_space(D65_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) => {
- self.reset_stroke_color_space();
- self.content.set_stroke_cmyk(f(c.c), f(c.m), f(c.y), f(c.k));
- }
- }
+ paint.set_as_stroke(self);
self.content.set_line_width(thickness.to_f32());
if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) {
@@ -379,14 +342,14 @@ impl PageContext<'_, '_> {
}
}
- fn set_stroke_color_space(&mut self, space: Name<'static>) {
+ pub 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);
}
}
- fn reset_stroke_color_space(&mut self) {
+ pub fn reset_stroke_color_space(&mut self) {
self.state.stroke_space = None;
}
}
diff --git a/crates/typst/src/export/pdf/postscript/hsl.ps b/crates/typst/src/export/pdf/postscript/hsl.ps
new file mode 100644
index 00000000..740bc3ed
--- /dev/null
+++ b/crates/typst/src/export/pdf/postscript/hsl.ps
@@ -0,0 +1,63 @@
+
+{
+ % Starting stack: H, S, L
+ % /!\ WARNING: The hue component **MUST** be encoded
+ % in the range [0, 1] before calling this function.
+ % This is because the function assumes that the
+ % hue component are divided by a factor of 360
+ % in order to meet the range requirements of the
+ % PDF specification.
+
+ % First we do H = (H * 360.0) % 360
+ 3 2 roll 360 mul 3 1 roll
+
+ % Compute C = (1 - |2 * L - 1|) * S
+ dup 1 exch 2 mul 1 sub abs sub 3 2 roll mul
+
+ % P = (H / 60) % 2
+ 3 2 roll dup 60 div 2
+ 2 copy div cvi mul exch sub abs
+
+ % X = C * (1 - |P - 1|)
+ 1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul
+
+ % Compute m = L - C / 2
+ exch dup 2 div 5 4 roll exch sub
+
+ % Rotate so H is top
+ 4 3 roll exch 4 1 roll
+
+ % Construct the RGB stack
+ dup 60 lt {
+ % We need to build: (C, X, 0)
+ pop 0 3 1 roll
+ } {
+ dup 120 lt {
+ % We need to build: (X, C, 0)
+ pop exch 0 3 1 roll
+ } {
+ dup 180 lt {
+ % We need to build: (0, C, X)
+ pop 0
+ } {
+ dup 240 lt {
+ % We need to build: (0, X, C)
+ pop exch 0
+ } {
+ 300 lt {
+ % We need to build: (X, 0, C)
+ 0 3 2 roll
+ } {
+ % We need to build: (C, 0, X)
+ 0 exch
+ } ifelse
+ } ifelse
+ } ifelse
+ } ifelse
+ } ifelse
+
+ 4 3 roll
+
+ % Add m to each component
+ dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch
+} \ No newline at end of file
diff --git a/crates/typst/src/export/pdf/postscript/hsv.ps b/crates/typst/src/export/pdf/postscript/hsv.ps
new file mode 100644
index 00000000..b29adf11
--- /dev/null
+++ b/crates/typst/src/export/pdf/postscript/hsv.ps
@@ -0,0 +1,62 @@
+{
+ % Starting stack: H, S, V
+ % /!\ WARNING: The hue component **MUST** be encoded
+ % in the range [0, 1] before calling this function.
+ % This is because the function assumes that the
+ % hue component are divided by a factor of 360
+ % in order to meet the range requirements of the
+ % PDF specification.
+
+ % First we do H = (H * 360.0) % 360
+ 3 2 roll 360 mul 3 1 roll
+
+ % Compute C = V * S
+ dup 3 1 roll mul
+
+ % P = (H / 60) % 2
+ 3 2 roll dup 60 div 2
+ 2 copy div cvi mul exch sub abs
+
+ % X = C * (1 - |P - 1|)
+ 1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul
+
+ % Compute m = V - C
+ exch dup 5 4 roll exch sub
+
+ % Rotate so H is top
+ 4 3 roll exch 4 1 roll
+
+ % Construct the RGB stack
+ dup 60 lt {
+ % We need to build: (C, X, 0)
+ pop 0 3 1 roll
+ } {
+ dup 120 lt {
+ % We need to build: (X, C, 0)
+ pop exch 0 3 1 roll
+ } {
+ dup 180 lt {
+ % We need to build: (0, C, X)
+ pop 0
+ } {
+ dup 240 lt {
+ % We need to build: (0, X, C)
+ pop exch 0
+ } {
+ 300 lt {
+ % We need to build: (X, 0, C)
+ 0 3 2 roll
+ } {
+ % We need to build: (C, 0, X)
+ 0 exch
+ } ifelse
+ } ifelse
+ } ifelse
+ } ifelse
+ } ifelse
+
+ 4 3 roll
+
+ % Add m to each component
+ dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch
+} \ No newline at end of file
diff --git a/crates/typst/src/export/pdf/postscript/oklab.ps b/crates/typst/src/export/pdf/postscript/oklab.ps
new file mode 100644
index 00000000..4d6e9ad5
--- /dev/null
+++ b/crates/typst/src/export/pdf/postscript/oklab.ps
@@ -0,0 +1,78 @@
+{
+ % Starting stack: L, A, B
+ % /!\ WARNING: The A and B components **MUST** be encoded
+ % in the range [0, 1] before calling this function.
+ % This is because the function assumes that the
+ % A and B components are offset by a factor of 0.4
+ % in order to meet the range requirements of the
+ % PDF specification.
+
+ exch 0.4 sub
+ exch 0.4 sub
+
+ % Load L a and b into the stack
+ 2 index
+ 2 index
+ 2 index
+
+ % Compute f1 = ((0.3963377774 * a) + (0.2158037573 * b) + L)^3
+ 0.2158037573 mul exch
+ 0.3963377774 mul add add
+ dup dup mul mul
+
+ % Load L, a, and b into the stack
+ 3 index
+ 3 index
+ 3 index
+
+ % Compute f2 = ((-0.1055613458 * a) + (-0.0638541728 * b) + L)^3
+ -0.0638541728 mul exch
+ -0.1055613458 mul add add
+ dup dup mul mul
+
+ % Load L, a, and b into the stack
+ 4 index
+ 4 index
+ 4 index
+
+ % Compute f3 = ((-0.0894841775 * a) + (-1.2914855480 * b) + L)^3
+ -1.2914855480 mul exch
+ -0.0894841775 mul add add
+ dup dup mul mul
+
+ % Discard L, a, and b by rolling the stack and popping
+ 6 3 roll pop pop pop
+
+ % Load f1, f2, and f3 into the stack
+ 2 index
+ 2 index
+ 2 index
+
+ % Compute R = f1 * 4.0767416621 + f2 * -3.3077115913 + f3 * 0.2309699292
+ 0.2309699292 mul exch
+ -3.3077115913 mul add exch
+ 4.0767416621 mul add
+
+ % Load f1, f2, and f3 into the stack
+ 3 index
+ 3 index
+ 3 index
+
+ % Compute G = f1 * -1.2684380046 + f2 * 2.6097574011 + f3 * -0.3413193965
+ -0.3413193965 mul exch
+ 2.6097574011 mul add exch
+ -1.2684380046 mul add
+
+ % Load f1, f2, and f3 into the stack
+ 4 index
+ 4 index
+ 4 index
+
+ % Compute B = f1 * -0.0041960863 + f2 * -0.7034186147 + f3 * 1.7076147010
+ 1.7076147010 mul exch
+ -0.7034186147 mul add exch
+ -0.0041960863 mul add
+
+ % Discard f1, f2, and f3 by rolling the stack and popping
+ 6 3 roll pop pop pop
+} \ No newline at end of file
diff --git a/crates/typst/src/export/render.rs b/crates/typst/src/export/render.rs
index 262bb2e2..c2ae888e 100644
--- a/crates/typst/src/export/render.rs
+++ b/crates/typst/src/export/render.rs
@@ -362,7 +362,7 @@ fn render_outline_glyph(
let mh = bitmap.height;
let Paint::Solid(color) = text.fill;
- let c = color.to_rgba();
+ let color = sk::ColorU8::from(color);
// Pad the pixmap with 1 pixel in each dimension so that we do
// not get any problem with floating point errors along their border
@@ -370,7 +370,14 @@ fn render_outline_glyph(
for x in 0..mw {
for y in 0..mh {
let alpha = bitmap.coverage[(y * mw + x) as usize];
- let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, alpha).premultiply();
+ let color = sk::ColorU8::from_rgba(
+ color.red(),
+ color.green(),
+ color.blue(),
+ alpha,
+ )
+ .premultiply();
+
pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] = color;
}
}
@@ -400,9 +407,7 @@ fn render_outline_glyph(
// Premultiply the text color.
let Paint::Solid(color) = text.fill;
- let c = color.to_rgba();
- let color =
- bytemuck::cast(sk::ColorU8::from_rgba(c.r, c.g, c.b, 255).premultiply());
+ let color = bytemuck::cast(sk::ColorU8::from(color).premultiply());
// Blend the glyph bitmap with the existing pixels on the canvas.
let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut());
@@ -629,8 +634,8 @@ impl From<&Paint> for sk::Paint<'static> {
impl From<Color> for sk::Color {
fn from(color: Color) -> Self {
- let c = color.to_rgba();
- sk::Color::from_rgba8(c.r, c.g, c.b, c.a)
+ let [r, g, b, a] = color.to_rgba().to_vec4_u8();
+ sk::Color::from_rgba8(r, g, b, a)
}
}
@@ -691,6 +696,13 @@ impl AbsExt for Abs {
}
}
+impl From<Color> for sk::ColorU8 {
+ fn from(value: Color) -> Self {
+ let [r, g, b, _] = value.to_rgba().to_vec4_u8();
+ sk::ColorU8::from_rgba(r, g, b, 255)
+ }
+}
+
// Alpha multiplication and blending are ported from:
// https://skia.googlesource.com/skia/+/refs/heads/main/include/core/SkColorPriv.h
diff --git a/crates/typst/src/export/svg.rs b/crates/typst/src/export/svg.rs
index c6807177..518c40c3 100644
--- a/crates/typst/src/export/svg.rs
+++ b/crates/typst/src/export/svg.rs
@@ -10,8 +10,8 @@ use xmlwriter::XmlWriter;
use crate::doc::{Frame, FrameItem, GroupItem, TextItem};
use crate::font::Font;
use crate::geom::{
- Abs, Axes, FixedStroke, Geometry, LineCap, LineJoin, Paint, PathItem, Ratio, Shape,
- Size, Transform,
+ Abs, Angle, Axes, Color, FixedStroke, Geometry, LineCap, LineJoin, Paint, PathItem,
+ Ratio, Shape, Size, Transform,
};
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::util::hash128;
@@ -299,13 +299,13 @@ impl SVGRenderer {
/// Write a fill attribute.
fn write_fill(&mut self, fill: &Paint) {
let Paint::Solid(color) = fill;
- self.xml.write_attribute("fill", &color.to_rgba().to_hex());
+ self.xml.write_attribute("fill", &color.encode());
}
/// Write a stroke attribute.
fn write_stroke(&mut self, stroke: &FixedStroke) {
let Paint::Solid(color) = stroke.paint;
- self.xml.write_attribute("stroke", &color.to_rgba().to_hex());
+ self.xml.write_attribute("stroke", &color.encode());
self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt());
self.xml.write_attribute(
"stroke-linecap",
@@ -654,3 +654,74 @@ impl ttf_parser::OutlineBuilder for SvgPathBuilder {
write!(&mut self.0, "Z ").unwrap();
}
}
+
+/// Encode the color as an SVG color.
+trait ColorEncode {
+ /// Encode the color.
+ fn encode(&self) -> EcoString;
+}
+
+impl ColorEncode for Color {
+ fn encode(&self) -> EcoString {
+ match *self {
+ c @ Color::Rgba(_)
+ | c @ Color::Luma(_)
+ | c @ Color::Cmyk(_)
+ | c @ Color::Hsv(_) => c.to_hex(),
+ Color::LinearRgb(rgb) => {
+ if rgb.alpha != 1.0 {
+ eco_format!(
+ "color(srgb-linear {:.3} {:.3} {:.3} / {:.3})",
+ rgb.red,
+ rgb.green,
+ rgb.blue,
+ rgb.alpha
+ )
+ } else {
+ eco_format!(
+ "color(srgb-linear {:.3} {:.3} {:.3})",
+ rgb.red,
+ rgb.green,
+ rgb.blue,
+ )
+ }
+ }
+ Color::Oklab(oklab) => {
+ if oklab.alpha != 1.0 {
+ eco_format!(
+ "oklab({:?} {:.3} {:.3} / {:.3})",
+ Ratio::new(oklab.l as f64),
+ oklab.a,
+ oklab.b,
+ oklab.alpha
+ )
+ } else {
+ eco_format!(
+ "oklab({:?} {:.3} {:.3})",
+ Ratio::new(oklab.l as f64),
+ oklab.a,
+ oklab.b,
+ )
+ }
+ }
+ Color::Hsl(hsl) => {
+ if hsl.alpha != 1.0 {
+ eco_format!(
+ "hsla({:?} {:?} {:?} / {:.3})",
+ Angle::deg(hsl.hue.into_degrees() as f64),
+ Ratio::new(hsl.saturation as f64),
+ Ratio::new(hsl.lightness as f64),
+ hsl.alpha,
+ )
+ } else {
+ eco_format!(
+ "hsl({:?} {:?} {:?})",
+ Angle::deg(hsl.hue.into_degrees() as f64),
+ Ratio::new(hsl.saturation as f64),
+ Ratio::new(hsl.lightness as f64),
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/geom/color.rs
index d3ced8e4..aec0c5cd 100644
--- a/crates/typst/src/geom/color.rs
+++ b/crates/typst/src/geom/color.rs
@@ -1,10 +1,21 @@
-use ecow::{eco_format, EcoString};
use std::str::FromStr;
+use ecow::{eco_format, EcoString};
+use palette::encoding::{self, Linear};
+use palette::{Darken, Desaturate, FromColor, Lighten, RgbHue, Saturate, ShiftHue};
+
use super::*;
-use crate::diag::{bail, At, SourceResult};
-use crate::eval::{cast, Args, Array, Cast, Func, Str};
-use crate::syntax::Spanned;
+use crate::diag::{bail, error, At, SourceResult};
+use crate::eval::{cast, Args, Array, Str};
+use crate::syntax::{Span, Spanned};
+
+// Type aliases for `palette` internal types in f32.
+type Oklab = palette::oklab::Oklaba<f32>;
+type LinearRgba = palette::rgb::Rgba<Linear<encoding::Srgb>, f32>;
+type Rgba = palette::rgb::Rgba<encoding::Srgb, f32>;
+type Hsl = palette::hsl::Hsla<encoding::Srgb, f32>;
+type Hsv = palette::hsv::Hsva<encoding::Srgb, f32>;
+type Luma = palette::luma::Luma<encoding::Srgb, f32>;
/// A color in a specific color space.
///
@@ -12,6 +23,10 @@ use crate::syntax::Spanned;
/// - sRGB through the [`rgb` function]($rgb)
/// - Device CMYK through [`cmyk` function]($cmyk)
/// - D65 Gray through the [`luma` function]($luma)
+/// - Oklab through the [`oklab` function]($oklab)
+/// - Linear RGB through the [`color.linear-rgb` function]($color.linear-rgb)
+/// - HSL through the [`color.hsl` function]($color.hsl)
+/// - HSV through the [`color.hsv` function]($color.hsv)
///
/// Typst provides the following built-in colors:
///
@@ -28,50 +43,52 @@ use crate::syntax::Spanned;
/// #rect(fill: color.aqua)
/// ```
#[ty(scope)]
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
+#[derive(Copy, Clone)]
pub enum Color {
- /// An 8-bit luma color.
- Luma(LumaColor),
- /// An 8-bit RGBA color.
- Rgba(RgbaColor),
- /// An 8-bit CMYK color.
- Cmyk(CmykColor),
-}
-
-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(),
- }
- }
+ /// A 32-bit luma color.
+ Luma(Luma),
+ /// A 32-bit L*a*b* color in the Oklab color space.
+ Oklab(Oklab),
+ /// A 32-bit RGBA color.
+ Rgba(Rgba),
+ /// A 32-bit linear RGB color.
+ LinearRgb(LinearRgba),
+ /// A 32-bit CMYK color.
+ Cmyk(Cmyk),
+ /// A 32-bit HSL color.
+ Hsl(Hsl),
+ /// A 32-bit HSV color.
+ Hsv(Hsv),
}
#[scope]
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));
+ pub const BLACK: Self = Self::Luma(Luma::new(0.0));
+ pub const GRAY: Self = Self::Luma(Luma::new(0.6666666));
+ pub const WHITE: Self = Self::Luma(Luma::new(1.0));
+ pub const SILVER: Self = Self::Luma(Luma::new(0.8666667));
+ pub const NAVY: Self = Self::Rgba(Rgba::new(0.0, 0.121569, 0.247059, 1.0));
+ pub const BLUE: Self = Self::Rgba(Rgba::new(0.0, 0.454902, 0.85098, 1.0));
+ pub const AQUA: Self = Self::Rgba(Rgba::new(0.4980392, 0.858823, 1.0, 1.0));
+ pub const TEAL: Self = Self::Rgba(Rgba::new(0.223529, 0.8, 0.8, 1.0));
+ pub const EASTERN: Self = Self::Rgba(Rgba::new(0.13725, 0.615686, 0.678431, 1.0));
+ pub const PURPLE: Self = Self::Rgba(Rgba::new(0.694118, 0.050980, 0.788235, 1.0));
+ pub const FUCHSIA: Self = Self::Rgba(Rgba::new(0.941177, 0.070588, 0.745098, 1.0));
+ pub const MAROON: Self = Self::Rgba(Rgba::new(0.521569, 0.078431, 0.294118, 1.0));
+ pub const RED: Self = Self::Rgba(Rgba::new(1.0, 0.254902, 0.211765, 1.0));
+ pub const ORANGE: Self = Self::Rgba(Rgba::new(1.0, 0.521569, 0.105882, 1.0));
+ pub const YELLOW: Self = Self::Rgba(Rgba::new(1.0, 0.8627451, 0.0, 1.0));
+ pub const OLIVE: Self = Self::Rgba(Rgba::new(0.239216, 0.6, 0.4392157, 1.0));
+ pub const GREEN: Self = Self::Rgba(Rgba::new(0.1803922, 0.8, 0.2509804, 1.0));
+ pub const LIME: Self = Self::Rgba(Rgba::new(0.0039216, 1.0, 0.4392157, 1.0));
/// Create a grayscale color.
///
+ /// A grayscale color is represented internally by a single `lightness` component.
+ ///
+ /// These components are also available using the [`components`]($color.components)
+ /// method.
+ ///
/// ```example
/// #for x in range(250, step: 50) {
/// box(square(fill: luma(x)))
@@ -79,16 +96,158 @@ impl Color {
/// ```
#[func]
pub fn luma(
- /// The gray component.
- gray: Component,
- ) -> Color {
- LumaColor::new(gray.0).into()
+ /// The real arguments (the other arguments are just for the docs, this
+ /// function is a bit involved, so we parse the arguments manually).
+ args: Args,
+ /// The lightness component.
+ #[external]
+ lightness: Component,
+ /// The color to convert to grayscale.
+ #[external]
+ color: Color,
+ ) -> SourceResult<Color> {
+ let mut args = args;
+ Ok(if let Some(color) = args.find::<Color>()? {
+ color.to_luma()
+ } else {
+ let Component(gray) =
+ args.expect("gray component").unwrap_or(Component(Ratio::one()));
+ Self::Luma(Luma::new(gray.get() as f32))
+ })
+ }
+
+ /// Create an [Oklab](https://bottosson.github.io/posts/oklab/) color.
+ ///
+ /// This color space is well suited for the following use cases:
+ /// - Color manipulation such as saturating while keeping perceived hue
+ /// - Creating grayscale images with uniform perceived lightness
+ /// - Creating smooth and uniform color transition and gradients
+ ///
+ /// A linear Oklab color is represented internally by an array of four components:
+ /// - lightness ([`ratio`]($ratio))
+ /// - a ([`float`]($float) in the range `[-0.4..0.4]`)
+ /// - b ([`float`]($float) in the range `[-0.4..0.4]`)
+ /// - alpha ([`ratio`]($ratio))
+ ///
+ /// These components are also available using the [`components`]($color.components)
+ /// method.
+ ///
+ /// ```example
+ /// #square(
+ /// fill: oklab(27%, 20%, -3%, 50%)
+ /// )
+ /// ```
+ #[func]
+ pub fn oklab(
+ /// The real arguments (the other arguments are just for the docs, this
+ /// function is a bit involved, so we parse the arguments manually).
+ args: Args,
+ /// The cyan component.
+ #[external]
+ lightness: RatioComponent,
+ /// The magenta component.
+ #[external]
+ a: ABComponent,
+ /// The yellow component.
+ #[external]
+ b: ABComponent,
+ /// The key component.
+ #[external]
+ alpha: RatioComponent,
+ /// The color to convert to Oklab.
+ #[external]
+ color: Color,
+ ) -> SourceResult<Color> {
+ let mut args = args;
+ Ok(if let Some(color) = args.find::<Color>()? {
+ color.to_oklab()
+ } else {
+ let RatioComponent(l) = args.expect("lightness component")?;
+ let ABComponent(a) = args.expect("A component")?;
+ let ABComponent(b) = args.expect("B component")?;
+ let RatioComponent(alpha) =
+ args.eat()?.unwrap_or(RatioComponent(Ratio::one()));
+ Self::Oklab(Oklab::new(
+ l.get() as f32,
+ a.get() as f32,
+ b.get() as f32,
+ alpha.get() as f32,
+ ))
+ })
+ }
+
+ /// Create an RGB(A) color with linear luma.
+ ///
+ /// This color space is similar to Srgb<f32>, but with the distinction that
+ /// the color component are not gamma corrected. This makes it easier to
+ /// perform color operations such as blending and interpolation. Although,
+ /// you should prefer to use the [`oklab` function]($oklab) for these.
+ ///
+ /// A linear RGB(A) color is represented internally by an array of four components:
+ /// - red ([`ratio`]($ratio))
+ /// - green ([`ratio`]($ratio))
+ /// - blue ([`ratio`]($ratio))
+ /// - alpha ([`ratio`]($ratio))
+ ///
+ /// These components are also available using the [`components`]($color.components)
+ /// method.
+ ///
+ /// ```example
+ /// #square(
+ /// fill: color.linear-rgb(30%, 50%, 10%)
+ /// )
+ /// ```
+ #[func(title = "Linear RGB")]
+ pub fn linear_rgb(
+ /// The real arguments (the other arguments are just for the docs, this
+ /// function is a bit involved, so we parse the arguments manually).
+ args: Args,
+ /// The red component.
+ #[external]
+ red: Component,
+ /// The green component.
+ #[external]
+ green: Component,
+ /// The blue component.
+ #[external]
+ blue: Component,
+ /// The alpha component.
+ #[external]
+ alpha: Component,
+ /// The color to convert to linear RGB(A).
+ #[external]
+ color: Color,
+ ) -> SourceResult<Color> {
+ let mut args = args;
+ Ok(if let Some(color) = args.find::<Color>()? {
+ color.to_linear_rgb()
+ } else {
+ 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(Ratio::one()));
+ Self::LinearRgb(LinearRgba::new(
+ r.get() as f32,
+ g.get() as f32,
+ b.get() as f32,
+ a.get() as f32,
+ ))
+ })
}
/// Create an RGB(A) color.
///
/// The color is specified in the sRGB color space.
///
+ /// An RGB(A) color is represented internally by an array of four components:
+ /// - red ([`ratio`]($ratio))
+ /// - green ([`ratio`]($ratio))
+ /// - blue ([`ratio`]($ratio))
+ /// - alpha ([`ratio`]($ratio))
+ ///
+ /// These components are also available using the [`components`]($color.components)
+ /// method.
+ ///
/// ```example
/// #square(fill: rgb("#b1f2eb"))
/// #square(fill: rgb(87, 127, 230))
@@ -125,16 +284,26 @@ impl Color {
/// The alpha component.
#[external]
alpha: Component,
+ /// The color to convert to RGB(A).
+ #[external]
+ color: Color,
) -> SourceResult<Color> {
let mut args = args;
Ok(if let Some(string) = args.find::<Spanned<Str>>()? {
- RgbaColor::from_str(&string.v).at(string.span)?.into()
+ Self::from_str(&string.v).at(string.span)?
+ } else if let Some(color) = args.find::<Color>()? {
+ color.to_rgba()
} else {
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).into()
+ let Component(a) = args.eat()?.unwrap_or(Component(Ratio::one()));
+ Self::Rgba(Rgba::new(
+ r.get() as f32,
+ g.get() as f32,
+ b.get() as f32,
+ a.get() as f32,
+ ))
})
}
@@ -144,6 +313,15 @@ impl Color {
/// to RGB for display preview might differ from how your printer reproduces
/// the color.
///
+ /// An HSL color is represented internally by an array of four components:
+ /// - cyan ([`ratio`]($ratio))
+ /// - magenta ([`ratio`]($ratio))
+ /// - yellow ([`ratio`]($ratio))
+ /// - key ([`ratio`]($ratio))
+ ///
+ /// These components are also available using the [`components`]($color.components)
+ /// method.
+ ///
/// ```example
/// #square(
/// fill: cmyk(27%, 0%, 3%, 5%)
@@ -151,77 +329,318 @@ impl Color {
/// ```
#[func(title = "CMYK")]
pub fn cmyk(
+ /// The real arguments (the other arguments are just for the docs, this
+ /// function is a bit involved, so we parse the arguments manually).
+ args: Args,
/// The cyan component.
+ #[external]
cyan: RatioComponent,
/// The magenta component.
+ #[external]
magenta: RatioComponent,
/// The yellow component.
+ #[external]
yellow: RatioComponent,
/// The key component.
+ #[external]
key: RatioComponent,
- ) -> Color {
- CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into()
+ /// The color to convert to CMYK.
+ #[external]
+ color: Color,
+ ) -> SourceResult<Color> {
+ let mut args = args;
+ Ok(if let Some(color) = args.find::<Color>()? {
+ color.to_cmyk()
+ } else {
+ 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/black component")?;
+ Self::Cmyk(Cmyk::new(
+ c.get() as f32,
+ m.get() as f32,
+ y.get() as f32,
+ k.get() as f32,
+ ))
+ })
}
- /// Returns the constructor function for this color's kind
- /// ([`rgb`]($color.rgb), [`cmyk`]($color.cmyk) or [`luma`]($color.luma)).
+ /// Create an HSL color.
+ ///
+ /// This color space is useful for specifying colors by hue, saturation and
+ /// lightness. It is also useful for color manipulation, such as saturating
+ /// while keeping perceived hue.
+ ///
+ /// An HSL color is represented internally by an array of four components:
+ /// - hue ([`angle`]($angle))
+ /// - saturation ([`ratio`]($ratio))
+ /// - lightness ([`ratio`]($ratio))
+ /// - alpha ([`ratio`]($ratio))
+ ///
+ /// These components are also available using the [`components`]($color.components)
+ /// method.
///
/// ```example
- /// #let color = cmyk(1%, 2%, 3%, 4%)
- /// #(color.kind() == cmyk)
+ /// #square(
+ /// fill: color.hsl(30deg, 50%, 60%)
+ /// )
/// ```
- #[func]
- pub fn kind(self) -> Func {
- match self {
- Self::Luma(_) => Self::luma_data().into(),
- Self::Rgba(_) => Self::rgb_data().into(),
- Self::Cmyk(_) => Self::cmyk_data().into(),
- }
- }
-
- /// Returns the color's RGB(A) hex representation (such as `#ffaa32` or
- /// `#020304fe`). The alpha component (last two digits in `#020304fe`) is
- /// omitted if it is equal to `ff` (255 / 100%).
- #[func]
- pub fn to_hex(self) -> EcoString {
- self.to_rgba().to_hex()
+ #[func(title = "HSL")]
+ pub fn hsl(
+ /// The real arguments (the other arguments are just for the docs, this
+ /// function is a bit involved, so we parse the arguments manually).
+ args: Args,
+ /// The hue angle.
+ #[external]
+ hue: Angle,
+ /// The saturation component.
+ #[external]
+ saturation: Component,
+ /// The lightness component.
+ #[external]
+ lightness: Component,
+ /// The alpha component.
+ #[external]
+ alpha: Component,
+ /// The color to convert to HSL.
+ #[external]
+ color: Color,
+ ) -> SourceResult<Color> {
+ let mut args = args;
+ Ok(if let Some(color) = args.find::<Color>()? {
+ color.to_hsl()
+ } else {
+ let h: Angle = args.expect("hue component")?;
+ let Component(s) = args.expect("saturation component")?;
+ let Component(l) = args.expect("lightness component")?;
+ let Component(a) = args.eat()?.unwrap_or(Component(Ratio::one()));
+ Self::Hsl(Hsl::new(
+ RgbHue::from_degrees(h.to_deg() as f32),
+ s.get() as f32,
+ l.get() as f32,
+ a.get() as f32,
+ ))
+ })
}
- /// Converts this color to sRGB and returns its components (R, G, B, A) as
- /// an array of [integers]($int).
- #[func(name = "to-rgba")]
- pub fn to_rgba_array(self) -> Array {
- self.to_rgba().to_array()
+ /// Create an HSV color.
+ ///
+ /// This color space is useful for specifying colors by hue, saturation and
+ /// value. It is also useful for color manipulation, such as saturating
+ /// while keeping perceived hue.
+ ///
+ /// An HSV color is represented internally by an array of four components:
+ /// - hue ([`angle`]($angle))
+ /// - saturation ([`ratio`]($ratio))
+ /// - value ([`ratio`]($ratio))
+ /// - alpha ([`ratio`]($ratio))
+ ///
+ /// These components are also available using the [`components`]($color.components)
+ /// method.
+ ///
+ /// ```example
+ /// #square(
+ /// fill: color.hsv(30deg, 50%, 60%)
+ /// )
+ /// ```
+ #[func(title = "HSV")]
+ pub fn hsv(
+ /// The real arguments (the other arguments are just for the docs, this
+ /// function is a bit involved, so we parse the arguments manually).
+ args: Args,
+ /// The hue angle.
+ #[external]
+ hue: Angle,
+ /// The saturation component.
+ #[external]
+ saturation: Component,
+ /// The value component.
+ #[external]
+ value: Component,
+ /// The alpha component.
+ #[external]
+ alpha: Component,
+ /// The color to convert to HSL.
+ #[external]
+ color: Color,
+ ) -> SourceResult<Color> {
+ let mut args = args;
+ Ok(if let Some(color) = args.find::<Color>()? {
+ color.to_hsv()
+ } else {
+ let h: Angle = args.expect("hue component")?;
+ let Component(s) = args.expect("saturation component")?;
+ let Component(v) = args.expect("value component")?;
+ let Component(a) = args.eat()?.unwrap_or(Component(Ratio::one()));
+ Self::Hsv(Hsv::new(
+ RgbHue::from_degrees(h.to_deg() as f32),
+ s.get() as f32,
+ v.get() as f32,
+ a.get() as f32,
+ ))
+ })
}
- /// Converts this color to Digital CMYK and returns its components
- /// (C, M, Y, K) as an array of [ratios]($ratio). Note that this function
- /// will throw an error when applied to an [rgb]($rgb) color, since its
- /// conversion to CMYK is not available.
+ /// Converts this color into its components.
+ ///
+ /// The size and values of this array depends on the color space. You can
+ /// obtain the color space using [`space`]($color.space). Below is a table
+ /// of the color spaces and their components:
+ ///
+ /// | Color space | C1 | C2 | C3 | C4 |
+ /// |-------------------------|-----------|------------|-----------|--------|
+ /// | [`luma`]($color.luma) | Lightness | | | |
+ /// | [`oklab`]($color.oklab) | Lightness | `a` | `b` | Alpha |
+ /// | [`linear-rgb`]($color.linear-rgb) | Red | Green | Blue | Alpha |
+ /// | [`rgb`]($color.rgb) | Red | Green | Blue | Alpha |
+ /// | [`cmyk`]($color.cmyk) | Cyan | Magenta | Yellow | Key |
+ /// | [`hsl`]($color.hsl) | Hue | Saturation | Lightness | Alpha |
+ /// | [`hsv`]($color.hsv) | Hue | Saturation | Value | Alpha |
+ ///
+ /// For the meaning and type of each individual value, see the documentation
+ /// of the corresponding color space. The alpha component is optional and
+ /// only included if the `alpha` argument is `true`. The length of the
+ /// returned array depends on the number of components and whether the alpha
+ /// component is included.
+ ///
+ /// ```example
+ /// // note that the alpha component is included by default
+ /// #(rgb(40%, 60%, 80%).components() == (40%, 60%, 80%, 100%))
+ /// ```
#[func]
- pub fn to_cmyk(self) -> StrResult<Array> {
+ pub fn components(
+ self,
+ /// Whether to include the alpha component.
+ #[default(true)]
+ alpha: bool,
+ ) -> Array {
match self {
- Self::Luma(luma) => Ok(luma.to_cmyk().to_array()),
- Self::Rgba(_) => {
- bail!("cannot obtain cmyk values from rgba color")
+ Self::Luma(c) => array![Ratio::new(c.luma as _)],
+ Self::Oklab(c) => {
+ if alpha {
+ array![
+ Ratio::new(c.l as _),
+ (c.a as f64 * 1000.0).round() / 1000.0,
+ (c.b as f64 * 1000.0).round() / 1000.0,
+ Ratio::new(c.alpha as _),
+ ]
+ } else {
+ array![
+ Ratio::new(c.l as _),
+ (c.a as f64 * 1000.0).round() / 1000.0,
+ (c.b as f64 * 1000.0).round() / 1000.0,
+ ]
+ }
+ }
+ Self::LinearRgb(c) => {
+ if alpha {
+ array![
+ Ratio::new(c.red as _),
+ Ratio::new(c.green as _),
+ Ratio::new(c.blue as _),
+ Ratio::new(c.alpha as _),
+ ]
+ } else {
+ array![
+ Ratio::new(c.red as _),
+ Ratio::new(c.green as _),
+ Ratio::new(c.blue as _),
+ ]
+ }
+ }
+ Self::Rgba(c) => {
+ if alpha {
+ array![
+ Ratio::new(c.red as _),
+ Ratio::new(c.green as _),
+ Ratio::new(c.blue as _),
+ Ratio::new(c.alpha as _),
+ ]
+ } else {
+ array![
+ Ratio::new(c.red as _),
+ Ratio::new(c.green as _),
+ Ratio::new(c.blue as _),
+ ]
+ }
+ }
+ Self::Cmyk(c) => array![
+ Ratio::new(c.c as _),
+ Ratio::new(c.m as _),
+ Ratio::new(c.y as _),
+ Ratio::new(c.k as _),
+ ],
+ Self::Hsl(c) => {
+ if alpha {
+ array![
+ Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
+ Ratio::new(c.saturation as _),
+ Ratio::new(c.lightness as _),
+ Ratio::new(c.alpha as _),
+ ]
+ } else {
+ array![
+ Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
+ Ratio::new(c.saturation as _),
+ Ratio::new(c.lightness as _),
+ ]
+ }
+ }
+ Self::Hsv(c) => {
+ if alpha {
+ array![
+ Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
+ Ratio::new(c.saturation as _),
+ Ratio::new(c.value as _),
+ Ratio::new(c.alpha as _),
+ ]
+ } else {
+ array![
+ Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
+ Ratio::new(c.saturation as _),
+ Ratio::new(c.value as _),
+ ]
+ }
}
- Self::Cmyk(cmyk) => Ok(cmyk.to_array()),
}
}
- /// If this color was created with [luma]($luma), returns the
- /// [integer]($int) value used on construction. Otherwise (for [rgb]($rgb)
- /// and [cmyk]($cmyk) colors), throws an error.
+ /// Returns the constructor function for this color's space:
+ /// - [`oklab`]($color.oklab)
+ /// - [`luma`]($color.luma)
+ /// - [`linear-rgb`]($color.linear-rgb)
+ /// - [`rgb`]($color.rgb)
+ /// - [`cmyk`]($color.cmyk)
+ /// - [`hsl`]($color.hsl)
+ /// - [`hsv`]($color.hsv)
+ ///
+ /// ```example
+ /// #let color = cmyk(1%, 2%, 3%, 4%)
+ /// #(color.space() == cmyk)
+ /// ```
#[func]
- pub fn to_luma(self) -> StrResult<u8> {
+ pub fn space(self) -> ColorSpace {
match self {
- Self::Luma(luma) => Ok(luma.0),
- Self::Rgba(_) => {
- bail!("cannot obtain the luma value of rgba color")
- }
- Self::Cmyk(_) => {
- bail!("cannot obtain the luma value of cmyk color")
- }
+ Self::Luma(_) => ColorSpace::D65Gray,
+ Self::Oklab(_) => ColorSpace::Oklab,
+ Self::LinearRgb(_) => ColorSpace::LinearRgb,
+ Self::Rgba(_) => ColorSpace::Srgb,
+ Self::Cmyk(_) => ColorSpace::Cmyk,
+ Self::Hsl(_) => ColorSpace::Hsl,
+ Self::Hsv(_) => ColorSpace::Hsv,
+ }
+ }
+
+ /// Returns the color's RGB(A) hex representation (such as `#ffaa32` or
+ /// `#020304fe`). The alpha component (last two digits in `#020304fe`) is
+ /// omitted if it is equal to `ff` (255 / 100%).
+ #[func]
+ pub fn to_hex(self) -> EcoString {
+ let [r, g, b, a] = self.to_rgba().to_vec4_u8();
+ if a != 255 {
+ eco_format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
+ } else {
+ eco_format!("#{:02x}{:02x}{:02x}", r, g, b)
}
}
@@ -232,10 +651,15 @@ impl Color {
/// The factor to lighten the color by.
factor: Ratio,
) -> Color {
+ let factor = factor.get() as f32;
match self {
- Self::Luma(luma) => Self::Luma(luma.lighten(factor)),
- Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)),
- Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)),
+ Self::Luma(c) => Self::Luma(c.lighten(factor)),
+ Self::Oklab(c) => Self::Oklab(c.lighten(factor)),
+ Self::LinearRgb(c) => Self::LinearRgb(c.lighten(factor)),
+ Self::Rgba(c) => Self::Rgba(c.lighten(factor)),
+ Self::Cmyk(c) => Self::Cmyk(c.lighten(factor)),
+ Self::Hsl(c) => Self::Hsl(c.lighten(factor)),
+ Self::Hsv(c) => Self::Hsv(c.lighten(factor)),
}
}
@@ -246,29 +670,124 @@ impl Color {
/// The factor to darken the color by.
factor: Ratio,
) -> Color {
+ let factor = factor.get() as f32;
match self {
- Self::Luma(luma) => Self::Luma(luma.darken(factor)),
- Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)),
- Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)),
+ Self::Luma(c) => Self::Luma(c.darken(factor)),
+ Self::Oklab(c) => Self::Oklab(c.darken(factor)),
+ Self::LinearRgb(c) => Self::LinearRgb(c.darken(factor)),
+ Self::Rgba(c) => Self::Rgba(c.darken(factor)),
+ Self::Cmyk(c) => Self::Cmyk(c.darken(factor)),
+ Self::Hsl(c) => Self::Hsl(c.darken(factor)),
+ Self::Hsv(c) => Self::Hsv(c.darken(factor)),
}
}
+ /// Increases the saturation of a color by a given factor.
+ #[func]
+ pub fn saturate(
+ self,
+ /// The call span
+ span: Span,
+ /// The factor to saturate the color by.
+ factor: Ratio,
+ ) -> SourceResult<Color> {
+ Ok(match self {
+ Self::Luma(_) => {
+ bail!(error!(span, "cannot saturate grayscale color")
+ .with_hint("try converting your color to RGB first"));
+ }
+ Self::Oklab(_) => self.to_hsv().saturate(span, factor)?.to_oklab(),
+ Self::LinearRgb(_) => self.to_hsv().saturate(span, factor)?.to_linear_rgb(),
+ Self::Rgba(_) => self.to_hsv().saturate(span, factor)?.to_rgba(),
+ Self::Cmyk(_) => self.to_hsv().saturate(span, factor)?.to_cmyk(),
+ Self::Hsl(c) => Self::Hsl(c.saturate(factor.get() as f32)),
+ Self::Hsv(c) => Self::Hsv(c.saturate(factor.get() as f32)),
+ })
+ }
+
+ /// Decreases the saturation of a color by a given factor.
+ #[func]
+ pub fn desaturate(
+ self,
+ /// The call span
+ span: Span,
+ /// The factor to desaturate the color by.
+ factor: Ratio,
+ ) -> SourceResult<Color> {
+ Ok(match self {
+ Self::Luma(_) => {
+ bail!(error!(span, "cannot desaturate grayscale color")
+ .with_hint("try converting your color to RGB first"));
+ }
+ Self::Oklab(_) => self.to_hsv().desaturate(span, factor)?.to_oklab(),
+ Self::LinearRgb(_) => self.to_hsv().desaturate(span, factor)?.to_linear_rgb(),
+ Self::Rgba(_) => self.to_hsv().desaturate(span, factor)?.to_rgba(),
+ Self::Cmyk(_) => self.to_hsv().desaturate(span, factor)?.to_cmyk(),
+ Self::Hsl(c) => Self::Hsl(c.desaturate(factor.get() as f32)),
+ Self::Hsv(c) => Self::Hsv(c.desaturate(factor.get() as f32)),
+ })
+ }
+
/// Produces the negative of the color.
#[func]
pub fn negate(self) -> Color {
match self {
- Self::Luma(luma) => Self::Luma(luma.negate()),
- Self::Rgba(rgba) => Self::Rgba(rgba.negate()),
- Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()),
+ Self::Luma(c) => Self::Luma(Luma::new(1.0 - c.luma)),
+ Self::Oklab(c) => Self::Oklab(Oklab::new(c.l, 1.0 - c.a, 1.0 - c.b, c.alpha)),
+ Self::LinearRgb(c) => Self::LinearRgb(LinearRgba::new(
+ 1.0 - c.red,
+ 1.0 - c.green,
+ 1.0 - c.blue,
+ c.alpha,
+ )),
+ Self::Rgba(c) => {
+ Self::Rgba(Rgba::new(1.0 - c.red, 1.0 - c.green, 1.0 - c.blue, c.alpha))
+ }
+ Self::Cmyk(c) => Self::Cmyk(Cmyk::new(1.0 - c.c, 1.0 - c.m, 1.0 - c.y, c.k)),
+ Self::Hsl(c) => Self::Hsl(Hsl::new(
+ RgbHue::from_degrees(360.0 - c.hue.into_degrees()),
+ c.saturation,
+ c.lightness,
+ c.alpha,
+ )),
+ Self::Hsv(c) => Self::Hsv(Hsv::new(
+ RgbHue::from_degrees(360.0 - c.hue.into_degrees()),
+ c.saturation,
+ c.value,
+ c.alpha,
+ )),
}
}
+ /// Rotates the hue of the color by a given angle.
+ #[func]
+ pub fn rotate(
+ self,
+ /// The call span
+ span: Span,
+ /// The angle to rotate the hue by.
+ angle: Angle,
+ ) -> SourceResult<Color> {
+ Ok(match self {
+ Self::Luma(_) => {
+ bail!(error!(span, "cannot rotate grayscale color")
+ .with_hint("try converting your color to RGB first"));
+ }
+ Self::Oklab(_) => self.to_hsv().rotate(span, angle)?.to_oklab(),
+ Self::LinearRgb(_) => self.to_hsv().rotate(span, angle)?.to_linear_rgb(),
+ Self::Rgba(_) => self.to_hsv().rotate(span, angle)?.to_rgba(),
+ Self::Cmyk(_) => self.to_hsv().rotate(span, angle)?.to_cmyk(),
+ Self::Hsl(c) => Self::Hsl(c.shift_hue(angle.to_deg() as f32)),
+ Self::Hsv(c) => Self::Hsv(c.shift_hue(angle.to_deg() as f32)),
+ })
+ }
+
/// Create a color by mixing two or more colors.
///
/// ```example
/// #set block(height: 20pt, width: 100%)
/// #block(fill: red.mix(blue))
- /// #block(fill: red.mix(blue, space: "srgb"))
+ /// #block(fill: red.mix(blue, space: rgb))
/// #block(fill: color.mix(red, blue, white))
/// #block(fill: color.mix((red, 70%), (blue, 30%)))
/// ```
@@ -282,7 +801,7 @@ impl Color {
#[variadic]
colors: Vec<WeightedColor>,
/// The color space to mix in. By default, this happens in a perceptual
- /// color space (Oklab).
+ /// color space ([`oklab`]($color.oklab)).
#[named]
#[default(ColorSpace::Oklab)]
space: ColorSpace,
@@ -290,8 +809,9 @@ impl Color {
let mut total = 0.0;
let mut acc = [0.0; 4];
- for WeightedColor(color, weight) in colors.into_iter() {
- let v = rgba_to_vec4(color.to_rgba(), space);
+ for WeightedColor { color, weight } in colors.into_iter() {
+ let weight = weight as f32;
+ let v = color.to_space(space).to_vec4();
acc[0] += weight * v[0];
acc[1] += weight * v[1];
acc[2] += weight * v[2];
@@ -303,218 +823,321 @@ impl Color {
bail!("sum of weights must be positive");
}
- let mixed = acc.map(|v| v / total);
- Ok(vec4_to_rgba(mixed, space).into())
+ let m = acc.map(|v| v / total);
+ Ok(match space {
+ ColorSpace::Oklab => Color::Oklab(Oklab::new(m[0], m[1], m[2], m[3])),
+ ColorSpace::Srgb => Color::Rgba(Rgba::new(m[0], m[1], m[2], m[3])),
+ ColorSpace::LinearRgb => {
+ Color::LinearRgb(LinearRgba::new(m[0], m[1], m[2], m[3]))
+ }
+ ColorSpace::Hsl => {
+ Color::Hsl(Hsl::new(RgbHue::from_degrees(m[0]), m[1], m[2], m[3]))
+ }
+ ColorSpace::Hsv => {
+ Color::Hsv(Hsv::new(RgbHue::from_degrees(m[0]), m[1], m[2], m[3]))
+ }
+ ColorSpace::Cmyk => Color::Cmyk(Cmyk::new(m[0], m[1], m[2], m[3])),
+ ColorSpace::D65Gray => Color::Luma(Luma::new(m[0])),
+ })
}
}
-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 Color {
+ /// Construct a new RGBA color from 8-bit values.
+ pub fn from_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
+ Self::Rgba(Rgba::new(
+ r as f32 / 255.0,
+ g as f32 / 255.0,
+ b as f32 / 255.0,
+ a as f32 / 255.0,
+ ))
}
-}
-/// A color with a weight.
-pub struct WeightedColor(Color, f32);
+ /// Converts a 32-bit integer to an RGBA color.
+ #[inline]
+ pub fn from_u32(color: u32) -> Self {
+ Self::from_u8(
+ ((color >> 24) & 0xFF) as u8,
+ ((color >> 16) & 0xFF) as u8,
+ ((color >> 8) & 0xFF) as u8,
+ (color & 0xFF) as u8,
+ )
+ }
-cast! {
- WeightedColor,
- v: Color => Self(v, 1.0),
- v: Array => {
- let mut iter = v.into_iter();
- match (iter.next(), iter.next(), iter.next()) {
- (Some(c), Some(w), None) => Self(c.cast()?, w.cast::<Weight>()?.0),
- _ => bail!("expected a color or color-weight pair"),
+ pub fn alpha(&self) -> Option<f32> {
+ match self {
+ Color::Luma(_) | Color::Cmyk(_) => None,
+ Color::Oklab(c) => Some(c.alpha),
+ Color::Rgba(c) => Some(c.alpha),
+ Color::LinearRgb(c) => Some(c.alpha),
+ Color::Hsl(c) => Some(c.alpha),
+ Color::Hsv(c) => Some(c.alpha),
}
}
-}
-
-/// A weight for color mixing.
-struct Weight(f32);
-
-cast! {
- Weight,
- v: f64 => Self(v as f32),
- v: Ratio => Self(v.get() as f32),
-}
-/// Convert an RGBA color to four components in the given color space.
-fn rgba_to_vec4(color: RgbaColor, space: ColorSpace) -> [f32; 4] {
- match space {
- ColorSpace::Oklab => {
- let RgbaColor { r, g, b, a } = color;
- let oklab = oklab::srgb_to_oklab(oklab::RGB { r, g, b });
- [oklab.l, oklab.a, oklab.b, a as f32 / 255.0]
- }
- ColorSpace::Srgb => {
- let RgbaColor { r, g, b, a } = color;
- [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0]
+ pub fn with_alpha(mut self, alpha: f32) -> Self {
+ match &mut self {
+ Color::Luma(_) | Color::Cmyk(_) => {}
+ Color::Oklab(c) => c.alpha = alpha,
+ Color::Rgba(c) => c.alpha = alpha,
+ Color::LinearRgb(c) => c.alpha = alpha,
+ Color::Hsl(c) => c.alpha = alpha,
+ Color::Hsv(c) => c.alpha = alpha,
}
+
+ self
}
-}
-/// Convert four components in the given color space to RGBA.
-fn vec4_to_rgba(vec: [f32; 4], space: ColorSpace) -> RgbaColor {
- match space {
- ColorSpace::Oklab => {
- let [l, a, b, alpha] = vec;
- let oklab::RGB { r, g, b } = oklab::oklab_to_srgb(oklab::Oklab { l, a, b });
- RgbaColor { r, g, b, a: (alpha * 255.0).round() as u8 }
- }
- ColorSpace::Srgb => {
- let [r, g, b, a] = vec;
- RgbaColor {
- r: (r * 255.0).round() as u8,
- g: (g * 255.0).round() as u8,
- b: (b * 255.0).round() as u8,
- a: (a * 255.0).round() as u8,
+ pub fn to_vec4(&self) -> [f32; 4] {
+ match self {
+ Color::Luma(c) => [c.luma; 4],
+ Color::Oklab(c) => [c.l, c.a, c.b, c.alpha],
+ Color::Rgba(c) => [c.red, c.green, c.blue, c.alpha],
+ Color::LinearRgb(c) => [c.red, c.green, c.blue, c.alpha],
+ Color::Cmyk(c) => [c.c, c.m, c.y, c.k],
+ Color::Hsl(c) => [
+ c.hue.into_degrees().rem_euclid(360.0),
+ c.saturation,
+ c.lightness,
+ c.alpha,
+ ],
+ Color::Hsv(c) => {
+ [c.hue.into_degrees().rem_euclid(360.0), c.saturation, c.value, c.alpha]
}
}
}
-}
-/// A color space for mixing.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum ColorSpace {
- /// A perceptual color space.
- Oklab,
- /// The standard RGB color space.
- Srgb,
-}
-
-/// An 8-bit grayscale 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)
+ pub fn to_vec4_u8(&self) -> [u8; 4] {
+ self.to_vec4().map(|x| (x * 255.0).round() as u8)
}
- /// Convert to an opque RGBA color.
- pub const fn to_rgba(self) -> RgbaColor {
- RgbaColor::new(self.0, self.0, self.0, u8::MAX)
- }
-
- /// Convert to CMYK as a fraction of true black.
- pub fn to_cmyk(self) -> CmykColor {
- CmykColor::new(
- round_u8(self.0 as f64 * 0.75),
- round_u8(self.0 as f64 * 0.68),
- round_u8(self.0 as f64 * 0.67),
- round_u8(self.0 as f64 * 0.90),
- )
+ pub fn to_space(self, space: ColorSpace) -> Self {
+ match space {
+ ColorSpace::Oklab => self.to_oklab(),
+ ColorSpace::Srgb => self.to_rgba(),
+ ColorSpace::LinearRgb => self.to_linear_rgb(),
+ ColorSpace::Hsl => self.to_hsl(),
+ ColorSpace::Hsv => self.to_hsv(),
+ ColorSpace::Cmyk => self.to_cmyk(),
+ ColorSpace::D65Gray => self.to_luma(),
+ }
}
- /// Lighten this color by a factor.
- pub fn lighten(self, factor: Ratio) -> Self {
- let inc = round_u8((u8::MAX - self.0) as f64 * factor.get());
- Self(self.0.saturating_add(inc))
+ pub fn to_luma(self) -> Self {
+ Self::Luma(match self {
+ Self::Luma(c) => c,
+ Self::Oklab(c) => Luma::from_color(c),
+ Self::Rgba(c) => Luma::from_color(c),
+ Self::LinearRgb(c) => Luma::from_color(c),
+ Self::Cmyk(c) => Luma::from_color(c.to_rgba()),
+ Self::Hsl(c) => Luma::from_color(c),
+ Self::Hsv(c) => Luma::from_color(c),
+ })
}
- /// Darken this color by a factor.
- pub fn darken(self, factor: Ratio) -> Self {
- let dec = round_u8(self.0 as f64 * factor.get());
- Self(self.0.saturating_sub(dec))
+ pub fn to_oklab(self) -> Self {
+ Self::Oklab(match self {
+ Self::Luma(c) => Oklab::from_color(c),
+ Self::Oklab(c) => c,
+ Self::Rgba(c) => Oklab::from_color(c),
+ Self::LinearRgb(c) => Oklab::from_color(c),
+ Self::Cmyk(c) => Oklab::from_color(c.to_rgba()),
+ Self::Hsl(c) => Oklab::from_color(c),
+ Self::Hsv(c) => Oklab::from_color(c),
+ })
}
- /// Negate this color.
- pub fn negate(self) -> Self {
- Self(u8::MAX - self.0)
+ pub fn to_linear_rgb(self) -> Self {
+ Self::LinearRgb(match self {
+ Self::Luma(c) => LinearRgba::from_color(c),
+ Self::Oklab(c) => LinearRgba::from_color(c),
+ Self::Rgba(c) => LinearRgba::from_color(c),
+ Self::LinearRgb(c) => c,
+ Self::Cmyk(c) => LinearRgba::from_color(c.to_rgba()),
+ Self::Hsl(c) => LinearRgba::from_color(Rgba::from_color(c)),
+ Self::Hsv(c) => LinearRgba::from_color(Rgba::from_color(c)),
+ })
}
-}
-impl Debug for LumaColor {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "luma({})", self.0)
+ pub fn to_rgba(self) -> Self {
+ Self::Rgba(match self {
+ Self::Luma(c) => Rgba::from_color(c),
+ Self::Oklab(c) => Rgba::from_color(c),
+ Self::Rgba(c) => c,
+ Self::LinearRgb(c) => Rgba::from_linear(c),
+ Self::Cmyk(c) => c.to_rgba(),
+ Self::Hsl(c) => Rgba::from_color(c),
+ Self::Hsv(c) => Rgba::from_color(c),
+ })
}
-}
-impl From<LumaColor> for Color {
- fn from(luma: LumaColor) -> Self {
- Self::Luma(luma)
+ pub fn to_cmyk(self) -> Self {
+ Self::Cmyk(match self {
+ Self::Luma(c) => Cmyk::from_luma(c),
+ Self::Oklab(c) => Cmyk::from_rgba(Rgba::from_color(c)),
+ Self::Rgba(c) => Cmyk::from_rgba(c),
+ Self::LinearRgb(c) => Cmyk::from_rgba(Rgba::from_linear(c)),
+ Self::Cmyk(c) => c,
+ Self::Hsl(c) => Cmyk::from_rgba(Rgba::from_color(c)),
+ Self::Hsv(c) => Cmyk::from_rgba(Rgba::from_color(c)),
+ })
}
-}
-/// An 8-bit RGBA color.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-pub struct RgbaColor {
- /// Red channel.
- pub r: u8,
- /// Green channel.
- pub g: u8,
- /// Blue channel.
- pub b: u8,
- /// Alpha channel.
- pub a: u8,
-}
-
-impl RgbaColor {
- /// Construct a new RGBA color.
- pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
- Self { r, g, b, a }
+ pub fn to_hsl(self) -> Self {
+ Self::Hsl(match self {
+ Self::Luma(c) => Hsl::from_color(c),
+ Self::Oklab(c) => Hsl::from_color(c),
+ Self::Rgba(c) => Hsl::from_color(c),
+ Self::LinearRgb(c) => Hsl::from_color(Rgba::from_linear(c)),
+ Self::Cmyk(c) => Hsl::from_color(c.to_rgba()),
+ Self::Hsl(c) => c,
+ Self::Hsv(c) => Hsl::from_color(c),
+ })
}
- /// Lighten this color by a factor.
- ///
- /// The alpha channel is not affected.
- pub fn lighten(self, factor: Ratio) -> Self {
- let lighten =
- |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get()));
- Self {
- r: lighten(self.r),
- g: lighten(self.g),
- b: lighten(self.b),
- a: self.a,
- }
+ pub fn to_hsv(self) -> Self {
+ Self::Hsv(match self {
+ Self::Luma(c) => Hsv::from_color(c),
+ Self::Oklab(c) => Hsv::from_color(c),
+ Self::Rgba(c) => Hsv::from_color(c),
+ Self::LinearRgb(c) => Hsv::from_color(Rgba::from_linear(c)),
+ Self::Cmyk(c) => Hsv::from_color(c.to_rgba()),
+ Self::Hsl(c) => Hsv::from_color(c),
+ Self::Hsv(c) => c,
+ })
}
+}
- /// Darken this color by a factor.
- ///
- /// The alpha channel is not affected.
- pub fn darken(self, factor: Ratio) -> Self {
- let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get()));
- Self {
- r: darken(self.r),
- g: darken(self.g),
- b: darken(self.b),
- a: self.a,
+impl Debug for Color {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Luma(c) => write!(f, "luma({:?})", Ratio::new(c.luma as _)),
+ Self::Rgba(_) => write!(f, "rgb({:?})", self.to_hex()),
+ Self::LinearRgb(c) => {
+ if c.alpha == 1.0 {
+ write!(
+ f,
+ "color.linear-rgb({:?}, {:?}, {:?})",
+ Ratio::new(c.red as _),
+ Ratio::new(c.green as _),
+ Ratio::new(c.blue as _),
+ )
+ } else {
+ write!(
+ f,
+ "color.linear-rgb({:?}, {:?}, {:?}, {:?})",
+ Ratio::new(c.red as _),
+ Ratio::new(c.green as _),
+ Ratio::new(c.blue as _),
+ Ratio::new(c.alpha as _),
+ )
+ }
+ }
+ Self::Cmyk(c) => {
+ write!(
+ f,
+ "rgb({:?}, {:?}, {:?}, {:?})",
+ Ratio::new(c.c as _),
+ Ratio::new(c.m as _),
+ Ratio::new(c.y as _),
+ Ratio::new(c.k as _),
+ )
+ }
+ Self::Oklab(c) => {
+ if c.alpha == 1.0 {
+ write!(
+ f,
+ "oklab({:?}, {:.3}, {:.3})",
+ Ratio::new(c.l as _),
+ (c.a * 1000.0).round() / 1000.0,
+ (c.b * 1000.0).round() / 1000.0,
+ )
+ } else {
+ write!(
+ f,
+ "oklab({:?}, {:?}, {:?}, {:?})",
+ Ratio::new(c.l as _),
+ (c.a * 1000.0).round() / 1000.0,
+ (c.b * 1000.0).round() / 1000.0,
+ Ratio::new(c.alpha as _),
+ )
+ }
+ }
+ Self::Hsl(c) => {
+ if c.alpha == 1.0 {
+ write!(
+ f,
+ "color.hsl({:?}, {:?}, {:?})",
+ Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
+ Ratio::new(c.saturation as _),
+ Ratio::new(c.lightness as _),
+ )
+ } else {
+ write!(
+ f,
+ "color.hsl({:?}, {:?}, {:?}, {:?})",
+ Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
+ Ratio::new(c.saturation as _),
+ Ratio::new(c.lightness as _),
+ Ratio::new(c.alpha as _),
+ )
+ }
+ }
+ Self::Hsv(c) => {
+ if c.alpha == 1.0 {
+ write!(
+ f,
+ "color.hsv({:?}, {:?}, {:?})",
+ Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
+ Ratio::new(c.saturation as _),
+ Ratio::new(c.value as _),
+ )
+ } else {
+ write!(
+ f,
+ "color.hsv({:?}, {:?}, {:?}, {:?})",
+ Angle::deg(c.hue.into_degrees().rem_euclid(360.0) as _),
+ Ratio::new(c.saturation as _),
+ Ratio::new(c.value as _),
+ Ratio::new(c.alpha as _),
+ )
+ }
+ }
}
}
+}
- /// Negate this color.
- ///
- /// The alpha channel is not affected.
- pub fn negate(self) -> Self {
- Self {
- r: u8::MAX - self.r,
- g: u8::MAX - self.g,
- b: u8::MAX - self.b,
- a: self.a,
+impl PartialEq for Color {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ // Lower precision for comparison to avoid rounding errors.
+ // Keeps backward compatibility with previous versions of Typst.
+ (Self::Rgba(_), Self::Rgba(_)) => self.to_vec4_u8() == other.to_vec4_u8(),
+ (Self::Luma(a), Self::Luma(b)) => a == b,
+ (Self::Oklab(a), Self::Oklab(b)) => a == b,
+ (Self::LinearRgb(a), Self::LinearRgb(b)) => a == b,
+ (Self::Cmyk(a), Self::Cmyk(b)) => a == b,
+ (Self::Hsl(a), Self::Hsl(b)) => a == b,
+ (Self::Hsv(a), Self::Hsv(b)) => a == b,
+ _ => false,
}
}
+}
- /// Converts this color to a RGB Hex Code.
- pub fn to_hex(self) -> EcoString {
- if self.a != 255 {
- eco_format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
- } else {
- eco_format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
- }
- }
+impl Eq for Color {}
- /// Converts this color to an array of R, G, B, A components.
- pub fn to_array(self) -> Array {
- array![self.r, self.g, self.b, self.a]
+impl Hash for Color {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ core::mem::discriminant(self).hash(state);
+ let [x, y, z, w] = self.to_vec4();
+ x.to_bits().hash(state);
+ y.to_bits().hash(state);
+ z.to_bits().hash(state);
+ w.to_bits().hash(state);
}
}
-impl FromStr for RgbaColor {
+impl FromStr for Color {
type Err = &'static str;
/// Constructs a new color from hex strings like the following:
@@ -551,161 +1174,216 @@ impl FromStr for RgbaColor {
}
}
- Ok(Self::new(values[0], values[1], values[2], values[3]))
+ Ok(Self::from_u8(values[0], values[1], values[2], values[3]))
}
}
-impl Debug for RgbaColor {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- if f.alternate() {
- write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?;
- } else {
- write!(f, "rgb(\"{}\")", self.to_hex())?;
- }
- Ok(())
- }
-}
-
-impl<T: Into<RgbaColor>> From<T> for Color {
- fn from(rgba: T) -> Self {
- Self::Rgba(rgba.into())
- }
-}
-
-cast! {
- RgbaColor,
- self => Value::Color(self.into()),
-}
-
/// An 8-bit CMYK color.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-pub struct CmykColor {
+#[derive(Copy, Clone, PartialEq)]
+pub struct Cmyk {
/// The cyan component.
- pub c: u8,
+ pub c: f32,
/// The magenta component.
- pub m: u8,
+ pub m: f32,
/// The yellow component.
- pub y: u8,
+ pub y: f32,
/// The key (black) component.
- pub k: u8,
+ pub k: f32,
}
-impl CmykColor {
- /// Construct a new CMYK color.
- pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self {
+impl Cmyk {
+ fn new(c: f32, m: f32, y: f32, k: f32) -> Self {
Self { c, m, y, k }
}
- /// Convert this color to RGBA.
- pub fn to_rgba(self) -> RgbaColor {
- let k = self.k as f64 / 255.0;
- let f = |c| {
- let c = c as f64 / 255.0;
- round_u8(255.0 * (1.0 - c) * (1.0 - k))
- };
-
- RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 }
+ fn from_luma(luma: Luma) -> Self {
+ let l = luma.luma;
+ Cmyk::new(l * 0.75, l * 0.68, l * 0.67, l * 0.90)
}
- /// Lighten this color by a factor.
- pub fn lighten(self, factor: Ratio) -> Self {
- let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get()));
- Self {
- c: lighten(self.c),
- m: lighten(self.m),
- y: lighten(self.y),
- k: lighten(self.k),
+ fn from_rgba(rgba: Rgba) -> Self {
+ let r = rgba.red;
+ let g = rgba.green;
+ let b = rgba.blue;
+
+ let k = 1.0 - r.max(g).max(b);
+ if k == 1.0 {
+ return Cmyk::new(0.0, 0.0, 0.0, 1.0);
}
+
+ let c = (1.0 - r - k) / (1.0 - k);
+ let m = (1.0 - g - k) / (1.0 - k);
+ let y = (1.0 - b - k) / (1.0 - k);
+
+ Cmyk::new(c, m, y, k)
}
- /// Darken this color by a factor.
- pub fn darken(self, factor: Ratio) -> Self {
- let darken =
- |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get()));
- Self {
- c: darken(self.c),
- m: darken(self.m),
- y: darken(self.y),
- k: darken(self.k),
- }
+ fn to_rgba(self) -> Rgba {
+ let r = (1.0 - self.c) * (1.0 - self.k);
+ let g = (1.0 - self.m) * (1.0 - self.k);
+ let b = (1.0 - self.y) * (1.0 - self.k);
+
+ Rgba::new(r, g, b, 1.0)
}
- /// Negate this color.
- ///
- /// Does not affect the key component.
- pub fn negate(self) -> Self {
- Self {
- c: u8::MAX - self.c,
- m: u8::MAX - self.m,
- y: u8::MAX - self.y,
- k: self.k,
- }
+ fn lighten(self, factor: f32) -> Self {
+ let lighten = |u: f32| (u - u * factor).clamp(0.0, 1.0);
+ Self::new(lighten(self.c), lighten(self.m), lighten(self.y), lighten(self.k))
}
- /// Converts this color to an array of C, M, Y, K components.
- pub fn to_array(self) -> Array {
- // convert to ratio
- let g = |c| Ratio::new(c as f64 / 255.0);
- array![g(self.c), g(self.m), g(self.y), g(self.k)]
+ fn darken(self, factor: f32) -> Self {
+ let darken = |u: f32| (u + (1.0 - u) * factor).clamp(0.0, 1.0);
+ Self::new(darken(self.c), darken(self.m), darken(self.y), darken(self.k))
}
}
-impl Debug for CmykColor {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- let g = |c| 100.0 * (c as f64 / 255.0);
- write!(
- f,
- "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)",
- g(self.c),
- g(self.m),
- g(self.y),
- g(self.k),
- )
+/// A color with a weight.
+pub struct WeightedColor {
+ color: Color,
+ weight: f64,
+}
+
+impl WeightedColor {
+ /// Create a new weighted color.
+ pub const fn new(color: Color, weight: f64) -> Self {
+ Self { color, weight }
}
}
-impl From<CmykColor> for Color {
- fn from(cmyk: CmykColor) -> Self {
- Self::Cmyk(cmyk)
+cast! {
+ WeightedColor,
+ self => array![self.color, Value::Float(self.weight as _)].into_value(),
+ color: Color => Self { color, weight: 1.0 },
+ v: Array => {
+ let mut iter = v.into_iter();
+ match (iter.next(), iter.next(), iter.next()) {
+ (Some(c), Some(w), None) => Self {
+ color: c.cast()?,
+ weight: w.cast::<Weight>()?.0,
+ },
+ _ => bail!("expected a color or color-weight pair"),
+ }
}
}
+/// A weight for color mixing.
+struct Weight(f64);
+
cast! {
- CmykColor,
- self => Value::Color(self.into()),
+ Weight,
+ v: f64 => Self(v),
+ v: Ratio => Self(v.get()),
}
-/// An integer or ratio component.
-pub struct Component(u8);
+/// A color space for mixing.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum ColorSpace {
+ /// A perceptual color space.
+ Oklab,
+
+ /// The standard RGB color space.
+ Srgb,
+
+ /// The D65-gray color space.
+ D65Gray,
+
+ /// The linear RGB color space.
+ LinearRgb,
+
+ /// The HSL color space.
+ Hsl,
+
+ /// The HSV color space.
+ Hsv,
+
+ /// The CMYK color space.
+ Cmyk,
+}
cast! {
- Component,
- v: i64 => match v {
- 0 ..= 255 => Self(v as u8),
- _ => bail!("number must be between 0 and 255"),
- },
- v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
- Self((v.get() * 255.0).round() as u8)
- } else {
- bail!("ratio must be between 0% and 100%");
+ ColorSpace,
+ self => match self {
+ Self::Oklab => Color::oklab_data(),
+ Self::Srgb => Color::rgb_data(),
+ Self::D65Gray => Color::luma_data(),
+ Self::LinearRgb => Color::linear_rgb_data(),
+ Self::Hsl => Color::hsl_data(),
+ Self::Hsv => Color::hsv_data(),
+ Self::Cmyk => Color::cmyk_data(),
+ }.into_value(),
+ v: Value => {
+ let expected = "expected `rgb`, `luma`, `cmyk`, `oklab`, `color.linear-rgb`, `color.hsl`, or `color.hsv`";
+ let Value::Func(func) = v else {
+ bail!("{expected}, found {}", v.ty());
+ };
+
+ // Here comparing the function pointer since it's `Eq`
+ // whereas the `NativeFuncData` is not.
+ if func == Color::oklab_data() {
+ Self::Oklab
+ } else if func == Color::rgb_data() {
+ Self::Srgb
+ } else if func == Color::luma_data() {
+ Self::D65Gray
+ } else if func == Color::linear_rgb_data() {
+ Self::LinearRgb
+ } else if func == Color::hsl_data() {
+ Self::Hsl
+ } else if func == Color::hsv_data() {
+ Self::Hsv
+ } else if func == Color::cmyk_data() {
+ Self::Cmyk
+ } else {
+ bail!("{expected}");
+ }
},
}
/// A component that must be a ratio.
-pub struct RatioComponent(u8);
+pub struct RatioComponent(Ratio);
cast! {
RatioComponent,
+ self => self.0.into_value(),
v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
- Self((v.get() * 255.0).round() as u8)
+ Self(v)
} else {
bail!("ratio must be between 0% and 100%");
},
}
-/// Convert to the closest u8.
-fn round_u8(value: f64) -> u8 {
- value.round() as u8
+/// A component that must be a ratio between -40% and 40%.
+pub struct ABComponent(Ratio);
+
+cast! {
+ ABComponent,
+ v: Ratio => if (-0.4 ..= 0.4).contains(&v.get()) {
+ Self(v)
+ } else {
+ bail!("ratio must be between -40% and 40%");
+ },
+ v: f64 => if (-0.4 ..= 0.4).contains(&v) {
+ Self(Ratio::new(v))
+ } else {
+ bail!("ratio must be between -0.4 and 0.4");
+ },
+}
+
+/// An integer or ratio component.
+pub struct Component(Ratio);
+
+cast! {
+ Component,
+ self => self.0.into_value(),
+ v: i64 => match v {
+ 0 ..= 255 => Self(Ratio::new(v as f64 / 255.0)),
+ _ => bail!("number must be between 0 and 255"),
+ },
+ v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self(v)
+ } else {
+ bail!("ratio must be between 0% and 100%");
+ },
}
#[cfg(test)]
@@ -716,13 +1394,13 @@ mod tests {
fn test_parse_color_strings() {
#[track_caller]
fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) {
- assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a)));
+ assert_eq!(Color::from_str(hex), Ok(Color::from_u8(r, g, b, a)));
}
- test("f61243ff", 0xf6, 0x12, 0x43, 0xff);
- test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff);
+ test("f61243ff", 0xf6, 0x12, 0x43, 255);
+ test("b3d8b3", 0xb3, 0xd8, 0xb3, 255);
test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad);
- test("233", 0x22, 0x33, 0x33, 0xff);
+ test("233", 0x22, 0x33, 0x33, 255);
test("111b", 0x11, 0x11, 0x11, 0xbb);
}
@@ -730,7 +1408,7 @@ mod tests {
fn test_parse_invalid_colors() {
#[track_caller]
fn test(hex: &str, message: &str) {
- assert_eq!(RgbaColor::from_str(hex), Err(message));
+ assert_eq!(Color::from_str(hex), Err(message));
}
test("a5", "color string has wrong length");
diff --git a/crates/typst/src/geom/mod.rs b/crates/typst/src/geom/mod.rs
index b6ccfb3a..c5bcf84e 100644
--- a/crates/typst/src/geom/mod.rs
+++ b/crates/typst/src/geom/mod.rs
@@ -31,9 +31,7 @@ pub use self::abs::{Abs, AbsUnit};
pub use self::align::{Align, FixedAlign, HAlign, VAlign};
pub use self::angle::{Angle, AngleUnit};
pub use self::axes::{Axes, Axis};
-pub use self::color::{
- CmykColor, Color, ColorSpace, LumaColor, RgbaColor, WeightedColor,
-};
+pub use self::color::{Color, ColorSpace, WeightedColor};
pub use self::corners::{Corner, Corners};
pub use self::dir::Dir;
pub use self::ellipse::ellipse;
diff --git a/crates/typst/src/geom/scalar.rs b/crates/typst/src/geom/scalar.rs
index 71fb1755..71d30040 100644
--- a/crates/typst/src/geom/scalar.rs
+++ b/crates/typst/src/geom/scalar.rs
@@ -30,7 +30,7 @@ impl From<Scalar> for f64 {
impl Debug for Scalar {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.0.fmt(f)
+ Debug::fmt(&self.0, f)
}
}