Merge pull request #3224 from Matoo125/fix/emf-pattern-tile-budget

EMF export: pattern tiles stop at 250,000 in all
This commit is contained in:
Dylan M. Taylor
2026-10-20 12:17:30 -04:00
committed by GitHub
2 changed files with 57 additions and 7 deletions
+23 -6
View File
@@ -17,6 +17,9 @@ use crate::dib::Rgba;
/// How deep symbols, brush art and pattern tiles may nest in one another (deeper art is left out).
const MAX_NEST: u32 = 8;
/// Most pattern tiles written in all. One fill tiles at most 250,000 (`PatternDef::offsets_covering`),
/// but a tile whose art is filled with a pattern again multiplies that at every level of nesting.
const MAX_TILES: usize = 250_000;
/// Most pixels along a side of a gradient's image (its colours are smooth).
const MAX_GRADIENT_PX: f64 = 512.0;
/// Opacity at or above this is written as opaque.
@@ -29,6 +32,7 @@ const RASTER_FX: &str = "raster effects (shadows, glows, blurs, feathering) are
const CLIPPING: &str = "clipping masks are not applied: the art they clip is written whole";
const FLAT_GRADIENTS: &str = "gradients are written as one colour, the average of their stops";
const FLAT_PATTERNS: &str = "pattern fills are written as one colour";
const TOO_MANY_TILES: &str = "pattern fills past 250,000 tiles in all are written as one colour";
/// A stroke as a pen draws it.
#[derive(Clone, Debug, PartialEq)]
@@ -115,6 +119,8 @@ pub(crate) struct Scene<'a> {
opacity: f32,
/// Symbols, brush art and pattern tiles being written inside one another.
nest: u32,
/// Pattern tiles left to write ([`MAX_TILES`]).
tiles_left: usize,
}
impl<'a> Scene<'a> {
@@ -130,6 +136,7 @@ impl<'a> Scene<'a> {
brushes: None,
opacity: 1.0,
nest: 0,
tiles_left: MAX_TILES,
}
}
@@ -296,9 +303,16 @@ impl<'a> Scene<'a> {
match paint {
Paint::None => {}
Paint::Gradient(g) if self.clips => self.gradient(bp, rule, g, opacity, bounds),
Paint::Pattern { pattern, xf } if self.clips && self.nest < MAX_NEST && self.doc.pattern(pattern).is_some() => {
Paint::Pattern { pattern, xf } if self.clips && self.nest < MAX_NEST && self.tiles_left > 0 && self.doc.pattern(pattern).is_some() => {
let area = bp.bounding_box().intersect(self.region);
let tiles = self.doc.pattern(pattern).map(|def| def.instances_in(*xf, area)).unwrap_or_default();
if tiles.len() > self.tiles_left {
self.tiles_left = 0;
self.warn(TOO_MANY_TILES);
self.flat_fill(bp, rule, paint, opacity);
return;
}
self.tiles_left -= tiles.len();
let outer = self.opacity;
self.opacity *= opacity.clamp(0.0, 1.0);
self.ops.push(Op::Clip { path: bp.clone(), rule });
@@ -308,14 +322,17 @@ impl<'a> Scene<'a> {
self.ops.push(Op::Unclip);
self.opacity = outer;
}
p => {
let Some(rgb) = self.flat(p) else { return };
self.check_opacity(opacity);
self.ops.push(Op::Fill { path: bp.clone(), rule, rgb });
}
p => self.flat_fill(bp, rule, p, opacity),
}
}
/// Fill `bp` with one colour for `paint` at `opacity`.
fn flat_fill(&mut self, bp: &BezPath, rule: FillRule, paint: &Paint, opacity: f32) {
let Some(rgb) = self.flat(paint) else { return };
self.check_opacity(opacity);
self.ops.push(Op::Fill { path: bp.clone(), rule, rgb });
}
/// One colour for a paint: a solid colour, the average of a gradient's stops, or a pattern's
/// first solid fill.
fn flat(&mut self, p: &Paint) -> Option<[u8; 3]> {
+34 -1
View File
@@ -4,7 +4,9 @@
use std::sync::Arc;
use vectorcraft_color::{Color, Gradient, GradientPaint, Paint};
use vectorcraft_doc::{Appearance, CharStyle, Dash, Document, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, TextObject};
use vectorcraft_doc::{
Appearance, CharStyle, Dash, Document, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, PatternDef, TextObject,
};
use vectorcraft_geom::{Affine, PathData, Point, Rect, shapes};
use crate::*;
@@ -340,3 +342,34 @@ fn an_instance_whose_art_reaches_the_artboard_is_written() {
let k = kinds(&emf_records(&emf(&d).bytes));
assert!(k.contains(&39), "a brush fills the art: {k:?}");
}
/// A pattern `name` repeating `art` every point.
fn point_tiles(name: &str, art: Node) -> PatternDef {
let mut def = PatternDef::new(name, vec![Arc::new(art)]);
def.tile = Rect::new(0.0, 0.0, 1.0, 1.0);
def
}
fn pattern(name: &str) -> Paint {
Paint::Pattern { pattern: name.into(), xf: Affine::IDENTITY }
}
#[test]
fn patterns_inside_pattern_tiles_share_one_tile_budget() {
// About 3,700 tiles of a 12 pt square, each filled with about 140 tiles of a dot: over half a
// million fills when every tile is written.
let square = path(shapes::rectangle(Rect::new(0.0, 0.0, 12.0, 12.0)), pattern("dots"), Paint::None, 0.0);
let mut d = doc_with(50.0, 50.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), pattern("squares"), Paint::None, 0.0)]);
d.patterns.push(point_tiles("dots", path(shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), red(), Paint::None, 0.0)));
d.patterns.push(point_tiles("squares", square));
let (ops, warnings) = crate::scene::Scene::new(&d, d.artboards[0].rect, true).run();
assert!(ops.len() < 300_000, "{} operations", ops.len());
assert!(warnings.iter().any(|w| w.contains("one colour")), "{warnings:?}");
// A tile filled with its own pattern multiplies at every level down to the nesting limit.
let square = path(shapes::rectangle(Rect::new(0.0, 0.0, 4.0, 4.0)), pattern("self"), Paint::None, 0.0);
let mut d = doc_with(10.0, 10.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), pattern("self"), Paint::None, 0.0)]);
d.patterns.push(point_tiles("self", square));
let (ops, _) = crate::scene::Scene::new(&d, d.artboards[0].rect, true).run();
assert!(ops.len() < 300_000, "{} operations", ops.len());
}