From 60584e32217c264482601d1b2f1777eb0f7c8f5a Mon Sep 17 00:00:00 2001 From: Tom Main Date: Sun, 1 Nov 2026 15:08:46 -0500 Subject: [PATCH] #2108: prefer document content over InDesign filename extensions Keep native ZIP, legacy JSON and IDML bytes out of the INDD converter even when a browser supplies a misleading .indd or .indt name. Preserve signature and unknown-data routing to the converter. Check that every browser delivery adds a document before asserting its title. Co-Authored-By: OpenAI Codex --- ROADMAP.md | 2 +- crates/engine/src/cmd/file/open.rs | 25 +++++++++++++++++++++++++ crates/engine/src/cmd/interchange.rs | 6 ++++-- crates/ui-egui/src/app/files.rs | 14 +++++++++++--- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 054d0f28a..3e4e571a8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -388,7 +388,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. -- **2026-10-31** · A `.designcraft` file opened from bytes (`file.openBytes` with its file name, as a browser or host passes it) is titled without the extension, as an IDML file opened that way is. Saving it in the browser downloaded `Brochure.designcraft.designcraft`, and the File Name variable showed the doubled name. Browser Open and Drop pass the full file name, so only the engine removes the extension and a repeated suffix is kept in the title. No readiness percentage change. +- **2026-10-31** · A `.designcraft` file opened from bytes (`file.openBytes` with its file name, as a browser or host passes it) is titled without the extension, as an IDML file opened that way is. Saving it in the browser downloaded `Brochure.designcraft.designcraft`, and the File Name variable showed the doubled name. Browser Open and Drop pass the full file name, so only the engine removes the extension and a repeated suffix is kept in the title. Native and IDML content takes priority over a misleading `.indd` or `.indt` extension. No readiness percentage change. - **2026-10-30** · Import warnings about a story have a Show button: it closes the warnings (the status line keeps them) and selects the story's first frame, fitted in the window. `file.open` returns `warningItems`, the frame each IDML or InDesign warning is about (or null), matching the converter's INDD story numbers (`story 30892`, written to IDML as `u78ac`) and IDML `Self` names to the imported stories. No readiness percentage change. - **2026-10-30** · Parent pages: a running-header variable shows its name in angle brackets on the parent itself (it was blank), as InDesign shows it; on the document pages, and in text with no page, it shows its text as before. No readiness percentage change. - **2026-10-30** · Without facing pages, a page of a multi-page spread shows the parent page in its place, as in InDesign: the second page of a two-page spread shows the parent's second page. Every single-sided page showed the parent's first page, so an InDesign book set up without facing pages but laid out in two-page spreads repeated the left parent page's items (a black heading bar) on its right pages and lost the right parent page's. No readiness percentage change. diff --git a/crates/engine/src/cmd/file/open.rs b/crates/engine/src/cmd/file/open.rs index 4e90f30fb..6d1aee7ce 100644 --- a/crates/engine/src/cmd/file/open.rs +++ b/crates/engine/src/cmd/file/open.rs @@ -420,6 +420,29 @@ mod open_bytes_tests { use crate::Session; + #[test] + fn legacy_and_idml_bytes_named_as_indesign_files_open_without_conversion() { + let mut maker = Session::new(); + maker.execute("file.new", &json!({})).unwrap(); + let idml = designcraft_idml::export_idml(&maker.doc().unwrap().doc); + let base64 = super::super::bytes::base64_encode(&idml); + let legacy = super::super::bytes::base64_encode(&serde_json::to_vec(&maker.doc().unwrap().doc).unwrap()); + for name in ["Brochure.indd", "Brochure.INDT"] { + let mut s = Session::new(); + s.execute("file.openBytes", &json!({"name": name, "base64": legacy})).unwrap(); + assert_eq!(s.documents().len(), 1, "legacy: {name}"); + assert_eq!(s.doc().unwrap().doc.title, name, "legacy: {name}"); + } + for command in ["file.openBytes", "file.openIdml"] { + for name in ["Brochure.indd", "Brochure.INDT"] { + let mut s = Session::new(); + s.execute(command, &json!({"name": name, "base64": base64})).unwrap(); + assert_eq!(s.documents().len(), 1, "{command}: {name}"); + assert_eq!(s.doc().unwrap().doc.title, "Brochure", "{command}: {name}"); + } + } + } + /// A `.designcraft` file opened from bytes under its file name (a browser or host passes the /// name it has) is titled without the extension, as an IDML file is, so saving it gives /// `Brochure.designcraft` and not `Brochure.designcraft.designcraft`. @@ -433,6 +456,8 @@ mod open_bytes_tests { ("Brochure.DesignCraft", "Brochure"), ("Brochure", "Brochure"), ("Brochure.v2", "Brochure.v2"), + ("Brochure.indd", "Brochure.indd"), + ("Brochure.INDT", "Brochure.INDT"), ("Brochure.designcraft.designcraft", "Brochure.designcraft"), (".designcraft", ".designcraft"), // Names that aren't ASCII: cutting the extension's length off the end of the last one diff --git a/crates/engine/src/cmd/interchange.rs b/crates/engine/src/cmd/interchange.rs index 4ca22309c..0d0824f06 100644 --- a/crates/engine/src/cmd/interchange.rs +++ b/crates/engine/src/cmd/interchange.rs @@ -455,9 +455,11 @@ fn warning_item( } /// 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). +/// the bytes are neither a recognized DesignCraft document nor IDML. The converter explains +/// unrecognized data with an InDesign extension. pub(crate) fn is_indd(name: &str, bytes: &[u8]) -> bool { - bytes.starts_with(&indd::header::SIGNATURE) || has_indd_extension(name) + bytes.starts_with(&indd::header::SIGNATURE) + || (has_indd_extension(name) && !super::file::is_document(bytes) && !bytes.trim_ascii_start().starts_with(b"{")) } pub(crate) fn has_indd_extension(name: &str) -> bool { diff --git a/crates/ui-egui/src/app/files.rs b/crates/ui-egui/src/app/files.rs index 68155d884..cef32eae9 100644 --- a/crates/ui-egui/src/app/files.rs +++ b/crates/ui-egui/src/app/files.rs @@ -435,20 +435,28 @@ pub(crate) mod browser_import_tests { ("Brochure.designcraft", "Brochure"), ("Brochure.designcraft.designcraft", "Brochure.designcraft"), ("Brochure.v2.designcraft", "Brochure.v2"), + ("Brochure.indd", "Brochure.indd"), + ("Brochure.INDT", "Brochure.INDT"), ("日本.DESIGNCRAFT", "日本"), (".designcraft", ".designcraft"), ("Brochure", "Brochure"), ] { + let before = app.session.documents().len(); let request = app.import_request(purpose); deliver(&mut app, request, name, native.clone()); + assert_eq!(app.session.documents().len(), before + 1, "{purpose}: {name}"); assert_eq!(app.session.doc().unwrap().doc.title, title, "{purpose}: {name}"); } } let idml = designcraft_idml::export_idml(&app.session.doc().unwrap().doc); for purpose in ["open", "drop"] { - let request = app.import_request(purpose); - deliver(&mut app, request, "Brochure.idml", idml.clone()); - assert_eq!(app.session.doc().unwrap().doc.title, "Brochure", "{purpose}: IDML"); + for name in ["Brochure.idml", "Brochure.indd", "Brochure.INDT"] { + let before = app.session.documents().len(); + let request = app.import_request(purpose); + deliver(&mut app, request, name, idml.clone()); + assert_eq!(app.session.documents().len(), before + 1, "{purpose}: {name}"); + assert_eq!(app.session.doc().unwrap().doc.title, "Brochure", "{purpose}: {name}"); + } } } fn image() -> Vec {