mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
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:
@@ -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]> {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user