diff --git a/Cargo.lock b/Cargo.lock index 842e9abe7..df30f43dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1778,6 +1778,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "qcms" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edecfcd5d755a5e5d98e24cf43113e7cdaec5a070edd0f6b250c03a573da30fa" + [[package]] name = "quick-xml" version = "0.28.2" @@ -2554,6 +2560,7 @@ dependencies = [ "log", "once_cell", "palette", + "qcms", "phf", "rayon", "regex", diff --git a/Cargo.toml b/Cargo.toml index b711b35f6..9da3f904b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ pixglyph = "0.3" proc-macro2 = "1" pulldown-cmark = "0.9" quote = "1" +qcms = "0.3.0" rayon = "1.7.0" regex = "1" resvg = { version = "0.38.0", default-features = false, features = ["raster-images"] } diff --git a/NOTICE b/NOTICE index 1291b0b3f..129ee3214 100644 --- a/NOTICE +++ b/NOTICE @@ -2,7 +2,8 @@ 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/*` +* The ICC profiles found in `crates/typst-pdf/src/icc/*` and + `crates/typst/assets/*`. CC0 1.0 Universal diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index b9e3b494c..650f53c38 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -41,6 +41,7 @@ lipsum = { workspace = true } log = { workspace = true } once_cell = { workspace = true } palette = { workspace = true } +qcms = { workspace = true } phf = { workspace = true } rayon = { workspace = true } regex = { workspace = true } diff --git a/crates/typst/assets/CGATS001Compat-v2-micro.icc b/crates/typst/assets/CGATS001Compat-v2-micro.icc new file mode 100644 index 000000000..b5a73495b Binary files /dev/null and b/crates/typst/assets/CGATS001Compat-v2-micro.icc differ diff --git a/crates/typst/src/visualize/color.rs b/crates/typst/src/visualize/color.rs index 90ac8c3d0..735a922cb 100644 --- a/crates/typst/src/visualize/color.rs +++ b/crates/typst/src/visualize/color.rs @@ -9,6 +9,7 @@ use palette::encoding::{self, Linear}; use palette::{ Darken, Desaturate, FromColor, Lighten, Okhsva, OklabHue, RgbHue, Saturate, ShiftHue, }; +use qcms::Profile; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::foundations::{ @@ -30,6 +31,36 @@ pub type Luma = palette::luma::Luma; /// Equivalent of [`std::f32::EPSILON`] but for hue angles. const ANGLE_EPSILON: f32 = 1e-5; +/// The ICC profile used to convert from CMYK to RGB. +/// +/// This is a minimal CMYK profile that only contains the necessary information +/// to convert from CMYK to RGB. It is based on the CGATS TR 001-1995 +/// specification. See +/// https://github.com/saucecontrol/Compact-ICC-Profiles#cmyk. +static CGATS001_COMPACT_PROFILE: Lazy> = Lazy::new(|| { + let bytes = include_bytes!("../../assets/CGATS001Compat-v2-micro.icc"); + Profile::new_from_slice(bytes, false).unwrap() +}); + +/// The target sRGB profile. +static SRGB_PROFILE: Lazy> = Lazy::new(|| { + let mut out = Profile::new_sRGB(); + out.precache_output_transform(); + out +}); + +static TO_SRGB: Lazy = Lazy::new(|| { + qcms::Transform::new_to( + &CGATS001_COMPACT_PROFILE, + &SRGB_PROFILE, + qcms::DataType::CMYK, + qcms::DataType::RGB8, + // Our input profile only supports perceptual intent. + qcms::Intent::Perceptual, + ) + .unwrap() +}); + /// A color in a specific color space. /// /// Typst supports: @@ -1691,6 +1722,8 @@ impl Cmyk { Cmyk::new(l * 0.75, l * 0.68, l * 0.67, l * 0.90) } + // This still uses naive conversion, because qcms does not support + // converting to CMYK yet. fn from_rgba(rgba: Rgb) -> Self { let r = rgba.red; let g = rgba.green; @@ -1709,11 +1742,23 @@ impl Cmyk { } fn to_rgba(self) -> Rgb { - 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); + let mut dest: [u8; 3] = [0; 3]; + TO_SRGB.convert( + &[ + (self.c * 255.0).round() as u8, + (self.m * 255.0).round() as u8, + (self.y * 255.0).round() as u8, + (self.k * 255.0).round() as u8, + ], + &mut dest, + ); - Rgb::new(r, g, b, 1.0) + Rgb::new( + dest[0] as f32 / 255.0, + dest[1] as f32 / 255.0, + dest[2] as f32 / 255.0, + 1.0, + ) } fn lighten(self, factor: f32) -> Self { diff --git a/tests/ref/bugs/gradient-cmyk-encode.png b/tests/ref/bugs/gradient-cmyk-encode.png index 7bd82ccef..5002442f7 100644 Binary files a/tests/ref/bugs/gradient-cmyk-encode.png and b/tests/ref/bugs/gradient-cmyk-encode.png differ diff --git a/tests/ref/compiler/color.png b/tests/ref/compiler/color.png index a585bf30e..69dbe5e84 100644 Binary files a/tests/ref/compiler/color.png and b/tests/ref/compiler/color.png differ diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ index 262f14972..a3d8c658c 100644 --- a/tests/typ/compiler/methods.typ +++ b/tests/typ/compiler/methods.typ @@ -143,8 +143,8 @@ #test(rgb(1, 2, 3).to-hex(), "#010203") #test(rgb(1, 2, 3, 4).to-hex(), "#01020304") #test(luma(40).to-hex(), "#282828") -#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(cmyk(4%, 5%, 6%, 7%).to-hex(), "#e0dcda") +#test-repr(rgb(cmyk(4%, 5%, 6%, 7%)).components(), (87.84%, 86.27%, 85.49%, 100%)) #test-repr(rgb(luma(40%)).components(alpha: 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%))