mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 05:36:46 -06:00
.indd and .indt documents open through File › Open, drop, the web inbox,
file.open, file.openBytes, file.openIdml and the CLI. The engine converts
them to IDML in memory with storytold/indd-utils (pinned by rev, MIT OR
Apache-2.0, no dependencies, builds for wasm) and imports the result
through the existing IDML path. The converter's warnings come first in the
open result's `warnings`, before the IDML importer's and the font
warnings. A file the converter can't read is an open error, shown in the
Can't Open the File alert.
Routing: a path or name ending in .indd/.indt, or bytes starting with the
INDD signature (the web inbox passes the name without its extension).
opens_as_document and the desktop and web Open dialogs list indd/indt.
Tests use synthetic bytes only: signature-bearing junk, a zero-filled
.indd and a short .indt all fail as open errors with no document added;
a dropped unreadable .indd raises the open alert.
(cherry picked from commit bb8a6a23fa)
This commit is contained in:
Generated
+6
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-5
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -18,9 +18,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
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<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") {
|
||||
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<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) {
|
||||
// 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)?;
|
||||
|
||||
@@ -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<CommandSpec> {
|
||||
"{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<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()))
|
||||
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<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()))
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
+13
-11
@@ -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 |
|
||||
|
||||
+13
-9
@@ -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)
|
||||
|
||||
+9
-6
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user