Files
vectorcraft/crates/pdf/src/export/nodes.rs
T
dotCooCoo 84904020e8 Images: a colorized image's missing-link placeholder shows in its own colors
A linked 1-bit image mask from a .ai file keeps its fill color as a
colorize paint, so the image takes the color once its file is relinked.
Until then the image shows the missing-link placeholder, and the canvas
and PDF export colorized that placeholder too: its light tile turned
nearly transparent and its red cross took the fill color.

ImageBlob::takes_colorize is false for a preview whose pixels are not
grayscale. While it is false, the canvas draws the image and PDF export
writes it as it is, printed separations separate it as an uncolorized
image, and transparency flattening (EPS, PDF 1.3 and PDF/X-1a export,
Flatten Transparency) does not count its paint as transparency. The
paint stays on the image, and a grayscale preview is still colorized.
SVG and EPS export already write only grayscale images colorized.
2026-11-05 23:25:05 +07:00

600 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::exporter::{UNDECODED, address, blend, color, constant_mask, cover, in_range, norm, rects_overlap, rule, to_path, writable, xf};
use super::{
Affine, AppearanceItem, BezPath, BlendMode, Exporter, Fill, FillRule, LineCap, LineJoin, Mark, Node, NodeKind, NormalizedF32, Paint, Path, Rect,
Shape, Stroke, StrokeAlign, StrokeDash, StrokeLayer, Surface, WrittenShape, krilla, stroke,
};
impl Exporter<'_> {
/// Opacity mask → luminosity soft mask. Outside the art the backdrop is black (clip) or white;
/// invert is a white Difference rect on top (luminance is linear, so luma(1 − c) = 1 − luma(c)).
fn soft_mask(&mut self, s: &mut Surface, m: &vectorcraft_doc::OpacityMask, page: Rect) -> Option<krilla::mask::Mask> {
let mut sb = s.stream_builder();
let mut ms = sb.surface();
// An opaque backdrop when it matters: white outside the art (no clip), or black for the
// inverting Difference pass below to turn white.
if !m.clip || m.invert {
cover(&mut ms, page, if m.clip { 0 } else { 255 }, 1.0);
}
// Mask art is a picture of its own: it takes no part in a knockout group around the object,
// and has no inks to overprint.
let knockout = std::mem::take(&mut self.knockout);
let overprint = std::mem::take(&mut self.overprint);
let image_left_out = self.image_left_out.replace(false);
self.node(&mut ms, &m.art, page, true);
let incomplete = self.image_left_out.unwrap_or(false);
self.image_left_out = image_left_out.map(|outer| outer || incomplete);
(self.knockout, self.overprint) = (knockout, overprint);
if incomplete {
return None;
}
if m.invert {
ms.push_blend_mode(krilla::blend::BlendMode::Difference);
cover(&mut ms, page, 255, 1.0);
ms.pop();
}
ms.finish();
Some(krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Luminosity))
}
/// The children of a group, as the elements of a knockout group when it is one.
pub(super) fn children(&mut self, s: &mut Surface, children: &[std::sync::Arc<Node>], page: Rect) {
if !self.knockout {
for c in children {
self.node(s, c, page, false);
}
return;
}
// The PDF writer has no knockout groups: each element is drawn through a soft mask of
// where the elements above it don't paint (the same look for Normal blending). The masks
// nest, so element i sits inside the masks of elements i+1…n: open them outermost first.
self.warn("knockout groups are written as soft-masked groups (same look, but not editable as knockout groups)");
let elements: Vec<_> = Node::knockout_elements(children)
.into_iter()
.filter(|c| c.visual_bounds_with(&self.symbols).is_some_and(|b| rects_overlap(b.inflate(1.0, 1.0), page)))
.collect();
let Some((first, rest)) = elements.split_first() else { return };
for c in rest.iter().rev() {
let mask = self.knockout_mask(s, c, page);
s.push_mask(mask);
}
self.node(s, first, page, false);
for c in rest {
s.pop();
self.node(s, c, page, false);
}
}
/// A luminosity mask that is 1 − the knockout shape of `c`: white, then black through an alpha
/// mask of `c` (at full object opacity without its own mask, unless those define its shape).
fn knockout_mask(&mut self, s: &mut Surface, c: &Node, page: Rect) -> krilla::mask::Mask {
let shape = if c.knockout_shape {
c.clone()
} else {
let mut s = c.clone().with_mask(None);
s.opacity = 1.0;
s
};
let mut sb = s.stream_builder();
let mut ms = sb.surface();
cover(&mut ms, page, 255, 1.0);
let alpha = {
let mut ab = ms.stream_builder();
let mut als = ab.surface();
let overprint = std::mem::take(&mut self.overprint);
self.node(&mut als, &shape, page, false);
self.overprint = overprint;
als.finish();
krilla::mask::Mask::new(ab.finish(), krilla::mask::MaskType::Alpha)
};
ms.push_mask(alpha);
cover(&mut ms, page, 0, 1.0);
ms.pop();
ms.finish();
krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Luminosity)
}
pub(super) fn node(&mut self, s: &mut Surface, n: &Node, page: Rect, force: bool) {
if self.layers
&& self.in_layer != address(n)
&& let NodeKind::Layer { printable, .. } = n.kind
&& (printable || self.non_printing)
&& let Some(&i) = self.layer_index.get(&address(n))
{
// A PDF layer: hidden layers and sublayers are written too (their group is off).
let enclosing = std::mem::replace(&mut self.in_layer, address(n));
let paper = self.paper;
crate::forms::layer_form(s, i, paper, |s| self.node(s, n, page, true));
self.in_layer = enclosing;
return;
}
if !force && !n.visible {
return;
}
if let NodeKind::Layer { template, printable, .. } = n.kind
&& (template || !(printable || self.non_printing))
{
return;
}
match n.visual_bounds_with(&self.symbols) {
Some(b) if !force && !rects_overlap(b.inflate(1.0, 1.0), page) => return,
None if !n.is_container() => return,
_ => {}
}
self.warn_raster(&n.appearance.effects, || format!("{} objects", n.kind_label().to_lowercase()));
let container = n.is_container() && !matches!(n.kind, NodeKind::Compound { .. });
// Whether this container's children knock each other out (a knockout group is written as a group).
let knockout = n.knocks_out(self.knockout);
let enclosing = std::mem::replace(&mut self.knockout, knockout);
let mut pushes = 0;
if n.blend != BlendMode::Normal {
s.push_blend_mode(blend(n.blend));
pushes += 1;
}
if n.opacity < 1.0 || (container && (n.isolate || n.blend != BlendMode::Normal || knockout)) {
if !n.isolate && n.blends_through() {
// Not isolated: blending inside reaches the art below the group.
let alpha = constant_mask(s, page, n.opacity);
s.push_mask(alpha);
} else if n.opacity < 1.0 {
s.push_opacity(norm(n.opacity));
} else {
s.push_isolated();
}
pushes += 1;
}
let mask = n.extras().mask.as_deref().filter(|m| !m.disabled);
let alpha = match (&n.kind, mask) {
(NodeKind::Image(im), Some(m)) => self.masked_image(im, m, !n.extras().pixelated),
_ => None,
};
if let Some(m) = mask
&& alpha.is_none()
{
let Some(mask) = self.soft_mask(s, m, page) else {
self.knockout = enclosing;
for _ in 0..pushes {
s.pop();
}
return;
};
s.push_mask(mask);
pushes += 1;
}
match &n.kind {
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => self.children(s, children, page),
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => {
// The region every output clips to; nothing to clip by hides the clipped art. The
// clipping path's fill paints behind the clipped art and its stroke over it, unclipped.
if let Some((clip, rest)) = children.split_first()
&& let Some((p, r)) = vectorcraft_effects::clip_outline(clip).and_then(|(bp, r)| to_path(s, &bp).map(|p| (p, r)))
{
let paint = clip.clip_paint();
s.push_clip_path(&p, &rule(r));
if let Some(fill) = &paint.fill {
self.node(s, fill, page, false);
}
self.children(s, rest, page);
s.pop();
if let Some(stroke) = &paint.stroke {
self.node(s, stroke, page, false);
}
}
}
NodeKind::Path { path, rule, guide, .. } => {
if !*guide {
self.shape(s, n, &path.to_bezpath(), *rule, page);
}
}
NodeKind::Compound { children, rule } => {
let mut bp = BezPath::new();
for c in children {
if let Some(p) = c.path_data() {
bp.extend(p.to_bezpath());
}
}
self.shape(s, n, &bp, *rule, page);
}
NodeKind::Text(t) => self.text(s, n, t, page),
NodeKind::Image(im) => match n.extras().colorize.as_deref() {
Some(p @ Paint::Solid { .. }) if self.doc.image_blob(im).is_none_or(vectorcraft_doc::ImageBlob::takes_colorize) => {
if writable(s, im.xf, Rect::new(0.0, 0.0, im.width.max(1).into(), im.height.max(1).into())) {
if let Some(colorized) = self.colorized_image(im, p, !n.extras().pixelated) {
self.image(s, im, Some(colorized), n.extras().pixelated);
} else {
self.image_left_out = self.image_left_out.map(|_| true);
self.warn(UNDECODED);
}
} else if self.image_left_out.is_some() {
self.image_left_out = Some(true);
self.warn(super::exporter::UNWRITABLE_IMAGE);
}
}
_ => self.image(s, im, alpha, n.extras().pixelated),
},
NodeKind::SymbolInstance { symbol, xf } => {
// Within the objects a frame may draw of symbol instances, as on screen: nested
// instances multiply.
// Placing through 9-slice guides needs room for the most objects it can make
// (`NineSliceMap::most_placed`), and the objects it makes count.
// A dynamic symbol's instance shows its own appearance changes.
let doc = self.doc;
if let Some(sym) = doc.symbols.iter().find(|x| &x.name == symbol) {
let changed;
let art: &Node = if n.extras().symbol_overrides.is_empty() {
&sym.art
} else {
changed = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
&changed
};
let count = art.count() as u64;
let map = doc.symbol_slice_map(symbol, *xf);
let most = map.map_or(count, |m| m.most_placed(art, doc));
let placed = vectorcraft_doc::charge_symbol_art(&mut self.symbol_nodes, most, || {
let art = match map {
Some(m) => m.place(art, doc),
None => {
let mut a = art.clone();
a.transform(*xf, false);
a
}
};
// As `most_placed`, placed art counts its opacity-mask art.
let count = if map.is_some() { art.count_with_masks() } else { art.count() } as u64;
(art, count)
});
if let Some(art) = placed {
self.node(s, &art, page, true);
}
}
}
// A gradient mesh is a smooth patch shading where it can be written as one.
NodeKind::Mesh(m) if self.mesh(s, m) => {}
// Live blends/envelopes/meshes export their evaluated (expanded) form.
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(s, c, page, false);
}
}
}
self.knockout = enclosing;
for _ in 0..pushes {
s.pop();
}
}
/// Gradient mesh `m` as one PDF Type 6 (Coons patch) shading filling its outline (ISO 32000-1,
/// 8.7.4.5.7): each patch blends its corner colours bilinearly, as the canvas draws it, and
/// the viewer or RIP does so smoothly at its own resolution. Cutting the mesh into flat quads
/// instead banded, left seams between them, and made files with many meshes very large.
/// Corner colours are written as [`Self::page_color`] writes any colour; RGB among other
/// models goes through RGB, as the writer needs one colour space a shading (ink-based ones:
/// CMYK and grey, merge). `false` when it isn't written so: the caller draws it expanded.
/// That is a mesh with see-through points (their opacities would be a soft mask of another
/// patch shading, which a common viewer, Poppler, draws with gaps and too opaque; the quads
/// carry their own opacity), an invalid mesh, or one beyond the coordinates a file holds.
fn mesh(&mut self, s: &mut Surface, m: &vectorcraft_doc::live::GradientMesh) -> bool {
if !m.is_valid() || !m.points.iter().all(|p| p.opacity >= 1.0) {
return false;
}
let Some(bounds) = m.bounds() else { return false };
if !writable(s, Affine::IDENTITY, bounds) {
return false;
}
let Some(path) = to_path(s, &m.outline().to_bezpath()) else { return false };
let page: Vec<vectorcraft_color::Color> = m.points.iter().map(|p| self.page_color(&p.color)).collect();
let mixed = page.windows(2).any(|w| w[0].model() != w[1].model());
let rgb = page.iter().any(|c| matches!(c, vectorcraft_color::Color::Rgb { .. } | vectorcraft_color::Color::Lab { .. }));
let colors: Vec<krilla::color::Color> =
page.iter().map(|c| if mixed && rgb { color(&c.in_model(vectorcraft_color::cms::Model::Rgb)) } else { color(c) }).collect();
let pt = |p: vectorcraft_geom::Point| (p.x as f32, p.y as f32);
let mut patches = Vec::with_capacity(m.rows as usize * m.cols as usize);
for r in 0..m.rows as usize {
for c in 0..m.cols as usize {
// The corners in the order a Type 6 shading lists them: down the left edge (v),
// along the bottom (u), back up the right edge and back along the top.
let corners = [m.idx(r, c), m.idx(r + 1, c), m.idx(r + 1, c + 1), m.idx(r, c + 1)];
let (left, bottom, right, top) = (m.edge_v(r, c), m.edge_u(r + 1, c), m.edge_v(r, c + 1), m.edge_u(r, c));
let points =
[left.p0, left.p1, left.p2, left.p3, bottom.p1, bottom.p2, bottom.p3, right.p2, right.p1, right.p0, top.p2, top.p1].map(pt);
let Some(colors) =
corners.iter().map(|&i| colors.get(i).cloned()).collect::<Option<Vec<_>>>().and_then(|v| <[_; 4]>::try_from(v).ok())
else {
return false;
};
patches.push(krilla::paint::MeshPatch { points, colors, opacities: [NormalizedF32::ONE; 4] });
}
}
let mesh = krilla::paint::MeshGradient { patches, transform: xf(Affine::IDENTITY), anti_alias: false };
s.set_stroke(None);
s.set_fill(Some(Fill { paint: mesh.into(), opacity: NormalizedF32::ONE, rule: krilla::paint::FillRule::NonZero }));
s.draw_path(&path);
s.set_fill(None);
true
}
fn shape(&mut self, s: &mut Surface, n: &Node, bp: &BezPath, r: FillRule, page: Rect) {
let Some(path) = to_path(s, bp) else { return };
let bounds = bp.bounding_box();
for item in &n.appearance.items {
match item {
AppearanceItem::Fill(fl) => {
if !fl.visible || fl.paint.is_none() {
continue;
}
self.warn_raster(&fl.effects, || "fills".into());
self.overprinting(s, fl.overprint, &fl.paint, |ex, s| {
if ex.area(s, &path, rule(r), &fl.paint, (fl.opacity, fl.blend), bounds, bounds, true) {
return;
}
let Some(paint) = ex.paint(s, &fl.paint, bounds) else { return };
let bl = fl.blend != BlendMode::Normal;
if bl {
s.push_blend_mode(blend(fl.blend));
}
s.set_stroke(None);
s.set_fill(Some(Fill { paint, opacity: norm(fl.opacity), rule: rule(r) }));
s.draw_path(&path);
if bl {
s.pop();
}
});
}
AppearanceItem::Stroke(st) => {
if !st.visible || st.paint.is_none() || st.width <= 0.0 {
continue;
}
self.warn_raster(&st.effects, || "strokes".into());
self.overprinting(s, st.overprint, &st.paint, |ex, s| ex.stroke(s, bp, &path, r, st, page, bounds));
}
}
}
s.set_fill(None);
s.set_stroke(None);
}
/// Draw a fill or stroke of `paint` with `draw`: as a form marked to overprint when it
/// `overprints`, paints and overprints are kept (but not a white one with Discard White
/// Overprint).
pub(super) fn overprinting(&mut self, s: &mut Surface, overprints: bool, paint: &Paint, draw: impl FnOnce(&mut Self, &mut Surface)) {
let white = || self.doc.setup.discard_white_overprint && vectorcraft_doc::overprint::is_white(paint);
if overprints && self.overprint && !paint.is_none() && !white() {
self.overprinted = true;
crate::forms::form(s, Mark::Overprint, |s| draw(self, s));
} else {
draw(self, s);
}
}
/// Is `p` a paint PDF has no paint for when it paints a fill (`fill`) or a stroke, which
/// [`Self::area`] draws as art: a pattern swatch or a gradient drawn as pixels
/// ([`vectorcraft_color::GradientPaint::draws_as_pixels`]), but not a Perceptual one of spot
/// inks ([`Self::perceptual_spot`])?
pub(super) fn area_paint(&self, p: &Paint, fill: bool) -> bool {
match p {
Paint::Pattern { pattern, .. } => self.doc.pattern(pattern).is_some(),
Paint::Gradient(g) => g.draws_as_pixels(fill) && !self.perceptual_spot(g, fill),
_ => false,
}
}
/// Paint the area `clip` (filled by `rule`; bounds `area`) with `p` at `transparency` (opacity,
/// blend mode) when it is an [area paint](Self::area_paint) of a fill (`fill`) or a stroke: a
/// pattern's [tiling pattern](Self::tiling) (else its tile instances covering it), or an image
/// of a gradient drawn as pixels placed on `bounds` (the painted object's box, as on the
/// canvas), clipped to it. False (nothing drawn) for other paints.
#[allow(clippy::too_many_arguments)]
pub(super) fn area(
&mut self,
s: &mut Surface,
clip: &Path,
rule: krilla::paint::FillRule,
p: &Paint,
transparency: (f32, BlendMode),
bounds: Rect,
area: Rect,
fill: bool,
) -> bool {
if !self.area_paint(p, fill) {
return false;
}
let (opacity, mode) = transparency;
let tiling = match p {
Paint::Pattern { pattern, xf } => self.tiling(s, pattern, *xf),
_ => None,
};
let mut pushes = 0;
if mode != BlendMode::Normal {
s.push_blend_mode(blend(mode));
pushes += 1;
}
if let Some(paint) = tiling {
s.set_stroke(None);
s.set_fill(Some(Fill { paint, opacity: norm(opacity), rule }));
s.draw_path(clip);
s.set_fill(None);
} else {
s.push_clip_path(clip, &rule);
pushes += 1;
if opacity < 1.0 {
s.push_opacity(norm(opacity));
pushes += 1;
}
let doc = self.doc;
match p {
Paint::Pattern { pattern, xf } => {
if let Some(def) = doc.pattern(pattern) {
self.warn("pattern fills nested more than 4 patterns deep, or with tiles too large for PDF, are written as their tiles");
for inst in def.instances_in(*xf, area) {
self.node(s, &inst, area, true);
}
}
}
Paint::Gradient(g) => self.field_image(s, g, bounds, area, fill),
_ => {}
}
}
for _ in 0..pushes {
s.pop();
}
true
}
/// Paint stroke `st` of the shape `bp` (`path`), whose geometric bounds `bounds` place its
/// unplaced gradients.
#[allow(clippy::too_many_arguments)]
pub(super) fn stroke(&mut self, s: &mut Surface, bp: &BezPath, path: &Path, r: FillRule, st: &StrokeLayer, page: Rect, bounds: Rect) {
if !stroke::is_plain(st) && self.brush_art(s, bp, st, page) {
return;
}
if self.area_paint(&st.paint, false) && st.path_gradient().is_none() {
// The area the stroke paints (dashes, caps, joins, profile, arrowheads, alignment), as
// Outline Stroke makes it, painted with the pattern's tiles or the gradient's image (a
// gradient along or across the stroke is written as slices below).
let region = stroke::outline_region(&vectorcraft_geom::PathData::from_bezpath(bp), r, st).to_bezpath();
if let Some(clip) = to_path(s, &region) {
let area = region.bounding_box();
self.area(s, &clip, krilla::paint::FillRule::NonZero, &st.paint, (st.opacity, st.blend), st.paint_bounds(bounds), area, false);
}
return;
}
let Some(paint) = self.paint(s, &st.paint, st.paint_bounds(bounds)) else { return };
let w = stroke::for_writer(bp, st);
// A stroke whose width, caps or miter spikes reach beyond [`MAX_COORD`] is left out.
if !writable(s, Affine::IDENTITY, w.reach(st, bp.control_box()).inflate(1.0, 1.0)) {
return;
}
let mut pushes = 0;
if st.blend != BlendMode::Normal {
s.push_blend_mode(blend(st.blend));
pushes += 1;
}
match w.side {
Some(StrokeAlign::Inside) => {
s.push_clip_path(path, &rule(r));
pushes += 1;
}
Some(StrokeAlign::Outside) => {
// Clip to everything outside the path: a frame around all the stroke reaches
// (miter spikes included) plus the path, even-odd.
let mut outside = w.reach(st, bounds).inflate(1.0, 1.0).to_path(0.1);
outside.extend(bp.iter());
if let Some(p) = to_path(s, &outside) {
s.push_clip_path(&p, &krilla::paint::FillRule::EvenOdd);
pushes += 1;
}
}
_ => {}
}
match &w.shape {
WrittenShape::Stroke { width } => {
let dash = st.dash.as_ref().filter(|d| d.is_dashed()).map(|d| {
let mut pat: Vec<f32> = d.pattern.iter().map(|v| *v as f32).collect();
if pat.len() % 2 == 1 {
pat.extend(pat.clone());
}
// An offset beyond [`MAX_COORD`] is written as its remainder after whole
// periods of the pattern.
let period: f64 = pat.iter().copied().map(f64::from).sum();
let offset = if in_range(&[d.offset]) { d.offset } else { d.offset.rem_euclid(period) };
StrokeDash { array: pat, offset: if offset.is_finite() { offset as f32 } else { 0.0 } }
});
s.set_fill(None);
s.set_stroke(Some(Stroke {
paint,
width: *width as f32,
miter_limit: st.miter_limit.max(1.0) as f32,
line_cap: match st.cap {
LineCap::Butt => krilla::paint::LineCap::Butt,
LineCap::Round => krilla::paint::LineCap::Round,
LineCap::Square => krilla::paint::LineCap::Square,
},
line_join: match st.join {
LineJoin::Miter => krilla::paint::LineJoin::Miter,
LineJoin::Round => krilla::paint::LineJoin::Round,
LineJoin::Bevel => krilla::paint::LineJoin::Bevel,
},
opacity: norm(st.opacity),
dash,
}));
s.draw_path(path);
s.set_stroke(None);
}
WrittenShape::Fill(outlines) if st.path_gradient().is_some() => {
// A gradient along or across the stroke: slices clipped to its outlines, under
// the stroke's opacity as a group.
if let Some(ws) = stroke::written_slices(bp, r, st, outlines)
&& let Some(clip) = to_path(s, &ws.clip)
{
self.warn("gradients along or across strokes are exported as slices of linear gradients");
if st.opacity < 1.0 {
s.push_opacity(norm(st.opacity));
pushes += 1;
}
s.push_clip_path(&clip, &krilla::paint::FillRule::NonZero);
s.set_stroke(None);
for (shape, paint) in &ws.slices {
let b = shape.bounding_box();
if let (Some(paint), Some(p)) = (self.paint(s, paint, b), to_path(s, shape)) {
s.set_fill(Some(Fill { paint, opacity: NormalizedF32::ONE, rule: krilla::paint::FillRule::NonZero }));
s.draw_path(&p);
}
}
s.set_fill(None);
s.pop();
}
}
WrittenShape::Fill(outlines) => {
// The line and its arrowheads overlap: they take the opacity once, as a group.
let grouped = outlines.len() > 1 && st.opacity < 1.0;
if grouped {
s.push_opacity(norm(st.opacity));
pushes += 1;
}
let opacity = if grouped { NormalizedF32::ONE } else { norm(st.opacity) };
s.set_stroke(None);
s.set_fill(Some(Fill { paint, opacity, rule: krilla::paint::FillRule::NonZero }));
for outline in outlines {
if let Some(p) = to_path(s, outline) {
s.draw_path(&p);
}
}
s.set_fill(None);
}
}
for _ in 0..pushes {
s.pop();
}
}
/// Paint stroke `st` of the shape `bp` with its brush art (the stroke's opacity and blend mode
/// over all of it). False when it has no known brush.
fn brush_art(&mut self, s: &mut Surface, bp: &BezPath, st: &StrokeLayer, page: Rect) -> bool {
let doc = self.doc;
let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc));
let Some(b) = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name)) else { return false };
let art = vectorcraft_brush::stroke_pieces(doc, b, bp, st);
let mut pushes = 0;
if st.blend != BlendMode::Normal {
s.push_blend_mode(blend(st.blend));
pushes += 1;
}
if st.opacity < 1.0 {
s.push_opacity(norm(st.opacity));
pushes += 1;
}
for piece in &art {
self.node(s, piece, page, false);
}
for _ in 0..pushes {
s.pop();
}
true
}
}