mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 05:36:46 -06:00
M6.2: IDML import/export
New crate designcraft-idml (L3, zip + quick-xml): export_idml/import_idml for swatches (process/spot CMYK/RGB/Lab, tints, gradients), paragraph/character/ object styles with BasedOn and groups, fonts, preferences, layers, sections, parent spreads, spreads/pages (spine/centre origin mapping), frames, groups, corner options, wrap, transparency, embedded/linked images, formatted stories with special characters, breaks and threading. Engine commands file.exportIdml / file.openIdml, file.open/openBytes dispatch on IDML, CLI --in/--export .idml, File > Export IDML..., open dialogs accept .idml. Verified black-box in InDesign 2026 (exported files open with correct pages, stories, threads, styles, swatches; InDesign-exported IDML imports). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
37f59f1fc8
commit
b070cc343c
@@ -17,8 +17,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
always, file_new),
|
||||
cmd!(noundo "file.newSample", "Sample Document", ["Help"], None, "{} — a multi-page magazine sample", always, file_sample),
|
||||
cmd!(query "file.presets", "Document Presets", [], None, "{}", always, |_, _| Ok(serde_json::to_value(PRESETS).unwrap_or_default())),
|
||||
cmd!(noundo "file.open", "Open…", ["File"], Some("Cmd+O"), "{path} — .designcraft (JSON)", always, file_open),
|
||||
cmd!(noundo "file.openBytes", "Open Bytes", [], None, "{name, base64}", always, file_open_bytes),
|
||||
cmd!(noundo "file.open", "Open…", ["File"], Some("Cmd+O"), "{path} — .designcraft or .idml", always, file_open),
|
||||
cmd!(noundo "file.openBytes", "Open Bytes", [], None, "{name, base64} — DesignCraft JSON or an IDML package", always, file_open_bytes),
|
||||
cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?}", has_doc, file_save),
|
||||
cmd!(noundo "file.saveAs", "Save As…", ["File"], Some("Cmd+Shift+S"), "{path}", has_doc, file_save),
|
||||
cmd!(query "file.serialize", "Serialize", [], None, "{} → {json}", has_doc, |s, _| Ok(json!({"json": String::from_utf8_lossy(&to_bytes(&s.doc()?.doc)).to_string()}))),
|
||||
@@ -105,6 +105,9 @@ fn file_open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = str_param(p, "path").ok_or_else(|| bad("file.open", "missing `path`"))?;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if path.to_ascii_lowercase().ends_with(".idml") {
|
||||
return super::interchange::open_idml(s, p);
|
||||
}
|
||||
let bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let d = from_bytes(&bytes)?;
|
||||
let i = s.add_document(DocState::new(d, Some(path.to_string())));
|
||||
@@ -119,6 +122,10 @@ fn file_open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn file_open_bytes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let b = base64_decode(str_param(p, "base64").unwrap_or(""));
|
||||
// IDML packages are recognised by their stored `mimetype` first entry.
|
||||
if designcraft_idml::is_idml(&b) {
|
||||
return super::interchange::open_idml(s, p);
|
||||
}
|
||||
let mut d = from_bytes(&b)?;
|
||||
if let Some(n) = str_param(p, "name") {
|
||||
d.title = n.to_string();
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Interchange formats: IDML (InDesign Markup Language) import and export.
|
||||
|
||||
use designcraft_doc::Document;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::file::{base64_decode, base64_encode};
|
||||
use super::{CommandSpec, always, bad, cmd, has_doc, str_param};
|
||||
use crate::{DocState, EngineError, Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(query "file.exportIdml", "Export IDML…", [], None,
|
||||
"{path?, embedImages?: true} — writes an IDML package to `path`, or returns {base64} without a path",
|
||||
has_doc, export_idml),
|
||||
cmd!(noundo "file.openIdml", "Open IDML", [], None,
|
||||
"{path | base64, name?} — opens an IDML package as a new document (linked images are read next to the file or from its Links/ folder)",
|
||||
always, open_idml),
|
||||
]
|
||||
}
|
||||
|
||||
fn export_idml(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let opts = designcraft_idml::ExportOptions { embed_images: p.get("embedImages").and_then(Value::as_bool).unwrap_or(true) };
|
||||
let bytes = designcraft_idml::export_idml_with(&st.doc, &opts);
|
||||
match str_param(p, "path") {
|
||||
Some(path) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
std::fs::write(path, &bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
Ok(json!({"path": path, "bytes": bytes.len()}))
|
||||
}
|
||||
None => Ok(json!({"base64": base64_encode(&bytes), "bytes": bytes.len()})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Import IDML bytes; `dir` is the folder the package came from (for relative link lookup).
|
||||
pub fn import(bytes: &[u8], dir: Option<&std::path::Path>) -> Result<Document> {
|
||||
let dir = dir.map(|d| d.to_path_buf());
|
||||
let read = move |link: &str| -> Option<Vec<u8>> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if let Ok(b) = std::fs::read(link) {
|
||||
return Some(b);
|
||||
}
|
||||
let name = link.rsplit(['/', '\\']).next()?;
|
||||
let dir = dir.as_ref()?;
|
||||
std::fs::read(dir.join(name)).or_else(|_| std::fs::read(dir.join("Links").join(name))).ok()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = (link, &dir);
|
||||
None
|
||||
}
|
||||
};
|
||||
designcraft_idml::import_idml_with(bytes, &read).map_err(|e| EngineError::Other(e.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn open_idml(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (bytes, dir, name) = if let Some(b) = str_param(p, "base64") {
|
||||
(base64_decode(b), None, str_param(p, "name").map(|n| n.trim_end_matches(".idml").to_string()))
|
||||
} else if let Some(path) = str_param(p, "path") {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let b = std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let b: Vec<u8> = Vec::new();
|
||||
let pp = std::path::Path::new(path);
|
||||
(b, pp.parent().map(|d| d.to_path_buf()), pp.file_stem().map(|n| n.to_string_lossy().to_string()))
|
||||
} else {
|
||||
return Err(bad("file.openIdml", "missing `path` or `base64`"));
|
||||
};
|
||||
let mut d = import(&bytes, dir.as_deref())?;
|
||||
if let Some(n) = name {
|
||||
d.title = n;
|
||||
}
|
||||
// Never save over the .idml with the native format: the document starts unsaved.
|
||||
let i = s.add_document(DocState::new(d, None));
|
||||
Ok(json!({"index": i}))
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod export;
|
||||
mod file;
|
||||
mod find;
|
||||
mod inspect;
|
||||
pub mod interchange;
|
||||
mod layout;
|
||||
mod object;
|
||||
pub mod preflight;
|
||||
@@ -103,6 +104,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
SPECS.get_or_init(|| {
|
||||
let mut v = Vec::new();
|
||||
v.extend(file::specs());
|
||||
v.extend(interchange::specs());
|
||||
v.extend(export::specs());
|
||||
v.extend(edit::specs());
|
||||
v.extend(object::specs());
|
||||
|
||||
Reference in New Issue
Block a user