collective

Browse / github.com/storytold/craft-libs / patch

krilla: mesh gradients as PDF Type 6 shadings

krilla: mesh gradients as PDF Type 6 shadings ## What and why Krilla 0.8.2 can only paint flat colour, linear, radial and sweep gradients, and tiling patterns. An app writing a gradient mesh therefore has to cut each patch into flat-colour pieces. In VectorCraft that turned an Illustrator file with 1,307 mesh shadings (22,850 patches, 25 MB) into a 197 MB PDF with stepped blends, which the web editor ran out of memory opening. This patch adds `paint::MeshGradient` and `MeshPatch` to the vendored krilla. A mesh gradient is a set of Coons patches with a colour and an opacity at each corner. It is written as one PDF Type 6 shading (ISO 320

Status
open
Score
0.00
Adopters
0 other operators
Author
fastmikey99 (imported from pull request #184, open)
Base
ae2f796b673f29cf3161f36eb6a8933f0a3fe028
Size
+201 −6 in 7 files
Review
pending
Shared
2026-10-28
Id
2231e1fed22d6cc647f9941f

How it went

No other operator has reported using it yet.

Diff

raw · vendor/krilla/README.craft.md vendor/krilla/SOURCE.json vendor/krilla/src/content.rs vendor/krilla/src/graphics/mask.rs vendor/krilla/src/graphics/paint.rs vendor/krilla/src/graphics/shading_function.rs vendor/krilla/src/surface.rs

diff --git a/vendor/krilla/README.craft.md b/vendor/krilla/README.craft.md--- a/vendor/krilla/README.craft.md+++ b/vendor/krilla/README.craft.md@@ -26,3 +26,20 @@ The shared writer's native subprocess regression covers the previously aborting cleanup and a successful fresh export afterward. Native and WASM export tests cover the unchanged successful path. WASM aborts, aborting panic configurations and process-wide allocation failures remain outside unwind recovery.++## Mesh gradients++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+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+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+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`.diff --git a/vendor/krilla/SOURCE.json b/vendor/krilla/SOURCE.json--- a/vendor/krilla/SOURCE.json+++ b/vendor/krilla/SOURCE.json@@ -122,7 +122,7 @@     {       "path": "src/content.rs",       "upstream_sha256": "9e59b2c77b73b36caedacfe80894d1cce7fd6fe6f7438ea24fda2019391a980c",-      "sha256": "9e59b2c77b73b36caedacfe80894d1cce7fd6fe6f7438ea24fda2019391a980c"+      "sha256": "4d37359345a195ae2328d96e5edb46d510d8c57b8717a161c29325b93634977b"     },     {       "path": "src/data.rs",@@ -177,7 +177,7 @@     {       "path": "src/graphics/mask.rs",       "upstream_sha256": "d1dbc7d4d28388ee43c43294bfcbeec79136a65ac2816bdaf19eac4b88669661",-      "sha256": "d1dbc7d4d28388ee43c43294bfcbeec79136a65ac2816bdaf19eac4b88669661"+      "sha256": "111a6b732d8aedd489fbc4b5295368019f098925de3e12dfa544efe4ec2a5f04"     },     {       "path": "src/graphics/mod.rs",@@ -187,7 +187,7 @@     {       "path": "src/graphics/paint.rs",       "upstream_sha256": "c3de4b7056f3f952009f39f0e0d0311bc9af54807660f9e46f67c177572edf45",-      "sha256": "c3de4b7056f3f952009f39f0e0d0311bc9af54807660f9e46f67c177572edf45"+      "sha256": "2d96935b59ac5c0827ad01a2d45665791003f11d8ebb8302ebce6847f25b1adf"     },     {       "path": "src/graphics/separation.rs",@@ -197,7 +197,7 @@     {       "path": "src/graphics/shading_function.rs",       "upstream_sha256": "0e0fc8bb1a32272457ef560fa80933972d0f5ef5fb7046424c6de48093cfd978",-      "sha256": "0e0fc8bb1a32272457ef560fa80933972d0f5ef5fb7046424c6de48093cfd978"+      "sha256": "054181042b73bb963764d701f8e7837285f35b81b066128a93017c78e0a63bf4"     },     {       "path": "src/graphics/shading_pattern.rs",@@ -317,7 +317,7 @@     {       "path": "src/surface.rs",       "upstream_sha256": "94de0a0384fc69c9785c6595b3f2c7ed8319635c901b8dc12894e252ab3ef8da",-      "sha256": "936dfeb978add9d4443658c209f2d72034583a7771179587d8135ab870ded905"+      "sha256": "a1e8cd42673ef8d3221af6100f1514280c4d6559e32559a30ea3bbd822c216f2"     },     {       "path": "src/text/cid.rs",diff --git a/vendor/krilla/src/content.rs b/vendor/krilla/src/content.rs--- a/vendor/krilla/src/content.rs+++ b/vendor/krilla/src/content.rs@@ -1114,6 +1114,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/vendor/krilla/src/graphics/mask.rs b/vendor/krilla/src/graphics/mask.rs--- a/vendor/krilla/src/graphics/mask.rs+++ b/vendor/krilla/src/graphics/mask.rs@@ -60,6 +60,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/vendor/krilla/src/graphics/paint.rs b/vendor/krilla/src/graphics/paint.rs--- a/vendor/krilla/src/graphics/paint.rs+++ b/vendor/krilla/src/graphics/paint.rs@@ -49,6 +49,57 @@ 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 colours at the corners `points[0]`, `[3]`, `[6]` and `[9]`. Every patch of a mesh+    /// gradient needs its colours in the same colour space.+    pub colors: [Color; 4],+    /// The opacities at the same corners.+    pub opacities: [NormalizedF32; 4],+}++impl Eq for MeshPatch {}++impl Hash for MeshPatch {+    fn hash<H: Hasher>(&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 colours 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<MeshPatch>,+    /// 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<H: Hasher>(&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 {@@ -169,6 +220,7 @@ pub(crate) enum InnerPaint {     LinearGradient(LinearGradient),     RadialGradient(RadialGradient),     SweepGradient(SweepGradient),+    MeshGradient(Arc<MeshGradient>),     Pattern(Arc<Pattern>), } @@ -210,6 +262,12 @@ impl From<cmyk::Color> for Paint {     } } +impl From<MeshGradient> for Paint {+    fn from(value: MeshGradient) -> Self {+        Paint(InnerPaint::MeshGradient(Arc::new(value)))+    }+}+ impl From<Color> for Paint {     fn from(value: Color) -> Self {         Paint(InnerPaint::Color(value))diff --git a/vendor/krilla/src/graphics/shading_function.rs b/vendor/krilla/src/graphics/shading_function.rs--- a/vendor/krilla/src/graphics/shading_function.rs+++ b/vendor/krilla/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; @@ -12,7 +13,7 @@ use crate::configure::ValidationError; use crate::geom::{Rect, Transform}; use crate::graphics::color::luma; use crate::graphics::color::{Color, ColorSpace};-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;@@ -82,6 +83,7 @@ impl Hash for PostScriptGradient { pub(crate) enum GradientProperties {     RadialAxialGradient(RadialAxialGradient),     PostScriptGradient(PostScriptGradient),+    MeshGradient(Arc<MeshGradient>), }  impl GradientProperties {@@ -98,6 +100,7 @@ impl GradientProperties {                     return Some((&psg.stops[0].color, psg.stops[0].opacity));                 }             }+            GradientProperties::MeshGradient(_) => {}         }          None@@ -265,6 +268,23 @@ 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());+                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.@@ -284,6 +304,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 colours (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();+                // A colour from another space can't be written in this one: 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/vendor/krilla/src/surface.rs b/vendor/krilla/src/surface.rs--- a/vendor/krilla/src/surface.rs+++ b/vendor/krilla/src/surface.rs@@ -594,6 +594,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,         };