Files
vectorcraft/crates/engine/src/cmd/flatten/compose.rs
T
dotCooCoo c6e29fa227 PDF export: PDF/X-1a, X-3 and 1.3 keep the art under the hidden part of cropped art
Flattening for PDF/X-1a, PDF/X-3, PDF 1.3 and EPS rasterized a picture,
gradient, pattern, mesh or raster effect over its whole frame when a
clipping mask cropped it. The hidden part came out as opaque white and
covered the art under it, on the same page and on the next one.

- Flattening rasterizes clipped art only where its clipping path shows
  it, nested clip groups included.
- Art in a clipping layer is flattened as the layer's clipping path
  shows it. A flattened group that also holds art from outside the
  layer takes the place of that art, so the layer's clipping path no
  longer cuts it.

Documents in which no clip group hides flattened art, and no clipping
layer holds it, export the same bytes as before.
2026-11-10 17:44:40 -08:00

281 lines
12 KiB
Rust

use super::analysis::{has_effects, masked, reach};
use super::{BlendMode, Color, FillRule, Knockout, Node, NodeKind, Paint, PathData, Rect, cmyk_planes, composite, effects, node_path, po, shapes};
// ---------- the compositor ----------
/// What an arrangement shape stands for.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) enum Kind {
/// Solid fills and clipping paths: regions inside get a flat colour.
Vector,
/// A gradient or mesh: regions inside are rasterized at the gradient resolution.
Gradient,
/// Anything else that has no single colour (patterns, images, masks, raster effects).
Other,
}
/// A solid fill as the compositor uses it.
pub(super) struct Fill {
/// What it composites: its screen colour (twice), or in a CMYK document its two ink planes
/// ([`cmyk_planes`]).
pub(super) planes: [[f32; 3]; 2],
opacity: f32,
blend: BlendMode,
pub(super) paint: Paint,
pub(super) overprint: bool,
}
/// One object of the art being flattened, with the transparency the renderer gives it.
pub(super) struct Elem {
opacity: f32,
blend: BlendMode,
isolate: bool,
knockout: Knockout,
knockout_shape: bool,
/// Blending inside reaches the backdrop ([`Node::blends_through`]).
blends: bool,
/// Inside a knockout group its children are elements of their own ([`Node::passes_knockout_through`]).
pass_through: bool,
body: Body,
}
enum Body {
/// Solid fills (bottom first) of arrangement shape `shape`.
Fills { shape: usize, fills: Vec<Fill> },
/// A group's children, clipped to arrangement shape `clip` in a clip group.
Group { clip: Option<usize>, children: Vec<Elem> },
/// Art without a flat colour: its regions are rasterized, so it never composites here.
Complex,
}
/// Builds the compositor's objects and the arrangement shapes they cover.
#[derive(Default)]
pub(super) struct Builder {
pub(super) shapes: Vec<po::Shape>,
pub(super) kinds: Vec<Kind>,
/// Per shape, the clipping path of the innermost clip group it is in (a clipping path's own
/// entry is the clip group around its group).
pub(super) clip_of: Vec<Option<usize>>,
/// The clipping path of the clip group being built.
pub(super) clip: Option<usize>,
/// Building a CMYK document's art.
pub(super) cmyk: bool,
}
impl Builder {
/// What colour `c` composites as ([`Fill::planes`]).
fn planes(&self, c: &Color) -> [[f32; 3]; 2] {
if self.cmyk {
let cms = vectorcraft_color::cms::active();
cmyk_planes(cms.to_cmyk(c, cms.settings().intent))
} else {
[c.to_rgb(); 2]
}
}
fn shape(&mut self, path: PathData, rule: FillRule, kind: Kind) -> Option<usize> {
path.bounds()?;
self.shapes.push(po::Shape::new(path, rule, self.shapes.len() as u64));
self.kinds.push(kind);
self.clip_of.push(self.clip);
Some(self.shapes.len() - 1)
}
/// Whether a clip group hides shape `shape` in the region `sources` cover (ascending): the
/// region lies outside one of the clipping paths around the shape.
pub(super) fn hidden(&self, shape: usize, sources: &[usize]) -> bool {
let around = |shape: usize| self.clip_of.get(shape).copied().flatten();
let mut clip = around(shape);
while let Some(c) = clip {
if sources.binary_search(&c).is_err() {
return true;
}
clip = around(c);
}
false
}
pub(super) fn elem(&mut self, n: &Node) -> Option<Elem> {
if !n.visible {
return None;
}
let body = if masked(n) || has_effects(n) {
self.shape(shapes::rectangle(reach(n)?), FillRule::NonZero, Kind::Other)?;
Body::Complex
} else {
match &n.kind {
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
let (clip, rest) = if n.clips() {
let (first, rest) = children.split_first()?;
// Nothing to clip by hides the clipped art.
let (bp, rule) = effects::clip_outline(first)?;
(Some(self.shape(PathData::from_bezpath(&bp), rule, Kind::Vector)?), rest)
} else {
(None, &children[..])
};
let outer = self.clip;
self.clip = clip.or(outer);
let children = rest.iter().filter_map(|c| self.elem(c)).collect();
self.clip = outer;
Body::Group { clip, children }
}
NodeKind::Path { guide: true, .. } => return None,
NodeKind::Path { .. } | NodeKind::Compound { .. } => {
let (path, rule) = node_path(n)?;
let mut fills = vec![];
for item in n.appearance.items.iter().filter(|i| i.is_fill() && i.visible() && !i.paint().is_none()) {
match item.paint() {
Paint::Solid { color, .. } => fills.push(Fill {
planes: self.planes(color),
opacity: item.opacity(),
blend: item.blend(),
paint: item.paint().clone(),
overprint: item.overprint(),
}),
p => {
let kind = if matches!(p, Paint::Gradient(_)) { Kind::Gradient } else { Kind::Other };
self.shape(path.clone(), rule, kind);
}
}
}
if fills.is_empty() { Body::Complex } else { Body::Fills { shape: self.shape(path, rule, Kind::Vector)?, fills } }
}
NodeKind::Image(im) => {
let frame = shapes::rectangle(Rect::new(0.0, 0.0, im.width as f64, im.height as f64)).transformed(im.xf);
self.shape(frame, FillRule::NonZero, Kind::Other)?;
Body::Complex
}
NodeKind::Mesh(_) => {
self.shape(shapes::rectangle(reach(n)?), FillRule::NonZero, Kind::Gradient)?;
Body::Complex
}
_ => {
self.shape(shapes::rectangle(reach(n)?), FillRule::NonZero, Kind::Other)?;
Body::Complex
}
}
};
Some(Elem {
opacity: n.opacity,
blend: n.blend,
isolate: n.isolate,
knockout: n.knockout,
knockout_shape: n.knockout_shape,
blends: n.blends_through(),
pass_through: n.passes_knockout_through(),
body,
})
}
}
/// A straight (not premultiplied) RGBA colour in 0..=1.
pub(super) type Px = [f32; 4];
pub(super) const CLEAR: Px = [0.0; 4];
pub(super) const WHITE: Px = [1.0; 4];
fn premul(p: Px) -> Px {
[p[0] * p[3], p[1] * p[3], p[2] * p[3], p[3]]
}
fn unpremul(q: Px) -> Px {
if q[3] <= 0.0 { CLEAR } else { [q[0] / q[3], q[1] / q[3], q[2] / q[3], q[3].min(1.0)] }
}
/// Composites the objects covering one atomic region exactly as the renderer composites pixels
/// (its transparency groups, non-isolated groups, knockout groups and clip groups): the same
/// paints cover the whole region, so one colour stands for all of it.
pub(super) struct Composer<'a> {
/// Arrangement shapes covering the region, ascending.
pub(super) sources: &'a [usize],
/// The last solid fill composited.
pub(super) top: Option<&'a Fill>,
/// Which of the fills' [`Fill::planes`] composites.
pub(super) plane: usize,
}
impl<'a> Composer<'a> {
fn inside(&self, shape: usize) -> bool {
self.sources.binary_search(&shape).is_ok()
}
/// `elems` (paint order) composited over `start`.
pub(super) fn eval(&mut self, elems: &'a [Elem], start: Px) -> Px {
elems.iter().fold(start, |acc, e| self.draw(e, acc, false, false, false))
}
/// `e` drawn over backdrop `b` inside a group that knocks out when `enclosing`. `as_shape`
/// draws it as its knockout shape (full object opacity); `nested` is drawing inside a layer,
/// where a non-isolated group can't copy its backdrop and is drawn isolated.
fn draw(&mut self, e: &'a Elem, b: Px, enclosing: bool, as_shape: bool, nested: bool) -> Px {
let opacity = if as_shape { 1.0 } else { e.opacity };
let ko = matches!(e.body, Body::Group { .. }) && e.knockout.resolve(enclosing);
if opacity >= 1.0 && e.blend == BlendMode::Normal && !e.isolate && !ko {
return self.content(e, b, ko, None, nested);
}
if e.blends && !e.isolate && !nested {
// Drawn over a copy of the backdrop, so blending inside reaches it.
let c = self.content(e, b, ko, Some(b), false);
if e.blend == BlendMode::Normal {
let (pb, pc) = (premul(b), premul(c));
return unpremul([0, 1, 2, 3].map(|i| pb[i] + (pc[i] - pb[i]) * opacity));
}
// The group's own colour: the backdrop's share taken out by the group's coverage.
let (pc, pa, pb) = (premul(c), premul(self.content(e, CLEAR, ko, None, false)), premul(b));
let keep = 1.0 - pa[3];
let g = [0, 1, 2].map(|i| (pc[i] - keep * pb[i]).clamp(0.0, pa[3]));
let g = unpremul([g[0], g[1], g[2], pa[3]]);
return composite(e.blend, b, [g[0], g[1], g[2], g[3] * opacity]);
}
let inner_nested = !(e.blends && (e.isolate || !nested));
let g = self.content(e, CLEAR, ko, None, inner_nested);
composite(e.blend, b, [g[0], g[1], g[2], g[3] * opacity])
}
/// What `e` draws over `b` inside its group (`ko`: its children knock each other out, against
/// `backdrop` when the group was drawn over a copy of it).
fn content(&mut self, e: &'a Elem, b: Px, ko: bool, backdrop: Option<Px>, nested: bool) -> Px {
match &e.body {
Body::Fills { shape, fills } if self.inside(*shape) => fills.iter().fold(b, |acc, f| {
self.top = Some(f);
let [r, g, b] = f.planes[self.plane];
composite(f.blend, acc, [r, g, b, f.opacity])
}),
Body::Group { clip, children } if clip.is_none_or(|c| self.inside(c)) => {
if ko {
self.knockout(children, b, backdrop)
} else {
children.iter().fold(b, |acc, c| self.draw(c, acc, false, false, nested))
}
}
_ => b,
}
}
/// The children of a knockout group over `b`: each first erases what the ones below it drew
/// by its knockout shape, then adds itself composited against the group's backdrop.
fn knockout(&mut self, children: &'a [Elem], b: Px, backdrop: Option<Px>) -> Px {
fn elements<'e>(children: &'e [Elem], out: &mut Vec<&'e Elem>) {
for c in children {
match &c.body {
Body::Group { children, .. } if c.pass_through => elements(children, out),
_ => out.push(c),
}
}
}
let mut els = vec![];
elements(children, &mut els);
let mut acc = premul(b);
for c in els {
let s = self.draw(c, CLEAR, true, !c.knockout_shape, true)[3];
let own = match backdrop {
Some(b0) => premul(self.draw(c, b0, true, false, true)).map(|v| v * s),
None => premul(self.draw(c, CLEAR, true, false, true)),
};
acc = [0, 1, 2, 3].map(|i| acc[i] * (1.0 - s) + own[i]);
}
unpremul(acc)
}
}