mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 15:57:49 +00:00
A symbol instance colored with the Symbol Stainer lost its stain in export whenever the canvas's stained art was not used: - An instance with geometry effects was reshaped for export from the symbol's art with only its dynamic changes, so SVG, PDF, EPS, .ai, DXF, EMF/WMF and HPGL wrote the unstained colors. A color adjustment on an instance recolored the unstained art the same way. - PDF, EPS, legacy .ai, DXF and EMF/WMF wrote an instance without effects from the unstained art. tint_node, stain and instance_art now live in vectorcraft_doc::symbol_overrides, which both the effects and the exporters can use; vectorcraft_brush re-exports them. Baking, color adjustments and the writers above now use instance_art: dynamic changes first, then the stain.
744 lines
37 KiB
Rust
744 lines
37 KiB
Rust
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, GradientKind, LineCap, LineJoin, Mark, Node, NodeKind, NormalizedF32,
|
||
Paint, Path, Rect, Shape, Stroke, StrokeAlign, StrokeDash, StrokeLayer, Surface, WrittenShape, krilla, stroke,
|
||
};
|
||
|
||
/// An object's visual box for leaving out what is off the page. A plain group or layer also holds
|
||
/// the boxes of the members it is measured through ([`Node::editing_children`], in order), so that
|
||
/// the walk measures them once.
|
||
struct Measured {
|
||
bounds: Option<Rect>,
|
||
members: Vec<Measured>,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
std::thread_local! {
|
||
/// The objects this thread has measured for the walk and for the links (tests).
|
||
pub(crate) static MEASURES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||
}
|
||
|
||
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 transform = s.group_transform();
|
||
let mut sb = s.stream_builder();
|
||
let mut ms = Surface::stream(sb.surface());
|
||
ms.push_transform(&transform);
|
||
// 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 {
|
||
ms.pop();
|
||
ms.finish();
|
||
return None;
|
||
}
|
||
if m.invert {
|
||
ms.push_blend_mode(krilla::blend::BlendMode::Difference);
|
||
cover(&mut ms, page, 255, 1.0);
|
||
ms.pop();
|
||
}
|
||
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) {
|
||
self.measured_children(s, children, page, &[]);
|
||
}
|
||
|
||
/// [`Self::children`] with the boxes of the last members in `measured`.
|
||
fn measured_children(&mut self, s: &mut Surface, children: &[std::sync::Arc<Node>], page: Rect, measured: &[Measured]) {
|
||
if !self.knockout {
|
||
let unmeasured = children.len().saturating_sub(measured.len());
|
||
for (i, c) in children.iter().enumerate() {
|
||
self.measured_node(s, c, page, false, i.checked_sub(unmeasured).and_then(|i| measured.get(i)));
|
||
}
|
||
return;
|
||
}
|
||
let elements: Vec<_> = Node::knockout_elements(children)
|
||
.into_iter()
|
||
.map(|c| (c, self.measure(c)))
|
||
.filter(|(_, m)| m.bounds.is_some_and(|b| rects_overlap(b.inflate(1.0, 1.0), page)))
|
||
.collect();
|
||
if !elements.is_empty() && elements.iter().all(|(c, _)| c.knockout_outline().is_some()) {
|
||
let pieces: Vec<&Node> = elements.iter().map(|(c, _)| &***c).collect();
|
||
if vectorcraft_effects::warp_pieces_translucent(self.doc, &pieces) {
|
||
return self.warp_pieces(s, &elements, page);
|
||
}
|
||
for (c, m) in &elements {
|
||
self.measured_node(s, c, page, false, Some(m));
|
||
}
|
||
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 Some(((first, measured), rest)) = elements.split_first() else { return };
|
||
for (c, m) in rest.iter().rev() {
|
||
let mask = self.knockout_mask(s, c, page, m);
|
||
s.push_mask(mask);
|
||
}
|
||
self.measured_node(s, first, page, false, Some(measured));
|
||
for (c, m) in rest {
|
||
s.pop();
|
||
self.measured_node(s, c, page, false, Some(m));
|
||
}
|
||
}
|
||
|
||
/// [`Self::measured_children`] of a plain group or layer. When they are the pieces of a
|
||
/// mesh-warped image, every piece, on the page or not, takes part in the one size its pixels
|
||
/// are resampled for ([`Self::warp_image_size`]).
|
||
fn group_children(&mut self, s: &mut Surface, children: &[std::sync::Arc<Node>], page: Rect, measured: &[Measured]) {
|
||
let outer = self.warp_image_size(children).map(|w| self.warp_size.replace(w));
|
||
self.measured_children(s, children, page, measured);
|
||
if let Some(outer) = outer {
|
||
self.warp_size = outer;
|
||
}
|
||
}
|
||
|
||
/// A luminosity mask that is 1 − the knockout shape of `c`, whose box is in `measured`: 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, measured: &Measured) -> 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
|
||
};
|
||
self.shapes_mask(s, &[(&shape, Some(measured))], page)
|
||
}
|
||
|
||
/// The pieces of a partly transparent mesh-warped image (`elements`, each with its box) as the
|
||
/// elements of a knockout group ([`vectorcraft_effects::warp_pieces_translucent`]). Each piece
|
||
/// is drawn through one soft mask of where the pieces above it that reach its box don't cover
|
||
/// ([`vectorcraft_effects::knockout_silhouette`]): only neighbors overlap, so each mask holds a
|
||
/// few triangles and no mask nests in another. A piece above that overlaps it only over opaque
|
||
/// pixels ([`vectorcraft_effects::WarpSeams`]) is left out of its mask.
|
||
fn warp_pieces(&mut self, s: &mut Surface, elements: &[(&std::sync::Arc<Node>, Measured)], page: Rect) {
|
||
// Each piece's triangle and its box: a piece paints inside its triangle only.
|
||
let silhouette = |c: &Node| {
|
||
let b = c.knockout_outline()?.geometric_bounds()?;
|
||
Some((vectorcraft_effects::knockout_silhouette(c, vectorcraft_doc::color::Color::WHITE)?, b))
|
||
};
|
||
let silhouettes: Vec<Option<(Node, Rect)>> = elements.iter().map(|(c, _)| silhouette(c)).collect();
|
||
let pieces: Vec<&Node> = elements.iter().map(|(c, _)| &***c).collect();
|
||
let seams = vectorcraft_effects::WarpSeams::new(self.doc, &pieces);
|
||
for (i, (c, measured)) in elements.iter().enumerate() {
|
||
let Some((_, reach)) = &silhouettes[i] else { continue };
|
||
let reach = reach.inflate(1.0, 1.0);
|
||
let shapes: Vec<(&Node, Option<&Measured>)> = silhouettes
|
||
.iter()
|
||
.enumerate()
|
||
.skip(i + 1)
|
||
.filter_map(|(j, s)| s.as_ref().filter(|(_, b)| rects_overlap(*b, reach) && seams.shows_through(i, j)))
|
||
.map(|(n, _)| (n, None))
|
||
.collect();
|
||
if shapes.is_empty() {
|
||
self.measured_node(s, c, page, false, Some(measured));
|
||
continue;
|
||
}
|
||
let mask = self.shapes_mask(s, &shapes, page);
|
||
s.push_mask(mask);
|
||
self.measured_node(s, c, page, false, Some(measured));
|
||
s.pop();
|
||
}
|
||
}
|
||
|
||
/// A luminosity mask that is 1 − the coverage of `shapes` (each with its box when measured):
|
||
/// white, then black through an alpha mask of the shapes drawn one over another.
|
||
fn shapes_mask(&mut self, s: &mut Surface, shapes: &[(&Node, Option<&Measured>)], page: Rect) -> krilla::mask::Mask {
|
||
let transform = s.group_transform();
|
||
let mut sb = s.stream_builder();
|
||
let mut ms = Surface::stream(sb.surface());
|
||
ms.push_transform(&transform);
|
||
cover(&mut ms, page, 255, 1.0);
|
||
let alpha = {
|
||
let transform = ms.group_transform();
|
||
let mut ab = ms.stream_builder();
|
||
let mut als = Surface::stream(ab.surface());
|
||
als.push_transform(&transform);
|
||
let overprint = std::mem::take(&mut self.overprint);
|
||
for (shape, measured) in shapes {
|
||
self.measured_node(&mut als, shape, page, false, *measured);
|
||
}
|
||
self.overprint = overprint;
|
||
als.pop();
|
||
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.pop();
|
||
ms.finish();
|
||
krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Luminosity)
|
||
}
|
||
|
||
/// The box of `n`, and of the members of a plain group or layer.
|
||
fn measure(&self, n: &Node) -> Measured {
|
||
if !matches!(n.kind, NodeKind::Layer { clip: false, .. } | NodeKind::Group { clip: false, .. }) {
|
||
#[cfg(test)]
|
||
MEASURES.with(|m| m.set(m.get() + 1));
|
||
return Measured { bounds: n.visual_bounds_with(&self.symbols), members: vec![] };
|
||
}
|
||
let members: Vec<_> = n.editing_children().iter().map(|c| self.measure(c)).collect();
|
||
Measured { bounds: n.visual_of_members(members.iter().map(|m| m.bounds)), members }
|
||
}
|
||
|
||
pub(super) fn node(&mut self, s: &mut Surface, n: &Node, page: Rect, force: bool) {
|
||
self.measured_node(s, n, page, force, None);
|
||
}
|
||
|
||
/// [`Self::node`] with the box of `n` in `measured`, when its parent measured it.
|
||
fn measured_node(&mut self, s: &mut Surface, n: &Node, page: Rect, force: bool, measured: Option<&Measured>) {
|
||
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;
|
||
}
|
||
// A container written whatever its box is has no use for one.
|
||
let own = (measured.is_none() && !(force && n.is_container())).then(|| self.measure(n));
|
||
let measured = measured.or(own.as_ref());
|
||
match measured.map(|m| m.bounds) {
|
||
Some(Some(b)) if !force && !rects_overlap(b.inflate(1.0, 1.0), page) => return,
|
||
Some(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.group_children(s, children, page, measured.map_or(&[], |m| m.members.as_slice()));
|
||
}
|
||
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(), self.doc.shown_image(n, im)) {
|
||
(Some(p @ Paint::Solid { .. }), im) if self.doc.image_blob(&im).is_none_or(vectorcraft_doc::ImageBlob::takes_colorize) => {
|
||
let im = &*im;
|
||
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);
|
||
}
|
||
}
|
||
(_, im) => 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, and the instance's
|
||
// fill stains its art, as on the canvas.
|
||
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() && vectorcraft_doc::symbol_overrides::stain(n).is_none() {
|
||
&sym.art
|
||
} else {
|
||
changed = vectorcraft_doc::symbol_overrides::instance_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 within = self.symbol_nodes.saturating_add(most) <= self.symbol_limit;
|
||
let placed = within.then(|| {
|
||
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.flatten() {
|
||
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);
|
||
}
|
||
}
|
||
// Ruler guides draw on screen only.
|
||
NodeKind::RulerGuide { .. } => {}
|
||
}
|
||
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`]), a conic gradient that can't be a sweep shading (in
|
||
/// PDF/A, or with more than [`super::paint::MAX_SWEEP_STOPS`] stops), a diamond gradient
|
||
/// (four linear gradients, [`Self::diamond`]), or a linear or radial gradient that repeats or
|
||
/// reflects (its stops repeated over the area, [`Self::padded`])?
|
||
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) => {
|
||
let sweep_unwritable = || !self.sweeps || g.gradient.expanded().nth(super::paint::MAX_SWEEP_STOPS).is_some();
|
||
(g.draws_as_pixels(fill) && !self.perceptual_spot(g, fill))
|
||
|| (g.gradient.kind == GradientKind::Conic && sweep_unwritable())
|
||
|| g.gradient.kind == GradientKind::Diamond
|
||
|| (g.gradient.is_spread() && matches!(g.gradient.kind, GradientKind::Linear | GradientKind::Radial))
|
||
}
|
||
_ => 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), a diamond
|
||
/// gradient's four linear gradients, a repeating or reflecting linear or radial gradient's
|
||
/// stops repeated over `area`, 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) => {
|
||
let pixels = g.draws_as_pixels(fill) && !self.perceptual_spot(g, fill);
|
||
// A linear or radial gradient that repeats or reflects is drawn by `padded`.
|
||
let drawn = !pixels && self.padded(s, g, bounds, area);
|
||
if !drawn && (pixels || g.gradient.kind != GradientKind::Diamond || !self.diamond(s, g, bounds, area)) {
|
||
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, ®ion) {
|
||
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
|
||
}
|
||
}
|