From 0a836edb7df62023d79d970ed2d4883352372e7e Mon Sep 17 00:00:00 2001 From: "markus.doebele" Date: Thu, 15 Oct 2026 11:35:39 +0200 Subject: [PATCH] fix: IDML links in a subfolder beside the package load on another machine A package opened away from its author's machine kept image links like `/images/logo.png` that no longer exist; only the package folder and its Links/ folder were searched, so images in `images/` next to the .idml showed as missing. Also try the link's trailing folders (up to 8) under the package folder, longest first. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/engine/src/cmd/interchange.rs | 29 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index d60a6bf49..8144ffc1e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -360,6 +360,7 @@ Newest first. - **2026-10-14** · The Pathfinder panel is laid out as InDesign's: Paths (Join, Open, Close, Reverse), Pathfinder, Convert Shape and Convert Point, each a row of icon buttons. Join Path closes an open path or joins two at their nearest ends (`path.join`), and Convert Point makes the selected points plain, corner, smooth or symmetrical (`path.convertPoints`). No readiness percentage change. - **2026-10-12** · Linked text files (#597): placed Word, RTF and text files can stay linked; the Links panel shows them OK / Modified / Missing, and Update imports the file again into the same frames with the saved style map (the Links panel asks before replacing edits made in the layout). No readiness percentage change. - **2026-10-15** · IDML: the CJK leading model InDesign writes on [No paragraph style] of every document (Aki Below) only applies when the document uses a Japanese composer, as in InDesign; a Western document's paragraphs after a size change no longer sit lower than in InDesign (Montilla Design's Flex Layout file: its captions came out overset). No readiness percentage change. +- **2026-10-15** · IDML: linked images in a subfolder beside the package load when the package is opened on another machine (a link to `/images/logo.png` finds `images/logo.png` next to the .idml); they showed as missing. No readiness percentage change. - **2026-10-14** · Type › Fill with Placeholder Text fills a frame with paragraphs of a few sentences, as InDesign does, so one paragraph can be styled on its own (it was one long paragraph, and the filler ran sentences together, "interit.Ovid"). No readiness percentage change. - **2026-10-13** · Object › Fitting has InDesign's shortcuts: Fill Frame Proportionally ⌘⌥⇧C, Fit Content Proportionally ⌘⌥⇧E, Fit Frame to Content ⌘⌥C, Fit Content to Frame ⌘⌥E, Center Content ⌘⇧E. No readiness percentage change. - **2026-10-13** · Gradients between process colours blend as inks, as InDesign shows them: magenta to cyan passes through C 50 M 50 (a light lilac) on screen instead of the darker RGB mix, and a midpoint between CMYK or grey stops no longer turns the PDF shading RGB. No readiness percentage change. diff --git a/crates/engine/src/cmd/interchange.rs b/crates/engine/src/cmd/interchange.rs index 9b9520662..3718dfc70 100644 --- a/crates/engine/src/cmd/interchange.rs +++ b/crates/engine/src/cmd/interchange.rs @@ -80,6 +80,10 @@ pub fn import_report(bytes: &[u8], dir: Option<&std::path::Path>) -> Result<(Doc #[cfg(not(target_arch = "wasm32"))] const MAX_LINK_PATH_BYTES: usize = 128 * 1024; +/// How many trailing folders of a link are tried beside the opened package. +#[cfg(not(target_arch = "wasm32"))] +const MAX_LINK_SUFFIX_PARTS: usize = 8; + #[cfg(not(target_arch = "wasm32"))] fn packaged_link_candidates(link: &str, dir: Option<&std::path::Path>) -> Vec { // Enough for a maximum-length Windows path even with four-byte UTF-8 characters. @@ -103,6 +107,14 @@ fn packaged_link_candidates(link: &str, dir: Option<&std::path::Path>) -> Vec())); } } + // A package moved off its author's machine keeps its own subfolders (`/images/logo.png` + // beside the .idml as `images/logo.png`): try the link's trailing folders, longest first. + let first = parts.len().saturating_sub(MAX_LINK_SUFFIX_PARTS); + for start in first..parts.len().saturating_sub(1) { + if let Some(suffix) = parts.get(start..).filter(|s| s.iter().all(safe_part)) { + candidates.push(dir.join(suffix.iter().collect::())); + } + } if let Some(name) = parts.last().filter(|part| safe_part(part)) { candidates.push(dir.join(name)); candidates.push(dir.join("Links").join(name)); @@ -208,6 +220,23 @@ mod tests { std::fs::remove_dir_all(dir).unwrap(); } + #[test] + fn opens_links_from_a_subfolder_beside_the_package() { + let dir = std::env::temp_dir().join(format!("dc-idml-subfolder-links-{}", std::process::id())); + let path = dir.join("images").join("logo.png"); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + // The author's absolute `/images/…` does not exist here; `images/` beside the .idml does. + let (idml, png) = linked_idml("/images/logo.png", false).unwrap(); + std::fs::write(&path, &png).unwrap(); + + let doc = import(&idml, Some(&dir)).unwrap(); + let asset = doc.assets.values().next().unwrap(); + assert_eq!(*asset.data, png); + assert_eq!(asset.link.as_deref(), std::path::absolute(&path).unwrap().to_str()); + assert_eq!(super::super::links::status(asset), "ok"); + std::fs::remove_dir_all(dir).unwrap(); + } + #[test] fn remembers_flat_packaged_link_location() { let dir = std::env::temp_dir().join(format!("dc-idml-flat-links-{}", std::process::id()));