PDF/.ai import: a .ai saved without PDF compatibility is placed and pasted from its editing data

Squashed from #2182 onto main.
This commit is contained in:
bflatastic
2026-10-21 17:11:08 +07:00
parent a5ea3a8002
commit 30203e60a1
8 changed files with 148 additions and 12 deletions
+10 -2
View File
@@ -196,7 +196,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Load PDF into Clipboard",
[],
None,
"{dataBase64, page?: 1, password? (an encrypted PDF), editingData?: false (true: the data is a .ai or .ait file, read as document.open reads it; when document.open would use its editing data, every object of that data comes in, each layer's objects in stacking order and a hidden layer's objects hidden; else page `page`), center?: [x, y]} replace the clipboard with the objects of one PDF page, or of a .ai file's editing data (with the images, patterns and swatches they use), centred on `center` (default: the first artboard) → {count, warnings}; then run edit.paste",
"{dataBase64, page?: 1, password? (an encrypted PDF), editingData?: false (true: the data is a .ai or .ait file, read as document.open reads it; when document.open would use its editing data, every object of that data comes in, each layer's objects in stacking order and a hidden layer's objects hidden; else page `page`), center?: [x, y]} replace the clipboard with the objects of one PDF page, or of a .ai file's editing data (with the images, patterns and swatches they use; a .ai saved without PDF compatibility: every object of its editing data, as document.open reads it, with or without editingData), centred on `center` (default: the first artboard) → {count, warnings}; then run edit.paste",
has_doc,
import_pdf
),
@@ -510,7 +510,15 @@ fn import_pdf(s: &mut Session, p: &Value) -> Result<Value> {
{
return import_loaded(s, p, C, loaded);
}
let (mut doc, warnings) = fileio::page_document(&bytes, page - 1, &opts).map_err(|e| bad(C, e.to_string()))?;
let (mut doc, warnings) = match fileio::page_document(&bytes, page - 1, &opts) {
// For a .ai saved without PDF compatibility, the clipboard takes every object of its
// editing data, as document.open reads it.
Err(e) if fileio::is_placeholder(&e) => {
let loaded = fileio::load_with("Clipboard.ai", &bytes, &opts).map_err(|e| bad(C, e.to_string()))?;
return import_loaded(s, p, C, loaded);
}
read => read.map_err(|e| bad(C, e.to_string()))?,
};
doc.drop_edit_modes();
let clip = Clipboard::from_document(&doc);
if clip.is_empty() {
+27 -1
View File
@@ -197,12 +197,38 @@ fn from_editing_data_alone(bytes: &[u8], opts: &LoadOptions) -> Option<Result<(D
)
}
/// Is `e` the error of a file whose pages are only a placeholder
/// ([`vectorcraft_pdf::PdfError::PlaceholderOnly`])?
pub(crate) fn is_placeholder(e: &EngineError) -> bool {
e.to_string() == vectorcraft_pdf::PdfError::PlaceholderOnly.to_string()
}
/// Do `opts` read every page of the PDF `bytes`: no `pages`, or a range that names each one?
fn every_page(bytes: &[u8], opts: &LoadOptions) -> bool {
let Some(range) = &opts.pages else { return true };
vectorcraft_pdf::info(bytes, opts.password.as_deref()).is_ok_and(|i| parse_range(range, i.pages.len()).is_ok_and(|p| p.len() == i.pages.len()))
}
/// A page read from a PDF or `.ai` (`read`, as [`super::page_document`] reads it) → (document,
/// notes, false); for a `.ai` saved without PDF compatibility (its page only a placeholder), the
/// file `name` read as `document.open` reads it with `password`, from its editing copy alone →
/// (document, notes, true).
pub(crate) fn page_or_editing_data(
name: &str,
bytes: &[u8],
password: Option<&str>,
read: Result<(Document, Vec<String>)>,
) -> Result<(Document, Vec<String>, bool)> {
match read {
Ok((doc, notes)) => Ok((doc, notes, false)),
Err(e) if is_placeholder(&e) => {
let l = load_with(name, bytes, &LoadOptions { password: password.map(str::to_string), ..Default::default() })?;
Ok((l.doc, l.warnings, true))
}
Err(e) => Err(e),
}
}
/// [`load`] with `document.open` options (the PDF pages, box and password; the DXF options).
pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded> {
let format = detect(name, bytes).ok_or_else(|| match super::unsupported(&super::extension(name)) {
@@ -256,7 +282,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
}
// Saved without its PDF part: the editing copy is all the file has (`pages` that
// name every page read it as the whole file).
Err(e) if e.to_string() == vectorcraft_pdf::PdfError::PlaceholderOnly.to_string() => {
Err(e) if is_placeholder(&e) => {
let whole = LoadOptions { pages: None, ..opts.clone() };
from_editing_data_alone(bytes, if every_page(bytes, opts) { &whole } else { opts })
.unwrap_or(Err(e))
+2
View File
@@ -59,7 +59,9 @@ pub(crate) use export::{export_crop, isolated};
pub(crate) use load::check_not_lossy_overwrite;
use load::err;
pub(crate) use load::import_svg;
pub(crate) use load::is_placeholder;
pub(crate) use load::native_file;
pub(crate) use load::page_or_editing_data;
pub(crate) use load::source;
pub use load::{
Loaded, RasterImage, ReadFile, detect, file_name, load, load_with, open_bytes, open_bytes_with, open_read, open_template, raster_image,
@@ -346,6 +346,77 @@ fn an_ai_file_saved_without_its_pdf_part_opens_with_pages_naming_every_page() {
assert!(e.contains("placeholder"), "{e}");
}
/// [`PLACEHOLDER`]'s `.ai` files as `dataBase64`: the sample's editing data, damaged editing data
/// and none.
fn placeholder_files() -> [String; 3] {
[compressed(&ai::sample_data()), b"%AI12_CompressedData not zlib".to_vec(), vec![]]
.map(|private| vectorcraft_format::base64_encode(&ai::ai(&private, PLACEHOLDER)))
}
#[test]
fn an_ai_file_saved_without_its_pdf_part_pastes_from_its_editing_data() {
let [sample, damaged, plain] = placeholder_files();
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
// clipboard.importPdf (the PDF flavor's command) takes every object of both layers, as
// document.open reads them.
let r = s.execute("clipboard.importPdf", &json!({"dataBase64": sample})).unwrap();
assert_eq!(r["count"], json!(7), "{r}");
s.execute("edit.paste", &json!({})).unwrap();
let art = s.doc().unwrap().doc.layers[0].children().unwrap().clone();
assert_eq!(names(&art), ["Pair", "Ring", "Window", "Shaded", "", "Sub", ""]);
assert_eq!(art.iter().map(|n| n.visible).collect::<Vec<_>>(), [true, true, true, false, false, false, false], "the hidden layer's objects");
let e = s.execute("clipboard.importPdf", &json!({"dataBase64": damaged})).unwrap_err().to_string();
assert!(e.contains("without PDF compatibility") && e.contains("zlib"), "{e}");
let e = s.execute("clipboard.importPdf", &json!({"dataBase64": plain})).unwrap_err().to_string();
assert!(e.contains("placeholder"), "{e}");
}
#[test]
fn an_ai_file_saved_without_its_pdf_part_places_from_its_editing_data() {
let [sample, damaged, plain] = placeholder_files();
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
// The place takes the art of the layers that show on the artboard and clips it to the artboard.
let r = s.execute("file.place", &json!({"name": "art.ai", "dataBase64": sample, "link": false})).unwrap();
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(200.0), Some(100.0)), "{r}");
let placed = s.doc().unwrap().doc.layers[0].children().unwrap()[0].clone();
let kids = placed.children().unwrap();
assert_eq!(names(&kids[1..]), ["Pair", "Ring", "Window"], "after the clipping path");
// With `crop: "bounding"` the size is the bounds of the art that shows. They run from the red
// square's left edge to the window's right one; the pair's hidden square, stroked 2 pt wide,
// is left out.
let r = s.execute("file.place.info", &json!({"name": "art.ai", "dataBase64": sample, "crop": "bounding"})).unwrap();
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(170.0), Some(40.0)), "{r}");
// The place takes the art of both layers that show.
let square = |x: f64| format!("0 g\n{}f\n", ai::rect(x, 10.0, x + 40.0, 50.0));
let two = ai::editing_data(200.0, 100.0, &[ai::layer("Back", &square(10.0)), ai::layer("Front", &square(110.0))].concat());
let two = vectorcraft_format::base64_encode(&ai::ai(&compressed(two.as_bytes()), PLACEHOLDER));
let r = s.execute("file.place.info", &json!({"name": "two.ai", "dataBase64": two, "crop": "bounding"})).unwrap();
assert_eq!((r["width"].as_f64(), r["height"].as_f64()), (Some(140.0), Some(40.0)), "{r}");
// The file has two 100-point artboards and one placeholder page. The second artboard lies 50
// below the first. A square of 40 lies on the first and an oblong of 60 × 40 on the second.
let boards = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\
%_/Dictionary :\n%_0 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
%_/Dictionary :\n%_0 -50 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 -150 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
%_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n";
let art = format!("0 g\n{}f\n{}f\n", ai::rect(10.0, 10.0, 50.0, 50.0), ai::rect(10.0, -140.0, 70.0, -100.0));
let paged = ai::editing_data(100.0, 100.0, &(boards.to_string() + &ai::layer("Art", &art)));
let paged = vectorcraft_format::base64_encode(&ai::ai(&compressed(paged.as_bytes()), PLACEHOLDER));
let mut size = |page: u64| {
let r = s.execute("file.place.info", &json!({"name": "boards.ai", "dataBase64": paged, "page": page, "crop": "bounding"}));
r.map(|r| (r["width"].as_f64(), r["height"].as_f64())).map_err(|e| e.to_string())
};
assert_eq!(size(1), Ok((Some(40.0), Some(40.0))));
assert_eq!(size(2), Ok((Some(60.0), Some(40.0))));
let third = size(3);
assert!(third.as_ref().is_err_and(|e| e.contains("has 2 page(s)")), "{third:?}");
let e = s.execute("file.place", &json!({"name": "damaged.ai", "dataBase64": damaged})).unwrap_err().to_string();
assert!(e.contains("without PDF compatibility") && e.contains("zlib"), "{e}");
let e = s.execute("file.place", &json!({"name": "plain.ai", "dataBase64": plain})).unwrap_err().to_string();
assert!(e.contains("placeholder"), "{e}");
}
/// A page of text alone, as text-only art has in its PDF part: no path is painted.
const TEXT_ART: &str = "BT /F1 36 Tf 10 40 Td (Text only) Tj ET";
+21 -5
View File
@@ -62,7 +62,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Place…",
["File"],
Some("Cmd+Shift+P"),
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the bounds of the art that shows, with its strokes and effects, as an export measures them: hidden objects and guides left out) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at)} → {ids, name, format, linked, width, height, warnings}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, PDF/.ai, EMF/WMF (clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it) and native documents as one group, with the images, symbols, patterns and swatches they use. One undo step; selects what it placed (unless on a template layer); never touches the clipboard",
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the bounds of the art that shows, with its strokes and effects, as an export measures them: hidden objects and guides left out) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at)} → {ids, name, format, linked, width, height, warnings}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, PDF/.ai, EMF/WMF (clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it), a .ai saved without PDF compatibility (from its editing data, as document.open reads it: the art of the layers that show on artboard `page`, clipped to that artboard unless crop is \"bounding\") and native documents as one group, with the images, symbols, patterns and swatches they use. One undo step; selects what it placed (unless on a template layer); never touches the clipboard",
has_doc,
place
),
@@ -210,12 +210,28 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>) -> Result<Loaded> {
return Ok(Loaded { name, format, art: Art::Image(img), natural, warnings, link, board: None });
}
let anchored = format.id == "dxf" && (opts.dxf.fit || !opts.dxf.center);
let (mut doc, warnings) = match format.id {
let (mut doc, warnings, pdf) = match format.id {
// Only the page placed is read, its artboard the box asked for (Bounding Box: the art's
// bounds, below).
// bounds, below). A .ai saved without PDF compatibility is read from its editing data, as
// it opens, and placed as an EPS is: the art on artboard `page`.
"pdf" | "ai" | "ait" if pdf => {
let o = fileio::LoadOptions { crop: if crop { opts.crop } else { CropTo::Crop }, ..opts };
fileio::page_document(&src.bytes, page - 1, &o).map_err(|e| bad(cmd, format!("{name}: {e}")))?
let mut read = fileio::page_document(&src.bytes, page - 1, &o);
// Such a file can have more artboards than placeholder pages.
if page > 1
&& read.is_err()
&& let Err(e) = fileio::page_document(&src.bytes, 0, &o)
&& fileio::is_placeholder(&e)
{
read = Err(e);
}
let (mut doc, warnings, alone) =
fileio::page_or_editing_data(src.name, &src.bytes, o.password.as_deref(), read).map_err(|e| bad(cmd, format!("{name}: {e}")))?;
if alone {
// Linked images show their files, found from its folder.
super::links::resolve(&mut doc, src.path, false);
}
(doc, warnings, !alone)
}
_ => {
// Of the open options, only a DXF drawing's apply (the colour mode stays the file's).
@@ -233,7 +249,7 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>) -> Result<Loaded> {
}
// Linked images show their files, found from its folder.
super::links::resolve(&mut l.doc, src.path, false);
(l.doc, l.warnings)
(l.doc, l.warnings, false)
}
};
doc.drop_edit_modes();
+11 -2
View File
@@ -447,7 +447,8 @@ fn a_placed_document_brings_its_links_with_the_files_pixels() {
}
/// The placed (linked) files of a `.ai` file come in with the preview the file keeps of them: one
/// not found shows it, one found is read at once into the same bounds, which isn't a change.
/// not found shows it, one found is read at once into the same bounds, which isn't a change. A copy
/// saved without PDF compatibility, placed, reads the found file too.
#[test]
fn links_of_an_imported_file_are_read_where_they_are_found() {
use vectorcraft_testkit::ai;
@@ -459,7 +460,8 @@ fn links_of_an_imported_file_are_read_where_they_are_found() {
"%AI5_BeginPlace\n[0.1 0 0 -0.1 20 80] 0 0 600 300 4 4 0\n({pic})`\n%AI26_BeginPlacedObjectPreview\n2 1 8 32 17 \x7f\x10\x10\x60\x7f\x10\x10\x60\n%AI26_EndPlacedObjectPreview\n~\n%AI5_EndPlace\n"
);
let doc_path = dir.file("poster.ai");
write(&doc_path, &ai::ai(ai::editing_data(200.0, 100.0, &ai::layer("Art", &place)).as_bytes(), "0.56 0.56 0.87 rg 20 50 60 30 re f"));
let data = ai::editing_data(200.0, 100.0, &ai::layer("Art", &place));
write(&doc_path, &ai::ai(data.as_bytes(), "0.56 0.56 0.87 rg 20 50 60 30 re f"));
let placed = |s: &Session| {
let mut found = vec![];
s.doc().unwrap().doc.visit_images(|n, im| found.push((n.geometric_bounds().unwrap(), im.clone())));
@@ -481,4 +483,11 @@ fn links_of_an_imported_file_are_read_where_they_are_found() {
let (b, im) = placed(&s);
assert!(in_place(b) && (im.width, im.height) == (600, 300) && im.link.as_ref().is_some_and(|l| l.hash.is_some()), "{b:?} {im:?}");
assert!(!s.doc().unwrap().doc.images[&im.key].is_proxy());
let alone = dir.file("alone.ai");
write(&alone, &ai::ai(data.as_bytes(), "BT /F1 12 Tf 10 50 Td (Saved without its PDF part) Tj ET"));
let mut s = session();
s.execute("file.place", &json!({"path": alone})).unwrap();
let (_, im) = placed(&s);
assert!((im.width, im.height) == (600, 300) && !s.doc().unwrap().doc.images[&im.key].is_proxy(), "{im:?}");
}
+5 -2
View File
@@ -688,7 +688,9 @@ area type and type on a path), and what can't be is left out with a warning. Non
PDF, which Illustrator shows but doesn't edit and keeps as a PDF inside the editing data) is drawn by the PDF importer,
as a group named "Non-native art" fitted to the object's box; one whose PDF can't be read is left out with a warning.
Without editing data it says it can't be opened. `pages` that name every page of such a file also open it from its
editing data.
editing data. `file.place` reads it from its editing data too and places the art of the layers that show on artboard
`page`, clipped to that artboard unless `crop` is `bounding`. `clipboard.importPdf` takes every object of its editing
data.
What a PDF holds comes in as editable art: soft masks become opacity masks (an alpha mask as a white copy of its art;
the backdrop colour gives Clip, an inverting transfer function Invert), transparency groups keep isolation and knockout,
@@ -2993,7 +2995,8 @@ shows the first note of the PDF and PNG it offers in the status bar.
What other apps copied goes into the clipboard with `clipboard.importSvg`, `clipboard.importPdf {dataBase64, page?,
password?, editingData?}` (the page's objects; with `editingData: true` the data is a `.ai` or `.ait` file, and every
object of its editing data comes in as `document.open` reads it, each layer's objects in stacking order and a hidden
layer's objects hidden, or the page's objects when `document.open` would not use that copy), `clipboard.importEps
layer's objects hidden, or the page's objects when `document.open` would not use that copy; a `.ai` saved without PDF
compatibility comes in from its editing data the same way with or without `editingData`), `clipboard.importEps
{dataBase64}` (an EPS or other PostScript picture's objects, as `document.open` reads one, a hidden layer's objects
hidden), `clipboard.importImage {dataBase64, mime?}` (an embedded image at 100% of its physical size) or
`clipboard.importText {text}` (point text in the default type style), each centred on `center`
+1
View File
@@ -772,6 +772,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
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`).
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).
A `.ai` saved without PDF compatibility (its PDF part only a placeholder page) is placed and pasted from its editing data, as it opens: File › Place places the art of the layers that show on the first artboard (`file.place`'s `page` places another one), and Paste of it as PDF data (`clipboard.importPdf`) takes every object. `document.open` with `pages` naming every page of such a file opens it the same way (`crates/engine/src/cmd/fileio/tests_aiimport.rs`).
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export; grayscale TIFFs with an alpha channel or other extra channels place, relink and open, the alpha as the image's transparency (#1466 in part; CCITT Group 3 and planar files with extra channels are not read); PSD and PSB files open and place, linked or embedded, as their merged image in every colour mode and depth (#918). EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). PDF export writes type as real, searchable text in embedded subset fonts by default, as Illustrator's PDF presets do (Convert Text to Outlines is an option; printer's marks stay shapes), so text-heavy PDFs stay small (#733). DXF export writes each fill as its outlines followed by its hatch, so laser and cutter software, which skips hatches, gets filled art and type too (#546).
- The Open and Place dialogs list PostScript `.ps` files and `.epsf` files. A `.ps` file opens and places through the EPS reader (its first page), also when its header is only `%!`, as the `ps` format, which exports don't write (Print to PostScript does); an `.epsf` file opens and places as EPS. Copied in a file manager, these files paste as EPS, and `clipboard.importEps` also reads PostScript whose header is only `%!` (`crates/engine/src/cmd/fileio/tests_epsimport.rs`).
`document.pdfInfo` reads an EPS, a `.ps` file or a `.ai` in PostScript form as one page the size of its bounding box (a US Letter page without one), and its `thumbnail: 1` is a PNG of what that page prints (`crates/engine/src/cmd/fileio/tests_pdfimport.rs`).