# Conflicts:
#	crates/idml/src/export.rs
#	crates/idml/src/tests.rs
This commit is contained in:
Brandon Thomas
2026-10-14 22:44:44 -04:00
4 changed files with 183 additions and 1 deletions
+1
View File
@@ -1667,6 +1667,7 @@ impl<'a> Ex<'a> {
.attr("Self", uid(pg.id.0))
.attr("Name", name)
.attr("AppliedMaster", applied.map(|s| uid(s.0)).unwrap_or_else(|| "n".into()))
.attr("LayoutRule", names::liquid_rule_out(pg.liquid))
.attr("OverrideList", overrides.join(" "))
.attr("GeometricBounds", format!("0 0 {} {}", num(pg.height), num(pg.width)))
.attr("ItemTransform", format!("1 0 0 1 {} {}", num(pg.x - ox), num(-oy)))
+1 -1
View File
@@ -2168,7 +2168,7 @@ impl<'r> Importer<'r> {
overridden: vec![],
guides: self.guides_of(p, Rect::new(r.x0 - min_x, r.y0 - min_y, r.x1 - min_x, r.y1 - min_y), spine),
show_parent_items: show,
liquid: Default::default(),
liquid: names::liquid_rule_in(p.get("LayoutRule").unwrap_or("Off")),
transition: None,
view_rotation: 0,
show_parent_overlay: false,
+25
View File
@@ -74,6 +74,31 @@ pub fn fitting_in(s: &str) -> designcraft_doc::Fitting {
}
}
pub fn liquid_rule_out(rule: designcraft_doc::LiquidRule) -> &'static str {
use designcraft_doc::LiquidRule as R;
match rule {
R::Off => "Off",
R::Scale => "Scale",
R::ReCenter => "Recenter",
R::GuideBased => "GuideBased",
R::ObjectBased => "ObjectBased",
R::ControlledByParent => "UseMaster",
}
}
pub fn liquid_rule_in(s: &str) -> designcraft_doc::LiquidRule {
use designcraft_doc::LiquidRule as R;
match s {
"Off" => R::Off,
"Scale" => R::Scale,
"Recenter" => R::ReCenter,
"GuideBased" => R::GuideBased,
"ObjectBased" => R::ObjectBased,
"UseMaster" => R::ControlledByParent,
_ => R::Off,
}
}
pub fn escape_id(s: &str) -> String {
s.replace('%', "%25").replace(':', "%3a")
}
+156
View File
@@ -2387,3 +2387,159 @@ fn the_default_cjk_leading_model_applies_only_with_a_japanese_composer() {
assert_eq!(model("$ID/HL Composer J"), Some(designcraft_doc::cjk::LeadingModel::AkiBelow));
assert_eq!(model("$ID/HL Single J"), Some(designcraft_doc::cjk::LeadingModel::AkiBelow));
}
mod liquid_page_rules {
use super::*;
use designcraft_doc::LiquidRule as R;
use std::sync::Arc;
// Literal XML expectations, independent of names.rs and serde spellings.
const CASES: [(&str, R); 6] = [
("Scale", R::Scale),
("Off", R::Off),
("Recenter", R::ReCenter),
("GuideBased", R::GuideBased),
("ObjectBased", R::ObjectBased),
("UseMaster", R::ControlledByParent),
];
fn fixture(rule: Option<&str>, parent_rule: Option<&str>) -> Vec<u8> {
let attribute = |value: Option<&str>| value.map(|v| format!(r#" LayoutRule="{v}""#)).unwrap_or_default();
let (rule, parent_rule) = (attribute(rule), attribute(parent_rule));
let map = r#"<Document xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" Self="d">
<idPkg:Preferences src="Resources/Preferences.xml"/>
<idPkg:MasterSpread src="MasterSpreads/A.xml"/>
<idPkg:MasterSpread src="MasterSpreads/B.xml"/>
<idPkg:Spread src="Spreads/S.xml"/>
</Document>"#;
let parent = format!(
r#"<idPkg:MasterSpread xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
<MasterSpread Self="ma" NamePrefix="A" BaseName="Base" PageCount="2">
<Page Self="a1" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 -500 -350"/>
<Page Self="a2"{parent_rule} GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 0 -350"/>
</MasterSpread>
</idPkg:MasterSpread>"#
);
// Based-on belongs to the whole derived master: both pages apply the same master.
let derived = format!(
r#"<idPkg:MasterSpread xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
<MasterSpread Self="mb" NamePrefix="B" BaseName="Derived" PageCount="2">
<Page Self="b1"{rule} AppliedMaster="ma" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 -500 -350"/>
<Page Self="b2" AppliedMaster="ma" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 0 -350"/>
</MasterSpread>
</idPkg:MasterSpread>"#
);
let spread = format!(
r#"<idPkg:Spread xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
<Spread Self="s" PageCount="2">
<Page Self="p1" AppliedMaster="ma" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 -500 -350">
<MarginPreference ColumnCount="3" ColumnGutter="12" Top="10" Bottom="20" Left="30" Right="40"/>
</Page>
<Page Self="p2"{rule} AppliedMaster="ma" GeometricBounds="0 0 700 500" ItemTransform="1 0 0 1 0 -350"/>
</Spread>
</idPkg:Spread>"#
);
zip_files(&[
("designmap.xml", map),
("Resources/Preferences.xml", PREFS),
("MasterSpreads/A.xml", &parent),
("MasterSpreads/B.xml", &derived),
("Spreads/S.xml", &spread),
])
}
#[test]
fn import_literal_xml() {
let baseline = import_idml(&fixture(None, None)).unwrap();
assert_eq!((baseline.spreads.len(), baseline.parents.len()), (1, 2));
assert!(baseline.spreads.iter().chain(&baseline.parents).all(|sp| sp.pages.len() == 2));
assert!(baseline.spreads.iter().chain(&baseline.parents).flat_map(|sp| &sp.pages).all(|pg| pg.liquid == R::Off));
let cases = CASES.into_iter().map(|(literal, rule)| (Some(literal), rule)).chain([
(None, R::Off),
(Some("ReCenter"), R::Off),
(Some("controlledByParent"), R::Off),
(Some("PreserveExisting"), R::Off),
]);
for (literal, rule) in cases {
let actual = import_idml(&fixture(literal, Some("Recenter"))).unwrap();
assert_eq!(actual.spreads[0].pages[1].liquid, rule, "ordinary Page: {literal:?}");
assert_eq!(actual.parents[1].pages[0].liquid, rule, "master Page: {literal:?}");
// Same XML without rule attributes supplies the complete allowed-delta oracle.
let mut expected = baseline.clone();
Arc::make_mut(&mut expected.spreads[0]).pages[1].liquid = rule;
Arc::make_mut(&mut expected.parents[1]).pages[0].liquid = rule;
Arc::make_mut(&mut expected.parents[0]).pages[1].liquid = R::ReCenter;
// Import assigns a fresh creation timestamp; all other document fields are compared.
expected.created = actual.created;
assert_eq!(actual, expected, "{literal:?}: only the three explicit rule fields may change apart from the import timestamp");
assert_eq!(actual.spreads[0].pages[0].liquid, R::Off, "omitted does not infer inheritance");
assert_eq!(actual.spreads[0].pages[0].parent, Some(actual.parents[0].id));
assert_eq!(actual.parents[1].parent.as_ref().unwrap().based_on, Some(actual.parents[0].id));
assert!(actual.parents.iter().flat_map(|sp| &sp.pages).all(|pg| pg.parent.is_none()));
if rule == R::ControlledByParent {
// The right document page resolves to A's right page, not B's controlled page.
assert_eq!(actual.parent_page_for(1), Some((0, 1)));
assert_eq!(actual.parents[0].pages[1].liquid, R::ReCenter);
}
}
}
#[test]
fn export_and_round_trip() {
use std::io::Read;
let baseline = import_idml(&fixture(None, None)).unwrap();
for (literal, rule) in CASES {
// Set native enums directly; the literal importer under test does not build this input.
let mut source = baseline.clone();
let pages = &mut Arc::make_mut(&mut source.spreads[0]).pages;
pages[0].parent = None;
pages[0].liquid = R::ControlledByParent;
pages[1].liquid = rule;
Arc::make_mut(&mut source.parents[0]).pages[1].liquid = R::ReCenter;
Arc::make_mut(&mut source.parents[1]).pages[0].liquid = rule;
source.check().unwrap();
let bytes = export_idml(&source);
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
for (directory, tag, spread, rules) in [
("Spreads", "Spread", &source.spreads[0], ["UseMaster", literal]),
("MasterSpreads", "MasterSpread", &source.parents[0], ["Off", "Recenter"]),
("MasterSpreads", "MasterSpread", &source.parents[1], [literal, "Off"]),
] {
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 body = root.find(tag).unwrap();
let pages: Vec<_> = body.find_all("Page").collect();
assert_eq!(pages.len(), spread.pages.len(), "{path}");
for (page, expected) in spread.pages.iter().zip(rules) {
let id = format!("u{:x}", page.id.0);
let matches: Vec<_> = pages.iter().filter(|p| p.get("Self") == Some(id.as_str())).collect();
assert_eq!(matches.len(), 1, "{path}: {id}");
assert_eq!(matches[0].get("LayoutRule"), Some(expected), "{path}: {id}");
}
}
let actual = import_idml(&bytes).unwrap();
assert_eq!((actual.spreads.len(), actual.parents.len()), (1, 2));
assert_eq!(actual.spreads[0].pages[0].parent, None);
assert_eq!(actual.spreads[0].pages[1].parent, Some(actual.parents[0].id));
assert_eq!(actual.parent_page_for(1), Some((0, 1)));
assert!(actual.parents.iter().flat_map(|sp| &sp.pages).all(|pg| pg.parent.is_none()));
for (index, prefix, name, based_on) in [(0, "A", "Base", None), (1, "B", "Derived", Some(actual.parents[0].id))] {
let info = actual.parents[index].parent.as_ref().unwrap();
assert_eq!((&info.prefix[..], &info.name[..], info.based_on), (prefix, name, based_on));
}
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) {
// IDs are allocated afresh; parent associations were checked explicitly above.
let mut normalized = got.clone();
normalized.id = want.id;
normalized.parent = want.parent;
assert_eq!(&normalized, want, "{literal}: all other Page fields, including the stored rule");
}
}
}
}
}