From 7844f46c65a927ecd41e475027a9e62efb7e8c27 Mon Sep 17 00:00:00 2001 From: Dan Green Date: Tue, 13 Oct 2026 18:38:48 -0700 Subject: [PATCH] Part of #868: legacy .ai runs as a PostScript program, with a prolog of its own The file had an empty prolog, so a PostScript interpreter stopped at its first operator: it couldn't be printed, placed or previewed by anything that runs PostScript. The prolog's procedure set (VectorCraft_Legacy, written from the PostScript Language Reference and the version 7 specification) now gives every operator the writer emits its meaning: paths and their painting with separate fill and stroke colours and overprints, the fill rule, compound paths painted as one path, clipping groups and masks, guides that don't print, layers that are hidden or don't print marking nothing, and images drawn from the hex samples on the comment lines after XI. Gradients fill with the colour set before them (the first stop's), and the type and encoding operators take their operands and mark nothing. VectorCraft opens a file with that prolog from its program, as before: its page has flat gradients and no type. Every file the writer's tests write is now run through import_with(bytes, false) and must draw what the document it was written from draws: its paths, fills, strokes and images in the same places and colours, with type left out (type the writer outlines is compared as its glyphs' outlines), gradients as their first stop, layers that don't print left out and hidden objects shown when the options write them visible. A new test prints the sample with every layer shown. The engine's flattened-image test also opens the file as PostScript. --- crates/engine/src/tests_legacy_ai.rs | 10 +++ crates/eps/src/import/mod.rs | 19 +++- crates/eps/src/legacy_ai.rs | 93 +++++++++++++++++--- crates/eps/src/tests_legacy_ai.rs | 126 ++++++++++++++++++++++++++- docs/file-format-parity.md | 7 +- 5 files changed, 239 insertions(+), 16 deletions(-) diff --git a/crates/engine/src/tests_legacy_ai.rs b/crates/engine/src/tests_legacy_ai.rs index fef2b44cf..e38e0f2fa 100644 --- a/crates/engine/src/tests_legacy_ai.rs +++ b/crates/engine/src/tests_legacy_ai.rs @@ -250,6 +250,16 @@ fn a_transparent_image_is_flattened_with_the_art_under_it_which_stays() { } assert_eq!(colour_at(d, 20.0, 30.0), [255, 0, 0], "the square outside the image"); assert_eq!(colour_at(d, 80.0, 30.0), [255, 255, 255], "nothing beyond the image"); + // Run as a PostScript program (the file's prolog draws it), it prints the same: two images + // and the square, on a page that is the art's box (10 pt in from the artboard's top left). + s.execute("document.open", &json!({"name": "t.ai", "dataBase64": r["dataBase64"], "editingData": false})).unwrap(); + let page = &s.doc().unwrap().doc; + assert_eq!(images(page).0, 2); + let printed = [colour_at(page, 30.0, 20.0), colour_at(page, 50.0, 20.0)]; + for (a, b) in before.iter().zip(&printed) { + assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(*b) <= 3), "{before:?} {printed:?}"); + } + assert_eq!(colour_at(page, 10.0, 20.0), [255, 0, 0], "the square outside the image"); } #[test] diff --git a/crates/eps/src/import/mod.rs b/crates/eps/src/import/mod.rs index 276cc9816..b59d2dda7 100644 --- a/crates/eps/src/import/mod.rs +++ b/crates/eps/src/import/mod.rs @@ -18,7 +18,9 @@ //! `%%Creator`) and these operators: Adobe Illustrator File Format Specification, version 7.0 //! (1998), listed by PRONOM at https://www.nationalarchives.gov.uk/PRONOM/fmt/423. A file in that //! format whose program has its layers (`%AI5_BeginLayer`) comes in through them, as `native` reads -//! an Illustrator EPS's editing copy (#1027). +//! an Illustrator EPS's editing copy (#1027). One VectorCraft wrote (its prolog the procedure set +//! [`crate::legacy_ai::PROCSET`], which prints its gradients flat and leaves its type out) comes in +//! from those layers alone, without its page. //! //! The page is the file's `%%BoundingBox` (`%%HiResBoundingBox` when it has one), the first page //! of a PostScript file without one. A file whose program can't be read (an operator the @@ -92,6 +94,9 @@ struct Dsc { /// In the legacy Illustrator format: its header has `%AI…` comments or names the app as the /// creator (see the module docs for the specification). illustrator: bool, + /// Its prolog is the one VectorCraft's legacy `.ai` writer includes + /// ([`crate::legacy_ai::PROCSET`] in `%%DocumentSuppliedResources`). + own_legacy: bool, } /// The four numbers of a bounding box comment's value, when it has them and they make a box. @@ -126,6 +131,9 @@ impl Dsc { if header && (line.starts_with("%AI") || line.starts_with("%%AI") || creator) { d.illustrator = true; } + if header && value("%%DocumentSuppliedResources:").is_some_and(|v| v.strip_prefix("procset ") == Some(crate::legacy_ai::PROCSET)) { + d.own_legacy = true; + } if let Some(v) = value("%%HiResBoundingBox:") { if header || atend { hires = box_of(v).or(hires); @@ -185,6 +193,15 @@ pub fn import_with(bytes: &[u8], editing_data: bool) -> Result return Err("this is not a PostScript file".into()); } let dsc = Dsc::read(ps); + // A legacy `.ai` VectorCraft wrote: its prolog prints the art plainly (gradients flat, no + // type), and its program has all of it, which is read as the format (as before the file + // printed). + if editing_data + && dsc.own_legacy + && let Some(Ok((document, warnings))) = native::program_alone(ps) + { + return Ok(Imported { document, warnings, preview: false }); + } let [llx, lly, urx, ury] = dsc.bbox.unwrap_or(LETTER); let frame = Rect::new(0.0, 0.0, urx - llx, ury - lly); let mut doc = Document::new(frame.width(), frame.height()); diff --git a/crates/eps/src/legacy_ai.rs b/crates/eps/src/legacy_ai.rs index 4b774c2f7..96a9ff259 100644 --- a/crates/eps/src/legacy_ai.rs +++ b/crates/eps/src/legacy_ai.rs @@ -51,10 +51,9 @@ //! Windows-1252, vertical type, inline graphics, gradient or pattern fills) is written as its //! outlines, and all type with [`LegacyAiOptions::outline_text`]. //! -//! The prolog defines the one procedure the script calls that isn't an operator of the format, -//! the page trailer's `annotatepage` ([`PROLOG`]); the format's operators are left to the app that -//! reads the file, so PostScript printers can't print it, as the `.ai` files other apps write in -//! this format. +//! The prolog is a procedure set of the writer's own ([`PROLOG`]) that gives the operators their +//! meaning in PostScript, so the file also prints and previews as a PostScript program (its +//! gradients flat, without its type, which apps that read the format draw). use std::fmt::Write; use std::sync::Arc; @@ -198,7 +197,7 @@ pub fn export_legacy_ai(doc: &Document, o: &LegacyAiOptions) -> Result