mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge pull request #4783 from dotCooCoo/fix/pdf-eps-pattern-image-flatten
PDF/EPS export: see-through and colorized pattern tile images are flattened
This commit is contained in:
@@ -686,6 +686,12 @@ impl Node {
|
||||
/// stroke opacity below 100%, a blend mode, an opacity mask, effects, transparent gradient
|
||||
/// stops or a colorized image. Hidden objects show none.
|
||||
pub fn shows_transparency(&self) -> bool {
|
||||
self.shows_transparency_with(&|_| true)
|
||||
}
|
||||
|
||||
/// [`Self::shows_transparency`], counting a colorized image by itself only when `colorize`
|
||||
/// accepts it, for files that write some colorized images in their colorize paint (EPS).
|
||||
pub fn shows_transparency_with(&self, colorize: &dyn Fn(&Node) -> bool) -> bool {
|
||||
use vectorcraft_color::Paint;
|
||||
let see_through = |p: &Paint| matches!(p, Paint::Gradient(g) if g.gradient.stops.iter().any(|s| s.opacity < 1.0));
|
||||
let effects = |fx: &[crate::Effect]| fx.iter().any(|e| e.visible);
|
||||
@@ -693,14 +699,14 @@ impl Node {
|
||||
&& (self.opacity < 1.0
|
||||
|| self.blend != BlendMode::Normal
|
||||
|| self.extras().mask.as_ref().is_some_and(|m| !m.disabled)
|
||||
|| (matches!(self.kind, NodeKind::Image(_)) && self.extras().colorize.is_some())
|
||||
|| (matches!(self.kind, NodeKind::Image(_)) && self.extras().colorize.is_some() && colorize(self))
|
||||
|| effects(&self.appearance.effects)
|
||||
|| self
|
||||
.appearance
|
||||
.items
|
||||
.iter()
|
||||
.any(|i| i.visible() && (i.opacity() < 1.0 || i.blend() != BlendMode::Normal || see_through(i.paint()) || effects(i.effects())))
|
||||
|| self.children().is_some_and(|ch| ch.iter().any(|c| c.shows_transparency())))
|
||||
|| self.children().is_some_and(|ch| ch.iter().any(|c| c.shows_transparency_with(colorize))))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+259
-34
@@ -447,25 +447,206 @@ fn expand_pattern_fills(s: &mut Session) -> Result<()> {
|
||||
/// Expand the pattern fills under `root` that paint a transparent or colorized image into the tiles they paint
|
||||
/// ([`expand_patterns_where`]), so each tile image becomes an object of its own; the other
|
||||
/// pattern fills stay. A tile image counts when its key is in `see_through` (it has see-through
|
||||
/// pixels), when it has a colorize paint, or when it, a group
|
||||
/// pixels), when it has a colorize paint that `colorize` counts, or when it, a group
|
||||
/// in the tile art, the fill, the shape or a group the shape is in has opacity or a blending
|
||||
/// mode. Returns how many shapes changed.
|
||||
pub(crate) fn expand_transparent_image_patterns(d: &mut Document, root: NodeId, see_through: &BTreeSet<String>) -> Result<usize> {
|
||||
let (_, n) = replace_leaves(d, root, |d, l| {
|
||||
// Most leaves have no pattern fill: skip the ancestry walk for them.
|
||||
if !l.appearance.items.iter().any(|i| pattern_fill(d, i).is_some()) {
|
||||
return None;
|
||||
/// mode. Blends, envelopes, repeats and Intertwine groups that hold such a fill are expanded
|
||||
/// first ([`expand_live_with_transparent_image_patterns`]). Hidden shapes and shapes in hidden
|
||||
/// groups or layers and in template layers stay as they are. A pattern fill in tile art counts
|
||||
/// too, down to the [`PATTERN_LEVELS`] the canvas draws, and the tile art the expansion makes is
|
||||
/// expanded again for it. A pattern fill stays, and a live object stays live, when its art would
|
||||
/// take the expanded art past [`MAX_GENERATED`](vectorcraft_doc::live::MAX_GENERATED) objects.
|
||||
/// Returns how many objects changed.
|
||||
pub(crate) fn expand_transparent_image_patterns(
|
||||
d: &mut Document,
|
||||
root: NodeId,
|
||||
see_through: &BTreeSet<String>,
|
||||
colorize: &dyn Fn(&Document, &Node) -> bool,
|
||||
) -> Result<usize> {
|
||||
expand_transparent_image_patterns_within(d, root, see_through, colorize, vectorcraft_doc::live::MAX_GENERATED)
|
||||
}
|
||||
|
||||
/// [`expand_transparent_image_patterns`] with the expanded art held to `budget` objects.
|
||||
pub(crate) fn expand_transparent_image_patterns_within(
|
||||
d: &mut Document,
|
||||
root: NodeId,
|
||||
see_through: &BTreeSet<String>,
|
||||
colorize: &dyn Fn(&Document, &Node) -> bool,
|
||||
budget: usize,
|
||||
) -> Result<usize> {
|
||||
let mut changed = 0;
|
||||
let made = std::cell::Cell::new(0usize);
|
||||
// Each pass brings the patterns in the expanded tiles' art up one level.
|
||||
for levels in (1..=PATTERN_LEVELS).rev() {
|
||||
let live = expand_live_with_transparent_image_patterns(d, root, see_through, colorize, levels, &made, budget)?;
|
||||
let (_, n) = replace_leaves(d, root, |d, l| {
|
||||
// Past the budget, and for most leaves, which have no pattern fill, skip the ancestry walk.
|
||||
if made.get() >= budget || !l.appearance.items.iter().any(|i| pattern_fill(d, i).is_some()) {
|
||||
return None;
|
||||
}
|
||||
// The expanded art would be shown.
|
||||
if !d.ancestry(l.id).unwrap_or_default().iter().filter_map(|a| d.node(*a)).all(drawn) {
|
||||
return None;
|
||||
}
|
||||
let which = |d: &Document, i: &AppearanceItem| transparent_pattern_fill(d, l, i, see_through, colorize, levels);
|
||||
let art = expand_patterns_where(d, l, which, budget - made.get())?;
|
||||
// The copies of the shape's other items count too.
|
||||
let count = art.count_with_masks();
|
||||
if count > budget - made.get() {
|
||||
return None;
|
||||
}
|
||||
made.set(made.get() + count);
|
||||
Some(art)
|
||||
})?;
|
||||
if live + n == 0 {
|
||||
break;
|
||||
}
|
||||
let shape = d.ancestry(l.id).unwrap_or_default().iter().filter_map(|a| d.node(*a)).any(blended);
|
||||
let transparent = |d: &Document, i: &AppearanceItem| {
|
||||
pattern_fill(d, i).is_some_and(|(f, def, _)| {
|
||||
let on = shape || f.opacity < 1.0 || f.blend != BlendMode::Normal;
|
||||
def.art.iter().any(|a| transparent_image(a, see_through, on))
|
||||
})
|
||||
};
|
||||
expand_patterns_where(d, l, transparent)
|
||||
})?;
|
||||
Ok(n)
|
||||
changed += live + n;
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// How many levels of patterns the canvas draws: a pattern fill in the art of a pattern, and so
|
||||
/// on, as [`vectorcraft_render`] draws them.
|
||||
const PATTERN_LEVELS: u32 = 3;
|
||||
|
||||
/// Whether a layer of `d` holds a pattern fill that [`expand_transparent_image_patterns`] would
|
||||
/// expand with `see_through` and `colorize` ([`holds_transparent_image_patterns`]).
|
||||
pub(crate) fn has_transparent_image_patterns(d: &Document, see_through: &BTreeSet<String>, colorize: &dyn Fn(&Document, &Node) -> bool) -> bool {
|
||||
d.layers.iter().any(|l| holds_transparent_image_patterns(d, l, see_through, colorize, PATTERN_LEVELS))
|
||||
}
|
||||
|
||||
/// Whether `n`, art of `d`, is or holds a shown path or compound path (in groups, blends,
|
||||
/// envelopes, repeats and Intertwine groups) with a pattern fill that
|
||||
/// [`transparent_pattern_fill`] counts with `levels`. Guides, clipping paths and template layers
|
||||
/// don't count.
|
||||
fn holds_transparent_image_patterns(
|
||||
d: &Document,
|
||||
n: &Node,
|
||||
see_through: &BTreeSet<String>,
|
||||
colorize: &dyn Fn(&Document, &Node) -> bool,
|
||||
levels: u32,
|
||||
) -> bool {
|
||||
if !drawn(n) {
|
||||
return false;
|
||||
}
|
||||
match &n.kind {
|
||||
NodeKind::Path { guide: true, .. } | NodeKind::Path { clipping: true, .. } => false,
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } => {
|
||||
n.appearance.items.iter().any(|i| transparent_pattern_fill(d, n, i, see_through, colorize, levels))
|
||||
}
|
||||
_ => n.children().is_some_and(|ch| ch.iter().any(|c| holds_transparent_image_patterns(d, c, see_through, colorize, levels))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `n` is shown and isn't a template layer.
|
||||
fn drawn(n: &Node) -> bool {
|
||||
n.visible && !matches!(n.kind, NodeKind::Layer { template: true, .. })
|
||||
}
|
||||
|
||||
/// Expand the shown blends, envelopes, repeats and Intertwine groups under `root` that hold a
|
||||
/// pattern fill [`holds_transparent_image_patterns`] counts with `levels` into the art the exporters write for
|
||||
/// them ([`expand_live_deep`](vectorcraft_render::effects::expand_live_deep), under one budget for
|
||||
/// everything nested in each), so the shapes they draw are leaves. The objects that art holds count
|
||||
/// in `made`, and an object whose art would take `made` past `budget` stays live. Returns how many
|
||||
/// were expanded.
|
||||
fn expand_live_with_transparent_image_patterns(
|
||||
d: &mut Document,
|
||||
root: NodeId,
|
||||
see_through: &BTreeSet<String>,
|
||||
colorize: &dyn Fn(&Document, &Node) -> bool,
|
||||
levels: u32,
|
||||
made: &std::cell::Cell<usize>,
|
||||
budget: usize,
|
||||
) -> Result<usize> {
|
||||
fn outermost(
|
||||
d: &Document,
|
||||
n: &Node,
|
||||
see_through: &BTreeSet<String>,
|
||||
colorize: &dyn Fn(&Document, &Node) -> bool,
|
||||
levels: u32,
|
||||
out: &mut Vec<NodeId>,
|
||||
) {
|
||||
if !drawn(n) {
|
||||
return;
|
||||
}
|
||||
match &n.kind {
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Intertwine { .. } => {
|
||||
if holds_transparent_image_patterns(d, n, see_through, colorize, levels) {
|
||||
out.push(n.id);
|
||||
}
|
||||
}
|
||||
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
|
||||
children.iter().for_each(|c| outermost(d, c, see_through, colorize, levels, out));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// Fresh ids for everything under `n`: copies of the same art share theirs.
|
||||
fn fresh_ids(d: &mut Document, n: &mut Node) {
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
let c = Arc::make_mut(c);
|
||||
c.id = d.alloc_id();
|
||||
fresh_ids(d, c);
|
||||
}
|
||||
}
|
||||
let mut live = vec![];
|
||||
if let Some(n) = d.node(root) {
|
||||
outermost(d, n, see_through, colorize, levels, &mut live);
|
||||
}
|
||||
let mut count = 0;
|
||||
for id in &live {
|
||||
if made.get() >= budget {
|
||||
break;
|
||||
}
|
||||
let Some(n) = d.node(*id) else { continue };
|
||||
// Random Transform effects take their seed from the ids: bake them first ([`bake_seeded`]),
|
||||
// and leave the object live when that can't be done.
|
||||
let Some(mut g) = bake_seeded(&vectorcraft_render::effects::expand_live_deep(Some(d), n)) else { continue };
|
||||
// An object whose art would take the expanded art past the budget stays live.
|
||||
let size = g.count_with_masks();
|
||||
if size > budget - made.get() {
|
||||
continue;
|
||||
}
|
||||
made.set(made.get() + size);
|
||||
g.id = *id;
|
||||
fresh_ids(d, &mut g);
|
||||
*d.node_mut(*id).ok_or(EngineError::NoNode(*id))? = g;
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Whether `i`, an item of leaf `l`, is a pattern fill whose tile art holds a transparent image
|
||||
/// ([`transparent_image`], through pattern fills in that art down to `levels` patterns),
|
||||
/// counting the fill's and the shape's opacity and blending mode.
|
||||
fn transparent_pattern_fill(
|
||||
d: &Document,
|
||||
l: &Node,
|
||||
i: &AppearanceItem,
|
||||
see_through: &BTreeSet<String>,
|
||||
colorize: &dyn Fn(&Document, &Node) -> bool,
|
||||
levels: u32,
|
||||
) -> bool {
|
||||
let shape = d.ancestry(l.id).unwrap_or_default().iter().filter_map(|a| d.node(*a)).any(blended);
|
||||
pattern_fill_holds_transparent_image(d, i, see_through, colorize, shape, levels)
|
||||
}
|
||||
|
||||
/// Whether `i` is a pattern fill whose tile art holds a transparent image ([`transparent_image`]),
|
||||
/// down to `levels` patterns, counting the fill's opacity and blending mode, or anything when `on`.
|
||||
fn pattern_fill_holds_transparent_image(
|
||||
d: &Document,
|
||||
i: &AppearanceItem,
|
||||
keys: &BTreeSet<String>,
|
||||
colorize: &dyn Fn(&Document, &Node) -> bool,
|
||||
on: bool,
|
||||
levels: u32,
|
||||
) -> bool {
|
||||
if levels == 0 {
|
||||
return false;
|
||||
}
|
||||
let Some((f, def, _)) = pattern_fill(d, i) else { return false };
|
||||
let on = on || f.opacity < 1.0 || f.blend != BlendMode::Normal;
|
||||
def.art.iter().any(|a| transparent_image(d, a, keys, colorize, on, levels - 1))
|
||||
}
|
||||
|
||||
/// Whether `n` has opacity or a blending mode of its own.
|
||||
@@ -473,13 +654,21 @@ fn blended(n: &Node) -> bool {
|
||||
n.opacity < 1.0 || n.blend != BlendMode::Normal
|
||||
}
|
||||
|
||||
/// Whether `n` is or holds an image with see-through pixels (key in `keys`), a colorize paint,
|
||||
/// opacity or a blending mode of its own or of a group in `n`, or anywhere when `on`.
|
||||
fn transparent_image(n: &Node, keys: &BTreeSet<String>, on: bool) -> bool {
|
||||
/// Whether `n` (art of `d`) is or holds an image with see-through pixels (key in `keys`), a
|
||||
/// colorize paint that `colorize` counts, opacity or a blending mode of its own or of a group in
|
||||
/// `n`, or anywhere when `on`, also in the tile art of the pattern fills of the paths in `n`, down
|
||||
/// to `levels` patterns. Hidden art doesn't count.
|
||||
fn transparent_image(d: &Document, n: &Node, keys: &BTreeSet<String>, colorize: &dyn Fn(&Document, &Node) -> bool, on: bool, levels: u32) -> bool {
|
||||
if !n.visible {
|
||||
return false;
|
||||
}
|
||||
let on = on || blended(n);
|
||||
match &n.kind {
|
||||
NodeKind::Image(im) => on || keys.contains(&im.key) || n.extras().colorize.is_some(),
|
||||
_ => n.children().is_some_and(|ch| ch.iter().any(|c| transparent_image(c, keys, on))),
|
||||
NodeKind::Image(im) => on || keys.contains(&im.key) || (n.extras().colorize.is_some() && colorize(d, n)),
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } => {
|
||||
n.appearance.items.iter().any(|i| pattern_fill_holds_transparent_image(d, i, keys, colorize, on, levels))
|
||||
}
|
||||
_ => n.children().is_some_and(|ch| ch.iter().any(|c| transparent_image(d, c, keys, colorize, on, levels))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,12 +680,12 @@ fn transparent_image(n: &Node, keys: &BTreeSet<String>, on: bool) -> bool {
|
||||
/// ([`pattern_tiles`]). A pattern fill that lays no tile there stays. `None` when no pattern fill
|
||||
/// lays a tile.
|
||||
fn expand_patterns(d: &mut Document, l: &Node) -> Option<Node> {
|
||||
expand_patterns_where(d, l, |_, _| true)
|
||||
expand_patterns_where(d, l, |_, _| true, usize::MAX)
|
||||
}
|
||||
|
||||
/// [`expand_patterns`] for only the pattern fills `which` picks: the others stay like the other
|
||||
/// items.
|
||||
fn expand_patterns_where(d: &mut Document, l: &Node, which: impl Fn(&Document, &AppearanceItem) -> bool) -> Option<Node> {
|
||||
/// items, as does a pattern fill whose tiles would take the tiles of `l` past `budget` objects.
|
||||
fn expand_patterns_where(d: &mut Document, l: &Node, which: impl Fn(&Document, &AppearanceItem) -> bool, budget: usize) -> Option<Node> {
|
||||
if !matches!(l.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })
|
||||
|| !l.appearance.items.iter().any(|i| pattern_fill(d, i).is_some() && which(d, i))
|
||||
{
|
||||
@@ -509,19 +698,47 @@ fn expand_patterns_where(d: &mut Document, l: &Node, which: impl Fn(&Document, &
|
||||
let mut parts = vec![];
|
||||
let mut rest = vec![];
|
||||
let mut expanded = false;
|
||||
// What `assemble` adds around the tiles when every picked fill expands: a copy of the path for
|
||||
// each run of other items, a group around the parts and one for the path's transparency, and
|
||||
// the path's opacity mask.
|
||||
let (mut copies, mut run, mut picked) = (0usize, false, vec![]);
|
||||
for i in &l.appearance.items {
|
||||
if let Some((f, _, _)) = pattern_fill(d, i).filter(|_| which(d, i)) {
|
||||
run = false;
|
||||
picked.push(f.opacity < 1.0 || f.blend != BlendMode::Normal);
|
||||
} else if i.visible() && !i.paint().is_none() && !run {
|
||||
(copies, run) = (copies + 1, true);
|
||||
}
|
||||
}
|
||||
let group = copies + picked.len() > 1 || !l.appearance.effects.is_empty();
|
||||
// A lone tile group with transparency of its own goes in a group for the path's.
|
||||
let transparency = blended(l) && !group && picked.first().is_some_and(|b| *b);
|
||||
let around = copies.saturating_mul(l.count()) + usize::from(group) + usize::from(transparency) + l.count_with_masks().saturating_sub(l.count());
|
||||
let mut left = budget.saturating_sub(around);
|
||||
// A fill that expands while others follow leaves room for the copy and group that those take
|
||||
// if they don't fit, and once one doesn't fit, the ones after it stay too.
|
||||
let (mut later, mut full) = (picked.len(), false);
|
||||
for item in &l.appearance.items {
|
||||
let fill = pattern_fill(d, item).filter(|_| which(d, item)).map(|(f, def, xf)| (f, def.clone(), xf));
|
||||
let tiles = fill.and_then(|(f, def, xf)| {
|
||||
later -= usize::from(fill.is_some());
|
||||
let room = if later > 0 { left.saturating_sub(l.count() + 1) } else { left };
|
||||
let tiles = fill.filter(|_| !full).and_then(|(f, def, xf)| {
|
||||
let (fl, _, bounds) = fill_shape(l, item, &path, bounds)?;
|
||||
let t = pattern_tiles(d, &def, xf, bounds);
|
||||
let Some(t) = pattern_tiles(d, &def, xf, bounds, room) else {
|
||||
full = true;
|
||||
return None;
|
||||
};
|
||||
(!t.is_empty()).then_some((f, fl, t))
|
||||
});
|
||||
let Some(art) = tiles.and_then(|(f, fl, t)| tile_art(d, &fl, f, &t)) else {
|
||||
let art = tiles.and_then(|(f, fl, t)| tile_art(d, &fl, f, &t));
|
||||
full |= art.as_ref().is_some_and(|a| a.count_with_masks() > room);
|
||||
let Some(art) = art.filter(|a| a.count_with_masks() <= room) else {
|
||||
if item.visible() && !item.paint().is_none() {
|
||||
rest.push(item.clone());
|
||||
}
|
||||
continue;
|
||||
};
|
||||
left -= art.count_with_masks();
|
||||
if !rest.is_empty() {
|
||||
parts.push(styled_copy(d, l, std::mem::take(&mut rest)));
|
||||
}
|
||||
@@ -548,30 +765,38 @@ fn expand_patterns_where(d: &mut Document, l: &Node, which: impl Fn(&Document, &
|
||||
/// not, and when it does more than move art that holds a symbol instance, whose strokes the canvas
|
||||
/// draws scaled with the tile and an instance does not scale. Random Transform effects and brushes
|
||||
/// that vary at random in the art are baked first ([`seeds_baked`], [`random_brushes_baked`]).
|
||||
fn pattern_tiles(d: &mut Document, def: &PatternDef, xf: Affine, bounds: Rect) -> Vec<Node> {
|
||||
/// `None` when the tiles would hold more than `budget` objects.
|
||||
fn pattern_tiles(d: &mut Document, def: &PatternDef, xf: Affine, bounds: Rect, budget: usize) -> Option<Vec<Node>> {
|
||||
if !moves_only(xf) && def.art.iter().any(|a| has_instances(a)) {
|
||||
return vec![];
|
||||
return Some(vec![]);
|
||||
}
|
||||
let Some(def) = seeds_baked(def) else {
|
||||
return vec![];
|
||||
return Some(vec![]);
|
||||
};
|
||||
let brushes = vectorcraft_brush::library(d);
|
||||
let def = random_brushes_baked(d, &brushes, def);
|
||||
let def = &*def;
|
||||
let per_tile = def.art.iter().map(|a| a.count_with_masks()).sum::<usize>();
|
||||
if def.offsets_covering(xf.inverse().transform_rect_bbox(bounds)).len().saturating_mul(per_tile) > budget {
|
||||
return None;
|
||||
}
|
||||
let scaling = Scaling { strokes: true, effects: Some(vectorcraft_render::effects::scale_effect), patterns: true, ..Scaling::default() };
|
||||
if scales_evenly(xf) && (moves_only(xf) || !def.art.iter().any(|a| has_geometry_effects(a))) {
|
||||
return def.instances_in_with(xf, bounds, scaling);
|
||||
return Some(def.instances_in_with(xf, bounds, scaling));
|
||||
}
|
||||
if def.art.iter().any(|a| has_unbakeable_effects(a)) {
|
||||
return vec![];
|
||||
return Some(vec![]);
|
||||
}
|
||||
let placed = Scaling { patterns: true, ..Scaling::default() };
|
||||
let mut tiles = def.instances_in_with(Affine::IDENTITY, xf.inverse().transform_rect_bbox(bounds), placed);
|
||||
// Outlined strokes and effects can make more objects than the art had.
|
||||
let mut left = budget;
|
||||
for t in &mut tiles {
|
||||
*t = prepared(d, &brushes, t);
|
||||
left = left.checked_sub(t.count_with_masks())?;
|
||||
place_art(t, xf, scaling);
|
||||
}
|
||||
tiles
|
||||
Some(tiles)
|
||||
}
|
||||
|
||||
/// Pattern `def` with each path in its art (opacity masks included) that has a random Transform
|
||||
|
||||
@@ -12,7 +12,7 @@ use vectorcraft_doc::Document;
|
||||
use vectorcraft_eps::{EpsOptions, Level, Overprint, Preview, Raster};
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
|
||||
use super::super::flatten::{FlattenOptions, flatten_document};
|
||||
use super::super::flatten::{FlattenOptions, expand_transparent_tile_images, flatten_expanded};
|
||||
use super::super::{CommandSpec, EngineError, Result, Session, bad, bool_or, cmd, has_doc, str_param};
|
||||
use super::{ArtboardPick, Encoded, FormatOption};
|
||||
|
||||
@@ -299,11 +299,13 @@ fn encode_document(doc: &Document, p: &Value, saving: bool) -> Result<Encoded> {
|
||||
return Err(bad(C, "the document has no artboard"));
|
||||
}
|
||||
let regions: Vec<Rect> = pages.iter().map(|(_, region, _)| *region).collect();
|
||||
let transparent = doc.layers.iter().any(|l| l.shows_transparency());
|
||||
let flat = flatten_document(doc, &set.flatten)?;
|
||||
let drawn = flat.as_ref().unwrap_or(doc);
|
||||
let expanded = expand_transparent_tile_images(doc, &set.flatten)?;
|
||||
let source = expanded.as_ref().unwrap_or(doc);
|
||||
let transparent = source.layers.iter().any(|l| set.flatten.shows_transparency(source, l));
|
||||
let flat = flatten_expanded(source, &set.flatten)?;
|
||||
let drawn = flat.as_ref().unwrap_or(source);
|
||||
let mut enc = Encoded { warnings, joiner: Some("_"), ..Encoded::default() };
|
||||
if transparent && flat.is_some() {
|
||||
if transparent && (flat.is_some() || expanded.is_some()) {
|
||||
enc.warnings.push("transparency is flattened into opaque art and images (EPS has none): see the flattener preset".into());
|
||||
}
|
||||
let preview_limit = if saving { SAVE_PREVIEW } else { MAX_PREVIEW };
|
||||
|
||||
@@ -167,7 +167,7 @@ fn expand_image_patterns(d: &mut Document) -> Result<()> {
|
||||
let keys = super::super::flatten::see_through_images(d);
|
||||
let layers: Vec<NodeId> = d.layers.iter().map(|l| l.id).collect();
|
||||
for id in layers {
|
||||
super::super::expand::expand_transparent_image_patterns(d, id, &keys)?;
|
||||
super::super::expand::expand_transparent_image_patterns(d, id, &keys, &|_, _| true)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -317,9 +317,9 @@ pub const DEFAULT_FLATTENER: &str = "High Resolution";
|
||||
|
||||
/// `doc` with its transparency flattened for a PDF 1.3 file (asked for, or PDF/X-1a and PDF/X-3,
|
||||
/// whose files have none), with `flattenerPreset` (default High Resolution) and `p`'s `flattener`
|
||||
/// options over it; images with see-through pixels and colorized images count as transparency,
|
||||
/// rasterized areas are clipped to their regions, and discarded overprints are dropped. `None`
|
||||
/// when nothing changes (or the file keeps transparency).
|
||||
/// options over it; images with see-through pixels and colorized images, including those in
|
||||
/// pattern tiles, count as transparency. Rasterized areas are clipped to their regions, and
|
||||
/// discarded overprints are dropped. `None` when nothing changes (or the file keeps transparency).
|
||||
fn flatten_for(cmd: &str, doc: &Document, set: &PdfSettings, p: &Value) -> Result<Option<Document>> {
|
||||
if !set.pdf13() {
|
||||
return Ok(None);
|
||||
|
||||
@@ -49,7 +49,7 @@ mod presets;
|
||||
mod report;
|
||||
|
||||
pub(crate) use analysis::{outlined_text, see_through_images};
|
||||
pub(crate) use apply::flatten_document;
|
||||
pub(crate) use apply::{expand_transparent_tile_images, flatten_document, flatten_expanded, has_transparent_tile_images};
|
||||
pub use commands::specs;
|
||||
pub use options::FlattenOptions;
|
||||
pub use presets::{FlattenerPreset, PRESET_EXTS, PRESET_FORMAT};
|
||||
|
||||
@@ -74,7 +74,7 @@ pub(super) fn plan(src: &Document, roots: &[NodeId], o: &FlattenOptions, render:
|
||||
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| n.shows_transparency() || o.draws_see_through_image(n)).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).
|
||||
@@ -292,12 +292,46 @@ pub(super) fn opaque(n: &mut Node) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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). `None` when that changes nothing.
|
||||
/// 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| l.shows_transparency() || o.draws_see_through_image(l)) {
|
||||
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![];
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::analysis::draws_image;
|
||||
use super::apply::{apply, kept_work, plan};
|
||||
use super::presets::{FlattenerPreset, saved_preset};
|
||||
use super::report::target_roots;
|
||||
use super::{BTreeSet, Deserialize, EngineError, Node, Result, Serialize, Session, Value, bad, json, str_param};
|
||||
use super::{BTreeSet, Deserialize, Document, EngineError, Node, NodeKind, Paint, Result, Serialize, Session, Value, bad, json, str_param};
|
||||
|
||||
/// Object → Flatten Transparency settings: a preset's, adjusted by the command's params.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -34,6 +34,10 @@ pub struct FlattenOptions {
|
||||
/// amounts). `None` for ordinary flattening. Not a preset option.
|
||||
#[serde(skip)]
|
||||
pub no_transparency: Option<BTreeSet<String>>,
|
||||
/// The file writes a colorized image in its colorize paint (EPS), so a colorize paint by
|
||||
/// itself doesn't count as transparency. Not a preset option.
|
||||
#[serde(skip)]
|
||||
pub keeps_colorize: bool,
|
||||
}
|
||||
|
||||
impl Default for FlattenOptions {
|
||||
@@ -81,6 +85,7 @@ impl FlattenOptions {
|
||||
preserve_alpha: false,
|
||||
preserve_overprints: true,
|
||||
no_transparency: None,
|
||||
keeps_colorize: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +143,21 @@ impl FlattenOptions {
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
/// Whether `n`, art of `d`, shows transparency these options flatten
|
||||
/// ([`Node::shows_transparency`], colorized images as [`Self::counts_colorize`] says).
|
||||
pub(crate) fn shows_transparency(&self, d: &Document, n: &Node) -> bool {
|
||||
n.shows_transparency_with(&|c| self.counts_colorize(d, c))
|
||||
}
|
||||
|
||||
/// Whether colorized image `n`, art of `d`, counts as transparency by itself. With
|
||||
/// [`Self::keeps_colorize`], an image colorized with a solid paint whose pixels are grayscale
|
||||
/// or 1-bit doesn't: the EPS writer draws it in that paint.
|
||||
pub(crate) fn counts_colorize(&self, d: &Document, n: &Node) -> bool {
|
||||
let NodeKind::Image(im) = &n.kind else { return true };
|
||||
let solid = matches!(n.extras().colorize.as_deref(), Some(Paint::Solid { .. }));
|
||||
!self.keeps_colorize || !solid || d.image_blob(im).is_some_and(|b| b.gray_source().is_none())
|
||||
}
|
||||
|
||||
/// `n` shows an image whose see-through pixels count as transparency.
|
||||
pub(super) fn draws_see_through_image(&self, n: &Node) -> bool {
|
||||
self.no_transparency.as_ref().is_some_and(|keys| draws_image(n, keys))
|
||||
|
||||
@@ -11,6 +11,7 @@ use vectorcraft_pdf::{PrintLayers, PrintOptions, PrintPreview, PrintSettings};
|
||||
|
||||
use super::fileio::pdf::{merge, pdf_error};
|
||||
use super::fileio::{extension, write_or_return};
|
||||
use super::flatten::has_transparent_tile_images;
|
||||
use super::{CommandSpec, EngineError, Result, Session, bad, bool_or, cmd, has_doc, str_param};
|
||||
|
||||
const SETUP: &str = "print.setup";
|
||||
@@ -179,6 +180,12 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Raster effects print as images at the document's raster effects resolution.
|
||||
let flat = super::rasterfx::flatten_raster_effects(doc);
|
||||
let doc = flat.as_ref().unwrap_or(doc);
|
||||
let bitmap = set.advanced.print_as_bitmap && set.output.mode == vectorcraft_pdf::OutputMode::Composite;
|
||||
// The flattener expands pattern fills whose tile images need flattening, in the layers that print.
|
||||
let tile_images = postscript
|
||||
&& !bitmap
|
||||
&& super::printadvanced::flattener(PRINT, &set, &s.prefs.flattener_presets)?
|
||||
.is_some_and(|o| has_transparent_tile_images(&vectorcraft_pdf::printed_document(doc, &set), &o));
|
||||
// Print as Bitmap and the flattener preset (Advanced) render and flatten it first.
|
||||
let prepared = super::printadvanced::prepare(PRINT, doc, &set, &s.prefs.flattener_presets, s.prefs.black_output == "rich")?;
|
||||
let path = str_param(p, "path");
|
||||
@@ -201,8 +208,7 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => Level::default(),
|
||||
};
|
||||
let mut warnings = vec![];
|
||||
let bitmap = set.advanced.print_as_bitmap && set.output.mode == vectorcraft_pdf::OutputMode::Composite;
|
||||
if !bitmap && doc.layers.iter().any(|l| l.shows_transparency()) {
|
||||
if !bitmap && (tile_images || doc.layers.iter().any(|l| l.shows_transparency())) {
|
||||
warnings.push("transparency is flattened into opaque art and images (PostScript has none): see flattenerPreset".to_string());
|
||||
}
|
||||
if !set.color.profile.trim().is_empty() && set.output.mode == vectorcraft_pdf::OutputMode::Composite {
|
||||
|
||||
@@ -546,7 +546,7 @@ fn a_colorized_grayscale_image_pattern_fill_is_expanded() {
|
||||
let keys = crate::cmd::flatten::see_through_images(d);
|
||||
assert!(keys.is_empty());
|
||||
let layer = d.layers[0].id;
|
||||
assert_eq!(crate::cmd::expand::expand_transparent_image_patterns(d, layer, &keys).unwrap(), 1);
|
||||
assert_eq!(crate::cmd::expand::expand_transparent_image_patterns(d, layer, &keys, &|_, _| true).unwrap(), 1);
|
||||
let mut patterns = 0;
|
||||
let mut tile_images = 0;
|
||||
d.walk(|n| {
|
||||
@@ -688,7 +688,7 @@ fn only_the_transparent_pattern_fill_of_a_leaf_is_expanded() {
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let keys = crate::cmd::flatten::see_through_images(d);
|
||||
let layer = d.layers[0].id;
|
||||
assert_eq!(crate::cmd::expand::expand_transparent_image_patterns(d, layer, &keys).unwrap(), 1);
|
||||
assert_eq!(crate::cmd::expand::expand_transparent_image_patterns(d, layer, &keys, &|_, _| true).unwrap(), 1);
|
||||
let mut patterns = vec![];
|
||||
let mut images = 0;
|
||||
d.walk(|n| {
|
||||
@@ -704,3 +704,304 @@ fn only_the_transparent_pattern_fill_of_a_leaf_is_expanded() {
|
||||
assert_eq!(patterns.len(), 1, "{patterns:?}");
|
||||
assert!(images > 0);
|
||||
}
|
||||
|
||||
/// A pattern fill whose expanded art would hold more objects than the budget stays a pattern fill:
|
||||
/// the 32 pt tiles over the 128 pt square are 25 copies of the tile art's 3 objects, which are not
|
||||
/// made with a smaller budget, and the clip group around them counts too.
|
||||
#[test]
|
||||
fn a_transparent_image_pattern_past_the_budget_stays() {
|
||||
let expand = |budget: usize| {
|
||||
let mut s = pattern_over_blue(&checker([0, 200, 0, 255], [255, 255, 255, 0]), 1.0);
|
||||
assert_eq!(s.doc().unwrap().doc.patterns[0].art.iter().map(|a| a.count_with_masks()).sum::<usize>(), 3);
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let keys = crate::cmd::flatten::see_through_images(d);
|
||||
let layer = d.layers[0].id;
|
||||
let before = d.layers[0].count_with_masks();
|
||||
let n = crate::cmd::expand::expand_transparent_image_patterns_within(d, layer, &keys, &|_, _| true, budget).unwrap();
|
||||
// The objects the expansion made, with the square it replaced.
|
||||
(n, images(d).0, d.layers[0].count_with_masks() + 1 - before)
|
||||
};
|
||||
let (n, tiles, made) = expand(usize::MAX);
|
||||
assert_eq!((n, tiles), (1, 25));
|
||||
assert!(made > 75, "{made}");
|
||||
assert_eq!(expand(made), (1, 25, made));
|
||||
assert_eq!(expand(made - 1).0, 0);
|
||||
assert_eq!(expand(74).0, 0);
|
||||
}
|
||||
|
||||
/// On a shape with two transparent image pattern fills, both expand with a budget their tiles and
|
||||
/// the group around them fit, and one expands with one object less.
|
||||
#[test]
|
||||
fn two_transparent_image_pattern_fills_expand_within_the_budget() {
|
||||
let expand = |budget: usize| {
|
||||
let mut s = pattern_over_blue(&checker([0, 200, 0, 255], [255, 255, 255, 0]), 1.0);
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let mut square = None;
|
||||
d.walk(|n| {
|
||||
if n.appearance.items.iter().any(
|
||||
|i| matches!(i, vectorcraft_doc::appearance::AppearanceItem::Fill(f) if matches!(f.paint, vectorcraft_color::Paint::Pattern { .. })),
|
||||
) {
|
||||
square = Some(n.id);
|
||||
}
|
||||
});
|
||||
let n = d.node_mut(square.unwrap()).unwrap();
|
||||
assert_eq!(n.appearance.items.len(), 1);
|
||||
let fill = n.appearance.items[0].clone();
|
||||
n.appearance.items.push(fill);
|
||||
let keys = crate::cmd::flatten::see_through_images(d);
|
||||
let layer = d.layers[0].id;
|
||||
let before = d.layers[0].count_with_masks();
|
||||
let n = crate::cmd::expand::expand_transparent_image_patterns_within(d, layer, &keys, &|_, _| true, budget).unwrap();
|
||||
(n, images(d).0, d.layers[0].count_with_masks() + 1 - before)
|
||||
};
|
||||
let (n, tiles, made) = expand(usize::MAX);
|
||||
assert_eq!((n, tiles), (1, 50));
|
||||
assert_eq!(expand(made), (1, 50, made));
|
||||
assert_eq!(expand(made - 1).1, 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf13_flattens_see_through_pattern_tile_images() {
|
||||
let mut s = pattern_over_blue(&checker([0, 200, 0, 255], [255, 255, 255, 0]), 1.0);
|
||||
let before = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)];
|
||||
let r = s.execute("document.exportPdf", &json!({"compatibility": "1.3"})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
|
||||
let page = vectorcraft_pdf::import(&bytes).unwrap();
|
||||
let after = [colour_at(&page, 72.0, 72.0), colour_at(&page, 88.0, 72.0)];
|
||||
for (a, b) in before.iter().zip(after) {
|
||||
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 4), "before {before:?} after {after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf13_flattens_colorized_pattern_tile_images() {
|
||||
let mut s = colorized_pattern_over_blue();
|
||||
let before = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)];
|
||||
let r = s.execute("document.exportPdf", &json!({"compatibility": "1.3"})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
|
||||
let page = vectorcraft_pdf::import(&bytes).unwrap();
|
||||
let after = [colour_at(&page, 72.0, 72.0), colour_at(&page, 88.0, 72.0)];
|
||||
for (a, b) in before.iter().zip(after) {
|
||||
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 4), "before {before:?} after {after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_level3_writes_see_through_pattern_tile_images() {
|
||||
let mut s = pattern_over_blue(&checker([0, 200, 0, 255], [255, 255, 255, 0]), 1.0);
|
||||
let before = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)];
|
||||
let r = s.execute("document.exportEps", &json!({"previewFormat": "none", "useArtboards": true, "level": 3})).unwrap();
|
||||
s.execute("document.open", &json!({"name": "p.eps", "dataBase64": r["dataBase64"], "editingData": false})).unwrap();
|
||||
let page = &s.doc().unwrap().doc;
|
||||
let after = [colour_at(page, 72.0, 72.0), colour_at(page, 88.0, 72.0)];
|
||||
for (a, b) in before.iter().zip(after) {
|
||||
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 4), "before {before:?} after {after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_writes_colorized_pattern_tile_images_in_their_colors() {
|
||||
let mut s = colorized_pattern_over_blue();
|
||||
let before = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)];
|
||||
let r = s.execute("document.exportEps", &json!({"previewFormat": "none", "useArtboards": true, "level": 3})).unwrap();
|
||||
s.execute("document.open", &json!({"name": "p.eps", "dataBase64": r["dataBase64"], "editingData": false})).unwrap();
|
||||
let after = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)];
|
||||
for (a, b) in before.iter().zip(after) {
|
||||
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 4), "before {before:?} after {after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_that_keeps_colorize_leaves_colorized_pattern_tile_images_alone() {
|
||||
let s = colorized_pattern_over_blue();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(crate::cmd::flatten::flatten_document(d, &crate::cmd::FlattenOptions::default()).unwrap().is_some());
|
||||
let o = crate::cmd::FlattenOptions { keeps_colorize: true, ..Default::default() };
|
||||
assert!(crate::cmd::flatten::flatten_document(d, &o).unwrap().is_none(), "the pattern isn't expanded for a colorize paint alone");
|
||||
}
|
||||
|
||||
/// EPS export expands pattern tile images once: flattening the expanded
|
||||
/// document leaves the patterns the expansion kept, past its object limit, as they are.
|
||||
#[test]
|
||||
fn flattening_an_expanded_document_does_not_expand_patterns_again() {
|
||||
let s = colorized_pattern_over_blue();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(crate::cmd::flatten::flatten_expanded(d, &crate::cmd::FlattenOptions::default()).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_flattens_colorized_image_in_a_see_through_pattern_tile_group_and_warns() {
|
||||
let mut s = colorized_pattern_over_blue();
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let id = d.alloc_id();
|
||||
let art = std::mem::take(&mut d.patterns[0].art);
|
||||
let mut group = vectorcraft_doc::Node::group(id, art);
|
||||
group.opacity = 0.5;
|
||||
d.patterns[0].art.push(std::sync::Arc::new(group));
|
||||
let before = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)];
|
||||
let r = s.execute("document.exportEps", &json!({"previewFormat": "none", "useArtboards": true, "level": 3})).unwrap();
|
||||
assert!(warnings(&r).iter().any(|w| w.contains("transparency is flattened")), "{:?}", warnings(&r));
|
||||
s.execute("document.open", &json!({"name": "p.eps", "dataBase64": r["dataBase64"], "editingData": false})).unwrap();
|
||||
let after = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)];
|
||||
for (a, b) in before.iter().zip(after) {
|
||||
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 4), "before {before:?} after {after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postscript_print_warns_for_colorized_pattern_tile_image() {
|
||||
let mut s = colorized_pattern_over_blue();
|
||||
let r = s.execute("file.print", &json!({"format": "postscript"})).unwrap();
|
||||
assert!(warnings(&r).iter().any(|w| w.contains("transparency is flattened")), "{:?}", warnings(&r));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_pattern_does_not_flatten_a_separate_see_through_image() {
|
||||
let mut output = vec![];
|
||||
for tile in [checker([0, 200, 0, 255], [255, 230, 0, 255]), checker([0, 200, 0, 255], [255, 255, 255, 0])] {
|
||||
let mut s = pattern_over_blue(&tile, 1.0);
|
||||
s.execute("shape.rectangle", &json!({"x": 8, "y": 8, "width": 24, "height": 24})).unwrap();
|
||||
s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap();
|
||||
let p = json!({"name": "i.png", "dataBase64": vectorcraft_format::base64_encode(&png(20, 20, [0, 0, 0, 128])), "at": [20, 20]});
|
||||
s.execute("file.place", &p).unwrap();
|
||||
let r =
|
||||
s.execute("document.exportEps", &json!({"previewFormat": "none", "useArtboards": true, "level": 3, "flattenerPreset": "high"})).unwrap();
|
||||
let image_warning = warnings(&r).iter().any(|w| w.contains("partly transparent"));
|
||||
s.execute("document.open", &json!({"name": "p.eps", "dataBase64": r["dataBase64"], "editingData": false})).unwrap();
|
||||
output.push((colour_at(&s.doc().unwrap().doc, 20.0, 20.0), image_warning));
|
||||
}
|
||||
assert_eq!(output[0], output[1], "EPS layer image changed when a see-through pattern tile was present: {output:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opaque_pattern_tile_images_do_not_change_during_export_flattening() {
|
||||
let s = pattern_over_blue(&checker([0, 200, 0, 255], [255, 230, 0, 255]), 1.0);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(crate::cmd::flatten::flatten_document(d, &crate::cmd::FlattenOptions::default()).unwrap().is_none());
|
||||
let o = crate::cmd::FlattenOptions { no_transparency: Some(crate::cmd::flatten::see_through_images(d)), ..Default::default() };
|
||||
assert!(crate::cmd::flatten::flatten_document(d, &o).unwrap().is_none());
|
||||
}
|
||||
|
||||
/// The id of the square [`pattern_over_blue`] fills with its pattern.
|
||||
fn pattern_square(s: &Session) -> vectorcraft_doc::NodeId {
|
||||
let mut id = None;
|
||||
s.doc().unwrap().doc.walk(|n| {
|
||||
if n.appearance.items.iter().any(|i| matches!(i.paint(), vectorcraft_color::Paint::Pattern { .. })) {
|
||||
id = Some(n.id);
|
||||
}
|
||||
});
|
||||
id.unwrap()
|
||||
}
|
||||
|
||||
/// PDF 1.3 draws `s` as the canvas draws it at two points of the pattern square.
|
||||
fn assert_pdf13_draws_pattern_square_as_canvas(s: &mut Session) {
|
||||
let before = [colour_at(&s.doc().unwrap().doc, 72.0, 72.0), colour_at(&s.doc().unwrap().doc, 88.0, 72.0)];
|
||||
let r = s.execute("document.exportPdf", &json!({"compatibility": "1.3"})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
|
||||
let page = vectorcraft_pdf::import(&bytes).unwrap();
|
||||
let after = [colour_at(&page, 72.0, 72.0), colour_at(&page, 88.0, 72.0)];
|
||||
for (a, b) in before.iter().zip(after) {
|
||||
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 4), "before {before:?} after {after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf13_flattens_colorized_pattern_tile_images_in_a_repeat() {
|
||||
let mut s = colorized_pattern_over_blue();
|
||||
let square = pattern_square(&s);
|
||||
s.execute("object.repeat.grid", &json!({"ids": [square.0]})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.layers[0].count() > 3);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let mut repeats = 0;
|
||||
d.walk(|n| repeats += usize::from(matches!(n.kind, vectorcraft_doc::NodeKind::Repeat(_))));
|
||||
assert_eq!(repeats, 1);
|
||||
assert!(crate::cmd::flatten::flatten_document(d, &crate::cmd::FlattenOptions::default()).unwrap().is_some());
|
||||
assert_pdf13_draws_pattern_square_as_canvas(&mut s);
|
||||
}
|
||||
|
||||
/// A repeat whose art would take the expanded art past the budget stays live.
|
||||
#[test]
|
||||
fn a_repeat_past_the_budget_stays_live() {
|
||||
for (budget, live) in [(1, 1), (usize::MAX, 0)] {
|
||||
let mut s = colorized_pattern_over_blue();
|
||||
let square = pattern_square(&s);
|
||||
s.execute("object.repeat.grid", &json!({"ids": [square.0]})).unwrap();
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let layer = d.layers[0].id;
|
||||
crate::cmd::expand::expand_transparent_image_patterns_within(d, layer, &Default::default(), &|_, _| true, budget).unwrap();
|
||||
let mut repeats = 0;
|
||||
d.walk(|n| repeats += usize::from(matches!(n.kind, vectorcraft_doc::NodeKind::Repeat(_))));
|
||||
assert_eq!(repeats, live, "budget {budget}");
|
||||
}
|
||||
}
|
||||
|
||||
/// [`colorized_pattern_over_blue`] with the square filled with a pattern named `outer` whose tile
|
||||
/// art is a 32 pt square filled with the colorized image pattern.
|
||||
fn nested_colorized_pattern_over_blue() -> Session {
|
||||
let mut s = colorized_pattern_over_blue();
|
||||
let square = pattern_square(&s);
|
||||
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 32, "height": 32})).unwrap();
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let inner = d.patterns[0].name.clone();
|
||||
let rect = d.layers[0].children().unwrap().last().unwrap().id;
|
||||
let mut tile = d.remove(rect).unwrap();
|
||||
let paint = vectorcraft_color::Paint::Pattern { pattern: inner, xf: Default::default() };
|
||||
let tile_node = std::sync::Arc::make_mut(&mut tile);
|
||||
tile_node.appearance.items.retain(|i| matches!(i, vectorcraft_doc::appearance::AppearanceItem::Fill(_)));
|
||||
for i in &mut tile_node.appearance.items {
|
||||
if let vectorcraft_doc::appearance::AppearanceItem::Fill(f) = i {
|
||||
f.paint = paint.clone();
|
||||
}
|
||||
}
|
||||
let mut outer = d.patterns[0].clone();
|
||||
outer.name = "outer".into();
|
||||
outer.art = vec![tile];
|
||||
d.patterns.push(outer);
|
||||
for i in &mut d.node_mut(square).unwrap().appearance.items {
|
||||
if let vectorcraft_doc::appearance::AppearanceItem::Fill(f) = i {
|
||||
f.paint = vectorcraft_color::Paint::Pattern { pattern: "outer".into(), xf: Default::default() };
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf13_flattens_colorized_image_in_a_pattern_nested_in_tile_art() {
|
||||
let mut s = nested_colorized_pattern_over_blue();
|
||||
assert_eq!(colour_at(&s.doc().unwrap().doc, 72.0, 72.0), [255, 0, 0]);
|
||||
assert!(crate::cmd::flatten::flatten_document(&s.doc().unwrap().doc, &crate::cmd::FlattenOptions::default()).unwrap().is_some());
|
||||
assert_pdf13_draws_pattern_square_as_canvas(&mut s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_skips_a_pattern_nested_in_hidden_tile_art() {
|
||||
let mut s = nested_colorized_pattern_over_blue();
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let outer = d.pattern_mut("outer").unwrap();
|
||||
std::sync::Arc::make_mut(&mut outer.art[0]).visible = false;
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(crate::cmd::flatten::flatten_document(d, &crate::cmd::FlattenOptions::default()).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf13_leaves_a_hidden_shape_with_a_colorized_pattern_tile_image_hidden() {
|
||||
let mut s = colorized_pattern_over_blue();
|
||||
let square = pattern_square(&s);
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).node_mut(square).unwrap().visible = false;
|
||||
assert_pdf13_draws_pattern_square_as_canvas(&mut s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattening_skips_pattern_tile_images_on_hidden_and_template_layers() {
|
||||
for template in [false, true] {
|
||||
let mut s = colorized_pattern_over_blue();
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let layer = std::sync::Arc::make_mut(&mut d.layers[0]);
|
||||
match &mut layer.kind {
|
||||
vectorcraft_doc::NodeKind::Layer { template: t, .. } if template => *t = true,
|
||||
_ => layer.visible = false,
|
||||
}
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(crate::cmd::flatten::flatten_document(d, &crate::cmd::FlattenOptions::default()).unwrap().is_none(), "template {template}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user