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