mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 15:51:56 +00:00
Merge pull request #5187 from dotCooCoo/fix/ai-os22-g2
.ai import: placed EPS files in the version 7 form are read
This commit is contained in:
@@ -3426,6 +3426,19 @@ proptest! {
|
||||
let eps = ai::eps(data.as_bytes(), ai::page_ps());
|
||||
survive("multi-layer masks in an EPS", || vectorcraft_engine::cmd::fileio::load("x.eps", &eps).ok().map(|l| l.doc))?;
|
||||
}
|
||||
|
||||
/// The same file with placed EPS files in the version 7 form (no `%AI5_BeginPlace`), one
|
||||
/// linked and one included whole with a nested document and counted data in it, damaged.
|
||||
#[test]
|
||||
fn ps_form_ai_with_placed_files_never_panics(cut in 0usize..1_600, edits in prop::collection::vec((0usize..1_600, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '[', ']', '%', '`', '~', 'E', 'D'])), 0..12)) {
|
||||
let placed = "[1 0 0 1 10 20] 10 20 50 50 (a.eps) `\n%%IncludeFile: a.eps\n~\n\
|
||||
[-1 0 0 1 100 0] 10 20 50 50 4 4 0 (b.eps) `\n%%BeginDocument: b.eps\n%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 10 20 50 50\n\
|
||||
%%BeginDocument: c.eps\n~\n%%EndDocument\n%%BeginData: 14 Binary Bytes\n%%EndDocument\n0 0 1 setrgbcolor 10 20 40 30 rectfill\n%%EndDocument\n~\n";
|
||||
let fixture = NAMED_PROLOG_AI.replacen("0 Ap\nu\n", &format!("0 Ap\n{placed}u\n"), 1);
|
||||
prop_assert!(fixture.contains("(b.eps)"));
|
||||
let text = mutate_text(&fixture, cut, &edits);
|
||||
survive("mutated PostScript-form .ai with placed files", || vectorcraft_engine::cmd::fileio::load("x.ai", text.as_bytes()).ok().map(|l| l.doc))?;
|
||||
}
|
||||
}
|
||||
|
||||
/// An operand of a layer's options (`Lb`): a flag, a colour index or component, a percentage, or
|
||||
@@ -3665,6 +3678,16 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
|
||||
"\n%AI28_EndPlacedRelativePath\n",
|
||||
"`",
|
||||
"~",
|
||||
// The version 7 form (§16): an 11-value `` ` `` with no `%AI5_BeginPlace`, its file
|
||||
// linked or included whole, a nested document and counted data in that.
|
||||
"[1 0 0 1 10 20] 10 20 50 50 (a.eps) `",
|
||||
"[0 0 0 0 0 0] 0 0 0 0 (a.eps) `",
|
||||
"\n%%IncludeFile: a.eps\n",
|
||||
"\n%%BeginDocument: a.eps\n",
|
||||
"\n%%EndDocument\n",
|
||||
"\n%%BeginData: 14 Binary Bytes\n",
|
||||
"\n%%BeginBinary: 99999999999\n",
|
||||
"\n%%BeginData: 3 Hex Lines\n",
|
||||
"\n%AI5_EndPlace\n",
|
||||
"Xm",
|
||||
"50 -50 -100 -100 5000 -2000 Xm",
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
//! line starts a comment that is skipped.
|
||||
//! - The binary image data after `%%BeginData:` is one [`Tok::Data`]; the binary preview of a placed
|
||||
//! file is one [`Tok::Preview`].
|
||||
//! - A document included whole (`%%BeginDocument:` … `%%EndDocument` lines, DSC 3.0 p. 68, nested
|
||||
//! ones counted, the counted payloads of `%%BeginData:` and `%%BeginBinary:` inside skipped) is
|
||||
//! one [`Tok::Document`].
|
||||
|
||||
use super::super::lex::{find, hex_decode};
|
||||
|
||||
@@ -29,6 +32,9 @@ pub(super) enum Tok<'a> {
|
||||
Data(&'a [u8]),
|
||||
/// A placed file's preview: what is between `%AI26_BeginPlacedObjectPreview` and its end.
|
||||
Preview(&'a [u8]),
|
||||
/// A document included whole: what is between `%%BeginDocument:`'s line and the line of the
|
||||
/// `%%EndDocument` that ends it, or `None` when none does (the rest of the data is skipped).
|
||||
Document(Option<&'a [u8]>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -46,7 +52,7 @@ const PLACE: [&[u8]; 2] = [b"%AI5_BeginPlace", b"%AI5_EndPlace"];
|
||||
const NOTES: [&[u8]; 3] = [b"%AI3_Note:", b"%%Note:", b"%AI5_NoteMore:"];
|
||||
/// The markers around a placed file's relative folder, which a hidden one has on `%_` lines too.
|
||||
const RELATIVE: [&[u8]; 2] = [b"%AI28_BeginPlacedRelativePath", b"%AI28_EndPlacedRelativePath"];
|
||||
/// A binary section skipped whole: (begin, end) markers.
|
||||
/// A document included whole: (begin, end) markers.
|
||||
const DOCUMENT: (&[u8], &[u8]) = (b"%%BeginDocument", b"%%EndDocument");
|
||||
|
||||
fn is_space(b: u8) -> bool {
|
||||
@@ -93,6 +99,18 @@ impl<'a> Lexer<'a> {
|
||||
rest.get(..n).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// [`Self::line`], the position moved past its end too (`\r`, `\n` or `\r\n`).
|
||||
fn whole_line(&mut self) -> &'a [u8] {
|
||||
let line = self.line();
|
||||
if self.peek() == Some(b'\r') {
|
||||
self.pos += 1;
|
||||
}
|
||||
if self.peek() == Some(b'\n') {
|
||||
self.pos += 1;
|
||||
}
|
||||
line
|
||||
}
|
||||
|
||||
/// Where the next token is looked for.
|
||||
pub fn offset(&self) -> usize {
|
||||
self.pos
|
||||
@@ -185,13 +203,15 @@ impl<'a> Lexer<'a> {
|
||||
/// The token a comment line stands for when it starts a section read differently: image data
|
||||
/// (`%%BeginData:` followed by `XI`), a placed file's preview and skipped binary sections.
|
||||
fn special(&mut self, line: &'a [u8]) -> Option<Token<'a>> {
|
||||
if let Some((_, end)) = [PREVIEW, DOCUMENT].iter().find(|(b, _)| line.starts_with(b)) {
|
||||
if line.starts_with(PREVIEW.0) {
|
||||
let from = self.pos;
|
||||
let after = find(self.rest(), end).map_or(self.src.len(), |i| self.pos + i);
|
||||
let after = find(self.rest(), PREVIEW.1).map_or(self.src.len(), |i| self.pos + i);
|
||||
self.pos = after;
|
||||
self.line();
|
||||
let tok = if *end == PREVIEW.1 { Tok::Preview(self.slice(from, after)) } else { Tok::Comment(line.get(1..).unwrap_or_default()) };
|
||||
return Some(Token { tok, hidden: false });
|
||||
return Some(Token { tok: Tok::Preview(self.slice(from, after)), hidden: false });
|
||||
}
|
||||
if line.starts_with(DOCUMENT.0) {
|
||||
return Some(Token { tok: Tok::Document(self.document()), hidden: false });
|
||||
}
|
||||
let n = line.strip_prefix(b"%%BeginData:")?;
|
||||
let n: usize = std::str::from_utf8(n).ok()?.split_whitespace().next()?.parse().ok()?;
|
||||
@@ -226,6 +246,55 @@ impl<'a> Lexer<'a> {
|
||||
Some(Token { tok: Tok::Data(samples), hidden: false })
|
||||
}
|
||||
|
||||
/// After a `%%BeginDocument:` line: the document up to the start of the `%%EndDocument` line
|
||||
/// that ends it, the position moved past that line. A `%%BeginDocument:` line inside starts a
|
||||
/// nested document, and the bytes a `%%BeginData:` or `%%BeginBinary:` line inside counts are
|
||||
/// skipped (DSC 3.0 pp. 68 and 72): a marker in them ends nothing. `None` when no line ends it:
|
||||
/// the position moves to the end of the data.
|
||||
fn document(&mut self) -> Option<&'a [u8]> {
|
||||
// The rest of the `%%BeginDocument:` line is its end; the line after the document starts
|
||||
// tokens afresh (not hidden unless it starts with `%_`).
|
||||
self.whole_line();
|
||||
self.hidden = false;
|
||||
let from = self.pos;
|
||||
let mut depth = 0usize;
|
||||
while self.pos < self.src.len() {
|
||||
let start = self.pos;
|
||||
// A hidden object's document is on `%_` lines.
|
||||
let line = self.whole_line();
|
||||
let line = line.strip_prefix(b"%_").unwrap_or(line);
|
||||
if line.starts_with(DOCUMENT.1) {
|
||||
if depth == 0 {
|
||||
return Some(self.slice(from, start));
|
||||
}
|
||||
depth -= 1;
|
||||
} else if line.starts_with(DOCUMENT.0) {
|
||||
depth = depth.saturating_add(1);
|
||||
} else if let Some(n) = line.strip_prefix(b"%%BeginData:").or_else(|| line.strip_prefix(b"%%BeginBinary:")) {
|
||||
self.skip_counted(n);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Skip what a `%%BeginData:` or `%%BeginBinary:` comment counts (`n`, its text after the
|
||||
/// colon): `count [type [Bytes|Lines]]`, bytes unless it says `Lines`.
|
||||
fn skip_counted(&mut self, n: &[u8]) {
|
||||
let mut words = std::str::from_utf8(n).unwrap_or_default().split_whitespace();
|
||||
let Some(count) = words.next().and_then(|c| c.parse::<usize>().ok()) else { return };
|
||||
if words.nth(1).is_some_and(|unit| unit.eq_ignore_ascii_case("lines")) {
|
||||
for _ in 0..count {
|
||||
if self.pos >= self.src.len() {
|
||||
break;
|
||||
}
|
||||
self.whole_line();
|
||||
}
|
||||
} else {
|
||||
// The count starts after the comment's line end, which `document` has passed.
|
||||
self.pos = self.pos.saturating_add(count).min(self.src.len());
|
||||
}
|
||||
}
|
||||
|
||||
/// A regular token's characters.
|
||||
fn word(&mut self) -> &'a [u8] {
|
||||
let start = self.pos;
|
||||
@@ -372,11 +441,38 @@ mod tests {
|
||||
let toks = all(b"%AI26_BeginPlacedObjectPreview\r\n2 1 (\x00\xff\r\n%AI26_EndPlacedObjectPreview\r\nN\r\n");
|
||||
assert_eq!(toks, vec![t(Tok::Preview(b"\r\n2 1 (\x00\xff\r\n"), false), t(Tok::Word(b"N"), false)]);
|
||||
let toks = all(b"%%BeginDocument: a.eps\n(\x00 frobnicate\n%%EndDocument\nN\n");
|
||||
assert_eq!(toks, vec![t(Tok::Comment(b"%BeginDocument: a.eps"), false), t(Tok::Word(b"N"), false)]);
|
||||
assert_eq!(toks, vec![t(Tok::Document(Some(b"(\x00 frobnicate\n")), false), t(Tok::Word(b"N"), false)]);
|
||||
// One that doesn't end takes the rest.
|
||||
assert_eq!(all(b"%AI26_BeginPlacedObjectPreview\r1 (").len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_included_document_ends_at_its_own_end_line() {
|
||||
// A nested document, `%%EndDocument` later in a line and in counted data end nothing; `\r\n`,
|
||||
// `\r` and `\n` lines alike.
|
||||
let body = b"1 0 0 setrgbcolor\r\n%%BeginDocument: inner.eps\r(%%EndDocument) pop\r%%EndDocument\r\n\
|
||||
%%BeginData: 14 Binary Bytes\n%%EndDocument\n%%BeginBinary: 14\n%%EndDocument\n\
|
||||
%%BeginData: 1 Hex Lines\n%%EndDocument\n";
|
||||
let mut src = b"%%BeginDocument: outer.eps\r\n".to_vec();
|
||||
src.extend_from_slice(body);
|
||||
src.extend_from_slice(b"%%EndDocument\r\n~\r\n");
|
||||
assert_eq!(all(&src), vec![t(Tok::Document(Some(body)), false), t(Tok::Word(b"~"), false)]);
|
||||
// On a hidden object's `%_` lines.
|
||||
let toks = all(b"%_%%BeginDocument: a.eps\r%_1 0 0 setrgbcolor\r%_%%EndDocument\r%_~\r");
|
||||
assert_eq!(toks, vec![t(Tok::Document(Some(b"%_1 0 0 setrgbcolor\r")), true), t(Tok::Word(b"~"), true)]);
|
||||
// The line after it is hidden only when it starts with `%_`.
|
||||
let toks = all(b"%_%%BeginDocument: a.eps\r%_%%EndDocument\rN\r");
|
||||
assert_eq!(toks, vec![t(Tok::Document(Some(b"")), true), t(Tok::Word(b"N"), false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_included_document_that_doesnt_end_takes_the_rest() {
|
||||
for src in [&b"%%BeginDocument: a.eps\n%%BeginDocument: b.eps\n%%EndDocument\n~\n"[..], b"%%BeginDocument: a.eps\n%%BeginData: 99999999999\n"]
|
||||
{
|
||||
assert_eq!(all(src), vec![t(Tok::Document(None), false)]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_past_data() {
|
||||
let mut l = Lexer::new(b"/Binary : /ASCII85Decode ,\r\n%_(abc[%\r\n%x~>\r\n%_; 1");
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
//! box is as large as the file has pixels; the matrix takes them into art space from the canvas
|
||||
//! (y down, its centre the template box's), where they start at the point of art space's origin,
|
||||
//! the first row the top one. Each is a linked image showing the preview until its file is found.
|
||||
//! The version 7 form (§16) has no `%AI5_BeginPlace`: `[matrix] llx lly urx ury (path)` and
|
||||
//! `` ` ``, then `%%IncludeFile:` or the file between `%%BeginDocument:` and `%%EndDocument`,
|
||||
//! then `~`; its box is the file's `%%BoundingBox`. Newer files have three more numbers before
|
||||
//! the path. Operands of any other shape, a second `` ` `` before `~` or an included document
|
||||
//! that doesn't end leave the placed file unread.
|
||||
//! - Art with an appearance the format can't write as plain art (several fills or strokes,
|
||||
//! effects, brushes) comes first as that look, drawn (a group, or an image where an effect
|
||||
//! rasterizes it), then the object itself on `%_`
|
||||
@@ -454,6 +459,13 @@ impl<'a> Reader<'a> {
|
||||
p.preview = preview_png(d);
|
||||
}
|
||||
}
|
||||
// A placed file's document, included whole: its art isn't read here. When no
|
||||
// `%%EndDocument` ends it, the placed file isn't read.
|
||||
Tok::Document(d) => {
|
||||
if let Some(p) = self.place.as_mut() {
|
||||
p.broken |= d.is_none();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// A pattern's or symbol's reader opens a group of its own and checks it is the only frame
|
||||
@@ -468,6 +480,10 @@ impl<'a> Reader<'a> {
|
||||
if self.multi_mask.is_some() {
|
||||
return Err("it ends inside a multi-layer mask".into());
|
||||
}
|
||||
// A placed file that no `~` ends isn't read.
|
||||
if let Some(open) = self.place.take() {
|
||||
self.unreadable("placed files", open.hidden);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,6 +533,10 @@ impl<'a> Reader<'a> {
|
||||
/// without a preview, a path or a matrix isn't read.
|
||||
pub(super) fn end_place(&mut self, hidden: bool) -> Result<(), String> {
|
||||
let (m, b, path, png, w, h, folder) = match self.place.take() {
|
||||
Some(Place { broken: true, .. }) => {
|
||||
self.unreadable("placed files", hidden);
|
||||
return Ok(());
|
||||
}
|
||||
Some(Place { matrix: Some(m), bounds: Some(b), path, preview: Some((png, w, h)), folder, .. }) if !path.is_empty() => {
|
||||
(m, b, path, png, w, h, folder)
|
||||
}
|
||||
|
||||
@@ -149,7 +149,11 @@ impl<'a> Reader<'a> {
|
||||
return self.image_from(vals, &hex);
|
||||
}
|
||||
} else if line.starts_with("AI5_BeginPlace") {
|
||||
self.place = Some(Place::default());
|
||||
// One that is still open (no `~` ended it) isn't read.
|
||||
let unseen = hidden || self.gs.hidden || !self.shown();
|
||||
if let Some(open) = self.place.replace(Place { hidden: unseen, ..Place::default() }) {
|
||||
self.unreadable("placed files", open.hidden);
|
||||
}
|
||||
} else if line.starts_with("AI5_EndPlace") {
|
||||
// One that `~` didn't end isn't read.
|
||||
if self.place.is_some() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::containers::close;
|
||||
use super::document::slot_of;
|
||||
use super::types::{BLENDS, FLIP, Frame, GRAPH_OPS, GRAPH_PLAIN, GRAPH_UNENDED, IGNORED, Kind, MAX_DEPTH, MAX_NODES, line_width};
|
||||
use super::types::{BLENDS, FLIP, Frame, GRAPH_OPS, GRAPH_PLAIN, GRAPH_UNENDED, IGNORED, Kind, MAX_DEPTH, MAX_NODES, Place, line_width};
|
||||
use super::{
|
||||
Affine, Appearance, AppearanceItem, Arc, BlendMode, Dash, FillLayer, FillRule, Gradient, GradientDef, Graph, HashMap, HashSet, Instance,
|
||||
Knockout, LayerColor, LineCap, LineJoin, Node, NodeId, NodeKind, Paint, Reader, Rect, StrokeLayer, V, obj, paint,
|
||||
@@ -57,6 +57,22 @@ impl<'a> Reader<'a> {
|
||||
self.end_layer_in_mask()
|
||||
}
|
||||
|
||||
/// A placed file's `` ` ``: its matrix, box and path; a second one, or operands of another
|
||||
/// shape, leave it unreadable.
|
||||
fn place_tick(&mut self, vals: &[V], hidden: bool) {
|
||||
let unseen = hidden || self.gs.hidden || !self.shown();
|
||||
let p = self.place.get_or_insert_with(|| Place { hidden: unseen, ..Place::default() });
|
||||
if p.ticked {
|
||||
p.broken = true;
|
||||
} else {
|
||||
p.ticked = true;
|
||||
match tick(vals) {
|
||||
Some((m, b, path)) => (p.matrix, p.bounds, p.path) = (Some(m), Some(b), path),
|
||||
None => p.broken = true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn op(&mut self, w: &str, hidden: bool) -> Result<(), String> {
|
||||
if self.blend_record.is_some() {
|
||||
self.end_blend();
|
||||
@@ -288,16 +304,10 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
// Placed files.
|
||||
"`" if self.place.is_some() => {
|
||||
let matrix = vals.iter().find_map(|v| if let V::Arr(a) = v { Some(Self::nums(a)) } else { None });
|
||||
let path = vals.iter().rev().find_map(V::text).unwrap_or_default();
|
||||
if let Some(p) = self.place.as_mut() {
|
||||
p.matrix = matrix.and_then(|m| <[f64; 6]>::try_from(m).ok()).map(Affine::new);
|
||||
p.bounds =
|
||||
Self::nums(&vals).get(..4).and_then(|b| <[f64; 4]>::try_from(b).ok()).map(|[x0, y0, x1, y1]| Rect::new(x0, y0, x1, y1));
|
||||
p.path = path;
|
||||
}
|
||||
}
|
||||
// `[a b c d tx ty] llx lly urx ury (path) `` ` ``, three more numbers before the path in
|
||||
// newer files: the version 7 form has no `%AI5_BeginPlace` before it. Before the art,
|
||||
// the setup's procedures run.
|
||||
"`" if self.place.is_some() || !self.frames.is_empty() => self.place_tick(&vals, hidden),
|
||||
"~" if self.place.is_some() => self.end_place(hidden)?,
|
||||
// Type in the legacy format.
|
||||
"To" => self.begin_text(&vals, hidden),
|
||||
@@ -691,6 +701,20 @@ struct StylePaints<'a> {
|
||||
space: Affine,
|
||||
}
|
||||
|
||||
/// A placed file's `` ` `` operands → its matrix, its box and its path: an array of six numbers, four
|
||||
/// numbers, three more or none, and a string. `None` for any other shape or a number that isn't
|
||||
/// finite.
|
||||
fn tick(vals: &[V]) -> Option<(Affine, Rect, String)> {
|
||||
let [V::Arr(m), rest @ .., V::Str(path)] = vals else { return None };
|
||||
let m = <[f64; 6]>::try_from(m.iter().map(V::num).collect::<Option<Vec<f64>>>()?).ok()?;
|
||||
let nums: Vec<f64> = rest.iter().map(V::num).collect::<Option<_>>()?;
|
||||
let [x0, y0, x1, y1] = match nums.as_slice() {
|
||||
[x0, y0, x1, y1] | [x0, y0, x1, y1, _, _, _] => [*x0, *y0, *x1, *y1],
|
||||
_ => return None,
|
||||
};
|
||||
m.iter().chain(&nums).all(|v| v.is_finite()).then(|| (Affine::new(m), Rect::new(x0, y0, x1, y1), obj::text(path)))
|
||||
}
|
||||
|
||||
/// The gradients and patterns `look` paints with, placed as it draws them, by their swatch's name
|
||||
/// (the first of each; a pattern's name after `PATTERN_KEY`).
|
||||
fn placed_paints(look: &Node, out: &mut HashMap<String, Paint>, depth: usize) {
|
||||
|
||||
@@ -242,6 +242,13 @@ pub(super) struct Place {
|
||||
pub(super) folder: Option<String>,
|
||||
/// The operand stack's height at `%AI28_BeginPlacedRelativePath`, until its end.
|
||||
pub(super) folder_at: Option<usize>,
|
||||
/// Its `` ` `` was read.
|
||||
pub(super) ticked: bool,
|
||||
/// It began on a `%_` line, in a hidden object or on a layer that doesn't show.
|
||||
pub(super) hidden: bool,
|
||||
/// What it holds can't be read: operands of another shape, a second `` ` ``, or a document
|
||||
/// included in it that doesn't end.
|
||||
pub(super) broken: bool,
|
||||
}
|
||||
|
||||
/// The longest relative folder of a placed file read (bytes).
|
||||
@@ -278,7 +285,6 @@ pub(super) const IGNORED: &[&str] = &[
|
||||
"XD",
|
||||
"Xi",
|
||||
"XI",
|
||||
"`",
|
||||
"LB2",
|
||||
"Lc",
|
||||
"Xv",
|
||||
|
||||
@@ -423,7 +423,7 @@ pub(super) fn art_styles(data: &[u8]) -> HashMap<String, ArtStyle> {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Tok::Comment(_) | Tok::Data(_) | Tok::Preview(_) => {}
|
||||
Tok::Comment(_) | Tok::Data(_) | Tok::Preview(_) | Tok::Document(_) => {}
|
||||
}
|
||||
if operands.len() > 64 {
|
||||
operands.clear();
|
||||
|
||||
@@ -1513,6 +1513,173 @@ fn previewless_placement_records_box_corner_as_alternative() {
|
||||
assert_ne!(r.unread_elsewhere[0].1, r.doc.node(r.unread[0].0).unwrap().visual_bounds().unwrap());
|
||||
}
|
||||
|
||||
/// Editing data in the version 7 format (§2): art size 200 × 100 around the template box's centre
|
||||
/// (0, 0), then `body`.
|
||||
fn placed_stream7(body: &str) -> String {
|
||||
format!(
|
||||
"%!PS-Adobe-3.0\n%%Creator: test\n%%BoundingBox: 0 0 200 100\n%AI5_FileFormat 2.0\n%AI5_ArtSize: 200 100\n\
|
||||
%AI3_TemplateBox: 0 0 0 0\n%%EndComments\n%%BeginProlog\n%%EndProlog\n%%BeginSetup\n%%EndSetup\n{body}\n\
|
||||
%%PageTrailer\nshowpage\n%%Trailer\n%%EOF\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// One layer of the version 7 format (ten operands before `Lb`), showing or not, holding `art`.
|
||||
fn placed_layer7(visible: bool, art: &str) -> String {
|
||||
format!("%AI5_BeginLayer\n{} 1 1 1 0 0 1 79 128 255 Lb\n(L) Ln\n0 A\n{art}\nLB\n%AI5_EndLayer--\n", u8::from(visible))
|
||||
}
|
||||
|
||||
fn placed_read7(visible: bool, art: &str) -> Result<Structure, String> {
|
||||
read(placed_stream7(&placed_layer7(visible, art)).as_bytes())
|
||||
}
|
||||
|
||||
/// The placed file's box (`llx lly urx ury`) taken by `matrix` into the document: where the
|
||||
/// version 7 format puts it (§16.1).
|
||||
fn placed_box(r: &Structure, matrix: [f64; 6], b: [f64; 4]) -> Rect {
|
||||
(r.to_doc * kurbo::Affine::new(matrix)).transform_rect_bbox(Rect::new(b[0], b[1], b[2], b[3]))
|
||||
}
|
||||
|
||||
/// The placeholder's bounds and the other bounds [`Structure::unread_elsewhere`] keeps for it.
|
||||
fn placeholder_boxes(r: &Structure) -> Vec<Rect> {
|
||||
let id = r.unread[0].0;
|
||||
let mut boxes = vec![r.doc.node(id).unwrap().visual_bounds().unwrap()];
|
||||
boxes.extend(r.unread_elsewhere.iter().filter(|(o, _)| *o == id).map(|(_, b)| *b));
|
||||
boxes
|
||||
}
|
||||
|
||||
fn near(a: Rect, b: Rect) -> bool {
|
||||
[a.x0 - b.x0, a.y0 - b.y0, a.x1 - b.x1, a.y1 - b.y1].iter().all(|d| d.abs() < 1e-6)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version7_placed_files_are_read_without_a_begin_place() {
|
||||
// `~` was an operator the reader didn't know, so the file's layers weren't read.
|
||||
let matrices = [
|
||||
[1.0, 0.0, 0.0, 1.0, 10.0, 20.0],
|
||||
[2.0, 0.0, 0.0, 0.5, -5.0, 3.0],
|
||||
[-1.29904, 0.75, 0.375, 0.649519, 125.8461, 159.7668],
|
||||
[-1.0, 0.0, 0.0, 1.0, 100.0, 0.0],
|
||||
];
|
||||
for m in matrices {
|
||||
for extra in ["", " 4 4 0"] {
|
||||
let [a, b, c, d, e, f] = m;
|
||||
let body = format!(
|
||||
"[{a} {b} {c} {d} {e} {f}] 10 20 50 50{extra} (art.eps) `\n%%IncludeFile: art.eps\n~\n{RED}{}f",
|
||||
rect_path(0.0, 0.0, 5.0, 5.0)
|
||||
);
|
||||
let r = placed_read7(true, &body).unwrap_or_else(|e| panic!("{m:?}{extra}: {e}"));
|
||||
assert_eq!(art_kinds(&r.doc), ["path", "path"], "{m:?}{extra}: a placeholder, then the path after it");
|
||||
assert_unread(&r, "placed files");
|
||||
let want = placed_box(&r, m, [10.0, 20.0, 50.0, 50.0]);
|
||||
assert!(placeholder_boxes(&r).iter().any(|b| near(*b, want)), "{m:?}{extra}: {:?} {want:?}", placeholder_boxes(&r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kinds of the first layer's objects.
|
||||
fn art_kinds(doc: &Document) -> Vec<&'static str> {
|
||||
art(doc)
|
||||
.iter()
|
||||
.map(|n| match n.kind {
|
||||
NodeKind::Group { .. } => "group",
|
||||
NodeKind::Path { .. } => "path",
|
||||
NodeKind::Image(_) => "image",
|
||||
_ => "other",
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version7_placed_file_included_whole_is_read() {
|
||||
// The included document's operators, a `~`, a nested document and counted data in it aren't
|
||||
// the editing data's.
|
||||
let eps = "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 10 20 50 50\n%%EndComments\n1 0 0 setrgbcolor frobnicate ~\n\
|
||||
%%BeginDocument: inner.eps\n~\n%%EndDocument\n%%BeginData: 14 Binary Bytes\n%%EndDocument\n%%EOF\n";
|
||||
let body = format!("[1 0 0 1 10 20] 10 20 50 50 (art.eps) `\n%%BeginDocument: art.eps\n{eps}%%EndDocument\n~\n");
|
||||
let r = placed_read7(true, &body).unwrap();
|
||||
assert_eq!(art_kinds(&r.doc), ["path"]);
|
||||
assert_unread(&r, "placed files");
|
||||
// One that doesn't end leaves the layer without its end.
|
||||
let cut = format!("[1 0 0 1 10 20] 10 20 50 50 (art.eps) `\n%%BeginDocument: art.eps\n{eps}~\n");
|
||||
assert!(placed_read7(true, &cut).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version7_placed_file_without_a_file_is_read() {
|
||||
// `` ` `` followed by `~`: a placed file whose file isn't there.
|
||||
let r = placed_read7(true, "[1 0 0 1 10 20] 10 20 50 50 (gone.eps) `\n~\n").unwrap();
|
||||
assert_unread(&r, "placed files");
|
||||
// In a hidden layer it is left out, with a note.
|
||||
let r = placed_read7(false, "[1 0 0 1 10 20] 10 20 50 50 (gone.eps) `\n%%IncludeFile: gone.eps\n~\n").unwrap();
|
||||
assert!(r.unread.is_empty() && art(&r.doc).is_empty());
|
||||
assert!(r.hidden_unread.contains("placed files"), "{:?}", r.hidden_unread);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version7_placed_file_in_a_group_keeps_its_place() {
|
||||
// In a group, the group is what isn't read.
|
||||
let body = format!("u\n{RED}{}f\n[1 0 0 1 10 20] 10 20 50 50 (art.eps) `\n%%IncludeFile: art.eps\n~\nU\n", rect_path(0.0, 0.0, 5.0, 5.0));
|
||||
let r = placed_read7(true, &body).unwrap();
|
||||
assert_eq!(art_kinds(&r.doc), ["group"]);
|
||||
assert_eq!(r.unread.iter().map(|(id, kinds)| (*id, kinds.clone())).collect::<Vec<_>>(), [(art(&r.doc)[0].id, vec!["placed files".to_string()])]);
|
||||
// A locked one keeps its lock.
|
||||
let r = placed_read7(true, "1 A\n[1 0 0 1 10 20] 10 20 50 50 (art.eps) `\n%%IncludeFile: art.eps\n~\n").unwrap();
|
||||
assert_unread(&r, "placed files");
|
||||
assert!(r.doc.node(r.unread[0].0).unwrap().locked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placed_file_operands_of_another_shape_arent_read() {
|
||||
for operands in [
|
||||
// Five numbers in the matrix, three box numbers, two extra numbers, no path, a name for the
|
||||
// path, a number that isn't finite.
|
||||
"[1 0 0 1 10] 10 20 50 50 (a.eps)",
|
||||
"[1 0 0 1 10 20] 10 20 50 (a.eps)",
|
||||
"[1 0 0 1 10 20] 10 20 50 50 4 4 (a.eps)",
|
||||
"[1 0 0 1 10 20] 10 20 50 50",
|
||||
"[1 0 0 1 10 20] 10 20 50 50 /a.eps",
|
||||
"[1 0 0 1 10 20] 10 20 50 1e999 (a.eps)",
|
||||
] {
|
||||
// Without a box to put a placeholder in, the file isn't read.
|
||||
let e = placed_read7(true, &format!("{operands} `\n~\n")).err().unwrap_or_default();
|
||||
assert!(e.contains("placed files"), "{operands}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_placed_file_with_a_zero_area_box_or_a_second_tick_isnt_read() {
|
||||
for body in [
|
||||
"[1 0 0 1 10 20] 10 20 10 50 (a.eps) `\n~\n",
|
||||
// The operands of a second `` ` `` before `~` don't take the first one's place.
|
||||
"[1 0 0 1 10 20] 10 20 50 50 (a.eps) `\n[1 0 0 1 0 0] 0 0 5 5 (b.eps) `\n~\n",
|
||||
] {
|
||||
let e = placed_read7(true, body).err().unwrap_or_default();
|
||||
assert!(e.contains("placed files"), "{body}: {e}");
|
||||
}
|
||||
// A `%AI5_BeginPlace` while one is open: the open one isn't read either.
|
||||
let body = "%AI5_BeginPlace\n[1 0 0 1 10 20] 10 20 50 50 (a.eps) `\n%AI5_BeginPlace\n[1 0 0 1 0 0] 0 0 5 5 (b.eps) `\n~\n%AI5_EndPlace\n";
|
||||
assert!(placed_read7(true, body).err().unwrap_or_default().contains("placed files"));
|
||||
let r = placed_read7(false, body).unwrap();
|
||||
assert!(r.hidden_unread.contains("placed files") && art(&r.doc).is_empty());
|
||||
// One that no `~` ends isn't read; a hidden one, or one on a layer that doesn't show, is left
|
||||
// out with a note.
|
||||
let open = "[1 0 0 1 10 20] 10 20 50 50 (a.eps) `\n";
|
||||
assert!(placed_read7(true, &format!("%AI5_BeginPlace\n{open}")).err().unwrap_or_default().contains("placed files"));
|
||||
assert!(placed_read7(true, open).err().unwrap_or_default().contains("placed files"));
|
||||
for (visible, art) in [(true, format!("%_%AI5_BeginPlace\n%_{open}")), (true, format!("%_{open}")), (false, open.to_string())] {
|
||||
let r = placed_read7(visible, &art).unwrap_or_else(|e| panic!("{art}: {e}"));
|
||||
assert!(r.hidden_unread.contains("placed files"), "{art}: {:?}", r.hidden_unread);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tilde_without_a_placed_file_is_an_unknown_operator() {
|
||||
let e = placed_read7(true, "~\n").err().unwrap_or_default();
|
||||
assert!(e.contains('~'), "{e}");
|
||||
// An apostrophe isn't the placed file's operator.
|
||||
let e = placed_read7(true, "[1 0 0 1 10 20] 10 20 50 50 (a.eps) '\n~\n").err().unwrap_or_default();
|
||||
assert!(e.contains('\''), "{e}");
|
||||
}
|
||||
|
||||
/// A previewless locked placement keeps its edit lock on the page-art placeholder.
|
||||
#[test]
|
||||
fn previewless_placement_placeholder_inherits_lock() {
|
||||
|
||||
@@ -297,6 +297,33 @@ fn a_legacy_file_with_its_prolog_comes_in_through_its_layers() {
|
||||
assert_eq!(page.document.layers[0].name.as_deref(), Some("Layer 1"));
|
||||
}
|
||||
|
||||
/// A placed EPS in the legacy format (version 7 specification §16, no `%AI5_BeginPlace`), its file
|
||||
/// included whole: the layers come in, the placed file as the art the page draws in its box.
|
||||
#[test]
|
||||
fn a_legacy_file_with_a_placed_eps_comes_in_through_its_layers() {
|
||||
// `` ` `` takes the file's user space into the page's (`concat`); `~` restores it.
|
||||
let prolog = "/Lb {10 {pop} repeat} def /Ln {pop} def /LB {} def /A {pop} def /k {setcmykcolor} def\n\
|
||||
/` {gsave 5 {pop} repeat concat} def /~ {grestore} def";
|
||||
let eps = "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 10 20 50 50\n%%EndComments\n1 0 0 setrgbcolor\n\
|
||||
newpath 10 20 moveto 50 20 lineto 50 50 lineto 10 50 lineto closepath fill\n%%EOF\n";
|
||||
let placed = format!("[1 0 0 1 10 20] 10 20 50 50 (art.eps) `\n%%BeginDocument: art.eps\n{eps}%%EndDocument\n~\n");
|
||||
let body = format!("%AI5_BeginLayer\n1 1 1 1 0 0 0 79 128 255 Lb\n(Back) Ln\n0 A\n0 0 0 1 k\n{} {placed}LB\n%AI5_EndLayer--\n", tri(10));
|
||||
let file = String::from_utf8(ai(AI8, "", &body)).unwrap().replace("%%EndProlog", &format!("{prolog}\n%%EndProlog"));
|
||||
let r = import(file.as_bytes()).unwrap();
|
||||
assert!(!r.preview);
|
||||
let names: Vec<_> = r.document.layers.iter().map(|l| l.name.clone().unwrap_or_default()).collect();
|
||||
assert_eq!(names, ["Back"], "{:?}", r.warnings);
|
||||
let art = r.document.layers[0].children().unwrap();
|
||||
assert_eq!(tree(art), "p g[p]", "{:?}", r.warnings);
|
||||
// The file's box, moved by the matrix: 20 40 60 70 on the page, y up.
|
||||
let bounds = art[1].geometric_bounds().unwrap();
|
||||
assert!(
|
||||
(bounds.x0 - 20.0).abs() < 0.01 && (bounds.y0 - 30.0).abs() < 0.01 && (bounds.x1 - 60.0).abs() < 0.01 && (bounds.y1 - 60.0).abs() < 0.01,
|
||||
"{bounds:?}"
|
||||
);
|
||||
assert!(r.warnings.iter().any(|w| w.contains("placed files")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A version 7 palette writes no swatch names (the AI7 spec's §11 example): its swatches come in
|
||||
/// named by their paint, in its order, in place of the defaults.
|
||||
#[test]
|
||||
|
||||
@@ -96,6 +96,9 @@ private editing data. VectorCraft:
|
||||
without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868.
|
||||
lines lost (#779), some files 4× slower to open since the editing data is read whole (#758), live effects,
|
||||
brushes and symbols come in as their drawn look (#637; pattern fills are read since #1025); legacy `.ai`
|
||||
without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868, and its placed EPS files
|
||||
in the version 7 form (`` ` `` … `~` with no `%AI5_BeginPlace`) are read like other placed files without a
|
||||
preview: as the page's art in their box.
|
||||
without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868. Graphs (`%_Gs` … `%_GS`)
|
||||
come in as their drawn art, as plain groups, paths and type, without their data or settings, with a note.
|
||||
without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868, with its paragraphs' letter
|
||||
|
||||
Reference in New Issue
Block a user