diff --git a/crates/pdf/tests/print_color.rs b/crates/pdf/tests/print_color.rs index c1f2ccc9..b9e35681 100644 --- a/crates/pdf/tests/print_color.rs +++ b/crates/pdf/tests/print_color.rs @@ -933,8 +933,8 @@ fn mesh(patches: Vec) -> backend::paint::Paint { } /// Each Type 6 shading in the file: color space family and colorant names, components a -/// corner (from `/Decode`), and the corner color bytes of each patch. -type MeshSummary = ((String, Vec), usize, Vec>); +/// corner (from `/Decode`), and the 16-bit corner color components of each patch. +type MeshSummary = ((String, Vec), usize, Vec>); fn mesh_shadings(bytes: &[u8]) -> Vec { let pdf = hayro_syntax::Pdf::new(Arc::new(bytes.to_vec())).unwrap(); @@ -946,21 +946,27 @@ fn mesh_shadings(bytes: &[u8]) -> Vec { continue; } assert_eq!(dict.get::(b"BitsPerCoordinate".as_slice()), Some(32)); - assert_eq!(dict.get::(b"BitsPerComponent".as_slice()), Some(8)); + assert_eq!(dict.get::(b"BitsPerComponent".as_slice()), Some(16)); assert_eq!(dict.get::(b"BitsPerFlag".as_slice()), Some(8)); let cs = dict.get::>(b"ColorSpace".as_slice()).unwrap(); let decode = dict.get::>(b"Decode".as_slice()).unwrap().iter::().count(); let components = (decode - 4) / 2; let data = stream.decoded().unwrap(); - let patch_len = 1 + 12 * 8 + 4 * components; + let patch_len = 1 + 12 * 8 + 4 * 2 * components; assert_eq!(data.len() % patch_len, 0, "whole patches of {components} components a corner"); - let corners = data.chunks(patch_len).map(|patch| patch[1 + 12 * 8..].to_vec()).collect(); + let samples = |bytes: &[u8]| -> Vec { bytes.chunks(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect() }; + let corners = data.chunks(patch_len).map(|patch| samples(&patch[1 + 12 * 8..])).collect(); out.push((colorant_names(&cs), components, corners)); } out.sort_by(|a, b| a.0.cmp(&b.0)); out } +/// 8-bit components as the 16-bit components that carry the same value (`v * 257`). +fn wide(components: Vec) -> Vec { + components.into_iter().map(|v| v * 257).collect() +} + fn gold_spot(tint: u8) -> backend::color::Color { use backend::color::separation::{self, SeparationColorant, SeparationSpace}; separation::Color::new(tint, SeparationSpace::new(SeparationColorant::Custom("Gold".to_owned()), gold().into())).into() @@ -981,7 +987,7 @@ fn mesh_mixing_process_and_spot_corners_is_a_device_n_shading() { ] }; let names = |v: &[&str]| v.iter().map(|s| (*s).to_owned()).collect::>(); - let expected = vec![vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0], vec![0, 0, 255, 0, 0, 128, 255, 0, 0, 255, 0, 0]]; + let expected = vec![wide(vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0]), wide(vec![0, 0, 255, 0, 0, 128, 255, 0, 0, 255, 0, 0])]; let opaque = page_with(|surface| fill_rect(surface, 0.0, mesh(patches(1.0)))); assert_eq!(mesh_shadings(&opaque), vec![(("DeviceN".to_owned(), names(&["Cyan", "Black", "Gold"])), 3, expected.clone())]); @@ -989,7 +995,7 @@ fn mesh_mixing_process_and_spot_corners_is_a_device_n_shading() { let translucent = page_with(|surface| fill_rect(surface, 0.0, mesh(patches(0.5)))); let found = mesh_shadings(&translucent); assert_eq!(found.len(), 2, "{found:?}"); - assert_eq!(found[0], (("DeviceGray".to_owned(), vec![]), 1, vec![vec![255; 4], vec![255, 255, 255, 128]])); + assert_eq!(found[0], (("DeviceGray".to_owned(), vec![]), 1, vec![wide(vec![255; 4]), vec![65535, 65535, 65535, 32768]])); assert_eq!(found[1], (("DeviceN".to_owned(), names(&["Cyan", "Black", "Gold"])), 3, expected)); assert_eq!(soft_masks(&translucent).len(), 1); } @@ -1022,10 +1028,90 @@ fn rgb_mesh_is_a_device_rgb_shading() { let rgb = |r, g, b| -> backend::color::Color { backend::color::rgb::Color::new(r, g, b).into() }; let patch = square(10.0, 10.0, 40.0, [rgb(255, 0, 0), rgb(0, 255, 0), rgb(0, 0, 255), rgb(255, 255, 255)], [1.0; 4]); let bytes = page_with(|surface| fill_rect(surface, 0.0, mesh(vec![patch]))); - let corners = vec![vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255]]; + let corners = vec![wide(vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255])]; assert_eq!(mesh_shadings(&bytes), vec![(("DeviceRGB".to_owned(), vec![]), 3, corners)]); } +// Color precision (the `new_f32` constructors). + +/// The first page's content stream as text. +fn content(bytes: &[u8]) -> String { + let pdf = hayro_syntax::Pdf::new(Arc::new(bytes.to_vec())).unwrap(); + let page = pdf.pages().iter().next().unwrap(); + String::from_utf8_lossy(page.page_stream().unwrap()).into_owned() +} + +/// CMYK inks, gray, spot tints and DeviceN tints made with `new_f32` are written as given, so +/// C75 is 0.75 and not 191/255; the 8-bit constructors still write `value / 255`. +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn f32_colors_are_written_without_rounding() { + use backend::color::separation::{self, SeparationColorant, SeparationSpace}; + let gold_space = SeparationSpace::new(SeparationColorant::Custom("Gold".to_owned()), gold().into()); + let space = DeviceNSpace::new([Colorant::Process(Process::Cyan), spot("Gold", gold())]).unwrap(); + let colors: Vec = vec![ + cmyk::Color::new_f32(0.75, 0.0, 1.0, 0.0).into(), + cmyk::Color::new_f32(0.04, 0.04, 0.04, 0.04).into(), + luma::Color::new_f32(0.96).into(), + separation::Color::new_f32(0.75, gold_space).into(), + device_n::Color::new_f32(vec![0.45, 0.04], space).unwrap().into(), + cmyk::Color::new(191, 0, 255, 0).into(), + ]; + let text = content(&device_n_sample(cmyk_settings(), colors).unwrap()); + for op in ["0.75 0 1 0 k", "0.04 0.04 0.04 0.04 k", "0.96 g", "0.75 scn", "0.45 0.04 scn", "0.7490196 0 1 0 k"] { + assert!(text.contains(op), "{op:?} in {text:?}"); + } +} + +/// `new_f32` clamps to 0 to 1 and reads NaN and -0 as 0, so a color equals (and hashes like) +/// the 8-bit color with the same components. +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn f32_colors_clamp_and_compare_by_value() { + use std::collections::HashSet; + let odd = cmyk::Color::new_f32(1.5, -0.2, f32::NAN, -0.0); + assert_eq!(odd, cmyk::Color::new(255, 0, 0, 0)); + assert_eq!(HashSet::from([odd, cmyk::Color::new(255, 0, 0, 0)]).len(), 1); + assert_eq!(luma::Color::new_f32(2.0), luma::Color::white()); + let text = content(&page_with(|surface| fill_rect(surface, 0.0, odd.into()))); + assert!(text.contains("1 0 0 0 k"), "{text:?}"); +} + +/// The colors of a shading's function (`/C0` and `/C1` of the first shading's exponential +/// function). +fn shading_ends(bytes: &[u8]) -> (Vec, Vec) { + let pdf = hayro_syntax::Pdf::new(Arc::new(bytes.to_vec())).unwrap(); + let page = pdf.pages().iter().next().unwrap(); + let patterns = &page.resources().patterns; + let key = patterns.keys().next().unwrap(); + let pattern = patterns.get::>(key.as_ref()).unwrap(); + let shading = pattern.get::>(b"Shading".as_slice()).unwrap(); + let function = shading.get::>(b"Function".as_slice()).unwrap(); + let floats = |key: &[u8]| -> Vec { function.get::>(key).unwrap().iter::().collect() }; + (floats(b"C0"), floats(b"C1")) +} + +/// Gradient stops keep their inks when a spot and process colors are rewritten into one +/// DeviceN space, and mesh corners are written at 16 bits, so K 75% reads 0.749996 rather than +/// 191/255. +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn f32_colors_keep_their_inks_in_shadings() { + use backend::color::separation::{self, SeparationColorant, SeparationSpace}; + let gold_spot: backend::color::Color = + separation::Color::new_f32(0.75, SeparationSpace::new(SeparationColorant::Custom("Gold".to_owned()), gold().into())).into(); + 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([gold_spot, cyan]))); + assert_eq!(shadings(&bytes)[0].0, ("DeviceN".to_owned(), vec!["Cyan".to_owned(), "Gold".to_owned()])); + assert_eq!(shading_ends(&bytes), (vec![0.0, 0.75], vec![0.04, 0.0])); + + let k75 = || -> backend::color::Color { cmyk::Color::new_f32(0.0, 0.0, 0.0, 0.75).into() }; + let patch = square(10.0, 10.0, 40.0, [k75(), k75(), k75(), k75()], [1.0; 4]); + let bytes = page_with(|surface| fill_rect(surface, 0.0, mesh(vec![patch]))); + let corner = [0, 0, 0, 49151]; + assert_eq!(mesh_shadings(&bytes), vec![(("DeviceCMYK".to_owned(), vec![]), 4, vec![corner.repeat(4)])]); +} + 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 6171ba88..6c17cd8c 100644 --- a/vendor/krilla/README.craft.md +++ b/vendor/krilla/README.craft.md @@ -26,6 +26,7 @@ itself omits those texts. Its examples and four CC0 ICC profiles are retained wi | `patches/14-devicen-pdf-version.patch` | DeviceN attributes by PDF version | `graphics/device_n.rs` | | `patches/15-pdfx-conformance.patch` | PDF/X conformance key | `serialize.rs` | | `patches/16-modification-date.patch` | Modification date | `interchange/metadata.rs` | +| `patches/17-color-precision.patch` | Color precision | `graphics/{color,shading_function}.rs` | Each patch applies with `patch -p1` from this directory, in numeric order, to the pristine crate. `SOURCE.json` records, for every retained file, the upstream hash (`upstream_sha256`) @@ -410,3 +411,22 @@ Tests: `crates/pdf/tests/writing.rs` (`writes_pages_metadata_and_selected_versio `the_creation_date_is_the_modification_date_when_none_is_given`) and `crates/prepress/tests/flatten_document.rs` (`a_document_flattens_page_by_page_and_keeps_boxes_metadata_and_shared_fonts`). + +## 17: color precision + +Upstream stores gray, CMYK, Separation and DeviceN components as `u8`, so every ink is written +as a multiple of 1/255: C75 becomes 0.7490196 and C4 M4 Y4 K4 becomes 0.039215688. + +- `cmyk::Color::new_f32`, `luma::Color::new_f32`, `separation::Color::new_f32` and + `device_n::Color::new_f32` take components from 0 to 1 and write them as given. Values + outside 0 to 1 are clamped and NaN is 0. +- The `u8` constructors keep their signatures and write `value / 255`, as upstream does. +- The colors store each component as an `f32` whose bits `Eq` and `Hash` compare; -0 is + stored as 0, so equal components compare equal. `RegularColor::as_rgb` rounds gray to the + nearest 8-bit value. +- 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. +- 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`). diff --git a/vendor/krilla/SOURCE.json b/vendor/krilla/SOURCE.json index c802335c..9af9ac11 100644 --- a/vendor/krilla/SOURCE.json +++ b/vendor/krilla/SOURCE.json @@ -152,7 +152,7 @@ { "path": "src/graphics/color.rs", "upstream_sha256": "e5843d33ef184ab5c458025d1b1dc53df6ee6aaf085595ea14604d080b98f8a4", - "sha256": "553c2c1ce7a7c26dc11a227d70db0e30933e0b0e69d2d6c550e1384c0a3037ec" + "sha256": "5b0de85e0f3eac2905f975f676f1ef5d0eebb99a9a35670fefb419ed36fed6cf" }, { "path": "src/graphics/device_n.rs", @@ -207,7 +207,7 @@ { "path": "src/graphics/shading_function.rs", "upstream_sha256": "0e0fc8bb1a32272457ef560fa80933972d0f5ef5fb7046424c6de48093cfd978", - "sha256": "2831ec0d78a755db9311a16d01afd5c9f714e4d43bdf69b42c0cb291387ca743" + "sha256": "6da45a96a98d32d4ef878fa03ac1fddbccccea2e2d6333d3a0e90ca009061f81" }, { "path": "src/graphics/shading_pattern.rs", diff --git a/vendor/krilla/patches/17-color-precision.patch b/vendor/krilla/patches/17-color-precision.patch new file mode 100644 index 00000000..e0781f3b --- /dev/null +++ b/vendor/krilla/patches/17-color-precision.patch @@ -0,0 +1,411 @@ +diff --git a/src/graphics/color.rs b/src/graphics/color.rs +index 6fb0618..06a3098 100644 +--- a/src/graphics/color.rs ++++ b/src/graphics/color.rs +@@ -69,7 +69,7 @@ + //! [`SerializeSettings`]: crate::SerializeSettings + + use std::fmt::Debug; +-use std::hash::Hash; ++use std::hash::{Hash, Hasher}; + + use crate::configure::ValidationError; + use crate::graphics::icc::ICCBasedColorSpace; +@@ -82,6 +82,48 @@ pub(crate) const DEVICE_GRAY: &str = "DeviceGray"; + /// The PDF name for the device CMYK color space. + pub(crate) const DEVICE_CMYK: &str = "DeviceCMYK"; + ++/// A color component from 0 to 1: a gray level, a process ink or a spot tint. ++/// ++/// `new` clamps the value to 0 to 1 and turns NaN and -0 into 0, so equal components have equal ++/// bits; `Eq` and `Hash` compare the bits. ++#[derive(Debug, Clone, Copy)] ++pub(crate) struct Tint(f32); ++ ++impl Tint { ++ pub(crate) fn new(value: f32) -> Self { ++ // `max` returns the other operand for NaN; adding 0 turns -0 into 0. ++ Tint(value.max(0.0).min(1.0) + 0.0) ++ } ++ ++ /// The component `value / 255`, as the 8-bit constructors give it. ++ pub(crate) fn from_u8(value: u8) -> Self { ++ Tint(f32::from(value) / 255.0) ++ } ++ ++ pub(crate) fn get(self) -> f32 { ++ self.0 ++ } ++ ++ /// The nearest 8-bit value. ++ pub(crate) fn to_u8(self) -> u8 { ++ (self.0 * 255.0).round() as u8 ++ } ++} ++ ++impl PartialEq for Tint { ++ fn eq(&self, other: &Self) -> bool { ++ self.0.to_bits() == other.0.to_bits() ++ } ++} ++ ++impl Eq for Tint {} ++ ++impl Hash for Tint { ++ fn hash(&self, state: &mut H) { ++ self.0.to_bits().hash(state); ++ } ++} ++ + /// A wrapper for storing colors from different color spaces. + #[derive(Debug, Hash, Eq, PartialEq, Clone)] + pub enum Color { +@@ -160,7 +202,10 @@ impl RegularColor { + pub(crate) fn as_rgb(self) -> Option { + Some(match self { + Self::Rgb(r) => r, +- Self::Luma(l) => rgb::Color::new(l.0, l.0, l.0), ++ Self::Luma(l) => { ++ let v = l.0.to_u8(); ++ rgb::Color::new(v, v, v) ++ } + Self::Cmyk(_) => return None, + }) + } +@@ -183,16 +228,24 @@ impl SpecialColor { + + /// Gray-scale colors. + pub mod luma { +- use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColor, RegularColorSpace}; ++ use crate::color::{ ++ CieBasedColorSpace, DeviceColorSpace, RegularColor, RegularColorSpace, Tint, ++ }; + + /// A luma color. + #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] +- pub struct Color(pub(crate) u8); ++ pub struct Color(pub(crate) Tint); + + impl Color { + /// Create a new luma color. + pub fn new(lightness: u8) -> Color { +- Color(lightness) ++ Color(Tint::from_u8(lightness)) ++ } ++ ++ /// Create a luma color from a lightness from 0 (black) to 1 (white), written to the ++ /// PDF without rounding. Values outside 0 to 1 are clamped, and NaN is 0. ++ pub fn new_f32(lightness: f32) -> Color { ++ Color(Tint::new(lightness)) + } + + /// Create a black luma color. +@@ -206,7 +259,7 @@ pub mod luma { + } + + pub(crate) fn to_pdf_color(self) -> f32 { +- self.0 as f32 / 255.0 ++ self.0.get() + } + } + +@@ -251,27 +304,43 @@ impl From for Color { + + /// CMYK colors. + pub mod cmyk { +- use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace}; ++ use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace, Tint}; + use crate::graphics::icc::ICCBasedColorSpace; + use crate::SerializeSettings; + + /// A CMYK color. + #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] +- pub struct Color(pub(crate) u8, pub(crate) u8, pub(crate) u8, pub(crate) u8); ++ pub struct Color( ++ pub(crate) Tint, ++ pub(crate) Tint, ++ pub(crate) Tint, ++ pub(crate) Tint, ++ ); + + impl Color { + /// Create a new CMYK color. + pub fn new(cyan: u8, magenta: u8, yellow: u8, black: u8) -> Color { +- Color(cyan, magenta, yellow, black) ++ Color( ++ Tint::from_u8(cyan), ++ Tint::from_u8(magenta), ++ Tint::from_u8(yellow), ++ Tint::from_u8(black), ++ ) ++ } ++ ++ /// Create a CMYK color from inks from 0 to 1, written to the PDF without rounding. ++ /// Values outside 0 to 1 are clamped, and NaN is 0. ++ pub fn new_f32(cyan: f32, magenta: f32, yellow: f32, black: f32) -> Color { ++ Color( ++ Tint::new(cyan), ++ Tint::new(magenta), ++ Tint::new(yellow), ++ Tint::new(black), ++ ) + } + + pub(crate) fn to_pdf_color(self) -> [f32; 4] { +- [ +- self.0 as f32 / 255.0, +- self.1 as f32 / 255.0, +- self.2 as f32 / 255.0, +- self.3 as f32 / 255.0, +- ] ++ [self.0.get(), self.1.get(), self.2.get(), self.3.get()] + } + } + +@@ -390,23 +459,35 @@ pub mod rgb { + + /// Separation (spot) colors. + pub mod separation { +- use crate::color::RegularColor; ++ use crate::color::{RegularColor, Tint}; + + /// A spot color. + #[derive(Debug, Hash, Eq, PartialEq, Clone)] + pub struct Color { +- pub(crate) tint: u8, ++ pub(crate) tint: Tint, + pub(crate) space: SeparationSpace, + } + + impl Color { + /// Create a new spot color. + pub fn new(tint: u8, space: SeparationSpace) -> Self { +- Self { tint, space } ++ Self { ++ tint: Tint::from_u8(tint), ++ space, ++ } ++ } ++ ++ /// Create a spot color with a tint from 0 to 1, written to the PDF without rounding. ++ /// A tint outside 0 to 1 is clamped, and NaN is 0. ++ pub fn new_f32(tint: f32, space: SeparationSpace) -> Self { ++ Self { ++ tint: Tint::new(tint), ++ space, ++ } + } + + pub(crate) fn to_pdf_color(&self) -> f32 { +- self.tint as f32 / 255.0 ++ self.tint.get() + } + + pub(crate) fn color_space(&self) -> SeparationSpace { +@@ -498,7 +579,7 @@ pub mod separation { + pub mod device_n { + use std::sync::Arc; + +- use crate::color::cmyk; ++ use crate::color::{cmyk, Tint}; + + /// A process colorant. + #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] +@@ -656,7 +737,7 @@ pub mod device_n { + /// A DeviceN color: one tint per colorant of its space. + #[derive(Debug, Hash, Eq, PartialEq, Clone)] + pub struct Color { +- pub(crate) tints: Vec, ++ pub(crate) tints: Vec, + pub(crate) space: DeviceNSpace, + } + +@@ -666,20 +747,30 @@ pub mod device_n { + /// + /// Returns `None` unless there is one tint per colorant. + pub fn new(tints: Vec, space: DeviceNSpace) -> Option { ++ Self::new_f32( ++ tints.into_iter().map(|t| Tint::from_u8(t).get()).collect(), ++ space, ++ ) ++ } ++ ++ /// Create a DeviceN color with tints from 0 (no ink) to 1 (full ink), in the order ++ /// of the space's colorants, written to the PDF without rounding. Tints outside 0 ++ /// to 1 are clamped, and NaN is 0. ++ /// ++ /// Returns `None` unless there is one tint per colorant. ++ pub fn new_f32(tints: Vec, space: DeviceNSpace) -> Option { ++ let tints: Vec = tints.into_iter().map(Tint::new).collect(); + (tints.len() == space.colorants.len()).then_some(Self { tints, space }) + } + + pub(crate) fn to_pdf_color(&self) -> Vec { +- self.tints.iter().map(|t| *t as f32 / 255.0).collect() ++ self.tints.iter().map(|t| t.get()).collect() + } + + /// The color in the alternate CMYK space. + pub(crate) fn alternate(&self) -> cmyk::Color { +- let [c, m, y, k] = self +- .space +- .alternate(&self.to_pdf_color()) +- .map(|v| (v * 255.0).round().clamp(0.0, 255.0) as u8); +- cmyk::Color::new(c, m, y, k) ++ let [c, m, y, k] = self.space.alternate(&self.to_pdf_color()); ++ cmyk::Color::new_f32(c, m, y, k) + } + } + +diff --git a/src/graphics/shading_function.rs b/src/graphics/shading_function.rs +index 74c56f3..8aae81a 100644 +--- a/src/graphics/shading_function.rs ++++ b/src/graphics/shading_function.rs +@@ -277,12 +277,12 @@ impl GradientProperties { + } + } + +-/// The inks of one gradient color: process tints and spot tints (0–255). ++/// The inks of one gradient color: process tints and spot tints (0–1). + struct Inks { +- process: [u8; 4], ++ process: [f32; 4], + /// Process colorants a DeviceN color names, even at zero tint. + named_process: [bool; 4], +- spots: Vec<(String, u8)>, ++ spots: Vec<(String, f32)>, + } + + /// A spot named by a gradient color: the Separation space a stop used, and the CMYK +@@ -353,13 +353,13 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { + let mut all_inks = Vec::with_capacity(colors.len()); + for color in colors { + let mut inks = Inks { +- process: [0; 4], ++ process: [0.0; 4], + named_process: [false; 4], + spots: Vec::new(), + }; + match color { +- 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::Rgb(_)) => return None, + Color::Special(SpecialColor::Separation(s)) => { + let SeparationColorant::Custom(name) = &s.space.colorant else { +@@ -370,19 +370,19 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { + _ => None, + }; + add_spot(&mut spot_inks, name, Some(&s.space), alternate); +- inks.spots.push((name.clone(), s.tint)); ++ inks.spots.push((name.clone(), s.tint.get())); + } + Color::Special(SpecialColor::DeviceN(c)) => { + for (colorant, tint) in c.space.colorants().iter().zip(&c.tints) { + match colorant { + Colorant::Process(p) => { + let i = Process::ALL.iter().position(|q| q == p).unwrap_or(0); +- inks.process[i] = *tint; ++ inks.process[i] = tint.get(); + inks.named_process[i] = true; + } + Colorant::Spot { name, alternate } => { + add_spot(&mut spot_inks, name, None, Some(*alternate)); +- inks.spots.push((name.clone(), *tint)); ++ inks.spots.push((name.clone(), tint.get())); + } + } + } +@@ -395,14 +395,14 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { + .filter(|&i| { + all_inks + .iter() +- .any(|inks| inks.process[i] > 0 || inks.named_process[i]) ++ .any(|inks| inks.process[i] > 0.0 || inks.named_process[i]) + }) + .collect(); + let spot_tint = |inks: &Inks, name: &str| { + inks.spots + .iter() + .find(|(n, _)| n == name) +- .map_or(0, |(_, t)| *t) ++ .map_or(0.0, |(_, t)| *t) + }; + + match (process.as_slice(), spot_inks.as_slice()) { +@@ -411,7 +411,7 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { + .iter() + .map(|inks| { + let [c, m, y, k] = inks.process; +- cmyk::Color::new(c, m, y, k).into() ++ cmyk::Color::new_f32(c, m, y, k).into() + }) + .collect(), + ), +@@ -428,7 +428,8 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { + all_inks + .iter() + .map(|inks| { +- separation::Color::new(spot_tint(inks, &spot.name), space.clone()).into() ++ separation::Color::new_f32(spot_tint(inks, &spot.name), space.clone()) ++ .into() + }) + .collect(), + ) +@@ -448,9 +449,9 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { + all_inks + .iter() + .map(|inks| { +- let mut tints: Vec = process.iter().map(|&i| inks.process[i]).collect(); ++ let mut tints: Vec = process.iter().map(|&i| inks.process[i]).collect(); + tints.extend(spot_inks.iter().map(|s| spot_tint(inks, &s.name))); +- device_n::Color::new(tints, space.clone()).map(Color::from) ++ device_n::Color::new_f32(tints, space.clone()).map(Color::from) + }) + .collect() + } +@@ -547,7 +548,7 @@ fn shading_color_space(sc: &mut SerializeContext, color: Color, use_opacities: b + + /// A mesh gradient as a Type 6 (Coons patch mesh) shading: per patch an 8-bit edge flag (0, no + /// shared edge), the twelve points as 32-bit coordinates over the mesh's bounds (`Decode`), and +-/// the four corner colors (with `use_opacities`, the opacities as gray) at 8 bits a component, ++/// the four corner colors (with `use_opacities`, the opacities as gray) at 16 bits a component, + /// Flate-compressed. 32-bit coordinates keep the patches where they are to well under a printer + /// dot, as Illustrator writes its meshes. + fn serialize_mesh_shading( +@@ -584,8 +585,8 @@ fn serialize_mesh_shading( + * f64::from(u32::MAX)) + .round() as u32 + }; +- let byte = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; +- let mut data = Vec::with_capacity(mesh.patches.len() * (1 + 12 * 8 + 4 * components)); ++ let component = |v: f32| ((v.clamp(0.0, 1.0) * 65535.0).round() as u16).to_be_bytes(); ++ let mut data = Vec::with_capacity(mesh.patches.len() * (1 + 12 * 8 + 4 * 2 * components)); + for patch in &mesh.patches { + data.push(0u8); + for (x, y) in &patch.points { +@@ -594,12 +595,12 @@ fn serialize_mesh_shading( + } + for (color, opacity) in patch.colors.iter().zip(patch.opacities.iter()) { + if use_opacities { +- data.push(byte(opacity.get())); ++ data.extend(component(opacity.get())); + } else { + let values = color.to_pdf_color(); + // Mixed spaces fail the document (`mixed_mesh_colors`); still never misread bytes. + for i in 0..components { +- data.push(byte(values.get(i).copied().unwrap_or(0.0))); ++ data.extend(component(values.get(i).copied().unwrap_or(0.0))); + } + } + } +@@ -611,7 +612,7 @@ fn serialize_mesh_shading( + shading.shading_type(StreamShadingType::CoonsPatch); + set_colorspace(cs, shading.deref_mut().deref_mut()); + shading.bits_per_coordinate(32); +- shading.bits_per_component(8); ++ shading.bits_per_component(16); + shading.bits_per_flag(8); + let mut decode = vec![x0, x1, y0, y1]; + for _ in 0..components { diff --git a/vendor/krilla/src/graphics/color.rs b/vendor/krilla/src/graphics/color.rs index 6fb06180..06a30980 100644 --- a/vendor/krilla/src/graphics/color.rs +++ b/vendor/krilla/src/graphics/color.rs @@ -69,7 +69,7 @@ //! [`SerializeSettings`]: crate::SerializeSettings use std::fmt::Debug; -use std::hash::Hash; +use std::hash::{Hash, Hasher}; use crate::configure::ValidationError; use crate::graphics::icc::ICCBasedColorSpace; @@ -82,6 +82,48 @@ pub(crate) const DEVICE_GRAY: &str = "DeviceGray"; /// The PDF name for the device CMYK color space. pub(crate) const DEVICE_CMYK: &str = "DeviceCMYK"; +/// A color component from 0 to 1: a gray level, a process ink or a spot tint. +/// +/// `new` clamps the value to 0 to 1 and turns NaN and -0 into 0, so equal components have equal +/// bits; `Eq` and `Hash` compare the bits. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Tint(f32); + +impl Tint { + pub(crate) fn new(value: f32) -> Self { + // `max` returns the other operand for NaN; adding 0 turns -0 into 0. + Tint(value.max(0.0).min(1.0) + 0.0) + } + + /// The component `value / 255`, as the 8-bit constructors give it. + pub(crate) fn from_u8(value: u8) -> Self { + Tint(f32::from(value) / 255.0) + } + + pub(crate) fn get(self) -> f32 { + self.0 + } + + /// The nearest 8-bit value. + pub(crate) fn to_u8(self) -> u8 { + (self.0 * 255.0).round() as u8 + } +} + +impl PartialEq for Tint { + fn eq(&self, other: &Self) -> bool { + self.0.to_bits() == other.0.to_bits() + } +} + +impl Eq for Tint {} + +impl Hash for Tint { + fn hash(&self, state: &mut H) { + self.0.to_bits().hash(state); + } +} + /// A wrapper for storing colors from different color spaces. #[derive(Debug, Hash, Eq, PartialEq, Clone)] pub enum Color { @@ -160,7 +202,10 @@ impl RegularColor { pub(crate) fn as_rgb(self) -> Option { Some(match self { Self::Rgb(r) => r, - Self::Luma(l) => rgb::Color::new(l.0, l.0, l.0), + Self::Luma(l) => { + let v = l.0.to_u8(); + rgb::Color::new(v, v, v) + } Self::Cmyk(_) => return None, }) } @@ -183,16 +228,24 @@ impl SpecialColor { /// Gray-scale colors. pub mod luma { - use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColor, RegularColorSpace}; + use crate::color::{ + CieBasedColorSpace, DeviceColorSpace, RegularColor, RegularColorSpace, Tint, + }; /// A luma color. #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] - pub struct Color(pub(crate) u8); + pub struct Color(pub(crate) Tint); impl Color { /// Create a new luma color. pub fn new(lightness: u8) -> Color { - Color(lightness) + Color(Tint::from_u8(lightness)) + } + + /// Create a luma color from a lightness from 0 (black) to 1 (white), written to the + /// PDF without rounding. Values outside 0 to 1 are clamped, and NaN is 0. + pub fn new_f32(lightness: f32) -> Color { + Color(Tint::new(lightness)) } /// Create a black luma color. @@ -206,7 +259,7 @@ pub mod luma { } pub(crate) fn to_pdf_color(self) -> f32 { - self.0 as f32 / 255.0 + self.0.get() } } @@ -251,27 +304,43 @@ impl From for Color { /// CMYK colors. pub mod cmyk { - use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace}; + use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace, Tint}; use crate::graphics::icc::ICCBasedColorSpace; use crate::SerializeSettings; /// A CMYK color. #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] - pub struct Color(pub(crate) u8, pub(crate) u8, pub(crate) u8, pub(crate) u8); + pub struct Color( + pub(crate) Tint, + pub(crate) Tint, + pub(crate) Tint, + pub(crate) Tint, + ); impl Color { /// Create a new CMYK color. pub fn new(cyan: u8, magenta: u8, yellow: u8, black: u8) -> Color { - Color(cyan, magenta, yellow, black) + Color( + Tint::from_u8(cyan), + Tint::from_u8(magenta), + Tint::from_u8(yellow), + Tint::from_u8(black), + ) + } + + /// Create a CMYK color from inks from 0 to 1, written to the PDF without rounding. + /// Values outside 0 to 1 are clamped, and NaN is 0. + pub fn new_f32(cyan: f32, magenta: f32, yellow: f32, black: f32) -> Color { + Color( + Tint::new(cyan), + Tint::new(magenta), + Tint::new(yellow), + Tint::new(black), + ) } pub(crate) fn to_pdf_color(self) -> [f32; 4] { - [ - self.0 as f32 / 255.0, - self.1 as f32 / 255.0, - self.2 as f32 / 255.0, - self.3 as f32 / 255.0, - ] + [self.0.get(), self.1.get(), self.2.get(), self.3.get()] } } @@ -390,23 +459,35 @@ pub mod rgb { /// Separation (spot) colors. pub mod separation { - use crate::color::RegularColor; + use crate::color::{RegularColor, Tint}; /// A spot color. #[derive(Debug, Hash, Eq, PartialEq, Clone)] pub struct Color { - pub(crate) tint: u8, + pub(crate) tint: Tint, pub(crate) space: SeparationSpace, } impl Color { /// Create a new spot color. pub fn new(tint: u8, space: SeparationSpace) -> Self { - Self { tint, space } + Self { + tint: Tint::from_u8(tint), + space, + } + } + + /// Create a spot color with a tint from 0 to 1, written to the PDF without rounding. + /// A tint outside 0 to 1 is clamped, and NaN is 0. + pub fn new_f32(tint: f32, space: SeparationSpace) -> Self { + Self { + tint: Tint::new(tint), + space, + } } pub(crate) fn to_pdf_color(&self) -> f32 { - self.tint as f32 / 255.0 + self.tint.get() } pub(crate) fn color_space(&self) -> SeparationSpace { @@ -498,7 +579,7 @@ pub mod separation { pub mod device_n { use std::sync::Arc; - use crate::color::cmyk; + use crate::color::{cmyk, Tint}; /// A process colorant. #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] @@ -656,7 +737,7 @@ pub mod device_n { /// A DeviceN color: one tint per colorant of its space. #[derive(Debug, Hash, Eq, PartialEq, Clone)] pub struct Color { - pub(crate) tints: Vec, + pub(crate) tints: Vec, pub(crate) space: DeviceNSpace, } @@ -666,20 +747,30 @@ pub mod device_n { /// /// Returns `None` unless there is one tint per colorant. pub fn new(tints: Vec, space: DeviceNSpace) -> Option { + Self::new_f32( + tints.into_iter().map(|t| Tint::from_u8(t).get()).collect(), + space, + ) + } + + /// Create a DeviceN color with tints from 0 (no ink) to 1 (full ink), in the order + /// of the space's colorants, written to the PDF without rounding. Tints outside 0 + /// to 1 are clamped, and NaN is 0. + /// + /// Returns `None` unless there is one tint per colorant. + pub fn new_f32(tints: Vec, space: DeviceNSpace) -> Option { + let tints: Vec = tints.into_iter().map(Tint::new).collect(); (tints.len() == space.colorants.len()).then_some(Self { tints, space }) } pub(crate) fn to_pdf_color(&self) -> Vec { - self.tints.iter().map(|t| *t as f32 / 255.0).collect() + self.tints.iter().map(|t| t.get()).collect() } /// The color in the alternate CMYK space. pub(crate) fn alternate(&self) -> cmyk::Color { - let [c, m, y, k] = self - .space - .alternate(&self.to_pdf_color()) - .map(|v| (v * 255.0).round().clamp(0.0, 255.0) as u8); - cmyk::Color::new(c, m, y, k) + let [c, m, y, k] = self.space.alternate(&self.to_pdf_color()); + cmyk::Color::new_f32(c, m, y, k) } } diff --git a/vendor/krilla/src/graphics/shading_function.rs b/vendor/krilla/src/graphics/shading_function.rs index 74c56f36..8aae81a0 100644 --- a/vendor/krilla/src/graphics/shading_function.rs +++ b/vendor/krilla/src/graphics/shading_function.rs @@ -277,12 +277,12 @@ impl GradientProperties { } } -/// The inks of one gradient color: process tints and spot tints (0–255). +/// The inks of one gradient color: process tints and spot tints (0–1). struct Inks { - process: [u8; 4], + process: [f32; 4], /// Process colorants a DeviceN color names, even at zero tint. named_process: [bool; 4], - spots: Vec<(String, u8)>, + spots: Vec<(String, f32)>, } /// A spot named by a gradient color: the Separation space a stop used, and the CMYK @@ -353,13 +353,13 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { let mut all_inks = Vec::with_capacity(colors.len()); for color in colors { let mut inks = Inks { - process: [0; 4], + process: [0.0; 4], named_process: [false; 4], spots: Vec::new(), }; match color { - 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::Rgb(_)) => return None, Color::Special(SpecialColor::Separation(s)) => { let SeparationColorant::Custom(name) = &s.space.colorant else { @@ -370,19 +370,19 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { _ => None, }; add_spot(&mut spot_inks, name, Some(&s.space), alternate); - inks.spots.push((name.clone(), s.tint)); + inks.spots.push((name.clone(), s.tint.get())); } Color::Special(SpecialColor::DeviceN(c)) => { for (colorant, tint) in c.space.colorants().iter().zip(&c.tints) { match colorant { Colorant::Process(p) => { let i = Process::ALL.iter().position(|q| q == p).unwrap_or(0); - inks.process[i] = *tint; + inks.process[i] = tint.get(); inks.named_process[i] = true; } Colorant::Spot { name, alternate } => { add_spot(&mut spot_inks, name, None, Some(*alternate)); - inks.spots.push((name.clone(), *tint)); + inks.spots.push((name.clone(), tint.get())); } } } @@ -395,14 +395,14 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { .filter(|&i| { all_inks .iter() - .any(|inks| inks.process[i] > 0 || inks.named_process[i]) + .any(|inks| inks.process[i] > 0.0 || inks.named_process[i]) }) .collect(); let spot_tint = |inks: &Inks, name: &str| { inks.spots .iter() .find(|(n, _)| n == name) - .map_or(0, |(_, t)| *t) + .map_or(0.0, |(_, t)| *t) }; match (process.as_slice(), spot_inks.as_slice()) { @@ -411,7 +411,7 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { .iter() .map(|inks| { let [c, m, y, k] = inks.process; - cmyk::Color::new(c, m, y, k).into() + cmyk::Color::new_f32(c, m, y, k).into() }) .collect(), ), @@ -428,7 +428,8 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { all_inks .iter() .map(|inks| { - separation::Color::new(spot_tint(inks, &spot.name), space.clone()).into() + separation::Color::new_f32(spot_tint(inks, &spot.name), space.clone()) + .into() }) .collect(), ) @@ -448,9 +449,9 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option> { all_inks .iter() .map(|inks| { - let mut tints: Vec = process.iter().map(|&i| inks.process[i]).collect(); + let mut tints: Vec = process.iter().map(|&i| inks.process[i]).collect(); tints.extend(spot_inks.iter().map(|s| spot_tint(inks, &s.name))); - device_n::Color::new(tints, space.clone()).map(Color::from) + device_n::Color::new_f32(tints, space.clone()).map(Color::from) }) .collect() } @@ -547,7 +548,7 @@ fn shading_color_space(sc: &mut SerializeContext, color: Color, use_opacities: b /// A mesh gradient as a Type 6 (Coons patch mesh) shading: per patch an 8-bit edge flag (0, no /// shared edge), the twelve points as 32-bit coordinates over the mesh's bounds (`Decode`), and -/// the four corner colors (with `use_opacities`, the opacities as gray) at 8 bits a component, +/// the four corner colors (with `use_opacities`, the opacities as gray) at 16 bits a component, /// Flate-compressed. 32-bit coordinates keep the patches where they are to well under a printer /// dot, as Illustrator writes its meshes. fn serialize_mesh_shading( @@ -584,8 +585,8 @@ fn serialize_mesh_shading( * f64::from(u32::MAX)) .round() as u32 }; - let byte = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; - let mut data = Vec::with_capacity(mesh.patches.len() * (1 + 12 * 8 + 4 * components)); + let component = |v: f32| ((v.clamp(0.0, 1.0) * 65535.0).round() as u16).to_be_bytes(); + let mut data = Vec::with_capacity(mesh.patches.len() * (1 + 12 * 8 + 4 * 2 * components)); for patch in &mesh.patches { data.push(0u8); for (x, y) in &patch.points { @@ -594,12 +595,12 @@ fn serialize_mesh_shading( } for (color, opacity) in patch.colors.iter().zip(patch.opacities.iter()) { if use_opacities { - data.push(byte(opacity.get())); + data.extend(component(opacity.get())); } else { let values = color.to_pdf_color(); // Mixed spaces fail the document (`mixed_mesh_colors`); still never misread bytes. for i in 0..components { - data.push(byte(values.get(i).copied().unwrap_or(0.0))); + data.extend(component(values.get(i).copied().unwrap_or(0.0))); } } } @@ -611,7 +612,7 @@ fn serialize_mesh_shading( shading.shading_type(StreamShadingType::CoonsPatch); set_colorspace(cs, shading.deref_mut().deref_mut()); shading.bits_per_coordinate(32); - shading.bits_per_component(8); + shading.bits_per_component(16); shading.bits_per_flag(8); let mut decode = vec![x0, x1, y0, y1]; for _ in 0..components {