diff --git a/ROADMAP.md b/ROADMAP.md index 5ac6c575e..2f96ab318 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -438,6 +438,7 @@ Newest first. - **2026-10-15** · The CJK leading model and character alignment take effect only in paragraphs set with a Japanese composer (`HL Composer J`, `HL Single J`), which IDML import and export keep: Western and Arabic paragraphs set baseline to baseline on the Roman baseline, a paragraph switched to a Japanese composer gets the file's Aki Below spacing, and under Aki Below a line's leading is the space below it. No readiness percentage change. - **2026-10-15** · Document pages follow their parent page's margins and columns unless margins were set on the page itself (`Page::local_margins`): applying a different parent gives a page that parent's margins, a page that changes side after an insert takes the other parent page's, and a page given margins equal to its parent's keeps them when the parent changes. IDML import and older files mark pages whose margins differ from their parent as their own. No readiness percentage change. - **2026-10-15** · Start-up picks the GPU that drives the display (or the kind `WGPU_POWER_PREF` or the adapter `WGPU_ADAPTER_NAME` names), and when a start-up fails or hangs it restarts on the next adapter, then the next backend, then a software renderer, keeping recovered documents; start-up errors are logged to standard error and the log file (#273, #334). No readiness percentage change. +- **2026-10-15** · IDML: liquid guides keep their type on document and parent pages instead of reopening as ordinary ruler guides. Positions, orientation, Fit to Page, locks and layers are preserved; object liquid constraints remain unmapped (#872). No readiness percentage change. - **2026-10-14** · The Type tool draws a new text frame over a locked empty frame, as InDesign does, instead of turning the locked frame into a text frame. No readiness percentage change. - **2026-10-14** · 100% black shows as pure black on screen by default, as InDesign's Appearance of Black does out of the box (Display All Blacks as Rich Black); Preferences › Appearance of Black › Display All Blacks Accurately still shows it as the dark grey it prints, and a saved choice is kept. No readiness percentage change. - **2026-10-14** · Pathfinder keeps the frontmost object, as InDesign does: a photo placed over shapes and intersected with them shows through the shapes (it was dropped, and the shapes kept their black fill); Add, Exclude Overlap and Minus Back take the front object's fill too, Subtract still keeps the back one. No readiness percentage change. @@ -465,7 +466,6 @@ Newest first. - **2026-10-14** · The Pages panel menu follows InDesign's order: Insert Pages…, Move Pages…, Duplicate Spread (and at End of Document), Delete Page/Spread, New Parent…, Apply Parent to Pages, Override All Parent Page Items, Parent Pages, Create Alternate Layout…, Numbering & Section Options…, Allow Selected Spread to Shuffle. No readiness percentage change. - **2026-10-14** · Pages panel: Alt-drag copies a page, a page dropped on Create new page is duplicated at the end, Edit page size offers the presets and Custom Page Size…; duplicated pages keep their own size, rule and guides; thumbnails of pages of their own size stay in their rows. No readiness percentage change. - **2026-10-12 (PDFW)** · PDF, booklet and book composition share guarded writing, metadata and font preparation through craft-pdf. Layout, shaping, placed graphics, colour policy, tagging and PDF/X postprocessing remain in DesignCraft. Failed native drawing returns an export error; failed writer cleanup cannot serialize an incomplete document. No readiness percentage change. - - **2026-10-13** · Copy with a placed graphic selected (Direct Selection) copies just the graphic, uncropped, where it sits on the page and without the frame's stroke, so Paste Into puts the whole image in place; Cut and Delete then remove only the graphic and leave its frame empty. Nested selections copy in their spread coordinates. No readiness percentage change. - **2026-10-13** · Object effects (Basic Feather, Inner Glow, shadows, glows, bevel, satin, directional and gradient feathers) apply to frames with pasted-in content, on screen and in image exports: the frame, its clipped content and its stroke are one object to the effect. PDF export still has no soft effects for any object. No readiness percentage change. - **2026-10-13** · Links rows show their source paths; selecting a link shows its full path wrapped inside the panel, so same-named files from different folders can be distinguished. No readiness percentage change. diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs index 4366833de..2a40a27d8 100644 --- a/crates/idml/src/export.rs +++ b/crates/idml/src/export.rs @@ -1840,7 +1840,7 @@ impl<'a> Ex<'a> { .attr("ViewThreshold", "5") .attr("Locked", bool_s(g.locked)) .attr("PageIndex", (first_page + i).to_string()) - .attr("GuideType", "Ruler"); + .attr("GuideType", if g.liquid { "Liquid" } else { "Ruler" }); if let Some(l) = g.layer { ge.set("ItemLayer", uid(l.0)); } diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index ffdaf9a40..d08c7c324 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -2451,7 +2451,7 @@ impl<'r> Importer<'r> { spread: g.get("FitToPage") == Some("false"), locked: g.get("Locked") == Some("true"), layer: g.get("ItemLayer").and_then(|l| self.layer_ids.get(l)).copied(), - liquid: false, + liquid: g.get("GuideType") == Some("Liquid"), }) }) .collect() diff --git a/crates/idml/src/lib.rs b/crates/idml/src/lib.rs index b939ec9d2..768ef730c 100644 --- a/crates/idml/src/lib.rs +++ b/crates/idml/src/lib.rs @@ -22,13 +22,13 @@ //! `Rectangle`, `Oval`, `Polygon`, `GraphicLine`, `Group`) with fill/stroke, corner options, text //! frame options (with column rules), text wrap, transparency (opacity, blend mode, drop shadow, //! feather, gradient feather, directional feather), images (embedded `Contents` or linked), stories with paragraph/character style ranges and local overrides, special -//! characters and breaks, and text threading. +//! characters and breaks, text threading, ruler and liquid guides, and stored page liquid rules. //! //! ## TODO (ignored on import / not written on export) //! - Tables, footnotes, anchored/inline objects, notes, tracked changes, hyperlinks, index and //! cross-reference markers, text variables, conditions, XML structure (`XML/*` parts). -//! - Guides, page-item overrides of parent items (only the page's `OverrideList` is read), -//! parent-of-parent page mapping beyond `AppliedMaster`, alternate layouts, liquid layout. +//! - Page-item overrides of parent items (only the page's `OverrideList` is read), +//! parent-of-parent page mapping beyond `AppliedMaster`, alternate layouts, liquid object constraints. //! - Nested/GREP/line styles, bullets & numbering details (bullet glyph, number format), OpenType //! feature flags, paragraph borders, grid alignment, custom dashed stroke styles; column rule //! stroke type and overprint. diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 866660820..a1c32f879 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -11,6 +11,7 @@ mod assets; mod clipping; mod decorations; mod index_sort; +mod liquid_guides; mod object_styles; mod rtl_binding; mod spread_rotation; diff --git a/crates/idml/src/tests/liquid_guides.rs b/crates/idml/src/tests/liquid_guides.rs new file mode 100644 index 000000000..f76ac1b04 --- /dev/null +++ b/crates/idml/src/tests/liquid_guides.rs @@ -0,0 +1,133 @@ +//! Literal guide types must survive independently of the stored page layout rule (#872). + +use super::*; +use designcraft_doc::{LiquidRule, Orientation}; +use std::sync::Arc; + +fn fixture(kind: Option<&str>, origin: &str) -> Vec { + let attribute = kind.map(|k| format!(r#" GuideType="{k}""#)).unwrap_or_default(); + let map = r#" + + + + +"#; + let prefs = format!( + r#" +"# + ); + let spread = |tag: &str, id: &str| { + let pages: String = [("left", -500), ("right", 0)] + .into_iter() + .map(|(side, x)| { + format!( + r#" + + +"# + ) + }) + .collect(); + format!( + r#" +<{tag} Self="{id}" NamePrefix="A" BaseName="Parent" PageCount="2">{pages} +"# + ) + }; + zip_files(&[ + ("designmap.xml", map), + ("Resources/Preferences.xml", &prefs), + ("MasterSpreads/A.xml", &spread("MasterSpread", "m")), + ("Spreads/S.xml", &spread("Spread", "s")), + ]) +} + +#[test] +fn liquid_guides_import_literal_types_without_changing_other_guide_fields() { + for origin in ["SpreadOrigin", "PageOrigin", "SpineOrigin"] { + let baseline = import_idml(&fixture(None, origin)).unwrap(); + for (kind, liquid) in [(Some("Liquid"), true), (Some("Ruler"), false), (None, false), (Some("liquid"), false), (Some("Unknown"), false)] { + let actual = import_idml(&fixture(kind, origin)).unwrap(); + let mut expected = baseline.clone(); + expected.created = actual.created; + for spread in expected.spreads.iter_mut().chain(&mut expected.parents) { + for page in &mut Arc::make_mut(spread).pages { + assert_eq!(page.guides.len(), 2); + page.guides[0].liquid = liquid; + } + } + assert_eq!(actual, expected, "{origin}, {kind:?}: only guide type may change"); + actual.check().unwrap(); + for spread in actual.spreads.iter().chain(&actual.parents) { + assert_eq!(spread.pages.len(), 2); + for (index, page) in spread.pages.iter().enumerate() { + assert_eq!(page.liquid, LiquidRule::GuideBased); + let [vertical, horizontal] = page.guides.as_slice() else { panic!("two guides expected") }; + let x = match origin { + "PageOrigin" => 72.0 + index as f64 * 500.0, + "SpineOrigin" => 572.0, + _ => 72.0, + }; + assert_eq!((vertical.orientation, vertical.position, vertical.spread, vertical.locked), (Orientation::Vertical, x, false, true)); + assert_eq!( + (horizontal.orientation, horizontal.position, horizontal.spread, horizontal.locked), + (Orientation::Horizontal, 90.0, true, false) + ); + assert!(!horizontal.liquid); + assert_eq!(actual.layer(vertical.layer.unwrap()).unwrap().name, "Guides"); + } + } + } + } +} + +#[test] +fn liquid_guides_export_literal_types_and_round_trip_on_pages_and_parents() { + use std::io::Read; + + let mut source = import_idml(&fixture(None, "PageOrigin")).unwrap(); + // Build the mixed native flags directly, without relying on the liquid-guide importer. + for spread in source.spreads.iter_mut().chain(&mut source.parents) { + for (index, page) in Arc::make_mut(spread).pages.iter_mut().enumerate() { + page.guides[index].liquid = true; + if index == 1 { + page.liquid = LiquidRule::Off; + } + } + } + source.check().unwrap(); + let bytes = export_idml(&source); + let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap(); + for (directory, tag, spread) in [("Spreads", "Spread", &source.spreads[0]), ("MasterSpreads", "MasterSpread", &source.parents[0])] { + let path = format!("{directory}/{tag}_u{:x}.xml", spread.id.0); + let mut part = Vec::new(); + archive.by_name(&path).unwrap().read_to_end(&mut part).unwrap(); + let root = xml::parse(&part).unwrap(); + let pages: Vec<_> = root.find(tag).unwrap().find_all("Page").collect(); + assert_eq!(pages.len(), spread.pages.len()); + for (xml_page, page) in pages.iter().zip(&spread.pages) { + let guides: Vec<_> = xml_page.find_all("Guide").collect(); + assert_eq!(guides.len(), page.guides.len()); + for (guide, expected) in guides.iter().zip(&page.guides) { + assert_eq!(guide.get("GuideType"), Some(if expected.liquid { "Liquid" } else { "Ruler" }), "{path}"); + } + } + } + let actual = import_idml(&bytes).unwrap(); + actual.check().unwrap(); + assert_eq!((actual.spreads.len(), actual.parents.len()), (1, 1)); + for (got, want) in actual.spreads.iter().chain(&actual.parents).zip(source.spreads.iter().chain(&source.parents)) { + assert_eq!(got.pages.len(), want.pages.len()); + for (got, want) in got.pages.iter().zip(&want.pages) { + assert_eq!(got.liquid, want.liquid); + assert_eq!(got.guides.len(), want.guides.len()); + for (got, want) in got.guides.iter().zip(&want.guides) { + assert_eq!( + (got.orientation, got.position, got.spread, got.locked, got.liquid), + (want.orientation, want.position, want.spread, want.locked, want.liquid) + ); + assert_eq!(actual.layer(got.layer.unwrap()).unwrap().name, source.layer(want.layer.unwrap()).unwrap().name); + } + } + } +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 356b2d546..507358dec 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-13 · **Change:** minor (.indd and .indt open through indd-utils; XLSX cached values preserve numeric precision and text; shared print spooler; Windows CI passes; content-indexed asset deduplication, #727) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (IDML liquid guide types, #872; .indd and .indt open through indd-utils; XLSX cached values preserve numeric precision and text; shared print spooler; Windows CI passes; content-indexed asset deduplication, #727) · **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`, @@ -31,6 +31,12 @@ score below). | PageMaker | `.pmd`, `.p65`, `.t65`, `.pmt` | read | ✗ | — | | <1% | | QuarkXPress | `.qxd`, `.qxt` | read (old versions) | ✗ (#73) | — | | <1% | +IDML preserves stored page liquid rules and distinguishes liquid guides from ordinary ruler +guides on document and parent pages. Guide positions, orientation, Fit to Page, lock state and +layer survive interchange; literal XML and round-trip tests are in +`crates/idml/src/tests/liquid_guides.rs`. Object liquid pins and resize constraints remain unmapped +(#872); this is not full liquid-layout interchange support. + ## Output | Format | Ext | InDesign | DesignCraft | Fidelity / tests | Weight | @@ -108,6 +114,7 @@ up to 30 points. | Date | Change | Summary | |---|---|---| +| 2026-10-15 | minor | IDML retains liquid versus ruler guide types on document and parent pages, with literal XML and mixed-guide round-trip coverage; object liquid constraints remain unsupported (#872) | | 2026-10-13 | minor | Asset loading and compaction use content fingerprints with exact equality checks; tests cover equal-sized distinct buffers, shared copies, independent links and missing data (#727) | | 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-12 | minor | XLSX table placement and data merge preserve small numeric values and cached formula strings; synthetic regressions in `textimport::xlsx::tests`. Scores unchanged. |