mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
IDML872.1: preserve liquid guides through interchange
Keep GuideType=Liquid on document and parent pages, with literal XML and mixed-guide regression coverage. Leave object liquid constraints separate. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -10,6 +10,7 @@ use super::*;
|
||||
mod assets;
|
||||
mod decorations;
|
||||
mod index_sort;
|
||||
mod liquid_guides;
|
||||
|
||||
fn zip_files(files: &[(&str, &str)]) -> Vec<u8> {
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
@@ -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<u8> {
|
||||
let attribute = kind.map(|k| format!(r#" GuideType="{k}""#)).unwrap_or_default();
|
||||
let map = r#"<Document xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" Self="d">
|
||||
<idPkg:Preferences src="Resources/Preferences.xml"/>
|
||||
<Layer Self="L1" Name="Guides" Visible="true" Locked="false"/>
|
||||
<idPkg:MasterSpread src="MasterSpreads/A.xml"/>
|
||||
<idPkg:Spread src="Spreads/S.xml"/>
|
||||
</Document>"#;
|
||||
let prefs = format!(
|
||||
r#"<Preferences><DocumentPreference PageWidth="500" PageHeight="700" FacingPages="true"/>
|
||||
<ViewPreference RulerOrigin="{origin}"/></Preferences>"#
|
||||
);
|
||||
let spread = |tag: &str, id: &str| {
|
||||
let pages: String = [("left", -500), ("right", 0)]
|
||||
.into_iter()
|
||||
.map(|(side, x)| {
|
||||
format!(
|
||||
r#"<Page Self="{id}-{side}" LayoutRule="GuideBased" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 {x} -350">
|
||||
<Guide Self="{id}-{side}-v" Orientation="Vertical" Location="72" FitToPage="true" Locked="true" ItemLayer="L1"{attribute}/>
|
||||
<Guide Self="{id}-{side}-h" Orientation="Horizontal" Location="90" FitToPage="false" Locked="false" ItemLayer="L1" GuideType="Ruler"/>
|
||||
</Page>"#
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
format!(
|
||||
r#"<idPkg:{tag} xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
|
||||
<{tag} Self="{id}" NamePrefix="A" BaseName="Parent" PageCount="2">{pages}</{tag}>
|
||||
</idPkg:{tag}>"#
|
||||
)
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user