mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
EPS export expanded the pattern fills whose tile images need flattening and then flattened the result with flatten_document, which expanded them again with a new object limit. PostScript printing did the same, on the layers shown rather than the layers that print. EPS export now flattens the expanded document without expanding it again (flatten_expanded), and PostScript printing leaves the expansion to the flattener, which runs on the layers that print, and only checks for such pattern fills to warn.
499 lines
21 KiB
Rust
499 lines
21 KiB
Rust
use super::analysis::{Scratch, outlined_text, overlapping, reach};
|
|
use super::compose::{Builder, CLEAR, Composer, Elem, Fill, Kind, WHITE};
|
|
use super::options::FlattenOptions;
|
|
use super::{
|
|
Affine, Appearance, AppearanceItem, Arc, BlendMode, Color, ColorMode, Document, EngineError, FillLayer, FillRule, ImageBlob, ImageObject, Inks,
|
|
MAX_PIXELS, Node, NodeId, NodeKind, Paint, PathData, Rect, Result, effects, isolated_doc, outline_strokes_under, planes_cmyk, po, separate,
|
|
shape_node, shapes, unique_key,
|
|
};
|
|
|
|
// ---------- plan ----------
|
|
|
|
/// What the flattening does, worked out before the document changes.
|
|
pub(super) struct Plan {
|
|
pub(super) flat: Vec<Flat>,
|
|
/// Objects without transparency (they stay, but for the outline options).
|
|
pub(super) kept: Vec<NodeId>,
|
|
/// The objects with transparency of their own.
|
|
pub(super) transparent: Vec<NodeId>,
|
|
}
|
|
|
|
/// One group of overlapping objects with transparency and what replaces it.
|
|
pub(super) struct Flat {
|
|
/// Its objects in paint order; the result takes the place of the last.
|
|
pub(super) roots: Vec<NodeId>,
|
|
pub(super) regions: Regions,
|
|
/// Where its image goes, and the image (once rendered).
|
|
pub(super) area: Option<Area>,
|
|
raster: Option<Raster>,
|
|
}
|
|
|
|
/// Where a group's image goes.
|
|
pub(super) struct Area {
|
|
rect: Rect,
|
|
ppi: f64,
|
|
/// The complex regions it is clipped to (Clip Complex Regions).
|
|
clip: Option<PathData>,
|
|
/// The raster/vector balance rasterizes the whole group (it splits into too many regions).
|
|
pub(super) whole: bool,
|
|
}
|
|
|
|
impl Area {
|
|
/// The area's outline.
|
|
pub(super) fn outline(&self) -> PathData {
|
|
self.clip.clone().unwrap_or_else(|| shapes::rectangle(self.rect))
|
|
}
|
|
}
|
|
|
|
/// Flat-colour regions: their outlines and paints.
|
|
pub(super) type Regions = Vec<(PathData, RegionFill)>;
|
|
|
|
/// A flat-colour region's paint.
|
|
pub(super) struct RegionFill {
|
|
paint: Paint,
|
|
alpha: f32,
|
|
overprint: bool,
|
|
}
|
|
|
|
/// A rendered image of complex regions, clipped to them or not.
|
|
struct Raster {
|
|
blob: ImageBlob,
|
|
width: u32,
|
|
height: u32,
|
|
xf: Affine,
|
|
clip: Option<PathData>,
|
|
}
|
|
|
|
/// The flattening of `roots` with `o`; `render` renders the images (the preview only needs where
|
|
/// they go). Fails only when an image can't be encoded.
|
|
pub(super) fn plan(src: &Document, roots: &[NodeId], o: &FlattenOptions, render: bool) -> Result<Plan> {
|
|
let tmp = isolated_doc(src, roots.iter().filter_map(|id| src.node(*id).cloned()).collect());
|
|
// Geometry effects first: what is left on the art are raster effects.
|
|
let baked = effects::bake_document(&tmp).unwrap_or(tmp);
|
|
let art: Vec<Node> = baked.layers[0].children().into_iter().flatten().map(|a| (**a).clone()).collect();
|
|
let mut sc = Scratch { brushes: vectorcraft_brush::library(&baked), doc: baked.clone() };
|
|
let plain: Vec<Node> = art.iter().map(|n| sc.plain(n)).collect();
|
|
let reaches: Vec<Option<Rect>> = plain.iter().map(reach).collect();
|
|
let see_through: Vec<bool> = plain.iter().map(|n| o.shows_transparency(&baked, n) || o.draws_see_through_image(n)).collect();
|
|
let cmyk = src.color_mode == ColorMode::Cmyk;
|
|
let mut out = Plan { flat: vec![], kept: vec![], transparent: roots.iter().zip(&see_through).filter(|(_, t)| **t).map(|(id, _)| *id).collect() };
|
|
// Baking above stopped early when cancelled; each group is a place to stop (a plan changes nothing).
|
|
vectorcraft_doc::cancel::check()?;
|
|
for g in overlapping(&reaches) {
|
|
vectorcraft_doc::cancel::check()?;
|
|
let ids: Vec<NodeId> = g.iter().map(|&i| roots[i]).collect();
|
|
let group = g.iter().any(|&i| see_through[i]).then(|| {
|
|
let plain: Vec<&Node> = g.iter().map(|&i| &plain[i]).collect();
|
|
flatten_group(&plain, o, cmyk)
|
|
});
|
|
let Some((regions, area)) = group.flatten() else {
|
|
out.kept.extend(ids);
|
|
continue;
|
|
};
|
|
let raster = match area.as_ref().filter(|_| render) {
|
|
Some(a) => {
|
|
let art: Vec<Node> = g.iter().map(|&i| art[i].clone()).collect();
|
|
Some(render_raster(&baked, &art, a.rect, a.ppi, a.clip.clone(), o)?)
|
|
}
|
|
None => None,
|
|
};
|
|
out.flat.push(Flat { roots: ids, regions, area, raster });
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
/// The regions and the image area replacing one group of overlapping objects (`plain`: the art as
|
|
/// composited; `cmyk`: in a CMYK document); `None` when it paints nothing.
|
|
fn flatten_group(plain: &[&Node], o: &FlattenOptions, cmyk: bool) -> Option<(Regions, Option<Area>)> {
|
|
let mut b = Builder { cmyk, ..Default::default() };
|
|
let elems: Vec<Elem> = plain.iter().filter_map(|n| b.elem(n)).collect();
|
|
if b.shapes.is_empty() {
|
|
return None;
|
|
}
|
|
// An unclipped full raster needs no regions.
|
|
let whole = o.balance <= 0.0 && !o.clip_complex_regions;
|
|
let regions = if whole { vec![] } else { po::regions(&b.shapes) };
|
|
// An arrangement that failed (degenerate geometry) leaves only rasterizing.
|
|
let all = regions.is_empty() || o.rasterize_all(regions.len());
|
|
let mut vector: Vec<(usize, RegionFill)> = vec![];
|
|
let mut complex: Vec<usize> = vec![];
|
|
let mut gradients_only = !all;
|
|
for (k, r) in regions.iter().enumerate() {
|
|
if all || r.sources.iter().any(|&i| b.kinds[i] != Kind::Vector) {
|
|
gradients_only &= r.sources.iter().all(|&i| b.kinds[i] == Kind::Gradient);
|
|
complex.push(k);
|
|
} else if let Some(fill) = region_fill(&elems, &r.sources, o, cmyk) {
|
|
vector.push((k, fill));
|
|
}
|
|
}
|
|
let bounds = |ks: &[usize]| ks.iter().filter_map(|&k| regions[k].path.bounds()).reduce(|a, b| a.union(b));
|
|
if !o.clip_complex_regions && !all {
|
|
// A rectangle of raster takes in every region it overlaps.
|
|
while let Some(r) = bounds(&complex) {
|
|
let before = complex.len();
|
|
let rect = shapes::rectangle(r);
|
|
vector.retain(|(k, _)| {
|
|
let path = ®ions[*k].path;
|
|
let hit = path.bounds().is_some_and(|b| b.intersect(r).area() > 0.0)
|
|
&& po::area(&po::boolean(path, FillRule::NonZero, &rect, FillRule::NonZero, po::BoolOp::Intersect), FillRule::NonZero) > 1e-6;
|
|
if hit {
|
|
complex.push(*k);
|
|
}
|
|
!hit
|
|
});
|
|
if complex.len() == before {
|
|
break;
|
|
}
|
|
gradients_only = false;
|
|
}
|
|
}
|
|
let raster_rect = if all { plain.iter().filter_map(|n| reach(n)).reduce(|a, b| a.union(b)) } else { bounds(&complex) };
|
|
let area = raster_rect.map(|rect| {
|
|
let clip = o.clip_complex_regions.then(|| PathData::new(complex.iter().flat_map(|&k| regions[k].path.subpaths.iter().cloned()).collect()));
|
|
let ppi = if gradients_only { o.gradient_ppi } else { o.line_art_ppi };
|
|
Area { rect, ppi, clip: clip.filter(|c| !c.is_empty()), whole: all }
|
|
});
|
|
let vector = vector.into_iter().map(|(k, f)| (regions[k].path.clone(), f)).collect();
|
|
Some((vector, area))
|
|
}
|
|
|
|
/// The flat colour of a region covered by `sources` (CMYK in a `cmyk` document); `None` where
|
|
/// nothing paints it.
|
|
fn region_fill(elems: &[Elem], sources: &[usize], o: &FlattenOptions, cmyk: bool) -> Option<RegionFill> {
|
|
let mut c = Composer { sources, top: None, plane: 0 };
|
|
let alone = c.eval(elems, CLEAR);
|
|
if alone[3] < 0.5 / 255.0 {
|
|
return None;
|
|
}
|
|
let start = if o.preserve_alpha { CLEAR } else { WHITE };
|
|
let px = if o.preserve_alpha {
|
|
alone
|
|
} else {
|
|
c.top = None;
|
|
c.eval(elems, start)
|
|
};
|
|
let rgb = [px[0], px[1], px[2]];
|
|
// The K plane composites the same way.
|
|
let k = cmyk.then(|| {
|
|
c.plane = 1;
|
|
c.eval(elems, start)[0]
|
|
});
|
|
// A paint showing as it is keeps its colour (spot, process, swatch) and overprint.
|
|
let near = |a: &[f32], b: &[f32]| a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-3);
|
|
let same = |f: &Fill| px[3] >= 1.0 - 1e-4 && near(&f.planes[0], &rgb) && k.is_none_or(|k| near(&f.planes[1][..1], &[k]));
|
|
let color = match k {
|
|
Some(k) => {
|
|
let [c, m, y, k] = planes_cmyk(rgb, k);
|
|
Color::cmyk(c, m, y, k)
|
|
}
|
|
None => Color::rgb(rgb[0], rgb[1], rgb[2]),
|
|
};
|
|
let (paint, overprint) = match c.top {
|
|
Some(f) if o.preserve_overprints && same(f) => (f.paint.clone(), f.overprint),
|
|
_ => (Paint::solid(color), false),
|
|
};
|
|
Some(RegionFill { paint, alpha: px[3], overprint })
|
|
}
|
|
|
|
/// `art` rendered over `rect` at `ppi`: over white with the white the art doesn't cover taken out
|
|
/// again (so blend modes see the white page), or over nothing with Preserve Alpha. The image and
|
|
/// its `clip` reach a pixel further. An opaque image in a CMYK document holds the art's ink
|
|
/// amounts (a CMYK TIFF), so its CMYK colours keep their numbers (#2160); other images are PNGs.
|
|
/// Fails when the image can't be encoded.
|
|
fn render_raster(doc: &Document, art: &[Node], rect: Rect, ppi: f64, clip: Option<PathData>, o: &FlattenOptions) -> Result<Raster> {
|
|
let mut scale = ppi / 72.0;
|
|
scale = scale.min((MAX_PIXELS / (rect.width() * rect.height()).max(1.0)).sqrt());
|
|
let px = 1.0 / scale;
|
|
let rect = rect.inflate(px, px);
|
|
let clip = clip.map(|c| Some(po::offset_path(&c, px, po::Join::Miter, 4.0)).filter(|g| !g.is_empty()).unwrap_or(c));
|
|
let region = Rect::new(
|
|
rect.x0,
|
|
rect.y0,
|
|
rect.x0 + (rect.width() * scale).ceil().max(1.0) / scale,
|
|
rect.y0 + (rect.height() * scale).ceil().max(1.0) / scale,
|
|
);
|
|
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
|
|
let tmp = isolated_doc(doc, art.to_vec());
|
|
let mut r = vectorcraft_render::Renderer::new();
|
|
let no_alpha = o.no_transparency.is_some() && clip.is_some() && !o.preserve_alpha;
|
|
if no_alpha && doc.color_mode == ColorMode::Cmyk {
|
|
// Drawn as inks over the paper: as screen colours, the file would separate them again,
|
|
// and a CMYK colour comes back with other inks (K moves into C, M and Y).
|
|
let opts = vectorcraft_render::RenderOptions { background: Some([255; 4]), skip_templates: true, ..Default::default() };
|
|
let inks = r.render_region_inks(&tmp, region, scale, &opts);
|
|
vectorcraft_doc::cancel::check()?;
|
|
let (width, height) = vectorcraft_render::region_pixels(region, scale);
|
|
// Past the size of a CMYK image: drawn in screen colours, below.
|
|
if let Some(blob) = Inks::new(width, height, inks).and_then(|i| ImageBlob::cmyk_tiff(&i)) {
|
|
return Ok(Raster { blob, width, height, xf, clip });
|
|
}
|
|
}
|
|
let mut img = r.render_region(&tmp, region, scale, !o.preserve_alpha);
|
|
// The drawing stops early when cancelled: this image is not whole.
|
|
vectorcraft_doc::cancel::check()?;
|
|
if no_alpha {
|
|
// Without transparency the clip alone shapes the image: it is opaque, over white (its
|
|
// edge pixels too).
|
|
for p in img.pixels.as_chunks_mut::<4>().0 {
|
|
p[3] = 255;
|
|
}
|
|
} else if !o.preserve_alpha || !o.anti_alias {
|
|
// The art's coverage: drawn opaque, so only its own edges (and soft effects) are partial.
|
|
let cover: Vec<Node> = art
|
|
.iter()
|
|
.map(|n| {
|
|
let mut c = n.clone();
|
|
opaque(&mut c);
|
|
c
|
|
})
|
|
.collect();
|
|
let cover = r.render_region(&isolated_doc(doc, cover), region, scale, false);
|
|
for (px, c) in img.pixels.as_chunks_mut::<4>().0.iter_mut().zip(cover.pixels.as_chunks::<4>().0) {
|
|
let k = c[3];
|
|
let k2 = if o.anti_alias || k == 0 || k == 255 {
|
|
k
|
|
} else if k >= 128 {
|
|
255
|
|
} else {
|
|
0
|
|
};
|
|
if o.preserve_alpha {
|
|
let f = k2 as f32 / k.max(1) as f32;
|
|
for v in px.iter_mut() {
|
|
*v = (*v as f32 * f).round().min(255.0) as u8;
|
|
}
|
|
} else {
|
|
let white = 255 - k2;
|
|
for v in &mut px[..3] {
|
|
*v = v.saturating_sub(white).min(k2);
|
|
}
|
|
px[3] = k2;
|
|
}
|
|
}
|
|
}
|
|
let png = img.to_png().map_err(|e| EngineError::Other(format!("Flatten Transparency: {e}")))?;
|
|
Ok(Raster { blob: ImageBlob::new("image/png", png), width: img.width, height: img.height, xf, clip })
|
|
}
|
|
|
|
/// `n` at full opacity with Normal blending throughout (objects, fills and strokes).
|
|
pub(super) fn opaque(n: &mut Node) {
|
|
n.opacity = 1.0;
|
|
n.blend = BlendMode::Normal;
|
|
for i in &mut n.appearance.items {
|
|
match i {
|
|
AppearanceItem::Fill(f) => (f.opacity, f.blend) = (1.0, BlendMode::Normal),
|
|
AppearanceItem::Stroke(s) => (s.opacity, s.blend) = (1.0, BlendMode::Normal),
|
|
}
|
|
}
|
|
if let Some(ch) = n.children_mut() {
|
|
for c in ch {
|
|
opaque(Arc::make_mut(c));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether `doc` has pattern fills whose image tiles need transparency flattening with `o`
|
|
/// ([`expand_transparent_tile_images`]).
|
|
pub(crate) fn has_transparent_tile_images(doc: &Document, o: &FlattenOptions) -> bool {
|
|
let keys = o.no_transparency.clone().unwrap_or_default();
|
|
let colorize = |d: &Document, n: &Node| o.counts_colorize(d, n);
|
|
!doc.patterns.is_empty() && crate::cmd::expand::has_transparent_image_patterns(doc, &keys, &colorize)
|
|
}
|
|
|
|
/// Expand pattern fills whose image tiles need transparency flattening.
|
|
pub(crate) fn expand_transparent_tile_images(doc: &Document, o: &FlattenOptions) -> Result<Option<Document>> {
|
|
if !has_transparent_tile_images(doc, o) {
|
|
return Ok(None);
|
|
}
|
|
let keys = o.no_transparency.clone().unwrap_or_default();
|
|
let colorize = |d: &Document, n: &Node| o.counts_colorize(d, n);
|
|
let mut d = doc.clone();
|
|
let layers: Vec<NodeId> =
|
|
d.layers.iter().filter(|l| l.visible && !matches!(l.kind, NodeKind::Layer { template: true, .. })).map(|l| l.id).collect();
|
|
let mut changed = 0;
|
|
for id in layers {
|
|
changed += crate::cmd::expand::expand_transparent_image_patterns(&mut d, id, &keys, &colorize)?;
|
|
}
|
|
Ok((changed > 0).then_some(d))
|
|
}
|
|
|
|
/// `doc` with everything it draws flattened with `o`, as formats without transparency (EPS)
|
|
/// write it: every visible object of the visible layers, locked ones too (a clipping layer's
|
|
/// clipping path stays). Pattern tile images with opacity, blending or a colorize paint that
|
|
/// `o` counts ([`FlattenOptions::counts_colorize`]) are expanded; see-through tile pixels are
|
|
/// expanded when `o.no_transparency` counts them. `None` when nothing changes.
|
|
pub(crate) fn flatten_document(doc: &Document, o: &FlattenOptions) -> Result<Option<Document>> {
|
|
let expanded = expand_transparent_tile_images(doc, o)?;
|
|
Ok(flatten_expanded(expanded.as_ref().unwrap_or(doc), o)?.or(expanded))
|
|
}
|
|
|
|
/// [`flatten_document`] for `doc` whose pattern tile images were expanded already
|
|
/// ([`expand_transparent_tile_images`]).
|
|
pub(crate) fn flatten_expanded(doc: &Document, o: &FlattenOptions) -> Result<Option<Document>> {
|
|
let outlining = o.text_to_outlines || o.strokes_to_outlines || !o.preserve_overprints;
|
|
if !outlining && !doc.layers.iter().any(|l| o.shows_transparency(doc, l) || o.draws_see_through_image(l)) {
|
|
return Ok(None);
|
|
}
|
|
let mut roots = vec![];
|
|
for l in doc.layers.iter().filter(|l| l.visible && !matches!(l.kind, NodeKind::Layer { template: true, .. })) {
|
|
let children = l.children().map_or(&[][..], Vec::as_slice);
|
|
roots.extend(children.iter().skip(usize::from(l.clips())).filter(|c| c.visible).map(|c| c.id));
|
|
}
|
|
let plan = plan(doc, &roots, o, true)?;
|
|
if plan.flat.is_empty() && !plan.kept.iter().any(|id| doc.node(*id).is_some_and(|n| kept_work(n, o))) {
|
|
return Ok(None);
|
|
}
|
|
let mut d = doc.clone();
|
|
apply(&mut d, &mut vectorcraft_doc::Selection::default(), plan, o)?;
|
|
Ok(Some(d))
|
|
}
|
|
|
|
// ---------- apply ----------
|
|
|
|
pub(super) fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan: Plan, o: &FlattenOptions) -> Result<(Vec<NodeId>, usize, usize)> {
|
|
let (mut vector, mut rasterized) = (0, 0);
|
|
let (mut ids, mut flat) = (vec![], vec![]);
|
|
// The groups replace their objects in one pass at the end. When an object is listed twice or
|
|
// lies inside another listed one, each group replaces its objects as it is made instead.
|
|
let one_pass = separate(d, &plan.flat.iter().flat_map(|f| f.roots.iter().copied()).collect::<Vec<_>>());
|
|
for f in plan.flat {
|
|
// A group always has objects.
|
|
let Some(&top) = f.roots.last() else { continue };
|
|
let mut children = vec![];
|
|
// The image goes under the regions, reaching a pixel past its own: their edges then fall on
|
|
// matching pixels instead of on what is behind the group.
|
|
if let Some(r) = f.raster {
|
|
children.push(Arc::new(raster_node(d, r)));
|
|
rasterized += 1;
|
|
}
|
|
for (path, fill) in f.regions {
|
|
let mut n = shape_node(d, path, None);
|
|
n.appearance = Appearance {
|
|
items: vec![AppearanceItem::Fill(FillLayer { overprint: fill.overprint, ..FillLayer::new(fill.paint) })],
|
|
..Appearance::default()
|
|
}
|
|
.into();
|
|
n.opacity = fill.alpha.min(1.0);
|
|
children.push(Arc::new(n));
|
|
vector += 1;
|
|
}
|
|
let g = Node::group(d.alloc_id(), children);
|
|
ids.push(g.id);
|
|
if one_pass {
|
|
// The group replaces the topmost object, and the other objects are removed.
|
|
flat.extend(f.roots.iter().filter(|r| **r != top).map(|r| (*r, vec![])));
|
|
flat.push((top, vec![g]));
|
|
} else {
|
|
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
|
d.insert(par, idx + 1, g)?;
|
|
for r in &f.roots {
|
|
d.remove(*r)?;
|
|
}
|
|
}
|
|
}
|
|
if !flat.is_empty() {
|
|
d.insert_beside(flat, true)?;
|
|
}
|
|
let brushes = if o.strokes_to_outlines { vectorcraft_brush::library(d) } else { vec![] };
|
|
for mut id in plan.kept {
|
|
if o.text_to_outlines {
|
|
id = outline_texts_under(d, id)?;
|
|
}
|
|
if o.strokes_to_outlines {
|
|
id = outline_strokes_under(d, &brushes, id)?.0;
|
|
}
|
|
if !o.preserve_overprints
|
|
&& let Some(n) = d.node_mut(id)
|
|
{
|
|
drop_overprints(n);
|
|
}
|
|
ids.push(id);
|
|
}
|
|
ids.sort_by_key(|id| d.index_path(*id));
|
|
sel.set(ids.iter().copied());
|
|
Ok((ids, vector, rasterized))
|
|
}
|
|
|
|
/// An image of `r` (in a clip group when it is clipped).
|
|
fn raster_node(d: &mut Document, r: Raster) -> Node {
|
|
let key = unique_key(d, "flattened");
|
|
d.images.insert(key.clone(), r.blob);
|
|
let image = Node::new(
|
|
d.alloc_id(),
|
|
NodeKind::Image(Box::new(ImageObject {
|
|
key,
|
|
width: r.width,
|
|
height: r.height,
|
|
xf: r.xf,
|
|
link: None,
|
|
placement: Default::default(),
|
|
pixels: None,
|
|
})),
|
|
);
|
|
let Some(path) = r.clip else { return image };
|
|
crate::cmd::rasterfx::clip_group(d, path, FillRule::NonZero, image)
|
|
}
|
|
|
|
/// Replace the type under `root` by its outlines: → the id now standing for `root`.
|
|
fn outline_texts_under(d: &mut Document, root: NodeId) -> Result<NodeId> {
|
|
let mut texts = vec![];
|
|
if let Some(n) = d.node(root) {
|
|
n.walk(&mut |c| {
|
|
if matches!(c.kind, NodeKind::Text(_)) {
|
|
texts.push(c.clone());
|
|
}
|
|
});
|
|
}
|
|
let mut out = root;
|
|
for t in texts {
|
|
let Some(o) = outlined_text(&t) else { continue };
|
|
let o = d.reid(&o);
|
|
let nid = o.id;
|
|
*d.node_mut(t.id).ok_or(EngineError::NoNode(t.id))? = o;
|
|
if t.id == root {
|
|
out = nid;
|
|
}
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
/// Whether the outline and overprint options change `n`, an object without transparency.
|
|
pub(super) fn kept_work(n: &Node, o: &FlattenOptions) -> bool {
|
|
let mut any = false;
|
|
n.walk(&mut |c| {
|
|
any |= (o.text_to_outlines && matches!(c.kind, NodeKind::Text(_)))
|
|
|| (o.strokes_to_outlines && stroked(c))
|
|
|| (!o.preserve_overprints && overprints(c));
|
|
});
|
|
any
|
|
}
|
|
|
|
/// Whether `n` is a path with a visible stroke (outlining turns it into a fill).
|
|
pub(super) fn stroked(n: &Node) -> bool {
|
|
matches!(n.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. })
|
|
&& n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(s) if s.visible && !s.paint.is_none() && s.width > 0.0))
|
|
}
|
|
|
|
fn overprints(n: &Node) -> bool {
|
|
n.appearance.items.iter().any(AppearanceItem::overprint)
|
|
|| matches!(&n.kind, NodeKind::Text(t) if t.runs.iter().any(|r| r.style.overprint_fill || r.style.overprint_stroke))
|
|
}
|
|
|
|
fn drop_overprints(n: &mut Node) {
|
|
if overprints(n) {
|
|
for i in &mut n.appearance.items {
|
|
*i.overprint_mut() = false;
|
|
}
|
|
if let NodeKind::Text(t) = &mut n.kind {
|
|
for r in &mut t.runs {
|
|
(r.style.overprint_fill, r.style.overprint_stroke) = (false, false);
|
|
}
|
|
}
|
|
}
|
|
if let Some(ch) = n.children_mut() {
|
|
for c in ch {
|
|
drop_overprints(Arc::make_mut(c));
|
|
}
|
|
}
|
|
}
|