diff --git a/crates/pdf/tests/print_color.rs b/crates/pdf/tests/print_color.rs index b9e35681..817323ef 100644 --- a/crates/pdf/tests/print_color.rs +++ b/crates/pdf/tests/print_color.rs @@ -1112,6 +1112,18 @@ fn f32_colors_keep_their_inks_in_shadings() { assert_eq!(mesh_shadings(&bytes), vec![(("DeviceCMYK".to_owned(), vec![]), 4, vec![corner.repeat(4)])]); } +/// A gray stop in a CMYK gradient becomes the black ink `1 - gray`: gray 0.96 is K 0.04, and the +/// 8-bit gray 128 is K 127/255, as `cmyk::Color::new` writes it. +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn gray_stops_keep_their_exact_black_in_cmyk_shadings() { + let cyan = || -> backend::color::Color { cmyk::Color::new_f32(0.04, 0.0, 0.0, 0.0).into() }; + let bytes = page_with(|surface| fill_rect(surface, 0.0, linear([luma::Color::new_f32(0.96).into(), cyan()]))); + assert_eq!(shading_ends(&bytes), (vec![0.0, 0.0, 0.0, 0.04], vec![0.04, 0.0, 0.0, 0.0])); + let bytes = page_with(|surface| fill_rect(surface, 0.0, linear([luma::Color::new(128).into(), cyan()]))); + assert_eq!(shading_ends(&bytes).0, vec![0.0, 0.0, 0.0, 127.0 / 255.0]); +} + const FOGRA39: &[u8] = include_bytes!("../../cms/profiles/craft-coated-fogra39.icc"); fn text(dict: &Dict<'_>, key: &[u8]) -> Option { diff --git a/vendor/krilla/README.craft.md b/vendor/krilla/README.craft.md index 6c17cd8c..a2bf5c6a 100644 --- a/vendor/krilla/README.craft.md +++ b/vendor/krilla/README.craft.md @@ -427,6 +427,10 @@ as a multiple of 1/255: C75 becomes 0.7490196 and C4 M4 Y4 K4 becomes 0.03921568 - Gradient stops that are rewritten into one DeviceCMYK, Separation or DeviceN space keep their components, and a DeviceN color's CMYK alternate (annotations, tags) is no longer rounded. +- A gray stop rewritten into a CMYK space becomes the black ink `1 - gray`, subtracted from the + shortest decimal that writes the gray, so gray 0.96 is K 0.04. An 8-bit gray `n` is K + `(255 - n) / 255`, as `cmyk::Color::new` writes it. - Mesh gradients write their corner colors and opacities at 16 bits a component instead of 8. -Tests: `crates/pdf/tests/print_color.rs` (`f32_colors_*`, `mesh_*`, `rgb_mesh_is_a_device_rgb_shading`). +Tests: `crates/pdf/tests/print_color.rs` (`f32_colors_*`, `gray_stops_keep_their_exact_black_in_cmyk_shadings`, +`mesh_*`, `rgb_mesh_is_a_device_rgb_shading`). diff --git a/vendor/krilla/SOURCE.json b/vendor/krilla/SOURCE.json index 9af9ac11..c3b497bb 100644 --- a/vendor/krilla/SOURCE.json +++ b/vendor/krilla/SOURCE.json @@ -152,7 +152,7 @@ { "path": "src/graphics/color.rs", "upstream_sha256": "e5843d33ef184ab5c458025d1b1dc53df6ee6aaf085595ea14604d080b98f8a4", - "sha256": "5b0de85e0f3eac2905f975f676f1ef5d0eebb99a9a35670fefb419ed36fed6cf" + "sha256": "e052d835a4f113ec3730f32c1708e80ee4a284978110f58c77d88c48f3cd15ae" }, { "path": "src/graphics/device_n.rs", @@ -207,7 +207,7 @@ { "path": "src/graphics/shading_function.rs", "upstream_sha256": "0e0fc8bb1a32272457ef560fa80933972d0f5ef5fb7046424c6de48093cfd978", - "sha256": "6da45a96a98d32d4ef878fa03ac1fddbccccea2e2d6333d3a0e90ca009061f81" + "sha256": "502571de1a0d1f1d32310d3801ae70250c7ac129506fdf4dd99e786c24268d12" }, { "path": "src/graphics/shading_pattern.rs", diff --git a/vendor/krilla/patches/17-color-precision.patch b/vendor/krilla/patches/17-color-precision.patch index e0781f3b..f8b295a5 100644 --- a/vendor/krilla/patches/17-color-precision.patch +++ b/vendor/krilla/patches/17-color-precision.patch @@ -1,5 +1,5 @@ diff --git a/src/graphics/color.rs b/src/graphics/color.rs -index 6fb0618..06a3098 100644 +index 6fb0618..581de00 100644 --- a/src/graphics/color.rs +++ b/src/graphics/color.rs @@ -69,7 +69,7 @@ @@ -11,7 +11,7 @@ index 6fb0618..06a3098 100644 use crate::configure::ValidationError; use crate::graphics::icc::ICCBasedColorSpace; -@@ -82,6 +82,48 @@ pub(crate) const DEVICE_GRAY: &str = "DeviceGray"; +@@ -82,6 +82,60 @@ pub(crate) const DEVICE_GRAY: &str = "DeviceGray"; /// The PDF name for the device CMYK color space. pub(crate) const DEVICE_CMYK: &str = "DeviceCMYK"; @@ -41,6 +41,18 @@ index 6fb0618..06a3098 100644 + pub(crate) fn to_u8(self) -> u8 { + (self.0 * 255.0).round() as u8 + } ++ ++ /// `1 - value`, the black ink of a gray. An 8-bit value `n / 255` gives `(255 - n) / 255`, ++ /// as the 8-bit constructors write it. Any other value is subtracted from 1 in f64, from ++ /// the shortest decimal that writes it, so gray 0.96 gives 0.04 rather than 0.04000002. ++ pub(crate) fn complement(self) -> f32 { ++ let byte = self.to_u8(); ++ if Tint::from_u8(byte) == self { ++ return Tint::from_u8(255 - byte).0; ++ } ++ let written = self.0.to_string().parse::().unwrap_or(f64::from(self.0)); ++ (1.0 - written) as f32 ++ } +} + +impl PartialEq for Tint { @@ -60,7 +72,7 @@ index 6fb0618..06a3098 100644 /// A wrapper for storing colors from different color spaces. #[derive(Debug, Hash, Eq, PartialEq, Clone)] pub enum Color { -@@ -160,7 +202,10 @@ impl RegularColor { +@@ -160,7 +214,10 @@ impl RegularColor { pub(crate) fn as_rgb(self) -> Option { Some(match self { Self::Rgb(r) => r, @@ -72,7 +84,7 @@ index 6fb0618..06a3098 100644 Self::Cmyk(_) => return None, }) } -@@ -183,16 +228,24 @@ impl SpecialColor { +@@ -183,16 +240,24 @@ impl SpecialColor { /// Gray-scale colors. pub mod luma { @@ -100,7 +112,7 @@ index 6fb0618..06a3098 100644 } /// Create a black luma color. -@@ -206,7 +259,7 @@ pub mod luma { +@@ -206,7 +271,7 @@ pub mod luma { } pub(crate) fn to_pdf_color(self) -> f32 { @@ -109,7 +121,7 @@ index 6fb0618..06a3098 100644 } } -@@ -251,27 +304,43 @@ impl From for Color { +@@ -251,27 +316,43 @@ impl From for Color { /// CMYK colors. pub mod cmyk { @@ -162,7 +174,7 @@ index 6fb0618..06a3098 100644 } } -@@ -390,23 +459,35 @@ pub mod rgb { +@@ -390,23 +471,35 @@ pub mod rgb { /// Separation (spot) colors. pub mod separation { @@ -202,7 +214,7 @@ index 6fb0618..06a3098 100644 } pub(crate) fn color_space(&self) -> SeparationSpace { -@@ -498,7 +579,7 @@ pub mod separation { +@@ -498,7 +591,7 @@ pub mod separation { pub mod device_n { use std::sync::Arc; @@ -211,7 +223,7 @@ index 6fb0618..06a3098 100644 /// A process colorant. #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] -@@ -656,7 +737,7 @@ pub mod device_n { +@@ -656,7 +749,7 @@ pub mod device_n { /// A DeviceN color: one tint per colorant of its space. #[derive(Debug, Hash, Eq, PartialEq, Clone)] pub struct Color { @@ -220,7 +232,7 @@ index 6fb0618..06a3098 100644 pub(crate) space: DeviceNSpace, } -@@ -666,20 +747,30 @@ pub mod device_n { +@@ -666,20 +759,30 @@ pub mod device_n { /// /// Returns `None` unless there is one tint per colorant. pub fn new(tints: Vec, space: DeviceNSpace) -> Option { @@ -258,7 +270,7 @@ index 6fb0618..06a3098 100644 } diff --git a/src/graphics/shading_function.rs b/src/graphics/shading_function.rs -index 74c56f3..8aae81a 100644 +index 74c56f3..47b55c5 100644 --- a/src/graphics/shading_function.rs +++ b/src/graphics/shading_function.rs @@ -277,12 +277,12 @@ impl GradientProperties { @@ -290,7 +302,7 @@ index 74c56f3..8aae81a 100644 - Color::Regular(RegularColor::Cmyk(c)) => inks.process = [c.0, c.1, c.2, c.3], - Color::Regular(RegularColor::Luma(l)) => inks.process[3] = 255 - l.0, + Color::Regular(RegularColor::Cmyk(c)) => inks.process = c.to_pdf_color(), -+ Color::Regular(RegularColor::Luma(l)) => inks.process[3] = 1.0 - l.to_pdf_color(), ++ Color::Regular(RegularColor::Luma(l)) => inks.process[3] = l.0.complement(), Color::Regular(RegularColor::Rgb(_)) => return None, Color::Special(SpecialColor::Separation(s)) => { let SeparationColorant::Custom(name) = &s.space.colorant else { diff --git a/vendor/krilla/src/graphics/color.rs b/vendor/krilla/src/graphics/color.rs index 06a30980..581de00e 100644 --- a/vendor/krilla/src/graphics/color.rs +++ b/vendor/krilla/src/graphics/color.rs @@ -108,6 +108,18 @@ impl Tint { pub(crate) fn to_u8(self) -> u8 { (self.0 * 255.0).round() as u8 } + + /// `1 - value`, the black ink of a gray. An 8-bit value `n / 255` gives `(255 - n) / 255`, + /// as the 8-bit constructors write it. Any other value is subtracted from 1 in f64, from + /// the shortest decimal that writes it, so gray 0.96 gives 0.04 rather than 0.04000002. + pub(crate) fn complement(self) -> f32 { + let byte = self.to_u8(); + if Tint::from_u8(byte) == self { + return Tint::from_u8(255 - byte).0; + } + let written = self.0.to_string().parse::().unwrap_or(f64::from(self.0)); + (1.0 - written) as f32 + } } impl PartialEq for Tint { diff --git a/vendor/krilla/src/graphics/shading_function.rs b/vendor/krilla/src/graphics/shading_function.rs index 8aae81a0..47b55c58 100644 --- a/vendor/krilla/src/graphics/shading_function.rs +++ b/vendor/krilla/src/graphics/shading_function.rs @@ -359,7 +359,7 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { }; match color { Color::Regular(RegularColor::Cmyk(c)) => inks.process = c.to_pdf_color(), - Color::Regular(RegularColor::Luma(l)) => inks.process[3] = 1.0 - l.to_pdf_color(), + Color::Regular(RegularColor::Luma(l)) => inks.process[3] = l.0.complement(), Color::Regular(RegularColor::Rgb(_)) => return None, Color::Special(SpecialColor::Separation(s)) => { let SeparationColorant::Custom(name) = &s.space.colorant else {