mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -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
@@ -45,6 +45,9 @@ pub(super) fn serialize(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
pub(super) fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = str_param(p, "path");
|
||||
if let Some(path) = path {
|
||||
super::check_not_lossy_overwrite(s.doc()?, path, p, "document.export")?;
|
||||
}
|
||||
let f = writable("document.export", str_param(p, "format"), path)?;
|
||||
let expanded = expand_presets(s, "document.export", p)?;
|
||||
let p = &*expanded;
|
||||
|
||||
@@ -133,6 +133,23 @@ 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.
|
||||
fn through_editing_data(bytes: &[u8], opts: &LoadOptions, doc: Document, warnings: Vec<String>) -> (Document, Vec<String>) {
|
||||
if opts.pages.is_some() || !opts.layers {
|
||||
return (doc, warnings);
|
||||
}
|
||||
let Some(private) = vectorcraft_pdf::illustrator_data(bytes, opts.password.as_deref()) else { return (doc, warnings) };
|
||||
let (doc, mut warnings) = vectorcraft_eps::layered_ai(&private, doc, warnings);
|
||||
// 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")) {
|
||||
return (doc, warnings);
|
||||
}
|
||||
warnings.retain(|w| w != vectorcraft_pdf::OFF_ARTBOARD_NOTE);
|
||||
(doc, warnings)
|
||||
}
|
||||
|
||||
/// [`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)) {
|
||||
@@ -171,7 +188,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
.then(|| vectorcraft_pdf::editing_with(bytes, opts.password.as_deref()))
|
||||
.flatten()
|
||||
.map(|e| (e.intact, move || Some(e.data)));
|
||||
restore_or_import(editing, || super::pdfimport::import(bytes, opts))?
|
||||
restore_or_import(editing, || {
|
||||
let (doc, warnings) = super::pdfimport::import(bytes, opts)?;
|
||||
Ok(through_editing_data(bytes, opts, doc, warnings))
|
||||
})?
|
||||
}
|
||||
"dxf" => {
|
||||
let (doc, warnings) = super::dxfimport::import(bytes, &opts.dxf)?;
|
||||
@@ -244,6 +264,9 @@ pub fn open_template(s: &mut Session, name: &str, bytes: &[u8], path: Option<&st
|
||||
/// Make a loaded file the new active document; `opts` are the options it was read with.
|
||||
fn open_loaded(s: &mut Session, loaded: Loaded, path: Option<String>, opts: &LoadOptions, as_template: bool) -> Result<Value> {
|
||||
let Loaded { mut doc, format, warnings, restored, converted, .. } = loaded;
|
||||
// What reading the file left out of the document, if anything.
|
||||
let losses: Vec<String> = warnings.iter().filter(|w| is_loss(w)).cloned().collect();
|
||||
let source_path = path.clone().filter(|_| !losses.is_empty());
|
||||
let links = crate::cmd::links::resolve(&mut doc, path.as_deref(), s.prefs.update_links == "automatically");
|
||||
// A template (saved by Save as Template, or an .ait/.vctemplate file) opens as a new untitled
|
||||
// document.
|
||||
@@ -266,6 +289,8 @@ fn open_loaded(s: &mut Session, loaded: Loaded, path: Option<String>, opts: &Loa
|
||||
st.format = format.id;
|
||||
}
|
||||
st.converted = converted;
|
||||
st.imported_from = source_path;
|
||||
st.import_losses = losses;
|
||||
let title = s.documents()[index].title();
|
||||
Ok(super::merge(json!({ "index": index, "title": title, "format": format.id, "warnings": warnings, "restored": restored }), links.to_json()))
|
||||
}
|
||||
@@ -360,3 +385,36 @@ pub(super) fn raster_doc(name: &str, bytes: &[u8]) -> Result<Document> {
|
||||
d.insert(Some(layer), 0, n).map_err(err)?;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Does this import note say that the file has something the document doesn't?
|
||||
fn is_loss(note: &str) -> bool {
|
||||
note == vectorcraft_pdf::OFF_ARTBOARD_NOTE || vectorcraft_eps::is_loss(note)
|
||||
}
|
||||
|
||||
/// Writing the file `target` for document `st`: not over the file `st` was read from when reading
|
||||
/// it left something out (hidden text, art or layers), unless `p` has `acknowledgeLoss: true`.
|
||||
/// Another name keeps the original file, with all it has.
|
||||
pub(crate) fn check_not_lossy_overwrite(st: &crate::DocState, target: &str, p: &Value, cmd: &str) -> Result<()> {
|
||||
let Some(source) = st.imported_from.as_deref().filter(|_| !st.import_losses.is_empty()) else { return Ok(()) };
|
||||
if bool_or(p, "acknowledgeLoss", false) || !same_file(source, target) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut what = st.import_losses.iter().take(3).map(|n| format!("“{n}”")).collect::<Vec<_>>().join("; ");
|
||||
if st.import_losses.len() > 3 {
|
||||
what.push_str(&format!("; {} more", st.import_losses.len() - 3));
|
||||
}
|
||||
Err(bad(
|
||||
cmd,
|
||||
format!(
|
||||
"{target} is the file this document was read from, and reading it left things out ({what}): writing over it would lose them for good. Write another file instead, or pass acknowledgeLoss: true to replace it anyway"
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
/// Do `a` and `b` name one file (spelled differently, or through a link)?
|
||||
fn same_file(a: &str, b: &str) -> bool {
|
||||
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
|
||||
(Ok(a), Ok(b)) => a == b,
|
||||
_ => a == b,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ pub use encode::{ARTBOARD_PARAMS, ArtboardPick, Encoded, encode, encode_all, enc
|
||||
pub(crate) use encode::{anti_alias, background, with_single_artboard};
|
||||
pub use export::export_source;
|
||||
pub(crate) use export::isolated;
|
||||
pub(crate) use load::check_not_lossy_overwrite;
|
||||
use load::err;
|
||||
pub(crate) use load::import_svg;
|
||||
pub(crate) use load::native_file;
|
||||
|
||||
@@ -232,6 +232,9 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result<SavePlan> {
|
||||
_ => st.path.as_deref().and_then(parent_folder),
|
||||
};
|
||||
let modified = date_param(p, "modified", cmd)?;
|
||||
if let Some(path) = &path {
|
||||
super::check_not_lossy_overwrite(st, path, p, cmd)?;
|
||||
}
|
||||
Ok(SavePlan { mode, path, format, options, name, folder, modified })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user