//! The document walk both writers share: each visible object over the region becomes drawing //! operations ([`Op`]) in paint order, in document space. What a format can't hold is decided //! here once: with clipping (EMF), gradients become images clipped to what they paint and //! patterns their tiles clipped to the shape; without (WMF), clipped art is written whole and //! gradients and patterns as one colour. use std::collections::HashMap; use std::sync::Arc; use vectorcraft_color::freeform::{painted_box, spread_scale}; use vectorcraft_color::{BlendMode, GradientKind, GradientPaint, Paint}; use vectorcraft_doc::{AppearanceItem, Dash, Document, ImageObject, LineCap, LineJoin, Node, NodeKind, StrokeLayer, SymbolBoxes}; use vectorcraft_effects::stroke; use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect, Shape}; 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. const OPAQUE: f32 = 0.998; const TRANSPARENCY: &str = "transparency is left out of paths: transparent fills and strokes are written opaque"; const BLENDING: &str = "blending modes are left out"; const MASKS: &str = "opacity masks are left out"; const RASTER_FX: &str = "raster effects (shadows, glows, blurs, feathering) are left out"; 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)] pub(crate) struct Pen { pub rgb: [u8; 3], /// Points. pub width: f64, pub cap: LineCap, pub join: LineJoin, pub miter: f64, /// A dash pattern that draws dashes (points). pub dash: Option, } /// One drawing operation, in document space. #[derive(Clone, Debug)] pub(crate) enum Op { Fill { path: BezPath, rule: FillRule, rgb: [u8; 3], }, Stroke { path: BezPath, pen: Pen, }, /// An image: its pixel space (0..w, 0..h) maps into the document by `xf`; its alpha times /// `opacity`. Image { image: Arc, xf: Affine, opacity: f32, }, /// What follows, up to the matching [`Op::Unclip`], is clipped to `path` (inside the clips /// around it). Only formats with clipping get these. Clip { path: BezPath, rule: FillRule, }, Unclip, } fn overlaps(a: Rect, b: Rect) -> bool { a.x0 <= b.x1 && b.x0 <= a.x1 && a.y0 <= b.y1 && b.y0 <= a.y1 } fn rgb_of(c: &vectorcraft_color::Color) -> [u8; 3] { let [r, g, b, _] = c.to_rgba8(1.0); [r, g, b] } /// The first visible solid fill colour in `n` (a pattern tile's art). fn first_solid(n: &Node) -> Option { let own = n.appearance.items.iter().find_map(|i| match i { AppearanceItem::Fill(f) if f.visible => f.paint.color(), _ => None, }); own.or_else(|| n.children().into_iter().flatten().find_map(|c| first_solid(c))) } /// `bp` cut into its dashes (as it is when `dash` draws none), for pens that can't draw them. pub(crate) fn dashed(bp: &BezPath, dash: Option<&Dash>) -> BezPath { dash.and_then(|d| stroke::dash(bp, d)).map_or_else(|| bp.clone(), |d| d.path) } fn q(v: f32) -> u8 { (v.clamp(0.0, 1.0) * 255.0).round() as u8 } pub(crate) struct Scene<'a> { doc: &'a Document, /// The art boxes of `doc`'s symbols, which the walk measures symbol instances by when it /// leaves out objects outside the region ([`SymbolBoxes::quick`]). symbols: SymbolBoxes<'a>, region: Rect, /// The format clips (EMF). clips: bool, ops: Vec, warnings: Vec, /// Decoded images by key (`None`: can't be decoded, left out). images: HashMap>>, brushes: Option>, /// The opacity of the containers around the current object. 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> { pub fn new(doc: &'a Document, region: Rect, clips: bool) -> Self { Self { doc, symbols: doc.symbol_boxes().quick(), region, clips, ops: vec![], warnings: vec![], images: HashMap::new(), brushes: None, opacity: 1.0, nest: 0, tiles_left: MAX_TILES, } } /// The operations (clipped to the region when the format clips) and the warnings. pub fn run(mut self) -> (Vec, Vec) { let doc = self.doc; if self.clips { self.ops.push(Op::Clip { path: self.region.to_path(0.1), rule: FillRule::NonZero }); } for l in &doc.layers { self.node(l, false); } if self.clips { self.ops.push(Op::Unclip); } (self.ops, self.warnings) } fn warn(&mut self, w: &str) { if !self.warnings.iter().any(|x| x == w) { self.warnings.push(w.to_string()); } } fn node(&mut self, n: &Node, force: bool) { if let NodeKind::Layer { template: true, .. } = n.kind { return; } if !force && !n.visible { return; } if !n.is_layer() { match n.visual_bounds_with(&self.symbols) { Some(b) if !overlaps(b, self.region) => return, None if !n.is_container() => return, _ => {} } } self.note_losses(n); let opacity = self.opacity; self.opacity *= n.opacity.clamp(0.0, 1.0); match &n.kind { NodeKind::Layer { children, clip, .. } | NodeKind::Group { children, clip } => self.children(children, *clip), NodeKind::Path { path, rule, guide, .. } => { if !*guide { self.shape(n, &path.to_bezpath(), *rule); } } NodeKind::Compound { children, rule } => { let mut bp = BezPath::new(); for p in children.iter().filter_map(|c| c.path_data()) { bp.extend(p.to_bezpath()); } self.shape(n, &bp, *rule); } NodeKind::Text(_) => { if let Some(o) = vectorcraft_effects::outline_text(n) { self.node(&o, true); } } NodeKind::Image(im) => self.image(im), NodeKind::SymbolInstance { symbol, xf } => { if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol) && self.nest < MAX_NEST { let mut art = (*sym.art).clone(); art.transform(*xf, false); self.nest += 1; self.node(&art, true); self.nest -= 1; } } // Live blends, envelopes, meshes and repeats are written as their evaluated art. NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) | NodeKind::Intertwine { .. } => { let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n); for c in g.children().into_iter().flatten() { self.node(c, false); } } } self.opacity = opacity; } /// A container's children. A clipping container's first child clips the rest (its fill paints /// below them, its stroke over them); without clipping they are written whole. fn children(&mut self, children: &[Arc], clip: bool) { let Some((first, rest)) = children.split_first().filter(|_| clip) else { children.iter().for_each(|c| self.node(c, false)); return; }; let paint = first.clip_paint(); let outline = if self.clips { // Nothing to clip by hides the clipped art (as on the canvas). let Some((path, rule)) = vectorcraft_effects::clip_outline(first) else { return }; Some((path, rule)) } else { self.warn(CLIPPING); None }; let clipped = outline.is_some(); if let Some((path, rule)) = outline { self.ops.push(Op::Clip { path, rule }); } if let Some(f) = &paint.fill { self.node(f, false); } rest.iter().for_each(|c| self.node(c, false)); if clipped { self.ops.push(Op::Unclip); } if let Some(s) = &paint.stroke { self.node(s, false); } } /// Warn about what a metafile can't hold in `n`'s look. fn note_losses(&mut self, n: &Node) { if n.blend != BlendMode::Normal { self.warn(BLENDING); } if n.extras().mask.as_ref().is_some_and(|m| !m.disabled) { self.warn(MASKS); } let raster = |fx: &[vectorcraft_doc::Effect]| fx.iter().any(|e| e.visible && vectorcraft_effects::is_raster(&e.id)); if raster(&n.appearance.effects) || n.appearance.items.iter().any(|i| raster(i.effects())) { self.warn(RASTER_FX); } } /// Warn when a path paints at `opacity` (times the containers'). fn check_opacity(&mut self, opacity: f32) { if self.opacity * opacity < OPAQUE { self.warn(TRANSPARENCY); } } /// A path's fills and strokes, in paint order. fn shape(&mut self, n: &Node, bp: &BezPath, rule: FillRule) { if bp.elements().is_empty() { return; } let bounds = bp.bounding_box(); for item in &n.appearance.items { match item { AppearanceItem::Fill(f) if f.visible => { if f.blend != BlendMode::Normal { self.warn(BLENDING); } self.fill(bp, rule, &f.paint, f.opacity, bounds); } AppearanceItem::Stroke(st) if st.visible && st.width > 0.0 && st.width.is_finite() => self.stroke(bp, rule, st, bounds), _ => {} } } } /// Fill `bp` with `paint` at `opacity`; `bounds` places a gradient that isn't placed. fn fill(&mut self, bp: &BezPath, rule: FillRule, paint: &Paint, opacity: f32, bounds: Rect) { 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.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 }); self.nest += 1; tiles.iter().for_each(|t| self.node(t, true)); self.nest -= 1; self.ops.push(Op::Unclip); self.opacity = outer; } 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]> { match p { Paint::None => None, Paint::Solid { color, .. } => Some(rgb_of(color)), Paint::Gradient(g) => { self.warn(FLAT_GRADIENTS); let stops = &g.gradient.stops; let n = stops.len().max(1) as f64; let sum = stops.iter().map(|s| rgb_of(&s.color)).fold([0.0; 3], |a, c| [0, 1, 2].map(|i| a[i] + f64::from(c[i]))); Some(sum.map(|v| (v / n).round().clamp(0.0, 255.0) as u8)) } Paint::Pattern { pattern, .. } => { self.warn(FLAT_PATTERNS); let first = self.doc.pattern(pattern).and_then(|def| def.art.iter().find_map(|a| first_solid(a))); Some(first.map_or([128; 3], |c| rgb_of(&c))) } } } /// A gradient fill: an image of its colours over the shape's box, clipped to the shape, /// sampled at the document's raster effects resolution. fn gradient(&mut self, bp: &BezPath, rule: FillRule, g: &GradientPaint, opacity: f32, bounds: Rect) { let area = bp.bounding_box().intersect(self.region); let long = area.width().max(area.height()); if !(long > 1e-9 && long.is_finite()) { return; } let k = (self.doc.raster_effects_ppi / 72.0).min(MAX_GRADIENT_PX / long); let along = |len: f64| (len * k).ceil().clamp(1.0, MAX_GRADIENT_PX) as u16; let (cols, rows) = (along(area.width()), along(area.height())); let pixels: Vec = if g.gradient.kind == GradientKind::Freeform { let Some(b) = painted_box(bounds) else { return }; let field = g.freeform_on(b).field_with(spread_scale(b), &|c| c.to_rgb()); field.grid(area, cols, rows).flat_map(|(c, a)| [q(c[0]), q(c[1]), q(c[2]), q(a)]).collect() } else { let geom = g.resolve(bounds); let (cw, ch) = (area.width() / f64::from(cols), area.height() / f64::from(rows)); (0..usize::from(rows) * usize::from(cols)) .flat_map(|i| { let (x, y) = (i % usize::from(cols), i / usize::from(cols)); let p = Point::new(area.x0 + (x as f64 + 0.5) * cw, area.y0 + (y as f64 + 0.5) * ch); let t = geom.param_at(g.gradient.kind, p); let t = if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 }; let (c, a) = g.gradient.sample(t as f32); c.to_rgba8(a) }) .collect() }; let image = Rgba { width: u32::from(cols), height: u32::from(rows), pixels }; let xf = Affine::translate(area.origin().to_vec2()) * Affine::scale_non_uniform(area.width() / f64::from(cols), area.height() / f64::from(rows)); self.ops.push(Op::Clip { path: bp.clone(), rule }); self.ops.push(Op::Image { image: Arc::new(image), xf, opacity: (self.opacity * opacity).clamp(0.0, 1.0) }); self.ops.push(Op::Unclip); } fn stroke(&mut self, bp: &BezPath, rule: FillRule, st: &StrokeLayer, bounds: Rect) { if st.brush.is_some() && self.nest < MAX_NEST && let Some(art) = self.brush_art(bp, st) { let opacity = self.opacity; self.opacity *= st.opacity.clamp(0.0, 1.0); self.nest += 1; art.iter().for_each(|piece| self.node(piece, true)); self.nest -= 1; self.opacity = opacity; return; } if st.blend != BlendMode::Normal { self.warn(BLENDING); } // A pen paints one colour along the path itself: anything else is the stroke's outline, // filled. let flat_paint = matches!(st.paint, Paint::Solid { .. }) || !self.clips; if !stroke::is_plain(st) || !flat_paint { let region = stroke::outline_region(&PathData::from_bezpath(bp), rule, st).to_bezpath(); self.fill(®ion, FillRule::NonZero, &st.paint, st.opacity, st.paint_bounds(bounds)); return; } let Some(rgb) = self.flat(&st.paint) else { return }; self.check_opacity(st.opacity); let pen = Pen { rgb, width: st.width, cap: st.cap, join: st.join, miter: st.miter_limit, dash: st.dash.clone().filter(|d| d.is_dashed()) }; self.ops.push(Op::Stroke { path: bp.clone(), pen }); } fn brush_art(&mut self, bp: &BezPath, st: &StrokeLayer) -> Option> { let doc = self.doc; let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc)); let b = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name))?; Some(vectorcraft_brush::stroke_pieces(doc, b, bp, st)) } /// A placed image, decoded once per image. fn image(&mut self, im: &ImageObject) { let Some(img) = self.decoded(&im.key) else { return }; let (dw, dh) = (f64::from(img.width), f64::from(img.height)); let xf = im.xf * Affine::scale_non_uniform(f64::from(im.width.max(1)) / dw, f64::from(im.height.max(1)) / dh); self.ops.push(Op::Image { image: img, xf, opacity: self.opacity.clamp(0.0, 1.0) }); } /// Image `key`'s pixels (scaled down to what a metafile stores), `None` with a warning when it /// has none or can't be decoded. fn decoded(&mut self, key: &str) -> Option> { if let Some(i) = self.images.get(key) { return i.clone(); } let blob = self.doc.images.get(key).filter(|b| !b.bytes.is_empty()); let img = blob .and_then(|b| image::load_from_memory(&b.bytes).ok()) .map(|i| Rgba::fitted(i.to_rgba8())) .filter(|i| i.width > 0 && i.height > 0) .map(Arc::new); if img.is_none() { self.warn(&format!("image '{key}' could not be decoded and was left out")); } self.images.insert(key.to_string(), img.clone()); img } }