diff --git a/crates/pdf/tests/print_color.rs b/crates/pdf/tests/print_color.rs index 8a3b92b7..c70cc975 100644 --- a/crates/pdf/tests/print_color.rs +++ b/crates/pdf/tests/print_color.rs @@ -16,7 +16,7 @@ use backend::image::{BitsPerComponent, CustomImage, Image, ImageColorspace}; use backend::mask::{Mask, MaskType, TransferFunction}; use backend::num::NormalizedF32; use backend::page::PageSettings; -use backend::paint::{Fill, LinearGradient, SpreadMethod, Stop, SweepGradient}; +use backend::paint::{Fill, LinearGradient, MeshGradient, MeshPatch, SpreadMethod, Stop, SweepGradient}; use backend::surface::Surface; use backend::{GroupColorSpace, SerializeSettings, Trapped}; use craft_pdf::write::print::{self, PrintColor}; @@ -810,6 +810,120 @@ fn device_n_shading_keeps_a_spot_to_paper_as_separation() { } } +// Mesh gradients (`paint::MeshGradient`, Type 6 shadings). + +/// A square Coons patch with straight edges, corners clockwise from the top left at (x, y). +fn square(x: f32, y: f32, size: f32, colors: [backend::color::Color; 4], opacities: [f32; 4]) -> MeshPatch { + let corners = [(x, y), (x + size, y), (x + size, y + size), (x, y + size)]; + let mut points = [(0.0, 0.0); 12]; + for (i, &(ax, ay)) in corners.iter().enumerate() { + let (bx, by) = corners[(i + 1) % 4]; + for step in 0..3 { + let t = step as f32 / 3.0; + points[3 * i + step] = (ax + (bx - ax) * t, ay + (by - ay) * t); + } + } + MeshPatch { points, colors, opacities: opacities.map(|o| NormalizedF32::new(o).unwrap()) } +} + +fn mesh(patches: Vec) -> backend::paint::Paint { + MeshGradient { patches, transform: Transform::identity(), anti_alias: false }.into() +} + +/// 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>); + +fn mesh_shadings(bytes: &[u8]) -> Vec { + let pdf = hayro_syntax::Pdf::new(Arc::new(bytes.to_vec())).unwrap(); + let mut out = Vec::new(); + for object in pdf.objects() { + let Object::Stream(stream) = object else { continue }; + let dict = stream.dict(); + if dict.get::(b"ShadingType".as_slice()) != Some(6) { + 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"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; + 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(); + out.push((colorant_names(&cs), components, corners)); + } + out.sort_by(|a, b| a.0.cmp(&b.0)); + out +} + +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() +} + +/// Mesh corner colors follow the gradient stop rule across every patch: CMYK, gray and a +/// spot become one DeviceN space with process colorants first, so a later patch's spot is +/// written as itself. Corner opacities below 1 add the same mesh in gray as a soft mask. +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn mesh_mixing_process_and_spot_corners_is_a_device_n_shading() { + let cyan = || -> backend::color::Color { cmyk::Color::new(255, 0, 0, 0).into() }; + let paper = || -> backend::color::Color { cmyk::Color::new(0, 0, 0, 0).into() }; + let patches = |opacity: f32| { + vec![ + square(10.0, 10.0, 40.0, [cyan(), cyan(), luma::Color::black().into(), paper()], [1.0; 4]), + square(50.0, 10.0, 40.0, [gold_spot(255), gold_spot(128), cyan(), cyan()], [1.0, 1.0, 1.0, opacity]), + ] + }; + 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 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())]); + + 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[1], (("DeviceN".to_owned(), names(&["Cyan", "Black", "Gold"])), 3, expected)); + assert_eq!(soft_masks(&translucent).len(), 1); +} + +/// RGB corners can't share a space with CMYK ones: the document fails instead of writing +/// one patch's colors in the other's space. +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn mesh_mixing_rgb_and_cmyk_corners_is_refused() { + let red: backend::color::Color = backend::color::rgb::Color::new(255, 0, 0).into(); + let cyan: backend::color::Color = cmyk::Color::new(255, 0, 0, 0).into(); + let patches = vec![ + square(10.0, 10.0, 40.0, [red.clone(), red.clone(), red.clone(), red.clone()], [1.0; 4]), + square(50.0, 10.0, 40.0, [cyan.clone(), cyan.clone(), red.clone(), red], [1.0; 4]), + ]; + let mut writer = Writer::new(cmyk_settings(), Metadata::default()); + let mut page = writer.document_mut().start_page_with(PageSettings::new(Size::from_wh(300.0, 60.0).unwrap())); + let mut surface = page.surface(); + fill_rect(&mut surface, 0.0, mesh(patches)); + surface.finish(); + page.finish(); + let Err(WriteError::Serialization(error)) = writer.finish() else { panic!("a mixed RGB and CMYK mesh was written") }; + assert!(matches!(*error, backend::error::KrillaError::MixedMeshColorSpaces(_)), "{error:?}"); +} + +/// A mesh in one color space is written in that space, three bytes a corner for RGB. +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +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]]; + assert_eq!(mesh_shadings(&bytes), vec![(("DeviceRGB".to_owned(), vec![]), 3, corners)]); +} + 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 313dac6a..40b8dae1 100644 --- a/vendor/krilla/README.craft.md +++ b/vendor/krilla/README.craft.md @@ -19,7 +19,7 @@ itself omits those texts. Its examples and four CC0 ICC profiles are retained wi | `patches/07-device-n-images-shadings.patch` | DeviceN images and shadings | `graphics/{image,paint,shading_function}.rs` | | `patches/08-print-output-intent.patch` | Print output intent and PDF/X markers | `chunk_container.rs`, `interchange/metadata.rs`, `lib.rs`, `serialize.rs` | | `patches/09-rendition-class.patch` | Master rendition in XMP | `chunk_container.rs` | -| `patches/10-mesh-gradients.patch` | Mesh gradients | `content.rs`, `graphics/{mask,paint,shading_function}.rs`, `surface.rs` | +| `patches/10-mesh-gradients.patch` | Mesh gradients | `content.rs`, `error.rs`, `graphics/{mask,paint,shading_function}.rs`, `serialize.rs`, `surface.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`) @@ -250,17 +250,25 @@ master rendition, so the patch writes `default`. From storytold/craft-libs#184 by @fastmikey99. -Krilla 0.8.2 paints only flat colour, linear, radial and sweep gradients and tiling patterns, so -an app writing a gradient mesh had to cut each patch into flat-colour pieces: a VectorCraft +Krilla 0.8.2 paints only flat color, linear, radial and sweep gradients and tiling patterns, so +an app writing a gradient mesh had to cut each patch into flat-color pieces: a VectorCraft document holding an Illustrator file's 22,850 mesh patches became a 197 MB PDF with stepped -blends. The patch adds `paint::MeshGradient` (and `MeshPatch`): Coons patches with a colour and +blends. The patch adds `paint::MeshGradient` (and `MeshPatch`): Coons patches with a color and an opacity at each corner, written as one PDF Type 6 shading (ISO 32000-1, 8.7.4.5.7) through pdf-writer's `StreamShading`, so the viewer or RIP blends them at its own resolution. Each patch is an 8-bit flag (no shared edge), its twelve points as 32-bit coordinates over the mesh's -bounds (`Decode`) and its four corner colours at 8 bits a component, Flate-compressed. It goes +bounds (`Decode`) and its four corner colors at 8 bits a component, Flate-compressed. It goes through the same path as the other gradients (`GradientProperties::MeshGradient`): a shading pattern for the fill, and for corner opacities below 1 a luminosity soft mask holding the same -mesh in gray (`use_opacities`), as cairo and Illustrator write transparent meshes. Every patch's -colours must share one colour space, as gradient stops must. Changed files: `src/content.rs`, -`src/surface.rs`, `src/graphics/mask.rs`, `src/graphics/paint.rs`, -`src/graphics/shading_function.rs`. +mesh in gray (`use_opacities`), as cairo and Illustrator write transparent meshes. + +The corner colors of every patch go through `common_color_space` from patch 07, the rule +gradient stops follow, before the shading is written: process colors alone become +DeviceCMYK, one spot alone a Separation space, and any other mix of inks a DeviceN space with +the process colorants in CMYK order, then the spots. A mesh mixing CMYK and a spot is a +DeviceN mesh with one component per colorant at each corner. Colors the rule can't bring into +one space (RGB with CMYK, `All` or `None` with anything else) would be written with another +space's component count, so the document fails with `KrillaError::MixedMeshColorSpaces` +instead. The soft mask is written in gray from the opacities and is unaffected. + +Tests: `crates/pdf/tests/print_color.rs` (`mesh_*`, `rgb_mesh_*`) and the plate-test scene S17. diff --git a/vendor/krilla/SOURCE.json b/vendor/krilla/SOURCE.json index f38eadea..789facb2 100644 --- a/vendor/krilla/SOURCE.json +++ b/vendor/krilla/SOURCE.json @@ -137,7 +137,7 @@ { "path": "src/error.rs", "upstream_sha256": "656b260bd99d250535099f17f2db8e6e6135e72c74fb6dff304371db9f646be5", - "sha256": "cedd848f73d9414bf70fb73e310ddc03331dc5b6bd1372e2a212070550e66580" + "sha256": "fdc6eaecce55bfd689f5034b57e9afaa3a3a21ea0444b95c409ac41190b61a4c" }, { "path": "src/geom.rs", @@ -192,7 +192,7 @@ { "path": "src/graphics/paint.rs", "upstream_sha256": "c3de4b7056f3f952009f39f0e0d0311bc9af54807660f9e46f67c177572edf45", - "sha256": "78db7aee7e93670b63272ba7e04b55057520cfcc314c7ccac26f35224efe9fa9" + "sha256": "4154beecc810f3d609d2c46ce82ec63b3421c199f01d922f28b9ed9fa4c7a1e5" }, { "path": "src/graphics/separation.rs", @@ -202,7 +202,7 @@ { "path": "src/graphics/shading_function.rs", "upstream_sha256": "0e0fc8bb1a32272457ef560fa80933972d0f5ef5fb7046424c6de48093cfd978", - "sha256": "84eab9895c6ead8787b9a95f245cbb345ed88cfb132a35c931902ceb91a8c5c3" + "sha256": "2831ec0d78a755db9311a16d01afd5c9f714e4d43bdf69b42c0cb291387ca743" }, { "path": "src/graphics/shading_pattern.rs", @@ -312,7 +312,7 @@ { "path": "src/serialize.rs", "upstream_sha256": "4f2c439dbd20006eea112e056b2f1a5ba98a1ede9227a45f66a6df46fb1a07e8", - "sha256": "9e3d5bf6bde7da3e4cd4dcb6305a9c0b74c64977987f3136b91efcc2cf5a64df" + "sha256": "bbff10ecb0cb48683d567021405ed2b2c83cf259d2c02d4dfaccfe683eedd5d4" }, { "path": "src/stream.rs", diff --git a/vendor/krilla/patches/10-mesh-gradients.patch b/vendor/krilla/patches/10-mesh-gradients.patch new file mode 100644 index 00000000..fb6c3577 --- /dev/null +++ b/vendor/krilla/patches/10-mesh-gradients.patch @@ -0,0 +1,397 @@ +diff --git a/src/content.rs b/src/content.rs +index f723f20..26ad665 100644 +--- a/src/content.rs ++++ b/src/content.rs +@@ -1122,6 +1122,16 @@ impl ContentBuilder { + let (gradient_props, transform) = sg.clone().gradient_properties(bounds); + write_gradient(gradient_props, sc, chunk_container, transform, self); + } ++ InnerPaint::MeshGradient(mg) => { ++ let transform = mg.transform; ++ write_gradient( ++ GradientProperties::MeshGradient(mg.clone()), ++ sc, ++ chunk_container, ++ transform, ++ self, ++ ); ++ } + InnerPaint::Pattern(pat) => { + let mut pat = Arc::unwrap_or_clone(pat.clone()); + pat.transform = pattern_transform(pat.transform); +diff --git a/src/error.rs b/src/error.rs +index 1807ac3..4c582ae 100644 +--- a/src/error.rs ++++ b/src/error.rs +@@ -29,6 +29,11 @@ pub enum KrillaError { + /// + /// [`GroupColorSpace::Cmyk`]: crate::GroupColorSpace::Cmyk + MissingGroupCmykProfile, ++ /// A mesh gradient's corner colors are in color spaces that can't be written as one ++ /// (see [`MeshGradient`]), for example RGB with CMYK. ++ /// ++ /// [`MeshGradient`]: crate::paint::MeshGradient ++ MixedMeshColorSpaces(Option), + /// An error while attempting to embed a font. + Font(Font, String), + /// A list of validation errors. Can only occur if you set the `validator` in +@@ -70,6 +75,10 @@ impl Display for KrillaError { + KrillaError::MissingGroupCmykProfile => { + f.write_str("a CMYK group color space with no_device_cs needs a CMYK profile") + } ++ KrillaError::MixedMeshColorSpaces(location) => { ++ f.write_str("a mesh gradient mixes color spaces that can't be written as one")?; ++ write_location(f, *location) ++ } + KrillaError::Font(_, message) => write!(f, "failed to embed font: {message}"), + KrillaError::Validation(errors) => { + let count = errors.len(); +diff --git a/src/graphics/mask.rs b/src/graphics/mask.rs +index 38b9ad0..2654b25 100644 +--- a/src/graphics/mask.rs ++++ b/src/graphics/mask.rs +@@ -99,6 +99,15 @@ impl Mask { + return None; + } + } ++ GradientProperties::MeshGradient(mg) => { ++ if mg ++ .patches ++ .iter() ++ .all(|p| p.opacities.iter().all(|o| o.get() == 1.0)) ++ { ++ return None; ++ } ++ } + } + + let shading_function = ShadingFunction::new(gradient_properties, true); +diff --git a/src/graphics/paint.rs b/src/graphics/paint.rs +index d4b9efb..abe8892 100644 +--- a/src/graphics/paint.rs ++++ b/src/graphics/paint.rs +@@ -52,6 +52,65 @@ impl Hash for LinearGradient { + } + } + ++/// One Coons patch of a [`MeshGradient`]. ++#[derive(Debug, Clone, PartialEq)] ++pub struct MeshPatch { ++ /// The twelve boundary control points, in the order a PDF Type 6 shading lists them ++ /// (ISO 32000-1, 8.7.4.5.7): starting at a corner, three cubic Bézier edges' worth of points ++ /// going one way round the patch, so the corners are `points[0]`, `[3]`, `[6]` and `[9]`. ++ pub points: [(f32, f32); 12], ++ /// The colors at the corners `points[0]`, `[3]`, `[6]` and `[9]`. ++ /// ++ /// Corner colors of a mesh in different color spaces built from inks (CMYK, luma as ++ /// black, spots with a CMYK fallback, DeviceN) are written in one space: Separation for a ++ /// single spot, DeviceCMYK for process colors alone, DeviceN otherwise. Other colors need ++ /// to share one color space across the whole mesh, or [`Document::finish`] fails with ++ /// [`KrillaError::MixedMeshColorSpaces`]. ++ /// ++ /// [`Document::finish`]: crate::Document::finish ++ /// [`KrillaError::MixedMeshColorSpaces`]: crate::error::KrillaError::MixedMeshColorSpaces ++ pub colors: [Color; 4], ++ /// The opacities at the same corners. ++ pub opacities: [NormalizedF32; 4], ++} ++ ++impl Eq for MeshPatch {} ++ ++impl Hash for MeshPatch { ++ fn hash(&self, state: &mut H) { ++ for (x, y) in &self.points { ++ x.to_bits().hash(state); ++ y.to_bits().hash(state); ++ } ++ self.colors.hash(state); ++ self.opacities.hash(state); ++ } ++} ++ ++/// A mesh gradient: Coons patches, each blending its four corner colors across its curved ++/// shape, written as one PDF Type 6 shading so the viewer or RIP blends them smoothly at its ++/// own resolution. Corner opacities below 1 add a soft mask, as for the other gradients. ++/// Patches paint in order (a later one over an earlier one); a fill paints only where they lie. ++#[derive(Debug, Clone, PartialEq)] ++pub struct MeshGradient { ++ /// The patches. ++ pub patches: Vec, ++ /// A transform that should be applied to the mesh gradient. ++ pub transform: Transform, ++ /// Whether the gradient should be anti-aliased. ++ pub anti_alias: bool, ++} ++ ++impl Eq for MeshGradient {} ++ ++impl Hash for MeshGradient { ++ fn hash(&self, state: &mut H) { ++ self.patches.hash(state); ++ self.transform.hash(state); ++ self.anti_alias.hash(state); ++ } ++} ++ + /// A radial gradient. + #[derive(Debug, Clone, PartialEq)] + pub struct RadialGradient { +@@ -178,6 +237,7 @@ pub(crate) enum InnerPaint { + LinearGradient(LinearGradient), + RadialGradient(RadialGradient), + SweepGradient(SweepGradient), ++ MeshGradient(Arc), + Pattern(Arc), + } + +@@ -220,6 +280,12 @@ impl From for Paint { + } + } + ++impl From for Paint { ++ fn from(value: MeshGradient) -> Self { ++ Paint(InnerPaint::MeshGradient(Arc::new(value))) ++ } ++} ++ + impl From for Paint { + fn from(value: Color) -> Self { + Paint(InnerPaint::Color(value)) +diff --git a/src/graphics/shading_function.rs b/src/graphics/shading_function.rs +index ad0e783..74c56f3 100644 +--- a/src/graphics/shading_function.rs ++++ b/src/graphics/shading_function.rs +@@ -4,6 +4,7 @@ use std::sync::Arc; + + use bumpalo::Bump; + use pdf_writer::types::{FunctionShadingType, PostScriptOp}; ++use pdf_writer::writers::StreamShadingType; + use pdf_writer::{Chunk, Finish, Ref}; + use tiny_skia_path::Point; + +@@ -14,7 +15,7 @@ use crate::graphics::color::device_n::{self, Colorant, DeviceNSpace, Process}; + use crate::graphics::color::separation::{self, SeparationColorant, SeparationSpace}; + use crate::graphics::color::{cmyk, luma}; + use crate::graphics::color::{Color, ColorSpace, RegularColor, SpecialColor}; +-use crate::graphics::paint::{LinearGradient, RadialGradient, SweepGradient}; ++use crate::graphics::paint::{LinearGradient, MeshGradient, RadialGradient, SweepGradient}; + use crate::graphics::paint::{SpreadMethod, Stop}; + use crate::num::NormalizedF32; + use crate::resource; +@@ -84,6 +85,7 @@ impl Hash for PostScriptGradient { + pub(crate) enum GradientProperties { + RadialAxialGradient(RadialAxialGradient), + PostScriptGradient(PostScriptGradient), ++ MeshGradient(Arc), + } + + impl GradientProperties { +@@ -100,6 +102,7 @@ impl GradientProperties { + return Some((&psg.stops[0].color, psg.stops[0].opacity)); + } + } ++ GradientProperties::MeshGradient(_) => {} + } + + None +@@ -233,8 +236,8 @@ impl ShadingFunction { + } + + impl GradientProperties { +- /// Rewrite stops in different ink-based color spaces into one space (see +- /// [`common_color_space`]); other gradients are returned unchanged. ++ /// Rewrite stops, or mesh corner colors, in different ink-based color spaces into one ++ /// space (see [`common_color_space`]); other gradients are returned unchanged. + fn with_common_color_space(self) -> Self { + fn convert(stops: &mut [Stop]) { + let colors: Vec = stops.iter().map(|s| s.color.clone()).collect(); +@@ -254,6 +257,22 @@ impl GradientProperties { + convert(&mut psg.stops); + GradientProperties::PostScriptGradient(psg) + } ++ GradientProperties::MeshGradient(mesh) => { ++ let colors: Vec = mesh ++ .patches ++ .iter() ++ .flat_map(|p| p.colors.iter().cloned()) ++ .collect(); ++ let Some(colors) = common_color_space(&colors) else { ++ return GradientProperties::MeshGradient(mesh); ++ }; ++ let mut mesh = Arc::unwrap_or_clone(mesh); ++ let corners = mesh.patches.iter_mut().flat_map(|p| p.colors.iter_mut()); ++ for (corner, color) in corners.zip(colors) { ++ *corner = color; ++ } ++ GradientProperties::MeshGradient(Arc::new(mesh)) ++ } + } + } + } +@@ -478,6 +497,35 @@ impl Cacheable for ShadingFunction { + registered_cs, + ) + } ++ GradientProperties::MeshGradient(mg) => { ++ // No patches paints nothing; the shading is still written, as it is referenced. ++ let first = mg ++ .patches ++ .first() ++ .map_or_else(|| luma::Color::black().into(), |p| p.colors[0].clone()); ++ // `ShadingFunction::new` has brought ink-based colors into one space; any ++ // others left can't share the stream's one color space. ++ if !self.0.use_opacities ++ && sc.mixed_mesh_colors.is_none() ++ && !mg ++ .patches ++ .iter() ++ .flat_map(|p| p.colors.iter()) ++ .all(|c| same_color_space(&first, c)) ++ { ++ sc.mixed_mesh_colors = Some(sc.location); ++ } ++ let shading_cs = shading_color_space(sc, first, self.0.use_opacities); ++ let registered_cs = sc.register_colorspace(chunk_container, shading_cs); ++ serialize_mesh_shading( ++ sc, ++ &mut stream_chunk, ++ root_ref, ++ mg, ++ self.0.use_opacities, ++ registered_cs, ++ ); ++ } + } + + // Note: The stream chunk might be empty. +@@ -497,6 +545,83 @@ 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, ++/// 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( ++ sc: &mut SerializeContext, ++ chunk: &mut Chunk, ++ root_ref: Ref, ++ mesh: &MeshGradient, ++ use_opacities: bool, ++ cs: MaybeDeviceColorSpace, ++) { ++ let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN); ++ for (x, y) in mesh.patches.iter().flat_map(|p| p.points.iter()) { ++ (x0, y0, x1, y1) = (x0.min(*x), y0.min(*y), x1.max(*x), y1.max(*y)); ++ } ++ if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) { ++ (x0, y0, x1, y1) = (0.0, 0.0, 1.0, 1.0); ++ } ++ // A zero-size range would divide by zero: widen it by a point either way. ++ if x1 - x0 < f32::EPSILON { ++ (x0, x1) = (x0 - 1.0, x1 + 1.0); ++ } ++ if y1 - y0 < f32::EPSILON { ++ (y0, y1) = (y0 - 1.0, y1 + 1.0); ++ } ++ let components = if use_opacities { ++ 1 ++ } else { ++ mesh.patches ++ .first() ++ .map_or(1, |p| p.colors[0].to_pdf_color().len()) ++ }; ++ let coord = |v: f32, lo: f32, hi: f32| { ++ (((f64::from(v) - f64::from(lo)) / (f64::from(hi) - f64::from(lo))).clamp(0.0, 1.0) ++ * 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)); ++ for patch in &mesh.patches { ++ data.push(0u8); ++ for (x, y) in &patch.points { ++ data.extend(coord(*x, x0, x1).to_be_bytes()); ++ data.extend(coord(*y, y0, y1).to_be_bytes()); ++ } ++ for (color, opacity) in patch.colors.iter().zip(patch.opacities.iter()) { ++ if use_opacities { ++ data.push(byte(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))); ++ } ++ } ++ } ++ } ++ let filter_stream = ++ FilterStreamBuilder::new_from_binary_data(&data).finish(&sc.serialize_settings()); ++ let mut shading = chunk.stream_shading(root_ref, filter_stream.encoded_data()); ++ filter_stream.write_filters(shading.deref_mut().deref_mut()); ++ 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_flag(8); ++ let mut decode = vec![x0, x1, y0, y1]; ++ for _ in 0..components { ++ decode.extend([0.0, 1.0]); ++ } ++ shading.decode(decode); ++ shading.anti_alias(mesh.anti_alias); ++ shading.finish(); ++} ++ + fn serialize_postscript_shading( + sc: &mut SerializeContext, + chunk: &mut Chunk, +diff --git a/src/serialize.rs b/src/serialize.rs +index 378d6ec..0093a25 100644 +--- a/src/serialize.rs ++++ b/src/serialize.rs +@@ -341,6 +341,9 @@ pub(crate) enum MaybeDeviceColorSpace { + pub(crate) struct SerializeContext { + /// An unfinished or failed page/surface must never serialize as a complete document. + pub(crate) poisoned: bool, ++ /// Where the first mesh gradient whose corner colors share no color space was drawn; ++ /// [`SerializeContext::finish`] fails with [`KrillaError::MixedMeshColorSpaces`]. ++ pub(crate) mixed_mesh_colors: Option>, + /// The ref of the page tree. + page_tree_ref: Ref, + /// PDF 2.0 namespaces. +@@ -401,6 +404,7 @@ impl SerializeContext { + + Self { + poisoned: false, ++ mixed_mesh_colors: None, + cached_mappings: HashMap::new(), + pdf2_ns, + global_objects: GlobalObjects::default(), +@@ -594,6 +598,9 @@ impl SerializeContext { + { + return Err(KrillaError::MissingGroupCmykProfile); + } ++ if let Some(location) = self.mixed_mesh_colors { ++ return Err(KrillaError::MixedMeshColorSpaces(location)); ++ } + + // We need to be careful here that we serialize the objects in the right order, + // as in some cases we use MaybeTake::take to remove an object, which means that +diff --git a/src/surface.rs b/src/surface.rs +index 0cbbc4b..15a728d 100644 +--- a/src/surface.rs ++++ b/src/surface.rs +@@ -615,6 +615,10 @@ impl<'a> Surface<'a> { + r.stops.iter().any(|s| s.opacity != NormalizedF32::ONE) + } + InnerPaint::SweepGradient(r) => r.stops.iter().any(|s| s.opacity != NormalizedF32::ONE), ++ InnerPaint::MeshGradient(m) => m ++ .patches ++ .iter() ++ .any(|p| p.opacities.iter().any(|o| *o != NormalizedF32::ONE)), + InnerPaint::Pattern(_) => false, + }; + diff --git a/vendor/krilla/src/error.rs b/vendor/krilla/src/error.rs index 1807ac3a..4c582aeb 100644 --- a/vendor/krilla/src/error.rs +++ b/vendor/krilla/src/error.rs @@ -29,6 +29,11 @@ pub enum KrillaError { /// /// [`GroupColorSpace::Cmyk`]: crate::GroupColorSpace::Cmyk MissingGroupCmykProfile, + /// A mesh gradient's corner colors are in color spaces that can't be written as one + /// (see [`MeshGradient`]), for example RGB with CMYK. + /// + /// [`MeshGradient`]: crate::paint::MeshGradient + MixedMeshColorSpaces(Option), /// An error while attempting to embed a font. Font(Font, String), /// A list of validation errors. Can only occur if you set the `validator` in @@ -70,6 +75,10 @@ impl Display for KrillaError { KrillaError::MissingGroupCmykProfile => { f.write_str("a CMYK group color space with no_device_cs needs a CMYK profile") } + KrillaError::MixedMeshColorSpaces(location) => { + f.write_str("a mesh gradient mixes color spaces that can't be written as one")?; + write_location(f, *location) + } KrillaError::Font(_, message) => write!(f, "failed to embed font: {message}"), KrillaError::Validation(errors) => { let count = errors.len(); diff --git a/vendor/krilla/src/graphics/paint.rs b/vendor/krilla/src/graphics/paint.rs index f79c1826..abe88920 100644 --- a/vendor/krilla/src/graphics/paint.rs +++ b/vendor/krilla/src/graphics/paint.rs @@ -59,8 +59,16 @@ pub struct MeshPatch { /// (ISO 32000-1, 8.7.4.5.7): starting at a corner, three cubic Bézier edges' worth of points /// going one way round the patch, so the corners are `points[0]`, `[3]`, `[6]` and `[9]`. pub points: [(f32, f32); 12], - /// The colours at the corners `points[0]`, `[3]`, `[6]` and `[9]`. Every patch of a mesh - /// gradient needs its colours in the same colour space. + /// The colors at the corners `points[0]`, `[3]`, `[6]` and `[9]`. + /// + /// Corner colors of a mesh in different color spaces built from inks (CMYK, luma as + /// black, spots with a CMYK fallback, DeviceN) are written in one space: Separation for a + /// single spot, DeviceCMYK for process colors alone, DeviceN otherwise. Other colors need + /// to share one color space across the whole mesh, or [`Document::finish`] fails with + /// [`KrillaError::MixedMeshColorSpaces`]. + /// + /// [`Document::finish`]: crate::Document::finish + /// [`KrillaError::MixedMeshColorSpaces`]: crate::error::KrillaError::MixedMeshColorSpaces pub colors: [Color; 4], /// The opacities at the same corners. pub opacities: [NormalizedF32; 4], @@ -79,7 +87,7 @@ impl Hash for MeshPatch { } } -/// A mesh gradient: Coons patches, each blending its four corner colours across its curved +/// A mesh gradient: Coons patches, each blending its four corner colors across its curved /// shape, written as one PDF Type 6 shading so the viewer or RIP blends them smoothly at its /// own resolution. Corner opacities below 1 add a soft mask, as for the other gradients. /// Patches paint in order (a later one over an earlier one); a fill paints only where they lie. diff --git a/vendor/krilla/src/graphics/shading_function.rs b/vendor/krilla/src/graphics/shading_function.rs index 72d84814..74c56f36 100644 --- a/vendor/krilla/src/graphics/shading_function.rs +++ b/vendor/krilla/src/graphics/shading_function.rs @@ -236,8 +236,8 @@ impl ShadingFunction { } impl GradientProperties { - /// Rewrite stops in different ink-based color spaces into one space (see - /// [`common_color_space`]); other gradients are returned unchanged. + /// Rewrite stops, or mesh corner colors, in different ink-based color spaces into one + /// space (see [`common_color_space`]); other gradients are returned unchanged. fn with_common_color_space(self) -> Self { fn convert(stops: &mut [Stop]) { let colors: Vec = stops.iter().map(|s| s.color.clone()).collect(); @@ -257,7 +257,22 @@ impl GradientProperties { convert(&mut psg.stops); GradientProperties::PostScriptGradient(psg) } - mesh @ GradientProperties::MeshGradient(_) => mesh, + GradientProperties::MeshGradient(mesh) => { + let colors: Vec = mesh + .patches + .iter() + .flat_map(|p| p.colors.iter().cloned()) + .collect(); + let Some(colors) = common_color_space(&colors) else { + return GradientProperties::MeshGradient(mesh); + }; + let mut mesh = Arc::unwrap_or_clone(mesh); + let corners = mesh.patches.iter_mut().flat_map(|p| p.colors.iter_mut()); + for (corner, color) in corners.zip(colors) { + *corner = color; + } + GradientProperties::MeshGradient(Arc::new(mesh)) + } } } } @@ -488,6 +503,18 @@ impl Cacheable for ShadingFunction { .patches .first() .map_or_else(|| luma::Color::black().into(), |p| p.colors[0].clone()); + // `ShadingFunction::new` has brought ink-based colors into one space; any + // others left can't share the stream's one color space. + if !self.0.use_opacities + && sc.mixed_mesh_colors.is_none() + && !mg + .patches + .iter() + .flat_map(|p| p.colors.iter()) + .all(|c| same_color_space(&first, c)) + { + sc.mixed_mesh_colors = Some(sc.location); + } let shading_cs = shading_color_space(sc, first, self.0.use_opacities); let registered_cs = sc.register_colorspace(chunk_container, shading_cs); serialize_mesh_shading( @@ -520,7 +547,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 colours (with `use_opacities`, the opacities as gray) at 8 bits a component, +/// the four corner colors (with `use_opacities`, the opacities as gray) at 8 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( @@ -570,7 +597,7 @@ fn serialize_mesh_shading( data.push(byte(opacity.get())); } else { let values = color.to_pdf_color(); - // A colour from another space can't be written in this one: never misread bytes. + // 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))); } diff --git a/vendor/krilla/src/serialize.rs b/vendor/krilla/src/serialize.rs index 378d6ecb..0093a25f 100644 --- a/vendor/krilla/src/serialize.rs +++ b/vendor/krilla/src/serialize.rs @@ -341,6 +341,9 @@ pub(crate) enum MaybeDeviceColorSpace { pub(crate) struct SerializeContext { /// An unfinished or failed page/surface must never serialize as a complete document. pub(crate) poisoned: bool, + /// Where the first mesh gradient whose corner colors share no color space was drawn; + /// [`SerializeContext::finish`] fails with [`KrillaError::MixedMeshColorSpaces`]. + pub(crate) mixed_mesh_colors: Option>, /// The ref of the page tree. page_tree_ref: Ref, /// PDF 2.0 namespaces. @@ -401,6 +404,7 @@ impl SerializeContext { Self { poisoned: false, + mixed_mesh_colors: None, cached_mappings: HashMap::new(), pdf2_ns, global_objects: GlobalObjects::default(), @@ -594,6 +598,9 @@ impl SerializeContext { { return Err(KrillaError::MissingGroupCmykProfile); } + if let Some(location) = self.mixed_mesh_colors { + return Err(KrillaError::MixedMeshColorSpaces(location)); + } // We need to be careful here that we serialize the objects in the right order, // as in some cases we use MaybeTake::take to remove an object, which means that