diff --git a/Cargo.lock b/Cargo.lock index ef525c84..2cec5566 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -981,6 +981,7 @@ dependencies = [ "hayro-cmap", "hayro-interpret", "hayro-syntax", + "indd", "kurbo", "log", "quick-xml 0.38.4", @@ -2265,6 +2266,11 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" +[[package]] +name = "indd" +version = "0.1.0" +source = "git+https://github.com/storytold/indd-utils?rev=7f3060e5bce78d21ea70e0d5eacb96edce15dfbe#7f3060e5bce78d21ea70e0d5eacb96edce15dfbe" + [[package]] name = "indexmap" version = "2.14.2" diff --git a/Cargo.toml b/Cargo.toml index b08a82fc..f6e70a0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,9 @@ krilla = { version = "0.8", features = ["pdf"] } hayro = "0.7" quick-xml = "0.38" zip = { version = "3", default-features = false, features = ["deflate"] } +# INDD/INDT → IDML conversion for File › Open: storytold/indd-utils, pinned by rev like the +# craft-libs crates (MIT OR Apache-2.0, no dependencies, builds for wasm). +indd = { git = "https://github.com/storytold/indd-utils", rev = "7f3060e5bce78d21ea70e0d5eacb96edce15dfbe" } # ui # Shared frontend components; keep this dependency above the engine boundary. diff --git a/README.md b/README.md index 0ec6540b..281325f1 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ by its own renderer. Try it yourself with `File → New → Sample Document`, or Line breaks are identical on screen and in PDF. - **Fast.** Multithreaded SIMD rendering (vello_cpu), copy-on-write documents with O(1) undo snapshots, and cached composition. -- **Open.** A documented native format, IDML import and export, and PNG, PDF (PDF/X-4, PDF/A-2b) - and EPUB export. +- **Open.** A documented native format, IDML import and export, INDD import (through + [indd-utils](https://github.com/storytold/indd-utils)), and PNG, PDF (PDF/X-4, PDF/A-2b) and EPUB export. No subscription, no licence server, no telemetry. - **Agent-native.** Every menu item, tool gesture, panel control and dialog can be driven over a JSON control channel and an **MCP server**, so Claude and other agents can lay out and edit diff --git a/ROADMAP.md b/ROADMAP.md index 8a0f8207..0939857e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: pre-alpha** · next: alpha, ~0% and ~15–25 h away; beta ~28 points and ~420–750 h beyond that (the core-workflow gate, not the percentage: ready for real work is already ~47%) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (cancelable macOS Dock Quit) · **Target:** Adobe InDesign 2026 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (.indd and .indt open through indd-utils) · **Target:** Adobe InDesign 2026 DesignCraft aims at full Adobe InDesign parity, and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web. @@ -21,7 +21,7 @@ format + IDML), scriptable by agents (MCP), and available on the web. | Audience | Ready | Opus 5.5 agent-hours to ~95% | Work that dominates | |---|---|---|---| | Full target (ready for real work) | ~47% | 1,150–1,950 (~70% parallel) | depth across all areas, scripting compatibility, localization, typography fidelity, INDD/IDML | -| Mainstream practitioner | ~42% | 500–850 (~65% parallel) | INDD import and IDML corpus, composition fidelity, weekly-area depth, PDF/X-1a and printing | +| Mainstream practitioner | ~42% | 500–850 (~65% parallel) | INDD and IDML real-file corpus, composition fidelity, weekly-area depth, PDF/X-1a and printing | | Essentials user | ~54% | 135–250 (~50% parallel) | start-up robustness, context menus and copy/paste, threading, fonts, Windows printing | **Why pre-alpha:** it fails one of InDesign's core workflows, the alpha gate in @@ -31,7 +31,7 @@ thread or unthread command. Long text flows only through autoflow on Place and P Frame, which users report adding and keeping pages wrongly (#165, #140, #194, #352). Everything else in the gate works end to end on macOS (document setup, styles, images, object editing, PDF export, save and reopen), and ready for real work (~47%) is above the alpha bar, so manual -threading is the whole distance to alpha. Beyond that, beta needs INDD opening, IDML proven on +threading is the whole distance to alpha. Beyond that, beta needs INDD and IDML proven on real files, composition compared with InDesign's, and basic editing fixed on Windows and Linux. Details: [docs/target-app-parity.md](docs/target-app-parity.md). @@ -41,7 +41,7 @@ Details: [docs/target-app-parity.md](docs/target-app-parity.md). |---|---|---|---| | Features (depth) | 55% | 250–400 | [target-app-parity.md](docs/target-app-parity.md) | | Typography and composition fidelity | 45% | 120–200 | [typography-parity.md](docs/typography-parity.md) | -| File formats | 40% | 150–250 + 20–60 (INDD) | [file-format-parity.md](docs/file-format-parity.md) | +| File formats | 40% | 150–250 + 10–30 (INDD corpus) | [file-format-parity.md](docs/file-format-parity.md) | | UI/UX fidelity | 45% | 120–180 | [ui-parity.md](docs/ui-parity.md) | | Stability | 45% | 60–100 | [gaps.md](docs/gaps.md) | | Performance | 65% | 40–80 | [gaps.md](docs/gaps.md) | @@ -109,7 +109,7 @@ Ranked; detail and estimates in [docs/roadmap.md](docs/roadmap.md), the full lis 1. **Alpha gate:** manual text threading (out-port click, thread/unthread commands) and Primary Text Frame fixes (#165) (15–25 h) 2. Basic editing on every platform: copy/paste, context menus, caret formatting (30–50 h) 3. CI running `cargo xtask ci` on pull requests (4–8 h) -4. Open INDD through a clean-room converter, owner decision on #114 (20–60 h) +4. INDD real-file corpus: measure what `.indd` files keep through indd-utils (10–30 h) 5. IDML real-file corpus and fixes (60–100 h) 6. Composition oracle against InDesign; optical kerning; overset rule (80–140 h) 7. Start on any GPU; find every installed font (25–50 h) @@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-13** · `.indd` and `.indt` documents open through File › Open, drop, the web inbox, `file.open`, `file.openBytes`, `file.openIdml` and the CLI: [storytold/indd-utils](https://github.com/storytold/indd-utils) converts them to IDML in memory and the IDML importer reads the result, on desktop and web. Converter warnings come first in the open result; an unreadable file is an open error (#21, #114). No readiness percentage change until a real-file corpus measures it. - **2026-10-12** · macOS Dock Quit uses the same unsaved-document prompts as window and menu Quit. Repeated requests keep the current answers; Cancel retains every tab and unsaved recovery checkpoint, and completed Save/Don't Save answers exit through the ordinary event loop. No readiness percentage change. - **2026-10-12** · Orderly native exit writes the latest recovery state after finalizing current quit approvals; incomplete or stale plans keep their documents, and Undo to the saved state clears stale recovery. Unavailable or failed recovery is reported. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change. - **2026-10-12** · Recovered documents keep a checkpoint until saved, closed or reverted, including a second crash before autosave. Restarted document IDs and explicit recovery folders preserve other snapshots; failed republishing retains the original. No readiness percentage change. diff --git a/apps/designcraft-web/src/web.rs b/apps/designcraft-web/src/web.rs index fe7cca4a..1ba0617a 100644 --- a/apps/designcraft-web/src/web.rs +++ b/apps/designcraft-web/src/web.rs @@ -5,7 +5,7 @@ use designcraft_engine::Session; use designcraft_ui_egui::{DesignApp, ImportRequest, ImportedFile, Inbox, Services}; use wasm_bindgen::JsCast as _; -const DOC_EXTS: &[&str] = &["designcraft", "idml"]; +const DOC_EXTS: &[&str] = &["designcraft", "idml", "indd", "indt"]; const IMAGE_EXTS: &[&str] = &[ "png", "jpg", @@ -126,7 +126,7 @@ fn services(inbox: Inbox, ctx: egui::Context) -> Services { } else if request.purpose == "place" { rfd::AsyncFileDialog::new().add_filter("Graphics", IMAGE_EXTS) } else { - rfd::AsyncFileDialog::new().add_filter("DesignCraft", DOC_EXTS) + rfd::AsyncFileDialog::new().add_filter("DesignCraft, IDML or InDesign", DOC_EXTS) }; wasm_bindgen_futures::spawn_local(async move { let Some(file) = dialog.pick_file().await else { diff --git a/apps/designcraft/src/main.rs b/apps/designcraft/src/main.rs index 26ce2ee4..02b4cd7d 100644 --- a/apps/designcraft/src/main.rs +++ b/apps/designcraft/src/main.rs @@ -191,9 +191,10 @@ fn open_filters(purpose: &str) -> &'static [OpenFilter] { OpenFilter { name: "Video and sound", extensions: &["mp4", "m4v", "mov", "webm", "mp3", "m4a", "wav", "ogg"] }, ], _ => &[ - OpenFilter { name: "DesignCraft or IDML", extensions: &["designcraft", "idml"] }, + OpenFilter { name: "DesignCraft, IDML or InDesign", extensions: &["designcraft", "idml", "indd", "indt"] }, OpenFilter { name: "DesignCraft", extensions: &["designcraft"] }, OpenFilter { name: "InDesign Markup (IDML)", extensions: &["idml"] }, + OpenFilter { name: "InDesign document or template (INDD, INDT)", extensions: &["indd", "indt"] }, ], } } @@ -382,7 +383,9 @@ mod tests { assert!(place.len() > 1, "the place dialog keeps a filter for each kind of file"); assert!(place.iter().any(|filter| filter.extensions.contains(&"png"))); let documents = super::open_filters(""); - assert!(documents.iter().any(|filter| filter.extensions.contains(&"designcraft"))); + for ext in ["designcraft", "idml", "indd", "indt"] { + assert!(documents.first().is_some_and(|filter| filter.extensions.contains(&ext)), "{ext} is missing from the first Open filter"); + } } } diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 62578ba2..0156804a 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -23,6 +23,7 @@ designcraft-textimport = { workspace = true } designcraft-pdf = { workspace = true } designcraft-tools = { workspace = true } designcraft-idml = { workspace = true } +indd = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index a1919b89..541bf97a 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -18,9 +18,9 @@ pub fn specs() -> Vec { 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 or .idml; the fonts in a `Document Fonts` folder beside it load first → {index, documentFonts: faces loaded, warnings: font files skipped (and, for .idml, package parts missing)}", + "{path} — .designcraft, .idml, or an InDesign .indd/.indt (converted to IDML; its conversion warnings join `warnings`); the fonts in a `Document Fonts` folder beside it load first → {index, documentFonts: faces loaded, warnings: conversion warnings, font files skipped and, for .idml, .indd and .indt, package parts missing}", 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.openBytes", "Open Bytes", [], None, "{name, base64} — DesignCraft JSON, an IDML package or an InDesign .indd/.indt", 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!(noundo "file.saveACopy", "Save a Copy…", ["File"], None, "{path} — writes the document without changing which file it is or its unsaved state", has_doc, |s, p| { @@ -161,7 +161,7 @@ fn file_open(s: &mut Session, p: &Value) -> Result { 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") { + if path.to_ascii_lowercase().ends_with(".idml") || super::interchange::has_indd_extension(path) { return super::interchange::open_idml(s, p); } let bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?; @@ -220,8 +220,9 @@ fn document_fonts_folder(dir: &std::path::Path) -> std::path::PathBuf { fn file_open_bytes(s: &mut Session, p: &Value) -> Result { 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) { + // IDML packages are recognised by their stored `mimetype` first entry, INDD files by their + // signature (or their name). + if designcraft_idml::is_idml(&b) || super::interchange::is_indd(str_param(p, "name").unwrap_or_default(), &b) { return super::interchange::open_idml(s, p); } let mut d = from_bytes(&b)?; diff --git a/crates/engine/src/cmd/interchange.rs b/crates/engine/src/cmd/interchange.rs index 96498e86..fba6ccbf 100644 --- a/crates/engine/src/cmd/interchange.rs +++ b/crates/engine/src/cmd/interchange.rs @@ -1,4 +1,5 @@ -//! Interchange formats: IDML (InDesign Markup Language) import and export. +//! Interchange formats: IDML (InDesign Markup Language) import and export, and INDD/INDT +//! import (converted to IDML in memory by indd-utils). use designcraft_doc::Document; use serde_json::{Value, json}; @@ -13,7 +14,7 @@ pub fn specs() -> Vec { "{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; the fonts in a `Document Fonts` folder beside it load first) → {index, documentFonts, warnings: package parts missing, font files skipped}", + "{path | base64, name?} — opens an IDML package, or an InDesign document or template (.indd, .indt: converted to IDML first; its conversion warnings join `warnings`), as a new document (linked images are read next to the file or from its Links/ folder; the fonts in a `Document Fonts` folder beside it load first) → {index, documentFonts, warnings: conversion warnings, package parts missing, font files skipped}", always, open_idml), ] } @@ -136,34 +137,64 @@ fn read_packaged_link(link: &str, dir: Option<&std::path::Path>) -> Option<(Stri } pub(crate) fn open_idml(s: &mut Session, p: &Value) -> Result { - 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())) + let (bytes, dir, file, title) = if let Some(b) = str_param(p, "base64") { + let name = str_param(p, "name").unwrap_or_default(); + (base64_decode(b), None, name.to_string(), without_document_extension(name).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 = 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())) + let file = pp.file_name().map_or_else(|| path.to_string(), |n| n.to_string_lossy().to_string()); + (b, pp.parent().map(|d| d.to_path_buf()), file, pp.file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_default()) } else { return Err(bad("file.openIdml", "missing `path` or `base64`")); }; - let (mut d, mut warnings) = import_report(&bytes, dir.as_deref())?; - if let Some(n) = name { - d.title = n; + let mut warnings = Vec::new(); + let bytes = if is_indd(&file, &bytes) { + let conversion = indd::convert(&bytes, &file).map_err(|e| EngineError::Other(format!("can't convert the InDesign file: {e}")))?; + warnings.extend(conversion.warnings.iter().map(|w| w.message().to_string())); + conversion.idml + } else { + bytes + }; + let (mut d, import_warnings) = import_report(&bytes, dir.as_deref())?; + warnings.extend(import_warnings); + if !title.is_empty() { + d.title = title; } let (fonts, faces, font_warnings) = match str_param(p, "path").filter(|_| str_param(p, "base64").is_none()) { Some(path) => super::file::load_document_fonts(&mut d, path), None => (None, 0, Vec::new()), }; warnings.extend(font_warnings); - // Never save over the .idml with the native format: the document starts unsaved. + // Never save over the .idml or .indd with the native format: the document starts unsaved. let mut st = DocState::new(d, None); st.fonts = fonts; let i = s.add_document(st); Ok(json!({"index": i, "documentFonts": faces, "warnings": warnings})) } +/// An InDesign document or template (.indd, .indt): known by its signature, or by its name when +/// the bytes are something else (the converter then says why it can't read them). +pub(crate) fn is_indd(name: &str, bytes: &[u8]) -> bool { + bytes.starts_with(&indd::header::SIGNATURE) || has_indd_extension(name) +} + +pub(crate) fn has_indd_extension(name: &str) -> bool { + let n = name.to_ascii_lowercase(); + n.ends_with(".indd") || n.ends_with(".indt") +} + +/// `name` without a trailing .idml, .indd or .indt (any case). +fn without_document_extension(name: &str) -> &str { + match name.rsplit_once('.') { + Some((stem, ext)) if ["idml", "indd", "indt"].iter().any(|e| ext.eq_ignore_ascii_case(e)) => stem, + _ => name, + } +} + #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use std::sync::Arc; @@ -262,4 +293,30 @@ mod tests { assert_eq!(*asset.data, png); std::fs::remove_dir_all(dir).unwrap(); } + + /// INDD routing without an InDesign file: bytes with the INDD signature (or an .indd/.indt + /// name) go to the converter, and what it can't read is an open error that adds no document. + #[test] + fn indd_files_go_through_the_converter_and_its_errors_are_open_errors() { + let mut signed = indd::header::SIGNATURE.to_vec(); + signed.resize(4096, 0); + let cases = [(signed, "Brochure"), (vec![0; 4096], "Brochure.indd"), (b"not an InDesign file".to_vec(), "Template.INDT")]; + let mut s = Session::new(); + for (bytes, name) in &cases { + let e = s.execute("file.openBytes", &json!({"name": name, "base64": base64_encode(bytes)})).unwrap_err().to_string(); + assert!(e.contains("can't convert the InDesign file"), "{name}: {e}"); + } + let dir = std::env::temp_dir().join(format!("dc-indd-open-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("Brochure.indd"); + std::fs::write(&path, vec![0u8; 64]).unwrap(); + let e = s.execute("file.open", &json!({"path": path.to_string_lossy()})).unwrap_err().to_string(); + assert!(e.contains("can't convert the InDesign file"), "{e}"); + assert!(s.documents().is_empty()); + std::fs::remove_dir_all(dir).unwrap(); + // Anything else keeps its own route. + assert!(!is_indd("Brochure.idml", b"PK")); + assert_eq!(without_document_extension("Brochure.INDD"), "Brochure"); + assert_eq!(without_document_extension("notes.txt"), "notes.txt"); + } } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index fc7a3e63..44e266ef 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1226,11 +1226,11 @@ impl DesignApp { } } -/// Files that open as documents (when dropped or picked) rather than being placed: DesignCraft -/// and IDML. +/// Files that open as documents (when dropped or picked) rather than being placed: DesignCraft, +/// IDML, and InDesign documents and templates (converted to IDML on open). pub fn opens_as_document(name: &str) -> bool { let n = name.to_ascii_lowercase(); - [".designcraft", ".idml"].iter().any(|ext| n.ends_with(ext)) + [".designcraft", ".idml", ".indd", ".indt"].iter().any(|ext| n.ends_with(ext)) } pub fn now_ms() -> f64 { @@ -1856,10 +1856,10 @@ mod tests { #[test] fn dropped_documents_open_and_the_rest_is_placed() { - for name in ["a.designcraft", "B.IDML"] { + for name in ["a.designcraft", "B.IDML", "c.indd", "D.INDT"] { assert!(opens_as_document(name), "{name}"); } - for name in ["a.png", "b.pdf", "idml.txt", "c.idml.zip"] { + for name in ["a.png", "b.pdf", "idml.txt", "c.idml.zip", "e.indd.zip"] { assert!(!opens_as_document(name), "{name}"); } } @@ -1888,6 +1888,18 @@ mod tests { assert_eq!(alert(&app), ("Can't Place the File".to_string(), e)); } + #[test] + fn a_dropped_indd_file_the_converter_rejects_alerts() { + let path = unreadable_file("brochure.indd"); + let mut app = DesignApp::new(Session::new(), Services::default()); + let r = app.open_dropped(&path.to_string_lossy()); + let _ = std::fs::remove_file(&path); + let e = r.unwrap_err(); + assert!(e.contains("InDesign file"), "{e}"); + assert_eq!(alert(&app), ("Can't Open the File".to_string(), e)); + assert!(app.session.documents().is_empty()); + } + #[test] fn file_open_failures_show_an_alert() { let path = unreadable_file("open.designcraft"); diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index fc8fbc3a..299ca6f4 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (.indd and .indt open through indd-utils) · **Target:** Adobe InDesign 2026 (21.6) Every format InDesign 2026 reads or writes, with DesignCraft's support, fidelity and tests. The InDesign list comes from its bundle's `Info.plist` (`CFBundleDocumentTypes`, @@ -8,8 +8,8 @@ InDesign list comes from its bundle's `Info.plist` (`CFBundleDocumentTypes`, the open/place/export filters in `apps/designcraft/src/main.rs` and the `file.*` commands. Overall numbers: [target-app-parity.md](target-app-parity.md); open problems: [gaps.md](gaps.md). -**Dimension score: ~40% ready (estimated), 150–250 h + 20–60 h for INDD integration.** The -single largest gap is that `.indd` can't be opened. +**Dimension score: ~40% ready (estimated), 150–250 h + 10–30 h for an INDD corpus.** The +largest gaps are that neither `.indd` opening nor IDML is measured on real files. Read/Write: **✓** supported · **~** partial · **✗** missing · **—** InDesign doesn't either. Weight: what share of an InDesign professional's file traffic the format carries (used for the @@ -19,8 +19,8 @@ score below). | Format | Ext | InDesign | DesignCraft read | DesignCraft write | Fidelity / tests | Weight | |---|---|---|---|---|---|---| -| InDesign document | `.indd` | read/write (native) | ✗ (#21; converters proposed in #98, #114) | ✗ | — | 30% | -| InDesign template | `.indt` | read/write | ✗ | ✗ | — | 2% | +| InDesign document | `.indd` | read/write (native) | ✓ (converted to IDML in memory by [storytold/indd-utils](https://github.com/storytold/indd-utils), then the IDML importer; #21, #114) | ✗ | Converter warnings join the open result; unreadable files are open errors. Synthetic-bytes tests only; indd-utils reports 96.9% of IDML values reproduced on ~650 pairs. No real-file corpus test | 30% | +| InDesign template | `.indt` | read/write | ✓ (as `.indd`) | ✗ | As `.indd` | 2% | | InDesign Markup | `.idml` | read/write | ✓ | ✓ | Synthetic fixtures in `crates/idml/src/tests.rs` (49 tests); exported files verified by opening in InDesign 2026 by hand; indd-utils' 98-document run imports 811 spreads / 2,866 stories / 5,812 items. Open: #193, #338, #339, #340, #211. No real-file corpus test | 20% | | DesignCraft native | `.designcraft` | — | ✓ | ✓ | zip + JSON, versioned; round-trip tests | — | | InDesign book | `.indb` | read/write | ✗ (own `.dcbook`) | ✗ | Books are shallow (see gaps.md) | 2% | @@ -86,25 +86,27 @@ score below). ## Score Weighted by the Weight column (documents, output and placed tables; resources unweighted), -counting ✓ = 1, ~ = 0.5, and IDML at 0.6 until a real-file corpus proves it: **~45% present**. -Taking fidelity into account (no INDD, IDML unproven, PDF/X-1a and presets missing), **~40% ready -for real work** (estimated). INDD alone moves this dimension by up to 30 points. +counting ✓ = 1, ~ = 0.5, IDML at 0.6 and INDD at 0 until a real-file corpus measures them: +**~45% present**. Taking fidelity into account (INDD and IDML unmeasured, PDF/X-1a and presets +missing), **~40% ready for real work** (estimated). A measured INDD corpus moves this dimension by +up to 30 points. ## Remaining effort | Work | Opus 5.5 h | |---|---| -| INDD import via a clean-room converter (owner decision, #114) | 20–60 | +| INDD real-file corpus, metric, fixes (#21, #114) | 10–30 | | IDML real-file corpus, metric, fixes (#193, #338, #339, #340) | 60–100 | | PDF/X-1a and X-3 (transparency flattener), PDF presets and `.joboptions`, external validator | 40–70 | | Print fidelity, PostScript level 3 output and print presets | 25–45 | -| ICML story import, IDMS snippets, INDT templates (once INDD reads) | 15–25 | +| ICML story import, IDMS snippets | 15–25 | | HTML / fixed-layout EPUB with live text | 20–35 | | JPEG export dialog, SVG export, EPS export | 10–20 | -| **Total** | **190–355** (150–250 excluding INDD and PostScript output) | +| **Total** | **180–325** (150–250 excluding INDD and PostScript output) | ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | `.indd` and `.indt` read through storytold/indd-utils; INDD remaining work is a real-file corpus (10–30 h); score unchanged until it is measured | | 2026-10-10 | major | Created from InDesign 2026's declared document types and filters, `plan/indesign/08-file-formats.md` and the code | diff --git a/docs/gaps.md b/docs/gaps.md index ebd2d7da..d50850db 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where DesignCraft falls short of InDesign -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (book chapter update safety) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (.indd and .indt open through indd-utils) · **Target:** Adobe InDesign 2026 (21.6) Every known shortfall against InDesign 2026, one entry each, ranked by how much it stops an InDesign professional from doing real work. This is the work list: agents pick from the top. @@ -43,14 +43,18 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format · - **Estimate:** 30–50 h (reproduce per platform through the control channel, regression tests). - **Doc:** [ui-parity.md](ui-parity.md) -### 2. InDesign documents (.indd) can't be opened (FF) -- **Missing:** INDD (and INDT, INDB, INDL) reading. Everything InDesign users receive is INDD. -- **Evidence:** #21. Two clean-room converters are proposed: PR #98 and `indd-utils` (#114), - which reports 96.9% of InDesign's IDML values reproduced on ~650 INDD/IDML pairs, 0 failures on - ~4,300 files, and output that DesignCraft's IDML importer already reads. -- **Impact:** without it, every job starts with "ask the client for an IDML". -- **Estimate:** 20–60 h to integrate indd-utils (INDD → IDML → existing importer), warnings UI, - corpus test; **owner decision** on provenance and where the crate lives. +### 2. INDD opening isn't measured on real files (FF) +- **Missing:** a real-file corpus test of opening `.indd` and `.indt`. They open through + [storytold/indd-utils](https://github.com/storytold/indd-utils), which converts them to IDML in + memory for the IDML importer, but DesignCraft's own tests use synthetic bytes only. INDB and + INDL are still not read. +- **Evidence:** #21, #114 (PR #98 proposed another converter). indd-utils reports 96.9% of + InDesign's IDML values reproduced on ~650 INDD/IDML pairs and 0 failures on ~4,300 files; what + DesignCraft draws from those files is unmeasured, and it inherits the IDML importer's open bugs + (gap 3). +- **Impact:** a client's `.indd` opens, but nobody knows how close it comes to the original. +- **Estimate:** 10–30 h (corpus pinned in a sibling of `storytold/photocraft-corpus`, metric, + fixes in indd-utils or the IDML importer). - **Doc:** [file-format-parity.md](file-format-parity.md) ### 3. IDML round-trip isn't proven on real files (FF) diff --git a/docs/roadmap.md b/docs/roadmap.md index d80fe7aa..592c7735 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # DesignCraft roadmap (detail) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (stage re-normalized to pre-alpha by the core-workflow gate; earlier today: major, milestones re-assessed; beta plan from the full re-measure) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (.indd and .indt open through indd-utils) · **Target:** Adobe InDesign 2026 (21.6) Forward-looking plan. The one-page summary is [ROADMAP.md](../ROADMAP.md); numbers are in [target-app-parity.md](target-app-parity.md); the work list is [gaps.md](gaps.md). Hours are @@ -25,7 +25,8 @@ but are next in line. | Export a print-ready PDF (bleed, marks, CMYK and spot, PDF/X-4) | yes | krilla export with subset fonts, Separations, marks and bleed, PDF/X-4 with validator; 53 PDF tests | 0 | Exchanging files with InDesign users (open INDD, round-trip IDML) is a beta requirement, not part -of the alpha gate: IDML works both ways today and INDD is gap 2. +of the alpha gate: IDML works both ways today, INDD opens through indd-utils, and measuring both +on real files is gaps 2 and 3. **Distance to alpha:** ~15–25 h (the threading row). Ready for real work (~47%) is already above the ~40% alpha bar. @@ -41,8 +42,9 @@ In order. Each closes a beta blocker from [gaps.md](gaps.md). macOS, Windows and Linux. 30–50 h. 2. **CI on pull requests** (gap 7): `cargo xtask ci` on every PR, at least macOS and Linux; Windows smoke start. 4–8 h. Needs the owner to make it a required check. -3. **Open INDD** (gap 2): decide with the owner how to bring in the clean-room converter - (#114 indd-utils, #98), then INDD → IDML → existing importer, warnings shown. 20–60 h. +3. **INDD real-file corpus** (gap 2): `.indd` and `.indt` open through storytold/indd-utils + (INDD → IDML in memory → existing importer, warnings in the open result); a pinned corpus and + a metric show what survives. 10–30 h. 4. **IDML real-file corpus** (gap 3): a pinned corpus, a survival metric, and fixes for #193, #338, #339, #340. 60–100 h. 5. **Composition oracle** (gap 4): synthetic stories set in InDesign 2026 vs DesignCraft; @@ -62,7 +64,7 @@ set exists; depth gaps are in gaps.md. | M3 | Styles (nested/GREP, bullets, keeps, span columns) | ✅; style editing reliability (#169) | | M4 | Color & effects | ✅ core; colour management depth | | M5 | Graphics & links | ✅ core; EPS preview only | -| M6 | Files & export (native, IDML, PDF, PNG/JPEG) | IDML ✅, PDF ✅ (X-4, A-2b, tagged); INDD ✗; PDF/X-1a/X-3 and presets ✗ | +| M6 | Files & export (native, IDML, PDF, PNG/JPEG) | IDML ✅, PDF ✅ (X-4, A-2b, tagged); INDD read ✅ (via indd-utils); PDF/X-1a/X-3 and presets ✗ | | M7 | Long documents (sections, TOC, index, footnotes, books) | ✅ except Books (shallow) | | M8 | Tables | ✅ core (table/cell styles, rotation); diagonal lines, anchored objects in cells (#163) | | M9 | Performance (MT composition, tiles) | in progress: parallel compose 6.7×, damage regions; incremental composition next | @@ -82,7 +84,7 @@ that is ~28 points and **420–750 h** (~120–200 wall-clock hours with four to | Work | Gap | Hours | |---|---|---| | Basic editing on every platform | 1 | 30–50 | -| INDD import | 2 | 20–60 | +| INDD real-file corpus | 2 | 10–30 | | IDML real-file corpus and fixes | 3 | 60–100 | | Composition oracle and fixes | 4, 13 | 80–140 | | Start-up robustness, fonts | 5, 6 | 25–50 | @@ -110,6 +112,7 @@ output intent · EPS place") is done except Books depth and EPS (preview only). | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | INDD and INDT open through storytold/indd-utils; focus item 3 shrinks to a real-file corpus (10–30 h) | | 2026-10-10 | trivial | Ready figure 42% → 47% (method aligned with the standard); beta distance 28 points | | 2026-10-10 | minor | Alpha gate table added (6 core workflows); gate fails on manual threading, stage pre-alpha; A1 milestone and focus item 0 | | 2026-10-10 | major | Created: milestones moved from ROADMAP.md and re-assessed; Current focus and the beta plan from the full re-measure | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index eb7d7e5c..b3c87362 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # DesignCraft parity with Adobe InDesign -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (cancelable macOS Dock Quit) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (.indd and .indt open through indd-utils) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS) The authoritative assessment of how close DesignCraft is to Adobe InDesign. [ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) lists every known shortfall one at a time; the feature-by-feature @@ -60,7 +60,7 @@ they receive, setting type exactly, and basic editing that never fails. |---|---|---|---|---|---| | Features (depth of each area) | 25% | 55% | 250–400 | 275 catalogue rows exist; many lack options (e.g. Books is 258 lines of engine code, Liquid Layout 329); see the area table | this file | | Typography and composition fidelity | 15% | 45% | 120–200 | Knuth–Plass composer, keeps, grids, CJK, Arabic all exist; no comparison with InDesign's line breaks; optical kerning falls back to metrics (#181); overset rule differs (#180); hyphenation patterns for English and Spanish only | [typography-parity.md](typography-parity.md) | -| File formats | 20% | 40% | 150–250 (+20–60 for INDD integration) | INDD cannot be opened (#21); IDML both ways and verified in InDesign 2026, but real-file bugs open (#193, #338, #339, #340); PDF export strong; PDF/X-4 only | [file-format-parity.md](file-format-parity.md) | +| File formats | 20% | 40% | 150–250 (+10–30 for an INDD corpus) | INDD and INDT open through indd-utils, measured on synthetic bytes only (#21); IDML both ways and verified in InDesign 2026, but real-file bugs open (#193, #338, #339, #340); PDF export strong; PDF/X-4 only | [file-format-parity.md](file-format-parity.md) | | UI/UX fidelity | 15% | 45% | 120–180 | Measured InDesign 2026 look; but users report object copy/paste (#351, #348, #185, #164), right-click menus (#238, #187, #160), out-port threading (#352, #194, #165), canvas IME (#322) failing | [ui-parity.md](ui-parity.md) | | Stability | 10% | 45% | 60–100 | Never-crash rules and last-resort guards; startup failures on some GPUs and machines (#334, #273, #220, #192, #167, #166); no CI runs the tests on pull requests | [gaps.md](gaps.md) | | Performance | 5% | 65% | 40–80 | 300k characters compose in ~38 ms warm; typing repaints in < 1 ms (damage regions); no incremental composition; nothing benchmarked against InDesign; no large real documents | [gaps.md](gaps.md) | @@ -127,7 +127,7 @@ Discounts (multiplicative): |---|---|---| | Interaction fidelity | ×0.88 | right-click menus fail for some macOS users (#160, #238) and on Linux (#354); Paste in Place lands on page 1 (#348); formatting at a caret discarded (#289); font search focus (#335, #86) | | Stability on real machines | ×0.92 | start-up failures on Intel Mac Metal (#334) and Windows drivers (#167, #273, #220, #192, #166); no CI on pull requests; macOS on Apple silicon is solid | -| File exchange with InDesign users | ×0.85 | `.indd` can't be opened (#21); IDML import/export bugs (#193, #338, #339, #340, #211); opened text reflows: optical kerning (#181), overset rule (#180). Harsher than VectorCraft's ×0.92 because InDesign pros receive and send native files daily | +| File exchange with InDesign users | ×0.85 | `.indd` opens through indd-utils but is unmeasured on real files (#21); IDML import/export bugs (#193, #338, #339, #340, #211); opened text reflows: optical kerning (#181), overset rule (#180). Harsher than VectorCraft's ×0.92 because InDesign pros receive and send native files daily | 60.7% × 0.88 × 0.92 × 0.85 = 41.8% → **~42%**. It is lower than the full number (47%) because the full number spreads weight over dimensions DesignCraft does relatively well @@ -237,7 +237,7 @@ cluster**, more when the oracle has to be built first. | Work | Opus 5.5 h | Parallelizes? | Needs a human | |---|---|---|---| -| Beta blockers (see [roadmap.md](roadmap.md)) | 420–750 | ~70% (per crate) | owner decision on INDD import (#114); InDesign oracle captures on the owner's Mac | +| Beta blockers (see [roadmap.md](roadmap.md)) | 420–750 | ~70% (per crate) | InDesign oracle captures on the owner's Mac | | Rest of full parity | 730–1,200 | ~70% | native-speaker review for 12 languages; owner decision on ExtendScript compatibility and AI models | | **Total to full parity** | **1,150–1,950** | | | @@ -445,6 +445,7 @@ Task notes that headed ROADMAP.md: | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | `.indd` and `.indt` open through storytold/indd-utils; INDD remaining work 10–30 h (real-file corpus); readiness unchanged until it is measured | | 2026-10-10 | minor | Full number 42% → 47%: method aligned with the standard (plain weighted sum, the ×0.9 discount dropped), no new evidence; readiness table with hours for full, mainstream and essentials; beta distance 33 → 28 points | | 2026-10-10 | minor | Added mainstream practitioner (~42%) and essentials user (~54%) numbers with written weights and discounts and a user-evidence count; full number (42%) now written as 47.2% × 0.9, value unchanged | | 2026-10-10 | minor | Stage alpha → pre-alpha under the core-workflow gate (manual threading missing); Type & text frames row updated |