mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Read Illustrator EPS and .ai layers from their editing data
An Illustrator EPS is a flattened page plus, after %%EOF, an editing copy of the document; a PDF-compatible .ai carries the same in its private data. Importing only the page lost layer names, order, visibility, hidden layers and hidden objects and hidden text. - eps: read the editing copy (Zstandard %AI24_DataStream, older zlib %AI9_DataStream) into layers, sublayers, groups, compound paths, clip groups and hidden objects, run by the existing PostScript interpreter. Falls back to the page, with a warning, when a shown layer uses an operator that isn't read or the art differs from the page by over 5%. - eps: read the text document (ate.rs) to place type on hidden layers and objects; the page's type is matched to the file's text objects by position (pieces of a line included) and kept with the nearest one when none claims it. Area type and type on a path are left out with a note. - pdf/engine: .ai files go through the same reader. - engine/cli: a file whose import left things out isn't overwritten unless asked (acknowledgeLoss, --replace-lossy). - eps: font names re-encoded with a *<n> suffix resolve to their family. - i18n: es/it/ru for the new import messages; ROADMAP updated. Container and text-document formats were inferred from user-owned files; the Adobe file format specification was not read. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
f149f10059
commit
ec43ac5a1e
@@ -132,7 +132,7 @@ fn group_layer(
|
||||
|
||||
/// The note on a file carrying an editor's private data (#472): its PDF part, the one read, holds
|
||||
/// only the art on its artboards (art on the pasteboard is in the private data alone).
|
||||
pub(crate) const OFF_ARTBOARD_NOTE: &str =
|
||||
pub const OFF_ARTBOARD_NOTE: &str =
|
||||
"only the PDF-compatible part of this file was read: art outside its artboards is kept in the editor's private data alone, so it doesn't open";
|
||||
|
||||
/// Import a PDF, returning the document plus warnings about content that was approximated or skipped.
|
||||
|
||||
@@ -69,8 +69,8 @@ mod syntax;
|
||||
pub use editing::{EDITING_FILE, Editing, LEGACY_EDITING_FILE, editing, editing_with};
|
||||
pub use encrypt::Encryption;
|
||||
pub use export::{export, export_with_report, page_areas};
|
||||
pub use import::{import, import_with_report};
|
||||
pub use pages::{PageInfo, PdfInfo, info, is_postscript};
|
||||
pub use import::{OFF_ARTBOARD_NOTE, import, import_with_report};
|
||||
pub use pages::{PageInfo, PdfInfo, illustrator_data, info, is_postscript};
|
||||
pub use post::{THUMBNAIL_SIZE, Thumbnail};
|
||||
pub use presets::*;
|
||||
pub use print::*;
|
||||
|
||||
+25
-1
@@ -1,7 +1,7 @@
|
||||
//! The pages of a PDF to import: opening with a password, the pages picked, the box each page
|
||||
//! is cropped to ([`CropTo`]) and [`info`].
|
||||
|
||||
use hayro_syntax::object::Dict;
|
||||
use hayro_syntax::object::{Dict, Stream};
|
||||
use hayro_syntax::page::Page;
|
||||
use hayro_syntax::{DecryptionError, LoadPdfError, Pdf};
|
||||
use kurbo::{Affine, Rect};
|
||||
@@ -128,6 +128,30 @@ pub(crate) fn frame(page: &Page<'_>, which: CropTo) -> (Affine, Rect) {
|
||||
(init, if b.is_finite() && b.area() > 0.0 { b } else { Rect::new(0.0, 0.0, w as f64, h as f64) })
|
||||
}
|
||||
|
||||
/// The `PieceInfo` key such files keep their private data under.
|
||||
const PRIVATE_DATA_OWNER: &[u8] = b"Illustrator"; // brand-ok: the key the files use
|
||||
|
||||
/// Most bytes of an editor's private data read.
|
||||
const MAX_PRIVATE: usize = 512 << 20;
|
||||
|
||||
/// The private data of an Illustrator `.ai` (its first page's `PieceInfo`): the `AIPrivateData`
|
||||
/// streams in order, joined. `None` for a PDF without them (or with a stream that can't be read).
|
||||
/// What it holds is the editor's own copy of the art (see `vectorcraft_eps::layered_ai`).
|
||||
pub fn illustrator_data(bytes: &[u8], password: Option<&str>) -> Option<Vec<u8>> {
|
||||
let pdf = open(bytes, password).ok()?;
|
||||
let page = pdf.pages().first()?;
|
||||
let private = page.raw().get::<Dict<'_>>(b"PieceInfo")?.get::<Dict<'_>>(PRIVATE_DATA_OWNER)?.get::<Dict<'_>>(b"Private")?;
|
||||
let mut data = vec![];
|
||||
for n in 1..=100_000u32 {
|
||||
let Some(stream) = private.get::<Stream<'_>>(format!("AIPrivateData{n}").as_bytes()) else { break };
|
||||
data.extend_from_slice(&stream.decoded().ok()?);
|
||||
if data.len() > MAX_PRIVATE {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
(!data.is_empty()).then_some(data)
|
||||
}
|
||||
|
||||
/// Does `page` carry an editor's private data (`PieceInfo` keys starting with `AIPrivateData`)?
|
||||
/// A file saved without PDF compatibility keeps its art there and shows a placeholder page.
|
||||
pub(crate) fn has_private_data(page: &Page<'_>) -> bool {
|
||||
|
||||
@@ -366,3 +366,23 @@ fn art_past_the_scan_limit_of_shown_layers_stays_shown() {
|
||||
assert_eq!(colors(&d.layers[1]), [Color::rgb(0.0, 0.0, 1.0), Color::rgb(0.0, 1.0, 0.0)]);
|
||||
assert!(r.warnings.iter().any(|w| w.contains("went to its page's layer")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// The editor's private data of a `.ai` is its `AIPrivateData` streams, joined in order.
|
||||
#[test]
|
||||
fn an_ai_files_private_data_is_its_streams_joined() {
|
||||
let (a, b) = (first_extra(1), first_extra(1) + 1);
|
||||
let page = |ai: bool| PdfPage {
|
||||
entries: if ai {
|
||||
format!("/PieceInfo << /Illustrator << /Private << /AIPrivateData1 {a} 0 R /AIPrivateData2 {b} 0 R /NumBlock 2 >> >> >>")
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
..PdfPage::new(100.0, 100.0, "1 0 0 rg 10 10 30 30 re f")
|
||||
};
|
||||
let (one, two) = (stream("", "%AI24_ZStandard_Data first "), stream("", "second"));
|
||||
let ai = pdf_with_catalog(&[page(true)], &[&one, &two], "", None);
|
||||
assert_eq!(illustrator_data(&ai, None).unwrap(), b"%AI24_ZStandard_Data first second");
|
||||
// A PDF without them has none; neither has bytes that aren't a PDF.
|
||||
assert!(illustrator_data(&pdf_with_catalog(&[page(false)], &[&one, &two], "", None), None).is_none());
|
||||
assert!(illustrator_data(b"not a pdf", None).is_none());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user