diff --git a/ROADMAP.md b/ROADMAP.md index d60a6bf4..d1763905 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -347,6 +347,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **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. diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs index 75f28a33..20f8e7ad 100644 --- a/crates/idml/src/export.rs +++ b/crates/idml/src/export.rs @@ -1686,7 +1686,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 9fd519ef..40de8390 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -2120,7 +2120,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 0526070d..e3ebe041 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), 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 042a0c8d..c8588a3e 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -10,6 +10,7 @@ use super::*; mod assets; mod decorations; mod index_sort; +mod liquid_guides; fn zip_files(files: &[(&str, &str)]) -> Vec { use zip::write::SimpleFileOptions; diff --git a/crates/idml/src/tests/liquid_guides.rs b/crates/idml/src/tests/liquid_guides.rs new file mode 100644 index 00000000..f76ac1b0 --- /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 cc02a740..bd041105 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 (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) · **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 | @@ -107,5 +113,6 @@ for real work** (estimated). INDD alone moves this dimension by 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-10 | major | Created from InDesign 2026's declared document types and filters, `plan/indesign/08-file-formats.md` and the code |