Files
designcraft/crates/engine/src/cmd/interchange.rs
T
Brandon ThomasandClaude Opus 5.5 b070cc343c 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>
2026-10-01 12:03:28 -04:00

78 lines
3.3 KiB
Rust

//! 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}))
}