From 163c2e1aa27169c1eba946204096d3e8fdfd3c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20d=27Herbais=20de=20Thun?= Date: Tue, 19 Sep 2023 15:48:33 +0200 Subject: [PATCH] Gradient Part 1 - Color rework (#2171) --- Cargo.lock | 49 +- NOTICE | 121 ++ crates/typst-library/src/lib.rs | 1 + crates/typst-library/src/text/deco.rs | 2 +- crates/typst-library/src/text/raw.rs | 9 +- crates/typst/Cargo.toml | 2 +- crates/typst/src/doc.rs | 6 +- crates/typst/src/eval/func.rs | 9 + crates/typst/src/eval/value.rs | 2 - crates/typst/src/export/pdf/color.rs | 428 +++++ crates/typst/src/export/pdf/icc/sGrey-v4.icc | Bin 0 -> 360 bytes crates/typst/src/export/pdf/icc/sRGB-v4.icc | Bin 0 -> 480 bytes crates/typst/src/export/pdf/mod.rs | 8 +- crates/typst/src/export/pdf/page.rs | 77 +- crates/typst/src/export/pdf/postscript/hsl.ps | 63 + crates/typst/src/export/pdf/postscript/hsv.ps | 62 + .../typst/src/export/pdf/postscript/oklab.ps | 78 + crates/typst/src/export/render.rs | 26 +- crates/typst/src/export/svg.rs | 79 +- crates/typst/src/geom/color.rs | 1542 ++++++++++++----- crates/typst/src/geom/mod.rs | 4 +- crates/typst/src/geom/scalar.rs | 2 +- tests/ref/compiler/color.png | Bin 444 -> 21457 bytes tests/ref/compiler/repr.png | Bin 31187 -> 51799 bytes tests/src/tests.rs | 15 +- tests/typ/compiler/color.typ | 56 +- tests/typ/compiler/methods.typ | 51 +- tests/typ/compiler/repr.typ | 10 + tests/typ/compute/construct.typ | 98 +- 29 files changed, 2222 insertions(+), 578 deletions(-) create mode 100644 crates/typst/src/export/pdf/color.rs create mode 100644 crates/typst/src/export/pdf/icc/sGrey-v4.icc create mode 100644 crates/typst/src/export/pdf/icc/sRGB-v4.icc create mode 100644 crates/typst/src/export/pdf/postscript/hsl.ps create mode 100644 crates/typst/src/export/pdf/postscript/hsv.ps create mode 100644 crates/typst/src/export/pdf/postscript/oklab.ps diff --git a/Cargo.lock b/Cargo.lock index 861370f19..a01fb6398 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -705,6 +714,12 @@ dependencies = [ "regex", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastrand" version = "1.9.0" @@ -1535,15 +1550,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31" -[[package]] -name = "oklab" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467e40ada50d13bab19019e3707862b5076ca15841f31ee1474c40397c1b9f11" -dependencies = [ - "rgb", -] - [[package]] name = "once_cell" version = "1.18.0" @@ -1591,6 +1597,29 @@ dependencies = [ "zopfli", ] +[[package]] +name = "palette" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "parking_lot_core" version = "0.9.8" @@ -2715,8 +2744,8 @@ dependencies = [ "indexmap 2.0.0", "log", "miniz_oxide", - "oklab", "once_cell", + "palette", "pdf-writer", "pixglyph", "regex", diff --git a/NOTICE b/NOTICE index cbe9da9b2..18ed0c621 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,126 @@ Licenses for third party components used by this project can be found below. +================================================================================ +The Creative Commons Zero v1.0 Universal License applies to: +* The ICC profiles found in `crates/typst/icc/*` + +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/ + ================================================================================ The 0BSD License applies to: diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index bdb97f844..03e964412 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 4f85abef3..6fd56bb63 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 1f46f94d7..688e69d8f 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::().unwrap())), + foreground: color.map(|s| to_syn(s.parse::().unwrap())), background: None, font_style, }, diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index e34281e3e..0775f67b6 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 1068fc20d..8f7e080a1 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 872373525..d8a2c3e8a 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 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 539cb516d..776608b3a 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 000000000..1ca0c3f79 --- /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, + srgb: Option, + d65_gray: Option, + hsv: Option, + hsl: Option, + 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> { + Arc::new(deflate(SRGB_ICC)) +} + +/// Deflated gray ICC profile +#[comemo::memoize] +fn gray_icc() -> Arc> { + Arc::new(deflate(GRAY_ICC)) +} + +/// Deflated Oklab PostScript function +#[comemo::memoize] +fn oklab_function() -> Arc> { + let code = minify(OKLAB_SOURCE); + Arc::new(deflate(code.as_bytes())) +} + +/// Deflated HSV PostScript function +#[comemo::memoize] +fn hsv_function() -> Arc> { + let code = minify(HSV_SOURCE); + Arc::new(deflate(code.as_bytes())) +} + +/// Deflated HSL PostScript function +#[comemo::memoize] +fn hsl_function() -> Arc> { + 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 0000000000000000000000000000000000000000..2187b6786aa7c6a6744aad62599eb70b46118352 GIT binary patch literal 360 zcmZQzV9dx#&Mjt9U|`72D=Bgha*T|Kj8b5K#K6oT!obPE#~_=STwLHA>=wcR1jUKv z#mOZ_IUqIye7nZL2;yDV%}C5k*}W@?WB0NR{3ok2x2G?Adk~=xD~mNHwKy3la{!1{ zk_(DTfb=_tX@BaV)S&e~# zeF6i+-%0=f|6ch2|KCLn49s_cPA@D-EJ_5cWd@Q=X_*WRpO!N)@J2E)2wz}e;EDzS D*d9sj literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d9f3c055bd4d7573124090569e1b9533932214eb GIT binary patch literal 480 zcmZQzV0@61oLkJIz`&53S5g$@?xYYA8KuDfh=G|wgn^TRk3lvuxwybL*e!$s2#OQS zi<3)=azJbZ_;!tf5yZQ!n~|88Qu%U!Sd<;(wr1tlP`B^?s}Sn2vba)Gi<5yee}Gse zxuB?ofq_v0$W|&ZDFCrufNYWEjKmZmI|9hoDFQkW$Zi3Nr$g8?Kf&j?%JgIr1!9bb=h-Dax8G;zx8JuvaVFRmiW^iUOU|;}+ z7myDGh;S%QGd2PXG4j|mFtFbJ|NpZZ0|Wa628O?r{{R2I@c;k6ix?P~?=UbNC`9Pb zU%lYv3v7D7w~sE)stfnnQ`g2bXkuszH`k|`~df#K6~1_s_p M1_t2^3=CY+0OR6cu>b%7 literal 0 HcmV?d00001 diff --git a/crates/typst/src/export/pdf/mod.rs b/crates/typst/src/export/pdf/mod.rs index 7a831445e..6a5aacf55 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 { 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_heights: Vec, 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 12d05bcf1..d9798f54c 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::().srgb(); - spaces.insert(D65_GRAY).start::().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, - content: Content, + pub content: Content, state: State, saves: Vec, 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 000000000..740bc3ed7 --- /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 000000000..b29adf11d --- /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 000000000..4d6e9ad57 --- /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 262bb2e23..c2ae888ed 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::(canvas.data_mut()); @@ -629,8 +634,8 @@ impl From<&Paint> for sk::Paint<'static> { impl From 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 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 c68071776..518c40c3a 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 d3ced8e45..aec0c5cd0 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; +type LinearRgba = palette::rgb::Rgba, f32>; +type Rgba = palette::rgb::Rgba; +type Hsl = palette::hsl::Hsla; +type Hsv = palette::hsv::Hsva; +type Luma = palette::luma::Luma; /// 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 { + let mut args = args; + Ok(if let Some(color) = args.find::()? { + 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 { + let mut args = args; + Ok(if let Some(color) = args.find::()? { + 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, 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 { + let mut args = args; + Ok(if let Some(color) = args.find::()? { + 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 { let mut args = args; Ok(if let Some(string) = args.find::>()? { - 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.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,31 +329,305 @@ 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 { + let mut args = args; + Ok(if let Some(color) = args.find::()? { + 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 + /// #square( + /// fill: color.hsl(30deg, 50%, 60%) + /// ) + /// ``` + #[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 { + let mut args = args; + Ok(if let Some(color) = args.find::()? { + 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, + )) + }) + } + + /// 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 { + let mut args = args; + Ok(if let Some(color) = args.find::()? { + 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 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 components( + self, + /// Whether to include the alpha component. + #[default(true)] + alpha: bool, + ) -> Array { + match self { + 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 _), + ] + } + } + } + } + + /// 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.kind() == cmyk) + /// #(color.space() == cmyk) /// ``` #[func] - pub fn kind(self) -> Func { + pub fn space(self) -> ColorSpace { match self { - Self::Luma(_) => Self::luma_data().into(), - Self::Rgba(_) => Self::rgb_data().into(), - Self::Cmyk(_) => Self::cmyk_data().into(), + 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, } } @@ -184,44 +636,11 @@ impl Color { /// omitted if it is equal to `ff` (255 / 100%). #[func] pub fn to_hex(self) -> EcoString { - self.to_rgba().to_hex() - } - - /// 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() - } - - /// 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. - #[func] - pub fn to_cmyk(self) -> StrResult { - match self { - Self::Luma(luma) => Ok(luma.to_cmyk().to_array()), - Self::Rgba(_) => { - bail!("cannot obtain cmyk values from rgba color") - } - 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. - #[func] - pub fn to_luma(self) -> StrResult { - 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") - } + 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 { + 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 { + 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 { + 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, /// 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 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, + )) + } + + /// 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, + ) + } + + pub fn alpha(&self) -> Option { + 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), + } + } + + 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 + } + + 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] + } + } + } + + pub fn to_vec4_u8(&self) -> [u8; 4] { + self.to_vec4().map(|x| (x * 255.0).round() as u8) + } + + 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(), + } + } + + 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), + }) + } + + 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), + }) + } + + 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)), + }) + } + + 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), + }) + } + + 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)), + }) + } + + 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), + }) + } + + 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, + }) } } 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), - } - } -} - -/// A color with a weight. -pub struct WeightedColor(Color, f32); - -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::()?.0), - _ => bail!("expected a color or color-weight pair"), - } - } -} - -/// 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] - } - } -} - -/// 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, + 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 _), + ) + } } } } } -/// 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) - } - - /// 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), - ) - } - - /// 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)) - } - - /// 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)) - } - - /// Negate this color. - pub fn negate(self) -> Self { - Self(u8::MAX - self.0) - } -} - -impl Debug for LumaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "luma({})", self.0) - } -} - -impl From for Color { - fn from(luma: LumaColor) -> Self { - Self::Luma(luma) - } -} - -/// 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 } - } - - /// 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, +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, } } +} - /// 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 Eq for Color {} - /// 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, - } - } - - /// 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) - } - } - - /// 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(&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> From 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)) + fn from_luma(luma: Luma) -> Self { + let l = luma.luma; + Cmyk::new(l * 0.75, l * 0.68, l * 0.67, l * 0.90) + } + + 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) + } + + 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) + } + + 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)) + } + + 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)) + } +} + +/// 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 } + } +} + +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::()?.0, + }, + _ => bail!("expected a color or color-weight pair"), + } + } +} + +/// A weight for color mixing. +struct Weight(f64); + +cast! { + Weight, + v: f64 => Self(v), + v: Ratio => Self(v.get()), +} + +/// 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! { + 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()); }; - RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 } - } - - /// 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), + // 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}"); } - } - - /// 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), - } - } - - /// 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, - } - } - - /// 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)] - } -} - -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), - ) - } -} - -impl From for Color { - fn from(cmyk: CmykColor) -> Self { - Self::Cmyk(cmyk) - } -} - -cast! { - CmykColor, - self => Value::Color(self.into()), -} - -/// An integer or ratio component. -pub struct Component(u8); - -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%"); }, } /// 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 b6ccfb3a2..c5bcf84ea 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 71fb1755f..71d300407 100644 --- a/crates/typst/src/geom/scalar.rs +++ b/crates/typst/src/geom/scalar.rs @@ -30,7 +30,7 @@ impl From for f64 { impl Debug for Scalar { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) + Debug::fmt(&self.0, f) } } diff --git a/tests/ref/compiler/color.png b/tests/ref/compiler/color.png index 8f329fab455d0b6541214a5e106fdd1fd4ecae4f..ff9514da1c66b13b3984ee4c8cd81a8bc05b4c57 100644 GIT binary patch literal 21457 zcmeHPeNYoe6b}?@s zzz~&IgrJ#P9c$GBbw=7M1Vai^g-q)gGgL6)BSldNQMe=LZQor2L>>8~6i4XZU%Pv6 z_jZ5x_I>PocWEDl29Fu_+9;7oG)5K@^r1-P;wBQgMu}g9mYfsce0df?{K1KUxk~0)KT~9*k=%KXgO{|CHF+#fyWK=hQi)3`=4o z65OE>N+ixkmp&IfmRjBlgcUOp>cA~$1A|!OzH}= zW^hW3TeXuqtR}@9#!p-%taiSu)no)$QPfFE?^Nikg+ISI?3Z!jkx@ zwdNhP62_WJf8?2YD9=I|=@WcWlMAhM%LwEdns?Yr=Ja`ucHh#mB%81#VQT)f1Za3X z0{mf?4rm0Fu8ih<^5(x|rrwgB7kjLZbUVwa?niI3$a|I~puH_qKzoeps}Om2Y{LG=b=i8W$L@Tc6in_ zlYgcZ|7L)>(5jUA=PcEYG{@kd!s?k;8^%s`IqIvf~?KlekS zJ^PJ64Sd+vdm!|j-vk@WE*;{&Xj{hy|6D*#2FdAuuaVT%{ZS0EWW^znR9lKKQOXC~ zODSdaCmqDMO&2M*07eBe{NO@clxcM ztxnd%(^vM$9lT8ZHhb9_GcDemSOf`T7fV`DHUB$>f(@vCExLi2r;qjR_c{1xZ<3Y6 zSX;!!v|4YoDf5$-a)InvT&6QqZ-L_X?``8T6P1d{49jY7zRT@zZiABnbbG8br7bdw7cbMFci*!#m=tX+ZQm= zSRbmINS$8?5u=F!a^rfS$_DawUl?jBttzZ)vl45p35>gC8k@5Hwht6OZsX>2vqO3k z2&tnEe_#TmDm0w55~lV!K=nS5(^)XoG`6Cqxr=cxWX?g;I4*F(gMDm@;C`SKSyRha%PCx1+&Xg zWEWmL;H=!i0f7zf#*!OLZmj6Bq95jV;d34v9@5?=+UGd9#x^&$xv`6eU9`S5zhUOl z<7C(Vl3jTvhCj}HhM$u12TXYeBdO!Ak4K)JMMaFNKwDfzDfirk9NX4S8zF;~+Nnlj zTP>uw*VNZFle!DNq^mjkBS%$uB#aa0C%jvRw^YF@ygwB_u>T%V5(^QtlhPpZ%8?^M z;nYE(Mc2tm;X{wEF3?SiI7n+g{ZIklptM2sBw8S?JkED>@z{lXJj2HN935e-Kq5>aIC(B)gTgJ!K5AKM;5&U7yIk0ht z?(V?Sbr7YiL{V*SC!*D8;z~Y~3DUFpoHNLbqB3T1kdy?OR($$c#goQ8?ufy`oHOKi m2bQk=N!R}@98yZU$_{ugFf>|zhhKGyWQ#(Betd8B?tcKp&snSh literal 444 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZ4{|UANx2rgZXo3v;1lBd|NsAYufN~D_U_!_ z=leF^U%%kSg5L9!YL2(&9LVZWDv#c&*QS@{vnkYWqKjdXiE6l-w1bqejv$XL7rOup zGdm+A6VN1q0=;^m4Mv_Wjv*Dd-rn5H)u154c0p!w+Wr4O=X7t&nx^35p>=-yUF#pk zX6qICgcKYa7#NvYI0O_#6qlS^C#MMKBQZW$917Wec-Hjdr$*j!eOr&;MCd|N1vQ5_ zkqe@S<}*Vq#-bskyWp?MO>V2>r>z7q?YgBbvEC8pY>;lJui8d68zy6_R4{Qjh-=2? jFego?{f%&mg2}=@wH9TY`0j-R1Chbg)z4*}Q$iB}&^~mf diff --git a/tests/ref/compiler/repr.png b/tests/ref/compiler/repr.png index 994a6a922049cf4f7c7af68794800f1cee490a39..82ece777c52456f23c0dc86010aaaecbc642a38b 100644 GIT binary patch delta 31654 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 ZIu>NyI0u_>1JARfNs7sd77Bm%`9Ip1)j Library { Ok(NoneValue) } + #[func] + fn test_repr(lhs: Value, rhs: Value) -> StrResult { + if lhs.repr() != rhs.repr() { + bail!("Assertion failed: {lhs:?} != {rhs:?}"); + } + Ok(NoneValue) + } + #[func] fn print(#[variadic] values: Vec) -> NoneValue { let mut stdout = io::stdout().lock(); @@ -188,13 +196,14 @@ fn library() -> Library { // Hook up helpers into the global scope. lib.global.scope_mut().define_func::(); + lib.global.scope_mut().define_func::(); lib.global.scope_mut().define_func::(); lib.global .scope_mut() - .define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); + .define("conifer", Color::from_u8(0x9f, 0xEB, 0x52, 0xFF)); lib.global .scope_mut() - .define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); + .define("forest", Color::from_u8(0x43, 0xA1, 0x27, 0xFF)); lib } diff --git a/tests/typ/compiler/color.typ b/tests/typ/compiler/color.typ index fbb1749ba..71fce1b10 100644 --- a/tests/typ/compiler/color.typ +++ b/tests/typ/compiler/color.typ @@ -18,9 +18,59 @@ box(square(size: 9pt, fill: c.darken(x * 10%))) } +--- +// The the different color spaces +#let col = rgb(50%, 64%, 16%) +#box(square(size: 9pt, fill: col)) +#box(square(size: 9pt, fill: rgb(col))) +#box(square(size: 9pt, fill: oklab(col))) +#box(square(size: 9pt, fill: luma(col))) +#box(square(size: 9pt, fill: cmyk(col))) +#box(square(size: 9pt, fill: color.linear-rgb(col))) +#box(square(size: 9pt, fill: color.hsl(col))) +#box(square(size: 9pt, fill: color.hsv(col))) + +--- +// Test hue rotation +#let col = rgb(50%, 64%, 16%) + +#for x in range(0, 11) { + box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg))) +} + +#for x in range(0, 11) { + box(square(size: 9pt, fill: color.hsv(col).rotate(x * 36deg))) +} + +#for x in range(0, 11) { + box(square(size: 9pt, fill: color.hsl(col).rotate(x * 36deg))) +} + +--- +// Test saturation +#let col = color.hsl(180deg, 0%, 50%) +#for x in range(0, 11) { + box(square(size: 9pt, fill: col.saturate(x * 10%))) +} + +#let col = color.hsl(180deg, 100%, 50%) +#for x in range(0, 11) { + box(square(size: 9pt, fill: col.desaturate(x * 10%))) +} + +#let col = color.hsv(180deg, 0%, 50%) +#for x in range(0, 11) { + box(square(size: 9pt, fill: col.saturate(x * 10%))) +} + +#let col = color.hsv(180deg, 100%, 50%) +#for x in range(0, 11) { + box(square(size: 9pt, fill: col.desaturate(x * 10%))) +} + --- // Test gray color modification. // Ref: false -#test(luma(20%).lighten(50%), luma(60%)) -#test(luma(80%).darken(20%), luma(63.9%)) -#test(luma(80%).negate(), luma(20%)) +#test-repr(luma(20%).lighten(50%), luma(60%)) +#test-repr(luma(80%).darken(20%), luma(64%)) +#test-repr(luma(80%).negate(), luma(20%)) diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ index e4c09ed25..8f70bd8c8 100644 --- a/tests/typ/compiler/methods.typ +++ b/tests/typ/compiler/methods.typ @@ -97,39 +97,40 @@ --- // Test color kind method. -#test(rgb(1, 2, 3, 4).kind(), rgb) -#test(cmyk(4%, 5%, 6%, 7%).kind(), cmyk) -#test(luma(40).kind(), luma) -#test(rgb(1, 2, 3, 4).kind() != luma, true) +#test(rgb(1, 2, 3, 4).space(), rgb) +#test(cmyk(4%, 5%, 6%, 7%).space(), cmyk) +#test(luma(40).space(), luma) +#test(rgb(1, 2, 3, 4).space() != luma, true) --- -// Test color '.rgba()', '.cmyk()' and '.luma()' without conversions -#test(rgb(1, 2, 3, 4).to-rgba(), (1, 2, 3, 4)) -#test(rgb(1, 2, 3).to-rgba(), (1, 2, 3, 255)) -#test(cmyk(20%, 20%, 40%, 20%).to-cmyk(), (20%, 20%, 40%, 20%)) -#test(luma(40).to-luma(), 40) +// Test color '.components()' without conversions +#test-repr(rgb(1, 2, 3, 4).components(), (0.39%, 0.78%, 1.18%, 1.57%)) +#test-repr(luma(40).components(), (15.69%, )) +#test-repr(cmyk(4%, 5%, 6%, 7%).components(), (4%, 5%, 6%, 7%)) +#test-repr(oklab(10%, 0.2, 0.3).components(), (10%, 0.2, 0.3, 100%)) +#test-repr(color.linear-rgb(10%, 20%, 30%).components(), (10%, 20%, 30%, 100%)) +#test-repr(color.hsv(10deg, 20%, 30%).components(), (10deg, 20%, 30%, 100%)) +#test-repr(color.hsl(10deg, 20%, 30%).components(), (10deg, 20%, 30%, 100%)) --- // Test color conversions. #test(rgb(1, 2, 3).to-hex(), "#010203") #test(rgb(1, 2, 3, 4).to-hex(), "#01020304") -#test(cmyk(4%, 5%, 6%, 7%).to-rgba(), (228, 225, 223, 255)) -#test(cmyk(4%, 5%, 6%, 7%).to-hex(), "#e4e1df") -#test(luma(40).to-rgba(), (40, 40, 40, 255)) #test(luma(40).to-hex(), "#282828") -#test(repr(luma(40).to-cmyk()), repr((11.76%, 10.59%, 10.59%, 14.12%))) - ---- -// Error: 2-27 cannot obtain cmyk values from rgba color -#rgb(1, 2, 3, 4).to-cmyk() - ---- -// Error: 2-27 cannot obtain the luma value of rgba color -#rgb(1, 2, 3, 4).to-luma() - ---- -// Error: 2-32 cannot obtain the luma value of cmyk color -#cmyk(4%, 5%, 6%, 7%).to-luma() +#test-repr(cmyk(4%, 5%, 6%, 7%).to-hex(), "#e4e1df") +#test-repr(rgb(cmyk(4%, 5%, 6%, 7%)).components(), (89.28%, 88.35%, 87.42%, 100%)) +#test-repr(rgb(luma(40%)).components(false), (40%, 40%, 40%)) +#test-repr(cmyk(luma(40)).components(), (11.76%, 10.67%, 10.51%, 14.12%)) +#test-repr(cmyk(rgb(1, 2, 3)), cmyk(66.67%, 33.33%, 0%, 98.82%)) +#test-repr(luma(rgb(1, 2, 3)), luma(0.73%)) +#test-repr(color.hsl(luma(40)), color.hsl(0deg, 0%, 15.69%)) +#test-repr(color.hsv(luma(40)), color.hsv(0deg, 0%, 15.69%)) +#test-repr(color.linear-rgb(luma(40)), color.linear-rgb(2.12%, 2.12%, 2.12%)) +#test-repr(color.linear-rgb(rgb(1, 2, 3)), color.linear-rgb(0.03%, 0.06%, 0.09%)) +#test-repr(color.hsl(rgb(1, 2, 3)), color.hsl(-150deg, 50%, 0.78%)) +#test-repr(color.hsv(rgb(1, 2, 3)), color.hsv(-150deg, 66.67%, 1.18%)) +#test-repr(oklab(luma(40)).components(), (27.68%, 0.0, 0.0, 100%)) +#test-repr(oklab(rgb(1, 2, 3)).components(), (8.23%, -0.004, -0.007, 100%)) --- // Test alignment methods. diff --git a/tests/typ/compiler/repr.typ b/tests/typ/compiler/repr.typ index 13593a868..ce5b29750 100644 --- a/tests/typ/compiler/repr.typ +++ b/tests/typ/compiler/repr.typ @@ -47,3 +47,13 @@ #int \ #type("hi") \ #type((a: 1)) + +--- +#set text(0.8em) +#blue \ +#color.linear-rgb(blue) \ +#oklab(blue) \ +#cmyk(blue) \ +#color.hsl(blue) \ +#color.hsv(blue) \ +#luma(blue) diff --git a/tests/typ/compute/construct.typ b/tests/typ/compute/construct.typ index bef86fae4..d3cea0b4c 100644 --- a/tests/typ/compute/construct.typ +++ b/tests/typ/compute/construct.typ @@ -3,7 +3,7 @@ --- // Compare both ways. -#test(rgb(0%, 30%, 70%), rgb("004db3")) +#test-repr(rgb(0%, 30.2%, 70.2%), rgb("004db3")) // Alpha channel. #test(rgb(255, 0, 0, 50%), rgb("ff000080")) @@ -15,24 +15,80 @@ #test(white.lighten(100%), white) // Color mixing, in Oklab space by default. -#test(color.mix(rgb("#ff0000"), rgb("#00ff00")), rgb("#d0a800")) -#test(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: "oklab"), rgb("#d0a800")) -#test(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: "srgb"), rgb("#808000")) +#test(rgb(color.mix(rgb("#ff0000"), rgb("#00ff00"))), rgb("#d0a800")) +#test(rgb(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: oklab)), rgb("#d0a800")) +#test(rgb(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: rgb)), rgb("#808000")) -#test(color.mix(red, green, blue), rgb("#909282")) -#test(color.mix(red, blue, green), rgb("#909282")) -#test(color.mix(blue, red, green), rgb("#909282")) +#test(rgb(color.mix(red, green, blue)), rgb("#909282")) +#test(rgb(color.mix(red, blue, green)), rgb("#909282")) +#test(rgb(color.mix(blue, red, green)), rgb("#909282")) // Mix with weights. -#test(color.mix((red, 50%), (green, 50%)), rgb("#c0983b")) -#test(color.mix((red, 0.5), (green, 0.5)), rgb("#c0983b")) -#test(color.mix((red, 5), (green, 5)), rgb("#c0983b")) -#test(color.mix((green, 5), (white, 0), (red, 5)), rgb("#c0983b")) -#test(color.mix((red, 100%), (green, 0%)), red) -#test(color.mix((red, 0%), (green, 100%)), green) -#test(color.mix((rgb("#aaff00"), 25%), (rgb("#aa00ff"), 75%), space: "srgb"), rgb("#aa40bf")) -#test(color.mix((rgb("#aaff00"), 50%), (rgb("#aa00ff"), 50%), space: "srgb"), rgb("#aa8080")) -#test(color.mix((rgb("#aaff00"), 75%), (rgb("#aa00ff"), 25%), space: "srgb"), rgb("#aabf40")) +#test(rgb(color.mix((red, 50%), (green, 50%))), rgb("#c0983b")) +#test(rgb(color.mix((red, 0.5), (green, 0.5))), rgb("#c0983b")) +#test(rgb(color.mix((red, 5), (green, 5))), rgb("#c0983b")) +#test(rgb(color.mix((green, 5), (white, 0), (red, 5))), rgb("#c0983b")) +#test(color.mix((rgb("#aaff00"), 25%), (rgb("#aa00ff"), 75%), space: rgb), rgb("#aa40bf")) +#test(color.mix((rgb("#aaff00"), 50%), (rgb("#aa00ff"), 50%), space: rgb), rgb("#aa8080")) +#test(color.mix((rgb("#aaff00"), 75%), (rgb("#aa00ff"), 25%), space: rgb), rgb("#aabf40")) + +--- +// Test color conversion method kinds +#test(rgb(rgb(10, 20, 30)).space(), rgb) +#test(color.linear-rgb(rgb(10, 20, 30)).space(), color.linear-rgb) +#test(oklab(rgb(10, 20, 30)).space(), oklab) +#test(color.hsl(rgb(10, 20, 30)).space(), color.hsl) +#test(color.hsv(rgb(10, 20, 30)).space(), color.hsv) +#test(cmyk(rgb(10, 20, 30)).space(), cmyk) +#test(luma(rgb(10, 20, 30)).space(), luma) + +#test(rgb(color.linear-rgb(10, 20, 30)).space(), rgb) +#test(color.linear-rgb(color.linear-rgb(10, 20, 30)).space(), color.linear-rgb) +#test(oklab(color.linear-rgb(10, 20, 30)).space(), oklab) +#test(color.hsl(color.linear-rgb(10, 20, 30)).space(), color.hsl) +#test(color.hsv(color.linear-rgb(10, 20, 30)).space(), color.hsv) +#test(cmyk(color.linear-rgb(10, 20, 30)).space(), cmyk) +#test(luma(color.linear-rgb(10, 20, 30)).space(), luma) + +#test(rgb(oklab(10%, 20%, 30%)).space(), rgb) +#test(color.linear-rgb(oklab(10%, 20%, 30%)).space(), color.linear-rgb) +#test(oklab(oklab(10%, 20%, 30%)).space(), oklab) +#test(color.hsl(oklab(10%, 20%, 30%)).space(), color.hsl) +#test(color.hsv(oklab(10%, 20%, 30%)).space(), color.hsv) +#test(cmyk(oklab(10%, 20%, 30%)).space(), cmyk) +#test(luma(oklab(10%, 20%, 30%)).space(), luma) + +#test(rgb(color.hsl(10deg, 20%, 30%)).space(), rgb) +#test(color.linear-rgb(color.hsl(10deg, 20%, 30%)).space(), color.linear-rgb) +#test(oklab(color.hsl(10deg, 20%, 30%)).space(), oklab) +#test(color.hsl(color.hsl(10deg, 20%, 30%)).space(), color.hsl) +#test(color.hsv(color.hsl(10deg, 20%, 30%)).space(), color.hsv) +#test(cmyk(color.hsl(10deg, 20%, 30%)).space(), cmyk) +#test(luma(color.hsl(10deg, 20%, 30%)).space(), luma) + +#test(rgb(color.hsv(10deg, 20%, 30%)).space(), rgb) +#test(color.linear-rgb(color.hsv(10deg, 20%, 30%)).space(), color.linear-rgb) +#test(oklab(color.hsv(10deg, 20%, 30%)).space(), oklab) +#test(color.hsl(color.hsv(10deg, 20%, 30%)).space(), color.hsl) +#test(color.hsv(color.hsv(10deg, 20%, 30%)).space(), color.hsv) +#test(cmyk(color.hsv(10deg, 20%, 30%)).space(), cmyk) +#test(luma(color.hsv(10deg, 20%, 30%)).space(), luma) + +#test(rgb(cmyk(10%, 20%, 30%, 40%)).space(), rgb) +#test(color.linear-rgb(cmyk(10%, 20%, 30%, 40%)).space(), color.linear-rgb) +#test(oklab(cmyk(10%, 20%, 30%, 40%)).space(), oklab) +#test(color.hsl(cmyk(10%, 20%, 30%, 40%)).space(), color.hsl) +#test(color.hsv(cmyk(10%, 20%, 30%, 40%)).space(), color.hsv) +#test(cmyk(cmyk(10%, 20%, 30%, 40%)).space(), cmyk) +#test(luma(cmyk(10%, 20%, 30%, 40%)).space(), luma) + +#test(rgb(luma(10%)).space(), rgb) +#test(color.linear-rgb(luma(10%)).space(), color.linear-rgb) +#test(oklab(luma(10%)).space(), oklab) +#test(color.hsl(luma(10%)).space(), color.hsl) +#test(color.hsv(luma(10%)).space(), color.hsv) +#test(cmyk(luma(10%)).space(), cmyk) +#test(luma(luma(10%)).space(), luma) --- // Test gray color conversion. @@ -70,9 +126,17 @@ #color.mix((red, 1, 2)) --- -// Error: 31-38 expected "oklab" or "srgb" +// Error: 31-38 expected `rgb`, `luma`, `cmyk`, `oklab`, `color.linear-rgb`, `color.hsl`, or `color.hsv`, found string #color.mix(red, green, space: "cyber") +--- +// Error: 31-36 expected `rgb`, `luma`, `cmyk`, `oklab`, `color.linear-rgb`, `color.hsl`, or `color.hsv` +#color.mix(red, green, space: image) + +--- +// Error: 31-41 expected `rgb`, `luma`, `cmyk`, `oklab`, `color.linear-rgb`, `color.hsl`, or `color.hsv` +#color.mix(red, green, space: calc.round) + --- // Ref: true #let envelope = symbol(