mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Part of #868: PDFs saved with the reference app's editing data open from it, as .ai files do
A PDF saved with the reference app's editing data keeps that data in streams named AIPDFPrivateData1, AIPDFPrivateData2, ..., where a .ai file uses AIPrivateData1, AIPrivateData2, .... illustrator_data read only the .ai names, so such a PDF opened like any other PDF: plain groups opened ungrouped, and hidden objects and guides were missing. The import gave no warning that the PDF's editing data went unread. illustrator_data now reads the AIPDFPrivateData streams when the file has no AIPrivateData1, and the PDF opens from its editing data as a .ai file does. editingData: false still opens it like any other PDF. has_private_data still looks for AIPrivateData keys only. layered_ai also returns whether it read the layers. When it did, art reaching past a PDF's page opens whole, and through_editing_data drops the PDF importer's note that such art was clipped (PAST_PAGE_NOTE, now public) as it drops the off-artboard note. When it didn't, both notes stay. On main, a .ai whose editing data can't be decoded or has no layers opened from its PDF part without the off-artboard note. testkit::ai::pdf writes the .ai file of testkit::ai::ai with the PDF key names. The editing-data tests open it as a PDF, and the import fuzz test opens the damaged sample as a PDF too. Docs: the document.open params, docs/mcp.md, docs/file-format-parity.md, docs/gaps.md (G9: the export direction of #868 remains), one evidence line in docs/target-app-parity.md, and #868 moved from layer structure to editing data in the File formats row of docs/target-app-parity.md and in docs/roadmap.md.
This commit is contained in:
@@ -148,21 +148,24 @@ pub fn load(name: &str, bytes: &[u8]) -> Result<Loaded> {
|
||||
load_with(name, bytes, &LoadOptions::default())
|
||||
}
|
||||
|
||||
/// A PDF-compatible `.ai` read through the editor's own copy of its art, which it carries: its
|
||||
/// layers and hidden objects as they were, and the art outside its artboard (its PDF part has
|
||||
/// only what is on the artboards), as an EPS of the editor is read. Anything else as it was.
|
||||
/// A PDF-compatible `.ai`, or a PDF saved with the editing data, read through the editor's own copy
|
||||
/// of its art, which it carries: its layers and hidden objects as they were, and the art outside
|
||||
/// its artboard (its PDF part has only what is on the artboards), as an EPS of the editor is read.
|
||||
/// Anything else as it was.
|
||||
fn through_editing_data(bytes: &[u8], opts: &LoadOptions, doc: Document, warnings: Vec<String>) -> (Document, Vec<String>) {
|
||||
if opts.pages.is_some() || !opts.layers || !opts.editing_data {
|
||||
return (doc, warnings);
|
||||
}
|
||||
let Some(private) = vectorcraft_pdf::illustrator_data(bytes, opts.password.as_deref()) else { return (doc, warnings) };
|
||||
let outlined = opts.text_as == vectorcraft_pdf::TextAs::Outlines;
|
||||
let (doc, mut warnings) = vectorcraft_eps::layered_ai(&private, doc, warnings, outlined);
|
||||
// The note that the art outside the artboard is lost is the reason the layers weren't read.
|
||||
if warnings.iter().any(|w| w.starts_with("the file's layers weren't read from its editing data")) {
|
||||
let (doc, mut warnings, read) = vectorcraft_eps::layered_ai(&private, doc, warnings, outlined);
|
||||
// The layers weren't read (or couldn't be): the notes of reading the PDF part still hold.
|
||||
if !read {
|
||||
return (doc, warnings);
|
||||
}
|
||||
warnings.retain(|w| w != vectorcraft_pdf::OFF_ARTBOARD_NOTE);
|
||||
// The layers hold the art that the PDF part leaves out or, in a PDF, that the import clipped
|
||||
// to the page.
|
||||
warnings.retain(|w| w != vectorcraft_pdf::OFF_ARTBOARD_NOTE && w != vectorcraft_pdf::PAST_PAGE_NOTE);
|
||||
(doc, warnings)
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Open Document",
|
||||
[],
|
||||
None,
|
||||
"{path} or {name, dataBase64}, PDF/.ai: pages?: \"2-3, 5\" (1-based, default all; one artboard each) | page?: n, cropTo?: bounding|art|crop (default)|trim|bleed|media (the box each artboard gets; bounding: the art's bounds; trim: the bleed box round it becomes the document's bleed; a PDF-compatible .ai opens on its trim boxes for crop), password? (encrypted PDFs; see document.pdfInfo), textAs?: text (default: point type per line, lines wrapped in a frame as area type where they lay out the same, in the file's fonts by name; missing ones are listed in warnings) | outlines (glyph paths), layers?: true (default: optional content groups become layers with their visibility, print state and lock — art that is off comes in as a hidden layer; other art goes to a layer per page) | false (one layer per page, without the art that is off; Save then asks for a name), editingData?: true (default: an EPS or .ai that carries its editor's own copy of the art opens from it, with its layers, hidden objects, guides, artboards and the art outside them; a .ai whose type shows opens from its PDF part with textAs: outlines) | false (only what its page or PDF part prints, for print pipelines; a .ai saved without PDF compatibility then can't be opened), colorMode?: rgb|cmyk (the mode the document opens in, its colours converted as file.documentColorMode does, grays? as there; default: the file's — a PDF keeps CMYK, Gray and spot inks (spot swatches at a tint) and opens in CMYK when painted mostly in CMYK), DXF: dxf?: {layout?: \"Model\" (default) | a paper layout's name (document.dxfInfo lists them), unit?, scale? (the ratio: 1 unit of the art = scale drawing units; default: the drawing at 1:1 in its own unit), fit?: false (scale to fit an artboard of fitTo?: [612, 792], turned to the art's orientation), scaleLineweights?: false (lineweights scale with the art), center?: true (false: the drawing's origin on the artboard's bottom-left corner; fitted art's bottom-left corner), mergeLayers?: false (all art on one layer)} → {index, title, format, warnings, restored, missingLinks, modifiedLinks, updatedLinks: [{name, path, ids}]}; any readable format (see document.formats): .vectorcraft/.drawcraft, .svg/.svgz, .pdf/.ai, .ait, ASCII .dxf (each DXF layer with art a layer; blocks symbols; what can't come in is listed in warnings), PNG/JPEG/GIF/WebP/TIFF/BMP (an image opens as a document of its size at the resolution it declares, as file.place sizes it; 72 ppi, 1 px = 1 pt, when it declares none), .emf/.wmf (one artboard, the picture's frame; records VectorCraft doesn't read are skipped with one warning), .eps and PostScript .ai (one artboard, the bounding box: an EPS file VectorCraft wrote restores the document it carries, restored: true; other files are read by a PostScript interpreter — paths, colours and spot inks, clips, gradients, images, type in the fonts named — and come in as their TIFF preview, with a warning, when their PostScript can't be read). A PDF/.ai/.ait or SVG saved with Preserve Editing restores the native document it carries (restored: true; a PDF only when no pages are picked), unless the file was changed elsewhere since or the data can't be read: then its artwork is imported and the first warning says why. Templates (native templates, .ait) open as a new untitled document; a restored .ai keeps its path (Save writes .ai again). Linked images are read from their files (looked for at their path, then relative to the document): missing ones show their saved preview (links.relink), modified ones are read again only with Preferences › Update Links: Automatically (else links.update). An SVG's <image> files (relative links from the SVG's folder) stay linked, SVG files become art, missing ones a placeholder in their box (a warning and missingLinks)",
|
||||
"{path} or {name, dataBase64}, PDF/.ai: pages?: \"2-3, 5\" (1-based, default all; one artboard each) | page?: n, cropTo?: bounding|art|crop (default)|trim|bleed|media (the box each artboard gets; bounding: the art's bounds; trim: the bleed box round it becomes the document's bleed; a PDF-compatible .ai opens on its trim boxes for crop), password? (encrypted PDFs; see document.pdfInfo), textAs?: text (default: point type per line, lines wrapped in a frame as area type where they lay out the same, in the file's fonts by name; missing ones are listed in warnings) | outlines (glyph paths), layers?: true (default: optional content groups become layers with their visibility, print state and lock — art that is off comes in as a hidden layer; other art goes to a layer per page) | false (one layer per page, without the art that is off; Save then asks for a name), editingData?: true (default: an EPS, .ai or PDF that carries its editor's own copy of the art opens from it, with its layers, hidden objects, guides, artboards and the art outside them; a .ai or PDF whose type shows opens from its PDF part with textAs: outlines) | false (only what its page or PDF part prints, for print pipelines; a .ai saved without PDF compatibility then can't be opened), colorMode?: rgb|cmyk (the mode the document opens in, its colours converted as file.documentColorMode does, grays? as there; default: the file's — a PDF keeps CMYK, Gray and spot inks (spot swatches at a tint) and opens in CMYK when painted mostly in CMYK), DXF: dxf?: {layout?: \"Model\" (default) | a paper layout's name (document.dxfInfo lists them), unit?, scale? (the ratio: 1 unit of the art = scale drawing units; default: the drawing at 1:1 in its own unit), fit?: false (scale to fit an artboard of fitTo?: [612, 792], turned to the art's orientation), scaleLineweights?: false (lineweights scale with the art), center?: true (false: the drawing's origin on the artboard's bottom-left corner; fitted art's bottom-left corner), mergeLayers?: false (all art on one layer)} → {index, title, format, warnings, restored, missingLinks, modifiedLinks, updatedLinks: [{name, path, ids}]}; any readable format (see document.formats): .vectorcraft/.drawcraft, .svg/.svgz, .pdf/.ai, .ait, ASCII .dxf (each DXF layer with art a layer; blocks symbols; what can't come in is listed in warnings), PNG/JPEG/GIF/WebP/TIFF/BMP (an image opens as a document of its size at the resolution it declares, as file.place sizes it; 72 ppi, 1 px = 1 pt, when it declares none), .emf/.wmf (one artboard, the picture's frame; records VectorCraft doesn't read are skipped with one warning), .eps and PostScript .ai (one artboard, the bounding box: an EPS file VectorCraft wrote restores the document it carries, restored: true; other files are read by a PostScript interpreter — paths, colours and spot inks, clips, gradients, images, type in the fonts named — and come in as their TIFF preview, with a warning, when their PostScript can't be read). A PDF/.ai/.ait or SVG saved with Preserve Editing restores the native document it carries (restored: true; a PDF only when no pages are picked), unless the file was changed elsewhere since or the data can't be read: then its artwork is imported and the first warning says why. Templates (native templates, .ait) open as a new untitled document; a restored .ai keeps its path (Save writes .ai again). Linked images are read from their files (looked for at their path, then relative to the document): missing ones show their saved preview (links.relink), modified ones are read again only with Preferences › Update Links: Automatically (else links.update). An SVG's <image> files (relative links from the SVG's folder) stay linked, SVG files become art, missing ones a placeholder in their box (a warning and missingLinks)",
|
||||
always,
|
||||
load::open
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Illustrator EPS and `.ai` files open from the editing data they carry: layers, groups, names,
|
||||
//! hidden objects, artboards, colours and images as they were; what the reader doesn't read yet
|
||||
//! opens as before, with a warning saying why.
|
||||
//! Illustrator EPS, `.ai` and PDF files open from the editing data they carry: layers, groups,
|
||||
//! names, hidden objects, artboards, colours and images as they were; what the reader doesn't read
|
||||
//! yet opens as before, with a warning saying why.
|
||||
|
||||
use std::io::Write as _;
|
||||
use std::sync::Arc;
|
||||
@@ -82,6 +82,50 @@ fn an_illustrator_ai_file_opens_from_its_editing_data() {
|
||||
assert_ne!(names(&s.doc().unwrap().doc.layers), ["Art", "Hidden"]);
|
||||
}
|
||||
|
||||
/// A PDF saved with the editing data opens from it, as a `.ai` does (#868).
|
||||
#[test]
|
||||
fn a_pdf_saved_with_the_editing_data_opens_from_it() {
|
||||
let mut s = Session::new();
|
||||
let file = ai::pdf(&compressed(&ai::sample_data()), ai::page_pdf());
|
||||
let r = open(&mut s, "art.pdf", &file, json!({})).unwrap();
|
||||
assert_eq!((&r["format"], &r["warnings"]), (&json!("pdf"), &json!([])), "{r}");
|
||||
check_sample(&s);
|
||||
// Without it: the PDF part.
|
||||
let r = open(&mut s, "art.pdf", &file, json!({"editingData": false})).unwrap();
|
||||
let layers = &s.doc().unwrap().doc.layers;
|
||||
assert_eq!(layers.len(), 1, "{r}");
|
||||
assert_ne!(names(layers), ["Art"]);
|
||||
}
|
||||
|
||||
/// Art crossing the page's edge opens whole from a PDF's editing data, without the note that the
|
||||
/// PDF's art was clipped to the page; without the editing data it is clipped, with the note.
|
||||
#[test]
|
||||
fn a_pdfs_art_past_the_page_opens_whole_from_its_editing_data() {
|
||||
let data = ai::editing_data(200.0, 100.0, &ai::layer("Art", &format!("0 g\n{}f\n", ai::rect(150.0, 20.0, 250.0, 80.0))));
|
||||
let file = ai::pdf(data.as_bytes(), "0 g 150 20 100 60 re f");
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "past.pdf", &file, json!({})).unwrap();
|
||||
assert_eq!(r["warnings"], json!([]), "{r}");
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
assert_eq!(names(&doc.layers), ["Art"]);
|
||||
assert!(doc.art_bounds().is_some_and(|b| b.x1 > 200.5), "{:?}", doc.art_bounds());
|
||||
let r = open(&mut s, "past.pdf", &file, json!({"editingData": false})).unwrap();
|
||||
assert_eq!(r["warnings"], json!([vectorcraft_pdf::PAST_PAGE_NOTE]), "{r}");
|
||||
assert!(s.doc().unwrap().doc.art_bounds().is_some_and(|b| b.x1 < 200.5));
|
||||
}
|
||||
|
||||
/// Editing data that can't be read leaves the file as its PDF part reads, with that part's notes:
|
||||
/// a `.ai`'s art outside its artboards is missing, and a PDF's art past the page is clipped.
|
||||
#[test]
|
||||
fn editing_data_that_cant_be_read_keeps_the_pdf_parts_notes() {
|
||||
let damaged = b"%AI12_CompressedData not zlib";
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "damaged.ai", &ai::ai(damaged, ai::page_pdf()), json!({})).unwrap();
|
||||
assert_eq!(r["warnings"], json!([vectorcraft_pdf::OFF_ARTBOARD_NOTE]), "{r}");
|
||||
let r = open(&mut s, "damaged.pdf", &ai::pdf(damaged, "0 g 150 20 100 60 re f"), json!({})).unwrap();
|
||||
assert_eq!(r["warnings"], json!([vectorcraft_pdf::PAST_PAGE_NOTE]), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_the_reader_doesnt_read_opens_as_before_with_a_warning() {
|
||||
let symbols = ai::editing_data(200.0, 100.0, &ai::layer("Art", "/SymbolInstance :\n(Dot) /SymbolRef ,\n;\n"));
|
||||
|
||||
Reference in New Issue
Block a user