Merge pull request #474 from dotCooCoo/fix/krilla-gray-black

krilla: a gray stop in a CMYK gradient becomes its exact black (patch 17)
This commit is contained in:
Dylan M. Taylor
2026-11-02 21:57:50 -05:00
committed by GitHub
6 changed files with 56 additions and 16 deletions
+12
View File
@@ -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<String> {
+5 -1
View File
@@ -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`).
+2 -2
View File
@@ -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",
+24 -12
View File
@@ -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::<f64>().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<rgb::Color> {
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<SpecialColor> for Color {
@@ -251,27 +316,43 @@ impl From<SpecialColor> 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<u8>, space: DeviceNSpace) -> Option<Self> {
@@ -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 {
+12
View File
@@ -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::<f64>().unwrap_or(f64::from(self.0));
(1.0 - written) as f32
}
}
impl PartialEq for Tint {
+1 -1
View File
@@ -359,7 +359,7 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
};
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 {