mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 21:57:12 -06:00
Merge pull request #5065 from dotCooCoo/fix/ai-os15
.ai import: envelopes open as live envelopes
This commit is contained in:
@@ -2954,6 +2954,40 @@ proptest! {
|
||||
survive("mutated EPS editing data", || vectorcraft_eps::import(&bytes).ok().map(|r| r.document))?;
|
||||
}
|
||||
|
||||
/// A `.ai` whose envelopes are damaged or hostile: any kind, warp style, options word,
|
||||
/// Fidelity and grid size, corners out of range or repeated, the contents block cut short.
|
||||
#[test]
|
||||
fn ai_envelope_never_panics(
|
||||
kind in prop::sample::select(vec![0i64, 1, 2, 3, 4, -1, 1 << 40]),
|
||||
style in prop::sample::select(vec![0i64, 1, 8, 15, 16, -3, i64::MAX]),
|
||||
word in prop::sample::select(vec![0i64, 1, 131073, 65536, 0x2_0007, 1 << 18, -1, i64::MAX]),
|
||||
fidelity in prop::sample::select(vec!["50", "0", "100", "101", "-1", "1e308", "nan"]),
|
||||
size in prop::sample::select(vec!["[1 1]", "[3 2]", "[0 0]", "[51 1]", "[100000 100000]", "[-1 2]", "[1e308 1]"]),
|
||||
corner in prop::sample::select(vec!["10 10", "1e308 -1e308", "nan 0", "10 10 10 10", "0"]),
|
||||
edit in 0i64..2,
|
||||
cut in 0usize..2_000,
|
||||
edits in prop::collection::vec((0usize..2_000, prop::sample::select(vec!['0', '9', '-', ' ', '\n', '(', ')', '/', ':', ';', ',', '[', ']', '%', '_', 'X', 'u', 'U'])), 0..8),
|
||||
) {
|
||||
use vectorcraft_testkit::ai;
|
||||
let n = |x: &str, y: &str| format!("%_[0.5 1 {x} {y} {x} {y} 1 {x} {y} 1] /N X#\n");
|
||||
let mesh = format!(
|
||||
"%_/Mesh X!\n%_2 /Version X#\n%_/Cartesian /Type X#\n%_{size} /Size X#\n%_/Data X#\n%_[0 0] /P X#\n{}{}{}%_[{corner}] /N X#\n%_/E X#\n%_[0 0] /P X#\n%_/E X#\n%_/End X!\n",
|
||||
n("10", "10"),
|
||||
n("10", "90"),
|
||||
n("90", "90")
|
||||
);
|
||||
let content = "%_u\n%_1 0 0 1 0 0 Xa\n%_10 10 m\n%_10 90 L\n%_90 90 L\n%_90 10 L\n%_f\n%_U\n";
|
||||
let keys = format!(
|
||||
"%_{word} /Int (AIDeformOptionsKey) ,\n%_{fidelity} /Int (AIDeformFidelityKey) ,\n%_{edit} /Bool (AIDeformEditModeKey) ,\n%_{style} /Int (DeformStyle) ,\n%_0.5 /Real (DeformValue) ,\n%_1 /Bool (Rotate) ,\n"
|
||||
);
|
||||
let art = format!(
|
||||
"%_u\n{mesh}%_U\n%_/ArtDictionary :\n{keys}%_X=\n{content}%_X+\n%_ (AIDeformArtKey) ,\n%_{kind} /Int (AIDeformStyle) ,\n%_;\n%_\nu\n1 0 0 1 0 0 Xa\n12 12 m\n12 88 L\n88 88 L\nf\nU\n(Adobe Deform Plugin) 1 0 0 XP\n0 0 1 1 0 0 Xa\n0 0 m\n5 5 L\nf\n"
|
||||
);
|
||||
let data = mutate_text(&ai::editing_data(100.0, 100.0, &ai::layer("L", &art)), cut, &edits);
|
||||
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&data)].concat();
|
||||
survive("hostile .ai envelope", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?;
|
||||
}
|
||||
|
||||
/// A `.ai` whose editing data is damaged or hostile.
|
||||
#[test]
|
||||
fn ai_editing_data_never_panics(cut in 0usize..3_000, edits in prop::collection::vec((0usize..3_000, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', ':', ';', '[', ']', '%', 'q', 'Q', 'W', 'u', 'U', 'L', 'k', 'x'])), 0..12), damage in prop::collection::vec((0usize..4_000, any::<u8>()), 0..4)) {
|
||||
@@ -3328,6 +3362,12 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
|
||||
"[1 1] /P X#",
|
||||
"(Adobe Path Blends) 1 0 92 XP",
|
||||
"(Adobe Path Blends) 1 0 999999 XP",
|
||||
"(Adobe Deform Plugin) 1 0 0 XP",
|
||||
"%_/ArtDictionary :\n%_131073 /Int (AIDeformOptionsKey) ,\n%_X=\n",
|
||||
"%_X+\n%_ (AIDeformArtKey) ,\n%_2 /Int (AIDeformStyle) ,\n%_;\n",
|
||||
"%_1 /Int (AIDeformStyle) ,\n%_9 /Int (DeformStyle) ,\n%_1e308 /Real (DeformValue) ,\n%_;\n",
|
||||
"%_1 /Bool (AIDeformEditModeKey) ,",
|
||||
"[1e308 1e308] /Size X#",
|
||||
"\n%44434241010000000000000000000000\n",
|
||||
"\n%0200000041000000000000000000000000000000\n",
|
||||
"\n%4G\n",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//! did before (the printed page, or the PDF), with a warning saying why.
|
||||
|
||||
mod blend;
|
||||
mod envelope;
|
||||
mod frames;
|
||||
mod lex;
|
||||
mod mesh;
|
||||
@@ -32,6 +33,7 @@ use std::io::Read as _;
|
||||
use super::lex::find;
|
||||
use crate::ps;
|
||||
|
||||
pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN;
|
||||
pub(crate) use frames::text_frames;
|
||||
pub use read::{IMAGE_TOO_LARGE, PaletteSwatches, Structure, artboards, palette, read, variables};
|
||||
pub(crate) use read::{join_and, slot_frame, slot_of};
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
//! Envelopes from the editing data (`(Adobe Deform Plugin) 1 0 0 XP`), read as live envelopes.
|
||||
//!
|
||||
//! What an envelope is and does is Illustrator's public documentation: Make with Warp, Make with
|
||||
//! Mesh (rows and columns) and Make with Top Object
|
||||
//! (<https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-objects-with-envelopes.html>),
|
||||
//! Edit Contents
|
||||
//! (<https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/edit-the-contents-of-envelopes.html>),
|
||||
//! Envelope Options
|
||||
//! (<https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/envelope-panel-options.html>)
|
||||
//! and the warp styles with their bend and distortions
|
||||
//! (<https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html>).
|
||||
//!
|
||||
//! An envelope is written in three parts. On `%_` lines, a group holding the mesh that shapes it,
|
||||
//! then its `/ArtDictionary`: Envelope Options as a bit word (`AIDeformOptionsKey`), Fidelity
|
||||
//! (`AIDeformFidelityKey`), Edit Contents (`AIDeformEditModeKey`), a warp's style (`DeformStyle`,
|
||||
//! numbered as Effect › Warp lists them), bend and distortions (`DeformValue`, `DeformHoriz`,
|
||||
//! `DeformVert`, fractions) and orientation (`Rotate`), the contents (`X=` … `X+`, then
|
||||
//! `(AIDeformArtKey)`) and the kind (`AIDeformStyle`: 1 a warp, 2 a mesh, 3 a top object). Then the
|
||||
//! distorted art on plain lines, and the plugin's `XP`. While the contents are edited, the group
|
||||
//! holds the contents and the dictionary the mesh. A top object's outline is the edge of its
|
||||
//! one-patch mesh. An envelope whose parts don't read, or that doesn't come out as drawn, keeps its
|
||||
//! drawn art.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use kurbo::{BezPath, Point, Rect, Vec2};
|
||||
use vectorcraft_doc::live::{EnvelopeKind, EnvelopeOptions, GradientMesh, H_DOWN, H_LEFT, H_RIGHT, H_UP, PreserveShape, default_fidelity};
|
||||
use vectorcraft_doc::{Document, ImageBlob, LayerColor, Node, NodeId, NodeKind, PatternDef};
|
||||
use vectorcraft_geom::PathData;
|
||||
|
||||
use super::obj::Obj;
|
||||
use crate::import::native::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, compare_scale, differ, differ_in_colour};
|
||||
|
||||
/// The plugin that writes envelopes.
|
||||
pub(super) const PLUGIN: &str = "Adobe Deform Plugin"; // brand-ok: the plugin name the files use
|
||||
/// The note for an envelope that keeps its drawn art.
|
||||
pub(crate) const DRAWN: &str = "an envelope VectorCraft couldn't read as a live envelope comes in as its drawn art";
|
||||
/// The most envelopes waiting for their drawn art at once.
|
||||
pub(super) const MAX_PENDING: usize = 64;
|
||||
/// The most envelopes one import draws to check; more keep their drawn art.
|
||||
pub(super) const MAX_CHECKED: usize = 256;
|
||||
/// The most rows or columns of a mesh read (Make with Mesh's own limit).
|
||||
const MAX_GRID: u32 = 50;
|
||||
/// The bits of the options word that are read: Distort Appearance, Distort Pattern Fills, Distort
|
||||
/// Linear Gradients, Anti-Alias (set: off) and Preserve Shape Using (set: Clipping Mask).
|
||||
const KNOWN_BITS: i64 = 0b111 | 1 << 16 | 1 << 17;
|
||||
/// More of the inked pixels than this differing, each with all its neighbours, and the envelope
|
||||
/// doesn't come out as drawn.
|
||||
const MAX_SOLID: f64 = 0.03;
|
||||
/// More of the inked pixels than this differing at all, and the envelope doesn't come out as drawn.
|
||||
const MAX_ANY: f64 = 0.1;
|
||||
|
||||
/// The whole number of entry `key`: `None` when it is missing, `Some(None)` when it isn't one.
|
||||
fn whole(o: &Obj, key: &str) -> Option<Option<i64>> {
|
||||
o.get(key)?;
|
||||
let n = o.nums(key);
|
||||
Some(match n.as_slice() {
|
||||
[v] if v.is_finite() && v.fract() == 0.0 && v.abs() < 1e12 => Some(*v as i64),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A switch (`0` or `1`), off when missing.
|
||||
fn switch(o: &Obj, key: &str) -> Option<bool> {
|
||||
match whole(o, key) {
|
||||
None | Some(Some(0)) => Some(false),
|
||||
Some(Some(1)) => Some(true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Envelope Options and Fidelity.
|
||||
pub(super) fn options(o: &Obj) -> Option<(EnvelopeOptions, f64)> {
|
||||
let word = whole(o, "AIDeformOptionsKey")??;
|
||||
if word < 0 || word & !KNOWN_BITS != 0 {
|
||||
return None;
|
||||
}
|
||||
let fidelity = match whole(o, "AIDeformFidelityKey") {
|
||||
None => default_fidelity(),
|
||||
Some(Some(f)) if (0..=100).contains(&f) => f as f64,
|
||||
Some(_) => return None,
|
||||
};
|
||||
let bit = |n: u32| word & (1 << n) != 0;
|
||||
let options = EnvelopeOptions {
|
||||
anti_alias: !bit(16),
|
||||
preserve_shape: if bit(17) { PreserveShape::ClippingMask } else { PreserveShape::Transparency },
|
||||
distort_appearance: bit(0),
|
||||
distort_pattern_fills: bit(1),
|
||||
distort_linear_gradients: bit(2),
|
||||
};
|
||||
Some((options, fidelity))
|
||||
}
|
||||
|
||||
/// A warp: its style, bend, distortions and orientation.
|
||||
fn warp(o: &Obj) -> Option<EnvelopeKind> {
|
||||
let style = whole(o, "DeformStyle")??;
|
||||
let id = super::style::warp_style(style)?;
|
||||
let percent = |key: &str| -> Option<f64> {
|
||||
if o.get(key).is_none() {
|
||||
return Some(0.0);
|
||||
}
|
||||
match o.nums(key).as_slice() {
|
||||
[v] if v.is_finite() && (-1.0..=1.0).contains(v) => Some(v * 100.0),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
o.get("DeformValue")?;
|
||||
let vertical = switch(o, "Rotate")?;
|
||||
Some(EnvelopeKind::Warp {
|
||||
style: id.into(),
|
||||
bend: percent("DeformValue")?,
|
||||
h: percent("DeformHoriz")?,
|
||||
v: percent("DeformVert")?,
|
||||
horizontal: !vertical,
|
||||
})
|
||||
}
|
||||
|
||||
/// The gradient mesh `n` is, or the one its plain groups hold alone.
|
||||
fn only_mesh(n: &Node) -> Option<&GradientMesh> {
|
||||
match &n.kind {
|
||||
NodeKind::Mesh(m) => Some(m),
|
||||
NodeKind::Group { children, clip: false } => match children.as_slice() {
|
||||
[c] => only_mesh(c),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A grid point and its handles.
|
||||
type GridPoint = (Point, [Vec2; 4]);
|
||||
|
||||
/// The points of mesh `m` as a `rows`×`cols` grid with its first row at the top and its first
|
||||
/// column at the left (y down), as an envelope's mesh lays them out; `None` for a grid out of range
|
||||
/// or with points that aren't finite.
|
||||
fn upright(m: &GradientMesh) -> Option<(u32, u32, Vec<GridPoint>)> {
|
||||
let (mut rows, mut cols) = (m.rows, m.cols);
|
||||
if rows == 0 || cols == 0 || rows > MAX_GRID || cols > MAX_GRID {
|
||||
return None;
|
||||
}
|
||||
let len = (rows as usize + 1) * (cols as usize + 1);
|
||||
if m.points.len() != len {
|
||||
return None;
|
||||
}
|
||||
let finite = |v: Vec2| v.x.is_finite() && v.y.is_finite();
|
||||
if !m.points.iter().all(|q| finite(q.p.to_vec2()) && q.handles.iter().all(|h| finite(*h))) {
|
||||
return None;
|
||||
}
|
||||
let mut pts: Vec<GridPoint> = m.points.iter().map(|q| (q.p, q.handles)).collect();
|
||||
let at = |pts: &[GridPoint], cols: u32, r: u32, c: u32| pts.get((r * (cols + 1) + c) as usize).map_or(Point::ZERO, |q| q.0);
|
||||
let remap = |h: [Vec2; 4], slots: [usize; 4]| slots.map(|s| h.get(s).copied().unwrap_or(Vec2::ZERO));
|
||||
// Columns that run down the page: the grid goes the other way round.
|
||||
let along = at(&pts, cols, 0, cols) - at(&pts, cols, 0, 0);
|
||||
if along.y.abs() > along.x.abs() {
|
||||
let mut t = Vec::with_capacity(len);
|
||||
for r in 0..=cols {
|
||||
for c in 0..=rows {
|
||||
let (p, h) = pts.get((c * (cols + 1) + r) as usize).copied()?;
|
||||
// The new next column is the old next row.
|
||||
let mut slots = [0; 4];
|
||||
(slots[H_RIGHT], slots[H_LEFT], slots[H_DOWN], slots[H_UP]) = (H_DOWN, H_UP, H_RIGHT, H_LEFT);
|
||||
t.push((p, remap(h, slots)));
|
||||
}
|
||||
}
|
||||
(rows, cols) = (cols, rows);
|
||||
pts = t;
|
||||
}
|
||||
if (at(&pts, cols, 0, cols) - at(&pts, cols, 0, 0)).x < 0.0 {
|
||||
for row in pts.chunks_mut(cols as usize + 1) {
|
||||
row.reverse();
|
||||
for (_, h) in row.iter_mut() {
|
||||
h.swap(H_RIGHT, H_LEFT);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (at(&pts, cols, rows, 0) - at(&pts, cols, 0, 0)).y < 0.0 {
|
||||
let mut flipped: Vec<GridPoint> = pts.chunks(cols as usize + 1).rev().flatten().copied().collect();
|
||||
for (_, h) in &mut flipped {
|
||||
h.swap(H_DOWN, H_UP);
|
||||
}
|
||||
pts = flipped;
|
||||
}
|
||||
Some((rows, cols, pts))
|
||||
}
|
||||
|
||||
/// The outline of an upright grid: its edge, clockwise from the top left corner.
|
||||
fn outline(rows: u32, cols: u32, pts: &[GridPoint]) -> Option<PathData> {
|
||||
let at = |r: u32, c: u32| pts.get((r * (cols + 1) + c) as usize).copied();
|
||||
// Each edge: from a point by one of its handles to the next point by one of its handles.
|
||||
type Edge = ((u32, u32), usize, (u32, u32), usize);
|
||||
let mut ring: Vec<Edge> = vec![];
|
||||
ring.extend((0..cols).map(|c| ((0, c), H_RIGHT, (0, c + 1), H_LEFT)));
|
||||
ring.extend((0..rows).map(|r| ((r, cols), H_DOWN, (r + 1, cols), H_UP)));
|
||||
ring.extend((1..=cols).rev().map(|c| ((rows, c), H_LEFT, (rows, c - 1), H_RIGHT)));
|
||||
ring.extend((1..=rows).rev().map(|r| ((r, 0), H_UP, (r - 1, 0), H_DOWN)));
|
||||
let mut bp = BezPath::new();
|
||||
bp.move_to(at(0, 0)?.0);
|
||||
for ((ar, ac), out, (br, bc), into) in ring {
|
||||
let ((a, ha), (b, hb)) = (at(ar, ac)?, at(br, bc)?);
|
||||
bp.curve_to(a + *ha.get(out)?, b + *hb.get(into)?, b);
|
||||
}
|
||||
bp.close_path();
|
||||
Some(PathData::from_bezpath(&bp))
|
||||
}
|
||||
|
||||
/// Has group `n` nothing of its own: no name, lock, transparency, appearance or other properties,
|
||||
/// shown?
|
||||
fn plain_group(n: &Node) -> bool {
|
||||
let plain = Node::new(n.id, NodeKind::Group { children: vec![], clip: false });
|
||||
matches!(n.kind, NodeKind::Group { clip: false, .. })
|
||||
&& (&n.name, n.visible, n.locked, n.opacity, n.blend, n.isolate, n.knockout, n.knockout_shape)
|
||||
== (&plain.name, plain.visible, plain.locked, plain.opacity, plain.blend, plain.isolate, plain.knockout, plain.knockout_shape)
|
||||
&& n.appearance == plain.appearance
|
||||
&& n.extras() == plain.extras()
|
||||
}
|
||||
|
||||
/// `nodes` without the plain group the editing data puts round an envelope's contents.
|
||||
fn unwrapped(mut nodes: Vec<Node>) -> Vec<Arc<Node>> {
|
||||
if nodes.len() == 1
|
||||
&& let Some(n) = nodes.first()
|
||||
&& plain_group(n)
|
||||
&& let Some(n) = nodes.pop()
|
||||
&& let NodeKind::Group { children, .. } = n.kind
|
||||
{
|
||||
return children;
|
||||
}
|
||||
nodes.into_iter().map(Arc::new).collect()
|
||||
}
|
||||
|
||||
/// The live envelope of dictionary `o`, the group on `%_` lines before it (`before`) and the art
|
||||
/// in its `AIDeformArtKey` block (`inner`); `None` where they don't make one.
|
||||
pub(super) fn live(o: &Obj, before: Option<Node>, inner: Vec<Node>) -> Option<NodeKind> {
|
||||
let editing = switch(o, "AIDeformEditModeKey")?;
|
||||
let (options, fidelity) = options(o)?;
|
||||
let before = before?;
|
||||
// While the contents are edited, they are the group before the dictionary and the mesh is in it.
|
||||
let (content, shape): (Vec<Arc<Node>>, Option<&GradientMesh>) = if editing {
|
||||
let NodeKind::Group { children, clip: false } = &before.kind else { return None };
|
||||
let shape = match inner.as_slice() {
|
||||
[n] => only_mesh(n),
|
||||
_ => None,
|
||||
};
|
||||
(children.clone(), shape)
|
||||
} else {
|
||||
let shape = only_mesh(&before);
|
||||
(unwrapped(inner), shape)
|
||||
};
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let kind = match whole(o, "AIDeformStyle")?? {
|
||||
1 => warp(o)?,
|
||||
2 => {
|
||||
let (rows, cols, pts) = upright(shape?)?;
|
||||
let (points, handles) = pts.into_iter().unzip();
|
||||
EnvelopeKind::Mesh { rows, cols, points, handles }
|
||||
}
|
||||
3 => {
|
||||
let (rows, cols, pts) = upright(shape?)?;
|
||||
EnvelopeKind::TopObject { path: outline(rows, cols, &pts)? }
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(NodeKind::Envelope { content, kind: Box::new(kind), fidelity, editing, options, frame: Box::default() })
|
||||
}
|
||||
|
||||
/// What the import's documents hold that art refers to: the patterns and image blobs.
|
||||
pub(super) struct Shared<'a> {
|
||||
pub(super) patterns: &'a [PatternDef],
|
||||
pub(super) images: &'a BTreeMap<String, ImageBlob>,
|
||||
}
|
||||
|
||||
/// A document of `node` alone, on a layer with id `layer`, with the patterns and images it shows.
|
||||
fn alone(node: &Node, layer: NodeId, shared: &Shared<'_>) -> Document {
|
||||
let mut doc = Document::new(1.0, 1.0);
|
||||
let mut l = Node::layer(layer, "", LayerColor::Preset(0));
|
||||
if let Some(c) = l.children_mut() {
|
||||
c.push(Arc::new(node.clone()));
|
||||
}
|
||||
doc.layers = vec![Arc::new(l)];
|
||||
doc.patterns = shared.patterns.to_vec();
|
||||
let mut keys = vec![];
|
||||
doc.visit_images(|_, im| keys.push(im.key.clone()));
|
||||
for k in keys {
|
||||
if let Some(blob) = shared.images.get(&k) {
|
||||
doc.images.insert(k, blob.clone());
|
||||
}
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
/// How `live` and `drawn`, drawn over `region` at `scale`, differ: the pixels that differ in
|
||||
/// lightness or colour, those whose neighbours all differ too (where one draws something the other
|
||||
/// doesn't, not an edge drawn a little apart), and the pixels either inks. `None` when they can't
|
||||
/// be drawn.
|
||||
fn differing(live: &Document, drawn: &Document, region: Rect, scale: f64) -> Option<(usize, usize, usize)> {
|
||||
let x = vectorcraft_render::Renderer::new().render_region(live, region, scale, false);
|
||||
let y = vectorcraft_render::Renderer::new().render_region(drawn, region, scale, false);
|
||||
if x.width == 0 || x.height == 0 || x.width != y.width || x.height != y.height {
|
||||
return None;
|
||||
}
|
||||
let (w, h) = (x.width as usize, x.height as usize);
|
||||
let (xs, ys) = (x.pixels.as_chunks::<4>().0, y.pixels.as_chunks::<4>().0);
|
||||
let differs: Vec<bool> = xs.iter().zip(ys).map(|(p, q)| differ(p, q) || differ_in_colour(p, q)).collect();
|
||||
let inked = xs.iter().zip(ys).filter(|(p, q)| p[3] > 8 || q[3] > 8).count();
|
||||
let at = |i: usize, j: usize| differs.get(j * w + i).copied().unwrap_or(false);
|
||||
let any = differs.iter().filter(|d| **d).count();
|
||||
let solid = (1..h.saturating_sub(1))
|
||||
.flat_map(|j| (1..w.saturating_sub(1)).map(move |i| (i, j)))
|
||||
.filter(|&(i, j)| (j - 1..=j + 1).all(|n| (i - 1..=i + 1).all(|m| at(m, n))))
|
||||
.count();
|
||||
Some((any, solid, inked))
|
||||
}
|
||||
|
||||
/// The differences from their drawn art that the live envelopes of one import may add up to, as
|
||||
/// shares of the page drawn as the page check draws it: half of what that check allows
|
||||
/// ([`MAX_DIFFERENCE`], [`MAX_SOLID_DIFFERENCE`]), so live envelopes never make the file open from
|
||||
/// its PDF part instead.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct Spent {
|
||||
any: f64,
|
||||
solid: f64,
|
||||
}
|
||||
|
||||
/// Do `live` and `drawn` draw alike: few of the pixels either inks differ, and fewer still where a
|
||||
/// whole patch differs; and, on `page` (when the file has one), do they differ little enough that
|
||||
/// with the differences in `spent` the page check still takes the layers' art (it is added to
|
||||
/// `spent` when they do)? Both are drawn on `layer`, a layer id no other node has.
|
||||
pub(super) fn drawn_alike(live: &Node, drawn: &Node, layer: NodeId, shared: &Shared<'_>, page: Option<Rect>, spent: &mut Spent) -> bool {
|
||||
let (Some(a), Some(b)) = (live.visual_bounds(), drawn.visual_bounds()) else { return false };
|
||||
let region = a.union(b).inflate(2.0, 2.0);
|
||||
if !(region.is_finite() && region.width() > 0.0 && region.height() > 0.0) || region.width().max(region.height()) > 1e6 {
|
||||
return false;
|
||||
}
|
||||
let (x, y) = (alone(live, layer, shared), alone(drawn, layer, shared));
|
||||
let Some((any, solid, inked)) = differing(&x, &y, region, compare_scale(region)) else { return false };
|
||||
if inked == 0 {
|
||||
return false;
|
||||
}
|
||||
let share = |n: usize| n as f64 / inked as f64;
|
||||
if share(solid) > MAX_SOLID || share(any) > MAX_ANY {
|
||||
return false;
|
||||
}
|
||||
let Some(page) = page.filter(|p| p.is_finite() && p.width() > 0.0 && p.height() > 0.0) else { return true };
|
||||
// Only what is on the page counts, drawn as the page check draws it (about 400 pixels along the
|
||||
// page's longer side).
|
||||
let on_page = region.intersect(page);
|
||||
let scale = compare_scale(page);
|
||||
if on_page.width() * scale < 1.0 || on_page.height() * scale < 1.0 {
|
||||
return true;
|
||||
}
|
||||
let Some((any, solid, _)) = differing(&x, &y, on_page, scale) else { return false };
|
||||
let pixels = (page.width() * scale * page.height() * scale).max(1.0);
|
||||
let (any, solid) = (spent.any + any as f64 / pixels, spent.solid + solid as f64 / pixels);
|
||||
if any > MAX_DIFFERENCE / 2.0 || solid > MAX_SOLID_DIFFERENCE / 2.0 {
|
||||
return false;
|
||||
}
|
||||
*spent = Spent { any, solid };
|
||||
true
|
||||
}
|
||||
@@ -84,7 +84,7 @@ use art::preview_png;
|
||||
pub use document::{PaletteSwatches, VariableLibrary, artboards, palette, variables};
|
||||
pub(crate) use document::{slot_frame, slot_of};
|
||||
pub use types::IMAGE_TOO_LARGE;
|
||||
use types::{BLEND_BUDGET, FLIP, Frame, GState, Kind, PatternHead, Place, Raster};
|
||||
use types::{BLEND_BUDGET, FLIP, Frame, GState, Kind, PatternHead, PendingEnvelope, Place, Raster};
|
||||
#[cfg(test)]
|
||||
pub use types::{NON_NATIVE_ART, NON_NATIVE_ART_UNREAD, VERSIONED_UNENDED};
|
||||
pub(super) use types::{blend_mode, blend_name};
|
||||
@@ -121,6 +121,9 @@ pub struct Structure {
|
||||
/// move, in its pixels, that places them there (the corner of the box they are written with,
|
||||
/// in place of the canvas point of art space's origin).
|
||||
pub placed_elsewhere: Vec<(NodeId, Affine)>,
|
||||
/// The live envelopes read from the editing data, by id, with the drawn art each took the place
|
||||
/// of: the importer puts that art back when the layers with the envelopes don't draw as the page.
|
||||
pub envelopes_drawn: Vec<(NodeId, Node)>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -200,6 +203,17 @@ struct Reader<'a> {
|
||||
blend_record: Option<(usize, Vec<u8>)>,
|
||||
/// How many more objects blends may lay out in this import ([`super::blend::live_blend`]).
|
||||
blend_budget: usize,
|
||||
/// The group on `%_` lines an envelope's dictionary just took (see [`Reader::drop_envelope_art`]).
|
||||
envelope_art: Option<Node>,
|
||||
/// Envelopes waiting for their drawn art, innermost last.
|
||||
envelopes: Vec<PendingEnvelope>,
|
||||
/// How many more envelopes this import may draw to check ([`super::envelope::drawn_alike`]).
|
||||
envelope_checks: usize,
|
||||
/// How far the live envelopes read so far draw from their drawn art ([`super::envelope::Spent`]).
|
||||
envelope_spent: super::envelope::Spent,
|
||||
/// The live envelopes made so far and the drawn art each took the place of
|
||||
/// ([`Structure::envelopes_drawn`]).
|
||||
envelopes_drawn: Vec<(NodeId, Node)>,
|
||||
/// A group just ended, its state (`Xy`, `Xd`, `XW`) not yet read.
|
||||
group_ended: bool,
|
||||
/// A gradient mesh just ended (`/End X!`), its own state (`Xy`) not yet read: also after its
|
||||
@@ -318,6 +332,11 @@ impl<'a> Reader<'a> {
|
||||
blend_roles: HashMap::new(),
|
||||
blend_record: None,
|
||||
blend_budget: BLEND_BUDGET,
|
||||
envelope_art: None,
|
||||
envelopes: Vec::new(),
|
||||
envelope_checks: super::envelope::MAX_CHECKED,
|
||||
envelope_spent: super::envelope::Spent::default(),
|
||||
envelopes_drawn: Vec::new(),
|
||||
clip_fills: HashSet::new(),
|
||||
looks: HashSet::new(),
|
||||
ruler_guides: HashMap::new(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::document::slot_of;
|
||||
use super::ops::{paints_apart, path_rule};
|
||||
use super::types::{Frame, GState, Kind, MAX_DEPTH, MAX_NODES};
|
||||
use super::types::{Frame, GState, Kind, MAX_DEPTH, MAX_NODES, PendingEnvelope};
|
||||
use super::{AppearanceItem, Arc, BezPath, BlendMode, FillRule, HashSet, LayerColor, Node, NodeId, NodeKind, Obj, Paint, PathEl, Rc, Reader, V, obj};
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
@@ -324,17 +324,30 @@ impl<'a> Reader<'a> {
|
||||
"ArtDictionary" => {
|
||||
let envelope = o.get("AIDeformOptionsKey").is_some();
|
||||
self.art_dictionary(&o);
|
||||
let before = self.envelope_art.take();
|
||||
if envelope {
|
||||
let mut slots = vec![];
|
||||
for child in &children {
|
||||
envelope_text_slots(child, &mut slots);
|
||||
}
|
||||
let typed = !slots.is_empty() || before.as_ref().is_some_and(has_text_slot);
|
||||
for mut slot in slots {
|
||||
// The editing data keeps the source text inside the envelope's options;
|
||||
// its visible result is the outlined art the PDF page carries.
|
||||
slot.visible = false;
|
||||
self.add(slot, false)?;
|
||||
}
|
||||
// An envelope of type keeps its drawn art and its type, hidden, as above: the
|
||||
// type is filled in after reading, which the envelope can't wait for.
|
||||
if typed {
|
||||
return Ok(());
|
||||
}
|
||||
if self.envelopes.len() >= crate::import::ai::envelope::MAX_PENDING {
|
||||
self.warn(crate::import::ai::envelope::DRAWN);
|
||||
} else {
|
||||
let siblings = self.frames.last().map_or(0, |f| f.children.len());
|
||||
self.envelopes.push(PendingEnvelope { obj: *o, before, inner: children, depth: self.frames.len(), siblings });
|
||||
}
|
||||
}
|
||||
}
|
||||
"Document" => self.document(&o),
|
||||
@@ -467,7 +480,8 @@ impl<'a> Reader<'a> {
|
||||
/// envelope's dictionary. It holds the mesh that shapes the envelope, or, while the envelope's
|
||||
/// contents are being edited, those contents (the mesh then is in the dictionary); either way the
|
||||
/// distorted art follows on plain lines. The source text in the contents stays, hidden, as the
|
||||
/// dictionary's does. Whether it was dropped.
|
||||
/// dictionary's does. The group is kept in [`Reader::envelope_art`] for the envelope to be made
|
||||
/// of. Whether it was dropped.
|
||||
pub(super) fn drop_envelope_art(&mut self) -> bool {
|
||||
let Some(n) = self.last_object() else { return false };
|
||||
let id = n.id;
|
||||
@@ -483,6 +497,7 @@ impl<'a> Reader<'a> {
|
||||
slot.visible = false;
|
||||
f.children.push(slot);
|
||||
}
|
||||
self.envelope_art = Some(group);
|
||||
}
|
||||
self.hidden_ids.remove(&id);
|
||||
self.after_object = false;
|
||||
@@ -543,6 +558,87 @@ impl<'a> Reader<'a> {
|
||||
// The blend's own state block follows its record, as a group's follows its end.
|
||||
self.after_object = true;
|
||||
}
|
||||
|
||||
/// An envelope's plugin `XP`: the envelope whose dictionary was read last in this container
|
||||
/// and its drawn art, the one object read since, become one live envelope, or, where they
|
||||
/// can't, the drawn art stays (see [`crate::import::ai::envelope`]). Envelopes left waiting
|
||||
/// in containers that closed keep their drawn art.
|
||||
pub(super) fn end_envelope(&mut self) {
|
||||
let depth = self.frames.len();
|
||||
while self.envelopes.last().is_some_and(|p| p.depth > depth) {
|
||||
self.envelopes.pop();
|
||||
self.warn(crate::import::ai::envelope::DRAWN);
|
||||
}
|
||||
if self.envelopes.last().is_none_or(|p| p.depth != depth) {
|
||||
return;
|
||||
}
|
||||
let Some(p) = self.envelopes.pop() else { return };
|
||||
let drawn = self.frames.last().filter(|f| f.children.len() == p.siblings + 1).and_then(|f| f.children.last());
|
||||
let Some(drawn) = drawn else {
|
||||
self.warn(crate::import::ai::envelope::DRAWN);
|
||||
return;
|
||||
};
|
||||
let live = (self.envelope_checks > 0).then(|| crate::import::ai::envelope::live(&p.obj, p.before, p.inner)).flatten();
|
||||
let Some(kind) = live else {
|
||||
self.warn(crate::import::ai::envelope::DRAWN);
|
||||
return;
|
||||
};
|
||||
// Objects the reader couldn't read come in as the page's art in their place: the envelope's
|
||||
// drawn art stays round them.
|
||||
let mut ids = HashSet::new();
|
||||
fn collect(n: &Node, ids: &mut HashSet<NodeId>) {
|
||||
ids.insert(n.id);
|
||||
for c in n.children().into_iter().flatten() {
|
||||
collect(c, ids);
|
||||
}
|
||||
}
|
||||
collect(drawn, &mut ids);
|
||||
if let NodeKind::Envelope { content, .. } = &kind {
|
||||
content.iter().for_each(|c| collect(c, &mut ids));
|
||||
}
|
||||
if self.unread.iter().any(|(id, _)| ids.contains(id)) {
|
||||
self.warn(crate::import::ai::envelope::DRAWN);
|
||||
return;
|
||||
}
|
||||
self.envelope_checks -= 1;
|
||||
// The drawn art's own properties (name, visibility, lock, transparency, appearance, mask,
|
||||
// attributes) become the envelope's.
|
||||
let mut envelope = drawn.clone();
|
||||
(envelope.id, envelope.kind) = (self.doc.alloc_id(), kind);
|
||||
let layer = self.doc.alloc_id();
|
||||
let shared = crate::import::ai::envelope::Shared { patterns: &self.doc.patterns, images: &self.doc.images };
|
||||
let mut shown = envelope.clone();
|
||||
(shown.visible, shown.opacity) = (true, 1.0);
|
||||
let mut plain = drawn.clone();
|
||||
(plain.visible, plain.opacity) = (true, 1.0);
|
||||
let page = self.page_in_doc();
|
||||
if !crate::import::ai::envelope::drawn_alike(&shown, &plain, layer, &shared, page, &mut self.envelope_spent) {
|
||||
self.warn(crate::import::ai::envelope::DRAWN);
|
||||
return;
|
||||
}
|
||||
// The contents are the envelope's own now, not a copy on `%_` lines.
|
||||
fn forget(n: &Node, hidden: &mut HashSet<NodeId>) {
|
||||
hidden.remove(&n.id);
|
||||
for c in n.children().into_iter().flatten() {
|
||||
forget(c, hidden);
|
||||
}
|
||||
}
|
||||
forget(&envelope, &mut self.hidden_ids);
|
||||
if let Some(f) = self.frames.last_mut()
|
||||
&& let Some(slot) = f.children.last_mut()
|
||||
{
|
||||
let id = envelope.id;
|
||||
let drawn = std::mem::replace(slot, envelope);
|
||||
self.envelopes_drawn.push((id, drawn));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `node` hold a text slot?
|
||||
fn has_text_slot(node: &Node) -> bool {
|
||||
let mut slots = vec![];
|
||||
envelope_text_slots(node, &mut slots);
|
||||
!slots.is_empty()
|
||||
}
|
||||
|
||||
/// The source text inside an envelope options dictionary, carried through its containing metadata.
|
||||
|
||||
@@ -235,7 +235,7 @@ impl<'a> Reader<'a> {
|
||||
// An envelope (a distortion, `AIDeformStyle` / `DeformValue`) is written on `%_` lines as
|
||||
// a group holding the mesh that shapes it (or its contents, while they are being edited);
|
||||
// the distorted art the file's PDF part draws follows on plain lines. That group isn't
|
||||
// art, so it goes (#2405).
|
||||
// art, so it goes (#2405), kept aside for the live envelope ([`Reader::end_envelope`]).
|
||||
if (o.get("AIDeformStyle").is_some() || o.get("DeformValue").is_some()) && self.drop_envelope_art() {
|
||||
return;
|
||||
}
|
||||
@@ -317,6 +317,12 @@ impl<'a> Reader<'a> {
|
||||
self.artboards.first().map(|(_, r)| *r).or(self.cropmarks).or_else(|| self.sized_board())
|
||||
}
|
||||
|
||||
/// The page the art is compared with in the document's space, once the art has started.
|
||||
pub(super) fn page_in_doc(&self) -> Option<Rect> {
|
||||
let board = self.first_board().or_else(|| self.bbox_board())?;
|
||||
Some(self.to_doc?.transform_rect_bbox(board))
|
||||
}
|
||||
|
||||
/// The page of a file that says nothing of its artboard (the legacy format as other apps write
|
||||
/// it): its art's bounding box.
|
||||
fn bbox_board(&self) -> Option<Rect> {
|
||||
@@ -380,6 +386,11 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
|
||||
pub(super) fn finish(mut self) -> Result<Structure, String> {
|
||||
// Envelopes whose plugin never closed them keep their drawn art.
|
||||
if !self.envelopes.is_empty() {
|
||||
self.envelopes.clear();
|
||||
self.warn(crate::import::ai::envelope::DRAWN);
|
||||
}
|
||||
if !self.unknown.is_empty() {
|
||||
let ops: Vec<String> = self.unknown.iter().map(|o| format!("`{o}`")).collect();
|
||||
self.unsupported.insert(format!("operators VectorCraft doesn't know ({})", ops.join(", ")));
|
||||
@@ -491,6 +502,7 @@ impl<'a> Reader<'a> {
|
||||
slot_names: self.slot_names,
|
||||
restyled: self.restyled,
|
||||
placed_elsewhere: self.placed_elsewhere,
|
||||
envelopes_drawn: self.envelopes_drawn,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -298,6 +298,8 @@ impl<'a> Reader<'a> {
|
||||
let len = Self::nums(&vals).last().copied().unwrap_or(0.0);
|
||||
if name == crate::import::ai::blend::PLUGIN && len.is_finite() && len > 0.0 && len <= crate::import::ai::blend::MAX_RECORD as f64 {
|
||||
self.blend_record = Some((len as usize, vec![]));
|
||||
} else if name == crate::import::ai::envelope::PLUGIN {
|
||||
self.end_envelope();
|
||||
}
|
||||
}
|
||||
_ if IGNORED.contains(&w) => {}
|
||||
|
||||
@@ -166,6 +166,21 @@ pub(super) struct Frame {
|
||||
pub(super) state: Option<(f32, BlendMode, bool, Knockout, bool)>,
|
||||
}
|
||||
|
||||
/// An envelope whose dictionary was read, waiting for its drawn art and its plugin's `XP` (see
|
||||
/// [`super::super::envelope`]).
|
||||
#[derive(Debug)]
|
||||
pub(super) struct PendingEnvelope {
|
||||
pub(super) obj: Obj,
|
||||
/// The group on `%_` lines before the dictionary.
|
||||
pub(super) before: Option<Node>,
|
||||
/// The art in the dictionary's `AIDeformArtKey` block.
|
||||
pub(super) inner: Vec<Node>,
|
||||
/// How many frames were open when the dictionary closed, and how many children the innermost
|
||||
/// had: its drawn art is the one child after them.
|
||||
pub(super) depth: usize,
|
||||
pub(super) siblings: usize,
|
||||
}
|
||||
|
||||
/// What a reader of a pattern's definition has of its head, `(name) llx lly urx ury`: the operands
|
||||
/// before its first operator.
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -82,6 +82,12 @@ const WARPS: [&str; 15] = [
|
||||
"warp.twist",
|
||||
];
|
||||
|
||||
/// The warp style (`arc`, `arcLower`, …) numbered `n` in `DeformStyle`.
|
||||
pub(super) fn warp_style(n: i64) -> Option<&'static str> {
|
||||
let id = WARPS.get(usize::try_from(n.checked_sub(1)?).ok()?)?;
|
||||
id.strip_prefix("warp.")
|
||||
}
|
||||
|
||||
/// A plain style's paint, as the object comes in.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub(super) struct ArtStyle {
|
||||
|
||||
@@ -1997,6 +1997,488 @@ fn an_envelope_s_contents_being_edited_are_not_read_as_art() {
|
||||
assert_eq!(fill(&a[0]).color(), Some(Color::rgb(1.0, 0.0, 0.0)));
|
||||
}
|
||||
|
||||
/// `art` on `%_` lines.
|
||||
fn commented(art: &str) -> String {
|
||||
art.lines().map(|l| format!("%_{l}\n")).collect()
|
||||
}
|
||||
|
||||
/// Envelope Options as new envelopes have them (Distort Appearance, Anti-Alias, Clipping Mask),
|
||||
/// Fidelity 50, the contents not being edited.
|
||||
const ENVELOPE_KEYS: &str = "131073 /Int (AIDeformOptionsKey) ,\n50 /Int (AIDeformFidelityKey) ,\n0 /Bool (AIDeformEditModeKey) ,\n";
|
||||
|
||||
/// An envelope as the editing data writes it: on `%_` lines a group holding `before`, then the
|
||||
/// dictionary with `keys`, the art `block` in its `AIDeformArtKey` block and the kind `kind`; then
|
||||
/// `drawn` in a group on plain lines, and the plugin's `XP`.
|
||||
fn envelope_parts(before: &str, keys: &str, block: &str, kind: u32, drawn: &str) -> String {
|
||||
format!(
|
||||
"{}%_/ArtDictionary :\n{}%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_{kind} /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW\nu\n{drawn}U\n9 () XW\n(Adobe Deform Plugin) 1 0 0 XP\n",
|
||||
commented(&format!("u\n{before}U\n")),
|
||||
commented(keys),
|
||||
commented(block),
|
||||
)
|
||||
}
|
||||
|
||||
/// An envelope of `content` (in the plain group the editing data puts round it) shaped by the mesh
|
||||
/// `shape`.
|
||||
fn envelope_of(shape: &str, keys: &str, content: &str, kind: u32, drawn: &str) -> String {
|
||||
envelope_parts(shape, keys, &format!("u\n{content}U\n9 () XW\n"), kind, drawn)
|
||||
}
|
||||
|
||||
/// A grid point's place in art space and its handle towards a neighbour.
|
||||
type MeshAt<'a> = &'a dyn Fn((usize, usize)) -> (f64, f64);
|
||||
type MeshHandle<'a> = &'a dyn Fn((usize, usize), (usize, usize)) -> (f64, f64);
|
||||
|
||||
/// A grey `/Mesh` of `cols`×`rows` patches (`/P [i j]`, `i` along x, column by column), each
|
||||
/// corner of patch `(i, j)` at `at`, from the one nearest the origin up, across and down, with its
|
||||
/// handles towards the next corner and the previous one at `handle`; `anchor`, written after the
|
||||
/// second corner of the first patch, puts an anchor on its second edge.
|
||||
fn mesh_text(cols: usize, rows: usize, at: MeshAt<'_>, handle: MeshHandle<'_>, anchor: Option<[f64; 6]>) -> String {
|
||||
let mut s = format!("/Mesh X!\n2 /Version X#\n/Cartesian /Type X#\n[{cols} {rows}] /Size X#\n/Data X#\n/DeviceGray /CS X#\n");
|
||||
for i in 0..cols {
|
||||
for j in 0..rows {
|
||||
s += &format!("[{i} {j}] /P X#\n");
|
||||
let (a, b, c, d) = ((i, j), (i, j + 1), (i + 1, j + 1), (i + 1, j));
|
||||
for (k, (p, next, prev)) in [(a, b, d), (b, c, a), (c, d, b), (d, a, c)].into_iter().enumerate() {
|
||||
let ((x, y), (nx, ny), (px, py)) = (at(p), handle(p, next), handle(p, prev));
|
||||
s += &format!("[0.8 1 {x} {y} {nx} {ny} 1 {px} {py} 1] /N X#\n");
|
||||
if let (1, Some([x, y, ax, ay, bx, by])) = (k, anchor.filter(|_| (i, j) == (0, 0))) {
|
||||
s += &format!("[{x} {y} {ax} {ay} {bx} {by} 1] /A X#\n");
|
||||
}
|
||||
}
|
||||
s += "/E X#\n";
|
||||
}
|
||||
}
|
||||
s + "/End X!\n"
|
||||
}
|
||||
|
||||
/// A grid in art space, its points `step` apart from (40, 20), handles a third of the way.
|
||||
fn straight_mesh(cols: usize, rows: usize, step: (f64, f64)) -> String {
|
||||
let at = move |(i, j): (usize, usize)| (40.0 + i as f64 * step.0, 20.0 + j as f64 * step.1);
|
||||
let third = move |p: (usize, usize), q: (usize, usize)| {
|
||||
let ((x, y), (u, v)) = (at(p), at(q));
|
||||
(x + (u - x) / 3.0, y + (v - y) / 3.0)
|
||||
};
|
||||
mesh_text(cols, rows, &at, &third, None)
|
||||
}
|
||||
|
||||
/// The red rectangle 40 20 160 80 in art space, which is the same box in the document's.
|
||||
fn envelope_content() -> String {
|
||||
format!("{RED}{}f\n", rect_path(40.0, 20.0, 160.0, 80.0))
|
||||
}
|
||||
|
||||
/// What envelope `kind` draws of [`envelope_content`]: the rectangle's edges, many points to a
|
||||
/// side, mapped as VectorCraft maps them, as a red path in art space.
|
||||
fn drawn_content(kind: &vectorcraft_doc::live::EnvelopeKind) -> String {
|
||||
let rect = Rect::new(40.0, 20.0, 160.0, 80.0);
|
||||
let map = vectorcraft_doc::live::envelope_mapper(kind, rect);
|
||||
let corners = [Point::new(40.0, 20.0), Point::new(160.0, 20.0), Point::new(160.0, 80.0), Point::new(40.0, 80.0)];
|
||||
let mut s = RED.to_string();
|
||||
for (k, a) in corners.iter().enumerate() {
|
||||
let b = corners[(k + 1) % 4];
|
||||
for t in 0..64 {
|
||||
let q = map(a.lerp(b, f64::from(t) / 64.0));
|
||||
s += &format!("{} {} {}\n", q.x, 100.0 - q.y, if k == 0 && t == 0 { "m" } else { "L" });
|
||||
}
|
||||
}
|
||||
s + "f\n"
|
||||
}
|
||||
|
||||
/// The live envelope `n` is: its content, kind, fidelity, editing and options.
|
||||
fn live_envelope(n: &Node) -> (&[Arc<Node>], &vectorcraft_doc::live::EnvelopeKind, f64, bool, vectorcraft_doc::live::EnvelopeOptions) {
|
||||
match &n.kind {
|
||||
NodeKind::Envelope { content, kind, fidelity, editing, options, frame } => {
|
||||
assert_eq!(**frame, kurbo::Affine::IDENTITY);
|
||||
(content, kind, *fidelity, *editing, *options)
|
||||
}
|
||||
other => panic!("not a live envelope: {other:#?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A warp's dictionary keys: style number `style`, bend, horizontal and vertical distortion as
|
||||
/// fractions, and the orientation (vertical with `rotate` 1).
|
||||
fn warp_keys(style: u32, bend: f64, h: f64, v: f64, rotate: u32) -> String {
|
||||
format!(
|
||||
"{ENVELOPE_KEYS}{style} /Int (DeformStyle) ,\n{h} /Real (DeformHoriz) ,\n{bend} /Real (DeformValue) ,\n{v} /Real (DeformVert) ,\n{rotate} /Bool (Rotate) ,\n(Warp) /UnicodeString (DisplayString) ,\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// An envelope made with a warp opens as that live envelope of its contents, its style, bend,
|
||||
/// distortions (fractions in the file) and orientation read; the warp's drawn art goes.
|
||||
#[test]
|
||||
fn a_warp_envelope_opens_live() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
for (style, bend, h, v, rotate, id) in
|
||||
[(1, 0.5, 0.0, 0.0, 0, "arc"), (1, 0.5, -0.2, 0.3, 1, "arc"), (8, 0.5, 0.0, 0.0, 0, "flag"), (15, 0.5, 0.0, 0.0, 0, "twist")]
|
||||
{
|
||||
let want = EnvelopeKind::Warp { style: id.into(), bend: bend * 100.0, h: h * 100.0, v: v * 100.0, horizontal: rotate == 0 };
|
||||
let body =
|
||||
envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), &warp_keys(style, bend, h, v, rotate), &envelope_content(), 1, &drawn_content(&want));
|
||||
let r = read_ok(&layer(&body));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
let (content, kind, fidelity, editing, options) = live_envelope(&a[0]);
|
||||
let EnvelopeKind::Warp { style: s, bend: b, h: dh, v: dv, horizontal } = kind else { panic!("{kind:?}") };
|
||||
assert_eq!((s.as_str(), *horizontal), (id, rotate == 0));
|
||||
assert!([(*b, bend), (*dh, h), (*dv, v)].iter().all(|(got, f)| (got - f * 100.0).abs() < 1e-9), "{kind:?}");
|
||||
assert_eq!((fidelity, editing, options), (50.0, false, vectorcraft_doc::live::EnvelopeOptions::NEW));
|
||||
assert_eq!(content.len(), 1, "{content:#?}");
|
||||
assert!(matches!(content[0].kind, NodeKind::Path { .. }), "{content:#?}");
|
||||
assert_eq!(fill(&content[0]).color(), Some(Color::rgb(1.0, 0.0, 0.0)));
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every warp style number reads as its style, in Effect › Warp's order.
|
||||
#[test]
|
||||
fn warp_envelopes_read_every_style() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
let ids = [
|
||||
"arc",
|
||||
"arcLower",
|
||||
"arcUpper",
|
||||
"arch",
|
||||
"bulge",
|
||||
"shellLower",
|
||||
"shellUpper",
|
||||
"flag",
|
||||
"wave",
|
||||
"fish",
|
||||
"rise",
|
||||
"fisheye",
|
||||
"inflate",
|
||||
"squeeze",
|
||||
"twist",
|
||||
];
|
||||
for (n, id) in ids.iter().enumerate() {
|
||||
let want = EnvelopeKind::Warp { style: (*id).into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true };
|
||||
let keys = warp_keys(n as u32 + 1, 0.3, 0.0, 0.0, 0);
|
||||
let r = read_ok(&layer(&envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), &keys, &envelope_content(), 1, &drawn_content(&want))));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{id}: {a:#?}");
|
||||
assert_eq!(live_envelope(&a[0]).1, &want, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An envelope made with a mesh opens as a live mesh envelope: its rows and columns, its points
|
||||
/// row by row from the top left, and each point's four handles, a moved point and an uneven handle
|
||||
/// included.
|
||||
#[test]
|
||||
fn a_mesh_envelope_opens_live_with_its_points_and_handles() {
|
||||
use vectorcraft_doc::live::{EnvelopeKind, H_DOWN, H_LEFT, H_RIGHT, H_UP};
|
||||
// 3 columns 40 apart and 2 rows 30 apart from (40, 20); the inner point (1, 1) moved, and its
|
||||
// handle towards (2, 1) longer and off the line.
|
||||
let at = |(i, j): (usize, usize)| if (i, j) == (1, 1) { (90.0, 55.0) } else { (40.0 + i as f64 * 40.0, 20.0 + j as f64 * 30.0) };
|
||||
let handle = |p: (usize, usize), q: (usize, usize)| {
|
||||
if (p, q) == ((1, 1), (2, 1)) {
|
||||
return (108.0, 62.0);
|
||||
}
|
||||
let ((x, y), (u, v)) = (at(p), at(q));
|
||||
(x + (u - x) / 3.0, y + (v - y) / 3.0)
|
||||
};
|
||||
let shape = mesh_text(3, 2, &at, &handle, None);
|
||||
// Row r of the envelope is row 2 - r of the file (art space has y up).
|
||||
let doc_pt = |(x, y): (f64, f64)| Point::new(x, 100.0 - y);
|
||||
let mut points = vec![];
|
||||
let mut handles = vec![];
|
||||
for r in 0..=2usize {
|
||||
for c in 0..=3usize {
|
||||
let g = (c, 2 - r);
|
||||
let p = doc_pt(at(g));
|
||||
points.push(p);
|
||||
let mut h = [kurbo::Vec2::ZERO; 4];
|
||||
let towards = |q: Option<(usize, usize)>| q.filter(|q| q.0 <= 3 && q.1 <= 2).map_or(kurbo::Vec2::ZERO, |q| doc_pt(handle(g, q)) - p);
|
||||
h[H_RIGHT] = towards(Some((g.0 + 1, g.1)));
|
||||
h[H_LEFT] = towards(g.0.checked_sub(1).map(|i| (i, g.1)));
|
||||
h[H_UP] = towards(Some((g.0, g.1 + 1)));
|
||||
h[H_DOWN] = towards(g.1.checked_sub(1).map(|j| (g.0, j)));
|
||||
handles.push(h);
|
||||
}
|
||||
}
|
||||
let want = EnvelopeKind::Mesh { rows: 2, cols: 3, points: points.clone(), handles: handles.clone() };
|
||||
let r = read_ok(&layer(&envelope_of(&shape, ENVELOPE_KEYS, &envelope_content(), 2, &drawn_content(&want))));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
let EnvelopeKind::Mesh { rows, cols, points: got, handles: got_h } = live_envelope(&a[0]).1 else { panic!("{a:#?}") };
|
||||
assert_eq!((*rows, *cols, got.len(), got_h.len()), (2, 3, 12, 12));
|
||||
for (k, (p, q)) in got.iter().zip(&points).enumerate() {
|
||||
assert!((*p - *q).hypot() < 1e-6, "point {k}: {p:?}, not {q:?}");
|
||||
}
|
||||
// The moved inner point (row 1, column 1): each of its handles, the uneven one towards the right.
|
||||
let inner = 4 + 1;
|
||||
assert!((got[inner] - Point::new(90.0, 45.0)).hypot() < 1e-6, "{:?}", got[inner]);
|
||||
for slot in [H_RIGHT, H_LEFT, H_DOWN, H_UP] {
|
||||
assert!((got_h[inner][slot] - handles[inner][slot]).hypot() < 1e-6, "slot {slot}: {:?} vs {:?}", got_h[inner], handles[inner]);
|
||||
}
|
||||
assert!((got_h[inner][H_RIGHT] - kurbo::Vec2::new(18.0, -7.0)).hypot() < 1e-6, "{:?}", got_h[inner]);
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// An envelope made with a top object opens as a live envelope whose top path is the edge of its
|
||||
/// one-patch mesh, an anchor on the edge included; its contents stay the rectangle.
|
||||
#[test]
|
||||
fn a_top_object_envelope_opens_live_with_its_outline() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
// Corners (40, 20) (40, 80) (160, 80) (160, 20); the top edge bows up through an anchor at (100, 95).
|
||||
let at = |(i, j): (usize, usize)| (40.0 + i as f64 * 120.0, 20.0 + j as f64 * 60.0);
|
||||
let handle = |p: (usize, usize), q: (usize, usize)| match (p, q) {
|
||||
((0, 1), (1, 1)) => (60.0, 88.0),
|
||||
((1, 1), (0, 1)) => (140.0, 88.0),
|
||||
_ => {
|
||||
let ((x, y), (u, v)) = (at(p), at(q));
|
||||
(x + (u - x) / 3.0, y + (v - y) / 3.0)
|
||||
}
|
||||
};
|
||||
let shape = mesh_text(1, 1, &at, &handle, Some([100.0, 95.0, 70.0, 95.0, 130.0, 95.0]));
|
||||
let mut bp = kurbo::BezPath::new();
|
||||
bp.move_to((40.0, 20.0));
|
||||
bp.curve_to((60.0, 12.0), (70.0, 5.0), (100.0, 5.0));
|
||||
bp.curve_to((130.0, 5.0), (140.0, 12.0), (160.0, 20.0));
|
||||
bp.curve_to((160.0, 40.0), (160.0, 60.0), (160.0, 80.0));
|
||||
bp.curve_to((120.0, 80.0), (80.0, 80.0), (40.0, 80.0));
|
||||
bp.curve_to((40.0, 60.0), (40.0, 40.0), (40.0, 20.0));
|
||||
bp.close_path();
|
||||
let want = EnvelopeKind::TopObject { path: vectorcraft_geom::PathData::from_bezpath(&bp) };
|
||||
let r = read_ok(&layer(&envelope_of(&shape, ENVELOPE_KEYS, &envelope_content(), 3, &drawn_content(&want))));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
let (content, kind, ..) = live_envelope(&a[0]);
|
||||
let EnvelopeKind::TopObject { path } = kind else { panic!("{kind:?}") };
|
||||
let anchors: Vec<Point> = path.subpaths.iter().flat_map(|s| s.anchors.iter().map(|a| a.p)).collect();
|
||||
for p in [(40.0, 20.0), (100.0, 5.0), (160.0, 20.0), (160.0, 80.0), (40.0, 80.0)] {
|
||||
assert!(anchors.iter().any(|q| (*q - Point::from(p)).hypot() < 1e-6), "{p:?} not in {anchors:?}");
|
||||
}
|
||||
// The curve through the anchor: its handles there run level.
|
||||
let mid = path.subpaths.iter().flat_map(|s| s.anchors.iter()).find(|a| (a.p - Point::new(100.0, 5.0)).hypot() < 1e-6).unwrap();
|
||||
assert!((mid.h_in.y - 5.0).abs() < 1e-6 && (mid.h_out.y - 5.0).abs() < 1e-6, "{mid:?}");
|
||||
assert_eq!(content.len(), 1, "{content:#?}");
|
||||
assert_eq!(fill(&content[0]).color(), Some(Color::rgb(1.0, 0.0, 0.0)));
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// Envelope Options come from the options word, Fidelity from its own key.
|
||||
#[test]
|
||||
fn an_envelope_reads_its_options_and_fidelity() {
|
||||
use vectorcraft_doc::live::{EnvelopeKind, EnvelopeOptions, PreserveShape};
|
||||
let shape = straight_mesh(1, 1, (120.0, 60.0));
|
||||
// A flat 1 × 1 mesh over the box draws the box as it is.
|
||||
let drawn = drawn_content(&EnvelopeKind::Mesh {
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
points: vec![Point::new(40.0, 20.0), Point::new(160.0, 20.0), Point::new(40.0, 80.0), Point::new(160.0, 80.0)],
|
||||
handles: vec![],
|
||||
});
|
||||
for (word, fidelity, options) in [
|
||||
(
|
||||
65536,
|
||||
80,
|
||||
EnvelopeOptions {
|
||||
anti_alias: false,
|
||||
preserve_shape: PreserveShape::Transparency,
|
||||
distort_appearance: false,
|
||||
distort_linear_gradients: false,
|
||||
distort_pattern_fills: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
0x20007,
|
||||
100,
|
||||
EnvelopeOptions {
|
||||
anti_alias: true,
|
||||
preserve_shape: PreserveShape::ClippingMask,
|
||||
distort_appearance: true,
|
||||
distort_linear_gradients: true,
|
||||
distort_pattern_fills: true,
|
||||
},
|
||||
),
|
||||
(
|
||||
0x20002,
|
||||
0,
|
||||
EnvelopeOptions {
|
||||
anti_alias: true,
|
||||
preserve_shape: PreserveShape::ClippingMask,
|
||||
distort_appearance: false,
|
||||
distort_linear_gradients: false,
|
||||
distort_pattern_fills: true,
|
||||
},
|
||||
),
|
||||
] {
|
||||
let keys = format!("{word} /Int (AIDeformOptionsKey) ,\n{fidelity} /Int (AIDeformFidelityKey) ,\n0 /Bool (AIDeformEditModeKey) ,\n");
|
||||
let r = read_ok(&layer(&envelope_of(&shape, &keys, &envelope_content(), 2, &drawn)));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{word}: {a:#?}");
|
||||
let (_, _, got_fidelity, _, got) = live_envelope(&a[0]);
|
||||
assert_eq!((got_fidelity, got), (f64::from(fidelity), options), "{word}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An envelope's contents keep their groups: a group of two rectangles stays that group.
|
||||
#[test]
|
||||
fn an_envelope_s_contents_keep_their_groups() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
let group = format!(
|
||||
"u\n{RED}{}f\n{}f\nU\n/ArtDictionary :\n(Pair) /String (AIArtName) ,\n;\n",
|
||||
rect_path(40.0, 20.0, 90.0, 80.0),
|
||||
rect_path(110.0, 20.0, 160.0, 80.0)
|
||||
);
|
||||
let mut drawn = RED.to_string();
|
||||
drawn += &format!("{}f\n{}f\n", rect_path(40.0, 20.0, 90.0, 80.0), rect_path(110.0, 20.0, 160.0, 80.0));
|
||||
let r = read_ok(&layer(&envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), ENVELOPE_KEYS, &group, 2, &drawn)));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
let (content, kind, ..) = live_envelope(&a[0]);
|
||||
assert!(matches!(kind, EnvelopeKind::Mesh { rows: 1, cols: 1, .. }), "{kind:?}");
|
||||
assert_eq!(content.len(), 1, "{content:#?}");
|
||||
assert_eq!(content[0].name.as_deref(), Some("Pair"));
|
||||
let kids = content[0].children().unwrap();
|
||||
assert_eq!(kids.len(), 2, "{kids:#?}");
|
||||
assert!(kids.iter().all(|k| matches!(k.kind, NodeKind::Path { .. })), "{kids:#?}");
|
||||
}
|
||||
|
||||
/// While an envelope's contents are edited, the contents come first and the mesh is in the
|
||||
/// dictionary: the envelope opens live all the same, in Edit Contents.
|
||||
#[test]
|
||||
fn an_envelope_whose_contents_are_edited_opens_live_in_edit_contents() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
let at = |(i, j): (usize, usize)| (40.0 + i as f64 * 120.0, 20.0 + j as f64 * 60.0);
|
||||
let handle = |p: (usize, usize), q: (usize, usize)| match (p, q) {
|
||||
((0, 1), (1, 1)) => (70.0, 95.0),
|
||||
((1, 1), (0, 1)) => (130.0, 95.0),
|
||||
_ => {
|
||||
let ((x, y), (u, v)) = (at(p), at(q));
|
||||
(x + (u - x) / 3.0, y + (v - y) / 3.0)
|
||||
}
|
||||
};
|
||||
let mesh = mesh_text(1, 1, &at, &handle, None);
|
||||
let mut bp = kurbo::BezPath::new();
|
||||
bp.move_to((40.0, 20.0));
|
||||
bp.curve_to((70.0, 5.0), (130.0, 5.0), (160.0, 20.0));
|
||||
bp.curve_to((160.0, 40.0), (160.0, 60.0), (160.0, 80.0));
|
||||
bp.curve_to((120.0, 80.0), (80.0, 80.0), (40.0, 80.0));
|
||||
bp.curve_to((40.0, 60.0), (40.0, 40.0), (40.0, 20.0));
|
||||
bp.close_path();
|
||||
let want = EnvelopeKind::TopObject { path: vectorcraft_geom::PathData::from_bezpath(&bp) };
|
||||
let keys = ENVELOPE_KEYS.replace("0 /Bool (AIDeformEditModeKey)", "1 /Bool (AIDeformEditModeKey)");
|
||||
let body = envelope_parts(&envelope_content(), &keys, &format!("0 g\n{mesh}"), 3, &drawn_content(&want));
|
||||
let r = read_ok(&layer(&body));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
let (content, kind, _, editing, _) = live_envelope(&a[0]);
|
||||
assert!(editing);
|
||||
assert!(matches!(kind, EnvelopeKind::TopObject { .. }), "{kind:?}");
|
||||
assert_eq!(content.len(), 1, "{content:#?}");
|
||||
assert!(matches!(content[0].kind, NodeKind::Path { .. }), "{content:#?}");
|
||||
assert_eq!(fill(&content[0]).color(), Some(Color::rgb(1.0, 0.0, 0.0)));
|
||||
}
|
||||
|
||||
/// An envelope that doesn't read (a warp style number past the list, a mesh envelope whose shape
|
||||
/// isn't a mesh, one whose plugin never closes it) keeps its drawn art, with one note; the art
|
||||
/// round it is read as ever.
|
||||
#[test]
|
||||
fn an_envelope_that_doesnt_read_keeps_its_drawn_art() {
|
||||
let blue = "0 0 1 1 0 0 1 Xa\n";
|
||||
let drawn = format!("{blue}{}f\n", rect_path(40.0, 20.0, 160.0, 80.0));
|
||||
let mesh = straight_mesh(1, 1, (120.0, 60.0));
|
||||
let cases = [
|
||||
envelope_of(&mesh, &warp_keys(99, 0.5, 0.0, 0.0, 0), &envelope_content(), 1, &drawn),
|
||||
envelope_of(&format!("{}f\n", rect_path(40.0, 20.0, 160.0, 80.0)), ENVELOPE_KEYS, &envelope_content(), 2, &drawn),
|
||||
envelope_of(&mesh, ENVELOPE_KEYS, &envelope_content(), 7, &drawn),
|
||||
envelope_of(&mesh, &ENVELOPE_KEYS.replace("131073", "131081"), &envelope_content(), 2, &drawn),
|
||||
envelope_of(&mesh, ENVELOPE_KEYS, &envelope_content(), 2, &drawn).replace("(Adobe Deform Plugin) 1 0 0 XP\n", ""),
|
||||
];
|
||||
for case in cases {
|
||||
let body = format!("{RED}{}f\n{case}{RED}{}f\n", rect_path(0.0, 0.0, 10.0, 10.0), rect_path(190.0, 0.0, 200.0, 10.0));
|
||||
let r = read_ok(&layer(&body));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 3, "{case}\n{a:#?}");
|
||||
assert!(matches!(a[0].kind, NodeKind::Path { .. }) && matches!(a[2].kind, NodeKind::Path { .. }), "{a:#?}");
|
||||
let NodeKind::Group { children, .. } = &a[1].kind else { panic!("{a:#?}") };
|
||||
assert_eq!(fill(&children[0]).color(), Some(Color::rgb(0.0, 0.0, 1.0)), "{case}");
|
||||
assert_eq!(r.warnings, [super::envelope::DRAWN], "{case}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An envelope whose contents don't come out as its drawn art (its warp bends them another way)
|
||||
/// keeps the drawn art.
|
||||
#[test]
|
||||
fn an_envelope_that_doesnt_come_out_as_drawn_keeps_its_drawn_art() {
|
||||
use vectorcraft_doc::live::EnvelopeKind;
|
||||
let other = EnvelopeKind::Warp { style: "arc".into(), bend: -50.0, h: 0.0, v: 0.0, horizontal: true };
|
||||
let body = envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), &warp_keys(1, 0.5, 0.0, 0.0, 0), &envelope_content(), 1, &drawn_content(&other));
|
||||
let r = read_ok(&layer(&body));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
assert!(matches!(a[0].kind, NodeKind::Group { .. }), "{a:#?}");
|
||||
assert_eq!(r.warnings, [super::envelope::DRAWN]);
|
||||
}
|
||||
|
||||
/// An envelope that comes out close to its drawn art, but with more of the page differing than the
|
||||
/// page check can take from the live envelopes (its drawn art 2 pt to the side on a 200 × 100
|
||||
/// page), keeps the drawn art, so the file still opens from its layers.
|
||||
#[test]
|
||||
fn an_envelope_that_differs_from_its_drawn_art_over_the_page_keeps_it() {
|
||||
let shifted = format!("{RED}{}f\n", rect_path(42.0, 20.0, 162.0, 80.0));
|
||||
let body = envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), ENVELOPE_KEYS, &envelope_content(), 2, &shifted);
|
||||
let r = read_ok(&layer(&body));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
assert!(matches!(a[0].kind, NodeKind::Group { .. }), "{a:#?}");
|
||||
assert_eq!(r.warnings, [super::envelope::DRAWN]);
|
||||
}
|
||||
|
||||
/// An envelope whose drawn art holds an object the reader can't read keeps that drawn art, which
|
||||
/// the page's art replaces.
|
||||
#[test]
|
||||
fn an_envelope_whose_drawn_art_holds_an_unread_object_keeps_it() {
|
||||
let flat = vectorcraft_doc::live::EnvelopeKind::Mesh {
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
points: vec![Point::new(40.0, 20.0), Point::new(160.0, 20.0), Point::new(40.0, 80.0), Point::new(160.0, 80.0)],
|
||||
handles: vec![],
|
||||
};
|
||||
let drawn = format!("{}frobnicate\n", drawn_content(&flat));
|
||||
let r = read_ok(&layer(&envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), ENVELOPE_KEYS, &envelope_content(), 2, &drawn)));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
assert!(matches!(a[0].kind, NodeKind::Group { .. }), "{a:#?}");
|
||||
assert_eq!(r.unread.iter().map(|(id, _)| *id).collect::<Vec<_>>(), [a[0].id]);
|
||||
assert_eq!(r.warnings, [super::envelope::DRAWN]);
|
||||
}
|
||||
|
||||
/// A live envelope takes its drawn art's own properties: its name here.
|
||||
#[test]
|
||||
fn a_live_envelope_keeps_its_drawn_art_s_name() {
|
||||
let flat = vectorcraft_doc::live::EnvelopeKind::Mesh {
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
points: vec![Point::new(40.0, 20.0), Point::new(160.0, 20.0), Point::new(40.0, 80.0), Point::new(160.0, 80.0)],
|
||||
handles: vec![],
|
||||
};
|
||||
let body = envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), ENVELOPE_KEYS, &envelope_content(), 2, &drawn_content(&flat))
|
||||
.replace("U\n9 () XW\n(Adobe Deform Plugin)", "U\n/ArtDictionary :\n(Bent) /String (AIArtName) ,\n;\n9 () XW\n(Adobe Deform Plugin)");
|
||||
let r = read_ok(&layer(&body));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
live_envelope(&a[0]);
|
||||
assert_eq!(a[0].name.as_deref(), Some("Bent"));
|
||||
}
|
||||
|
||||
/// The same envelope beside the page opens live: what differs off the page isn't counted.
|
||||
#[test]
|
||||
fn an_envelope_off_the_page_opens_live_when_it_draws_close_to_its_drawn_art() {
|
||||
let at = |(i, j): (usize, usize)| (300.0 + i as f64 * 120.0, 20.0 + j as f64 * 60.0);
|
||||
let third = |p: (usize, usize), q: (usize, usize)| {
|
||||
let ((x, y), (u, v)) = (at(p), at(q));
|
||||
(x + (u - x) / 3.0, y + (v - y) / 3.0)
|
||||
};
|
||||
let content = format!("{RED}{}f\n", rect_path(300.0, 20.0, 420.0, 80.0));
|
||||
let shifted = format!("{RED}{}f\n", rect_path(302.0, 20.0, 422.0, 80.0));
|
||||
let r = read_ok(&layer(&envelope_of(&mesh_text(1, 1, &at, &third, None), ENVELOPE_KEYS, &content, 2, &shifted)));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
live_envelope(&a[0]);
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gradient_mesh_reads_as_a_patch() {
|
||||
let corner = |rgb: &str, x: i32, y: i32, next: (i32, i32), prev: (i32, i32)| {
|
||||
|
||||
@@ -40,7 +40,7 @@ use super::ai::{self, Structure, slot_of};
|
||||
use super::ate::{Story, Texts};
|
||||
|
||||
mod build;
|
||||
mod compare;
|
||||
pub(super) mod compare;
|
||||
mod layers;
|
||||
mod plan;
|
||||
mod slots;
|
||||
|
||||
@@ -71,6 +71,7 @@ pub(super) fn build_from(
|
||||
slot_names,
|
||||
restyled,
|
||||
placed_elsewhere,
|
||||
envelopes_drawn,
|
||||
warnings,
|
||||
} = structure;
|
||||
// Editing data in the units of the page's user space ([`in_units`]) is compared with the page
|
||||
@@ -311,10 +312,10 @@ pub(super) fn build_from(
|
||||
doc = other;
|
||||
notes.extend(unseen_note(unseen).into_iter().chain(note));
|
||||
}
|
||||
_ if trust => {
|
||||
notes.extend(unseen_note(unseen).into_iter().chain([format!("{why}: they come from its editing data all the same")]))
|
||||
_ => {
|
||||
let more = drawn_or_trusted(&frame, &mut view, &mut doc, &envelopes_drawn, ¬es, unseen, trust, why, reflow)?;
|
||||
notes.extend(more);
|
||||
}
|
||||
_ => return Err(why),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -755,6 +756,46 @@ fn slot_content(
|
||||
Ok((plan(infos, stories, made, template, to_doc, pages, notes), false))
|
||||
}
|
||||
|
||||
/// The layers `doc` don't draw as the page (`why`), `unseen` text objects of them taken out: the
|
||||
/// notes to add when `doc` with each live envelope `drawn` lists replaced by the drawn art it took
|
||||
/// the place of (see [`ai::Structure::envelopes_drawn`]) draws as the page, `doc` then being that;
|
||||
/// else, with `trust`, the notes for `doc` as it is. An error, `why`, otherwise. The envelope note
|
||||
/// is added unless `notes` has it.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn drawn_or_trusted(
|
||||
frame: &Frame<'_>,
|
||||
view: &mut PageView,
|
||||
doc: &mut Document,
|
||||
drawn: &[(NodeId, Node)],
|
||||
notes: &[String],
|
||||
unseen: usize,
|
||||
trust: bool,
|
||||
why: String,
|
||||
reflow: bool,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let unseen_note = |n: usize| (n > 0).then(|| format!("{n} {UNSEEN_TEXT}"));
|
||||
let mut put_back = doc.clone();
|
||||
let mut any = false;
|
||||
for (id, art) in drawn {
|
||||
if let Some(n) = put_back.node_mut(*id) {
|
||||
*n = art.clone();
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if any {
|
||||
let more = prune_unseen(frame, view, &mut put_back, reflow);
|
||||
if let Ok(note) = compare(frame, view, &put_back, reflow) {
|
||||
*doc = put_back;
|
||||
let envelope = (!notes.iter().any(|n| n == ai::ENVELOPE_DRAWN)).then(|| ai::ENVELOPE_DRAWN.to_string());
|
||||
return Ok(envelope.into_iter().chain(unseen_note(unseen + more)).chain(note).collect());
|
||||
}
|
||||
}
|
||||
if trust {
|
||||
return Ok(unseen_note(unseen).into_iter().chain([format!("{why}: they come from its editing data all the same")]).collect());
|
||||
}
|
||||
Err(why)
|
||||
}
|
||||
|
||||
/// `doc` with the placed files `placed` lists at their other placement (see
|
||||
/// [`ai::Structure::placed_elsewhere`]): each image's transform followed by its move. `None` when
|
||||
/// no listed image survives replacement.
|
||||
|
||||
@@ -47,7 +47,7 @@ const WHITE: u32 = 8;
|
||||
|
||||
/// Do two pixels look different: in lightness, or one inked (as a light yellow is) where the other
|
||||
/// is white?
|
||||
pub(super) fn differ(p: &[u8; 4], q: &[u8; 4]) -> bool {
|
||||
pub(in crate::import) fn differ(p: &[u8; 4], q: &[u8; 4]) -> bool {
|
||||
let (p, q) = (on_white(p), on_white(q));
|
||||
let (a, b) = (ink(p), ink(q));
|
||||
lightness(p).abs_diff(lightness(q)) > SAME_PIXEL || (a.min(b) <= WHITE && a.abs_diff(b) > SAME_PIXEL)
|
||||
@@ -59,7 +59,7 @@ const SAME_COLOUR: u32 = 64;
|
||||
|
||||
/// Are two pixels different colours ([`differ`] also takes a red and a dark red of about the same
|
||||
/// lightness to be alike)?
|
||||
pub(super) fn differ_in_colour(p: &[u8; 4], q: &[u8; 4]) -> bool {
|
||||
pub(in crate::import) fn differ_in_colour(p: &[u8; 4], q: &[u8; 4]) -> bool {
|
||||
let (p, q) = (on_white(p), on_white(q));
|
||||
p.iter().zip(q).any(|(a, b)| a.abs_diff(b) > SAME_COLOUR)
|
||||
}
|
||||
@@ -84,7 +84,7 @@ impl Difference {
|
||||
}
|
||||
|
||||
/// The scale the comparisons draw a region at: about 400 pixels along its longer side.
|
||||
fn compare_scale(r: Rect) -> f64 {
|
||||
pub(in crate::import) fn compare_scale(r: Rect) -> f64 {
|
||||
(400.0 / r.width().max(r.height()).max(1.0)).min(4.0)
|
||||
}
|
||||
|
||||
@@ -362,14 +362,14 @@ const MARKED: f64 = 0.05;
|
||||
|
||||
/// More of the page than this differing between the layers' art and the page's own, and the
|
||||
/// layers aren't used.
|
||||
pub(super) const MAX_DIFFERENCE: f64 = 0.05;
|
||||
pub(in crate::import) const MAX_DIFFERENCE: f64 = 0.05;
|
||||
|
||||
/// More than this and the import says so.
|
||||
const NOTABLE_DIFFERENCE: f64 = 0.002;
|
||||
|
||||
/// More of the page than this where one draws an object that the other doesn't (see
|
||||
/// [`Difference`]), and the layers aren't used: about a centimetre square on a letter page.
|
||||
pub(super) const MAX_SOLID_DIFFERENCE: f64 = 0.001;
|
||||
pub(in crate::import) const MAX_SOLID_DIFFERENCE: f64 = 0.001;
|
||||
|
||||
/// How far (points) the art the layers print may reach past a page that holds it all.
|
||||
const PAST_THE_PAGE: f64 = 1.0;
|
||||
|
||||
@@ -2861,6 +2861,40 @@ fn envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance()
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A live envelope that draws a little apart from its drawn art, in layers that already differ
|
||||
/// from the page almost as much as the page check takes, would make them differ too much: its
|
||||
/// drawn art is put back, so the file still opens from its layers.
|
||||
#[test]
|
||||
fn a_live_envelope_that_would_tip_the_page_check_keeps_its_drawn_art() {
|
||||
let rect = |x0: f64, y0: f64, x1: f64, y1: f64| format!("{x0} {y0} m\n{x0} {y1} L\n{x1} {y1} L\n{x1} {y0} L\n{x0} {y0} L\n");
|
||||
let hidden = |s: &str| -> String { s.lines().map(|l| format!("%_{l}\n")).collect() };
|
||||
let corner = |x: f64, y: f64, nx: f64, ny: f64, px: f64, py: f64| format!("[0.8 1 {x} {y} {nx} {ny} 1 {px} {py} 1] /N X#\n");
|
||||
let mesh = format!(
|
||||
"/Mesh X!\n2 /Version X#\n/Cartesian /Type X#\n[1 1] /Size X#\n/Data X#\n/DeviceGray /CS X#\n[0 0] /P X#\n{}{}{}{}/E X#\n/End X!\n",
|
||||
corner(20.0, 5.0, 20.0, 35.0, 40.0, 5.0),
|
||||
corner(20.0, 95.0, 40.0, 95.0, 20.0, 65.0),
|
||||
corner(80.0, 95.0, 80.0, 65.0, 60.0, 95.0),
|
||||
corner(80.0, 5.0, 60.0, 5.0, 80.0, 35.0),
|
||||
);
|
||||
let red = "0 1 1 0 1 0 0 Xa\n";
|
||||
let envelope = format!(
|
||||
"{}%_/ArtDictionary :\n%_131073 /Int (AIDeformOptionsKey) ,\n%_50 /Int (AIDeformFidelityKey) ,\n%_0 /Bool (AIDeformEditModeKey) ,\n%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_2 /Int (AIDeformStyle) ,\n%_;\n%_\nu\n{red}{}f\nU\n(Adobe Deform Plugin) 1 0 0 XP\n",
|
||||
hidden(&format!("u\n{mesh}U\n")),
|
||||
hidden(&format!("u\n{red}{}f\nU\n", rect(20.0, 5.0, 80.0, 95.0))),
|
||||
rect(20.5, 5.0, 80.5, 95.0),
|
||||
);
|
||||
// Hairlines the page doesn't draw: about 4.5% of it differs before the envelope.
|
||||
let lines: String = [3, 6, 9, 12, 15, 85, 88, 91, 94].iter().map(|x| format!("0 0 0 1 K\n0.5 w\n{x} 0 m\n{x} 100 L\nS\n")).collect();
|
||||
let art = native(&layer("Art", true, &format!("{lines}{envelope}")));
|
||||
let page = "1 0 0 setrgbcolor 20.5 5 moveto 80.5 5 lineto 80.5 95 lineto 20.5 95 lineto closepath fill";
|
||||
let r = import(&eps(page, &art)).unwrap();
|
||||
assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings);
|
||||
let mut envelopes = 0;
|
||||
r.document.walk(|n| envelopes += usize::from(matches!(n.kind, NodeKind::Envelope { .. })));
|
||||
assert_eq!(envelopes, 0, "{:?}", r.warnings);
|
||||
assert!(r.warnings.iter().any(|w| w == crate::import::ai::ENVELOPE_DRAWN), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// An art style section with style `Comic`: a white fill offset and repeated by Transform under a
|
||||
/// red fill with its own blend mode, the characters at the bottom.
|
||||
fn comic_style() -> String {
|
||||
|
||||
@@ -1068,6 +1068,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
When the editing data of a `.ai` or EPS is for a page of another size than the file's, the warning that its layers weren't read gives both sizes in points, and the scale of the editing data's page to the file's page (1:10 for a tenth of its size) when their sides are in proportion (#2034; `crates/eps/src/import/tests_native.rs`).
|
||||
A `.ai`, or a PDF saved with Illustrator's editing data, whose editing data can't be decompressed opens from its PDF part with a warning that its layers weren't read and why, and an export or Save As to its own path asks first (`acknowledgeLoss`), as for a file whose layers the reader can't read (`crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
A `.ai` or EPS opened from its editing data with one object on a shown layer that the reader can't read (an unknown operator inside a group, a pattern fill whose pattern it doesn't have, a symbol instance, a placed file without a usable preview) keeps its layers and groups: in that object's place, its layer gets a group of the page's objects inside the object's box, named after it, with a warning that lists what came in as the page's art and an export or Save As to its own path asking first (`acknowledgeLoss`). The file still opens from its page or PDF part when no box can be found, when live art or another unread object shares the page art inside it, when the object spans two artboards, or when the page art doesn't carry the object's transparency (#4806; `crates/eps/src/import/tests_native.rs`, `crates/engine/src/cmd/fileio/tests_aiimport.rs`).
|
||||
In a `.ai` opened from its editing data, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type keeps its drawn art, with its type hidden (`crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); in PDF 1.3 to 1.5 files and PDF/X files such a page is written in points.
|
||||
Placed (linked) files in a `.ai`'s editing data come in as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (a file found next to the document or at its path is read as the document opens). A file with them no longer falls back to its PDF part, so its guides, sublayers and hidden objects open (#1264; `crates/eps/src/import/ai/tests.rs`, `crates/engine/src/tests/links.rs`). In a file opened that way, a gradient with stops in the middle runs its whole ramp across its shape and a gradient object no longer passes its fill to the next path (#1390); text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line, the frame made taller with a warning in the open result (#1392).
|
||||
`.ai` and EPS editing-data import keeps template layers, and opens canvas and artboard ruler guides as ruler guides on their layers; guide objects stay editable guide paths (#779; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
|
||||
Reference in New Issue
Block a user