Integration: IDML import warns when it repairs a tab list

#343's TabStop::sanitized_list repairs a file's tab list (dropped, clamped, shortened or
duplicate stops, more than MAX_TAB_STOPS); fix/idml-missing-spread's import_idml_report
carries the import warnings. The import counts the lists it had to repair and reports
them in one warning; sorting and a char stop's default '.' don't count.
This commit is contained in:
aiongg
2026-10-10 16:48:01 +00:00
parent 2589699057
commit b98d54ee38
2 changed files with 45 additions and 2 deletions
+30 -1
View File
@@ -112,6 +112,15 @@ pub fn import_idml_report(bytes: &[u8], read_link: &dyn Fn(&str) -> Option<Vec<u
im.modified = xmp_date(&m, "xmp:ModifyDate");
}
im.run(&root, &top)?;
if im.repaired_tab_lists > 0 {
warnings.push(format!(
"tab lists repaired to fit the limits (at most {} stops, finite positions up to {} in, leaders of at most {} characters, one stop per position): {}",
designcraft_doc::MAX_TAB_STOPS,
designcraft_doc::MAX_TAB_POSITION / 72.0,
designcraft_doc::MAX_TAB_LEADER,
im.repaired_tab_lists,
));
}
let document = im.finish(&root).map_err(|e| match e {
// Without its spread parts there is nothing to open: say which parts were missing.
IdmlError::Invalid(msg) if !warnings.is_empty() => IdmlError::Invalid(format!("{msg} ({})", warnings.join("; "))),
@@ -121,6 +130,19 @@ pub fn import_idml_report(bytes: &[u8], read_link: &dyn Fn(&str) -> Option<Vec<u
Ok(Imported { document, warnings })
}
/// Whether sanitizing a file's tab list dropped, moved or shortened a stop. Sorting, and the `.`
/// a char stop with no character aligns on, change nothing.
fn tab_list_repaired(read: &[TabStop], sanitized: &[TabStop]) -> bool {
let kept = |t: &TabStop| {
sanitized.iter().any(|o| {
o.position == t.position
&& o.leader == t.leader
&& (o.align_on == t.align_on || (t.align_on.is_empty() && o.align == designcraft_doc::TabAlign::Char))
})
};
read.len() != sanitized.len() || !read.iter().all(kept)
}
/// A package part name in the form the lookup compares: InDesign and other IDML writers differ
/// in percent-encoding, case, path separators and a leading `./` or `/`.
fn normalize_part_name(name: &str) -> String {
@@ -233,6 +255,8 @@ struct Importer<'r> {
/// Creation and modification times (Unix seconds) from the package metadata.
created: Option<i64>,
modified: Option<i64>,
/// Tab lists [`TabStop::sanitized_list`] had to repair (an import warning).
repaired_tab_lists: usize,
}
fn lab_to_color(l: f32, a: f32, b: f32) -> Color {
@@ -374,6 +398,7 @@ impl<'r> Importer<'r> {
text_var_ids: HashMap::new(),
created: None,
modified: None,
repaired_tab_lists: 0,
}
}
@@ -1593,7 +1618,11 @@ impl<'r> Importer<'r> {
align_on: get("AlignmentCharacter").filter(|c| c != ".").unwrap_or_default(),
});
}
a.tabs = Some(TabStop::sanitized_list(tabs));
let sanitized = TabStop::sanitized_list(tabs.clone());
if tab_list_repaired(&tabs, &sanitized) {
self.repaired_tab_lists = self.repaired_tab_lists.saturating_add(1);
}
a.tabs = Some(sanitized);
}
a.list_type = e.prop("BulletsAndNumberingListType").map(|v| match v.trim() {
"BulletList" => ListType::Bullets,
+15 -1
View File
@@ -2626,10 +2626,24 @@ fn hostile_tab_lists_import_within_the_limits() {
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>Many tabs</Content></CharacterStyleRange>
</ParagraphStyleRange></Story></idPkg:Story>"#
);
let doc = import_idml(&fixture_with_story(&story)).unwrap();
let report = import_idml_report(&fixture_with_story(&story), &|_| None).unwrap();
assert!(report.warnings.iter().any(|w| w.starts_with("tab lists repaired to fit the limits") && w.ends_with("): 1")), "{:?}", report.warnings);
let doc = report.document;
let tabs = doc.stories.values().find(|s| s.text.contains("Many tabs")).unwrap().paras[0].para.tabs.clone().unwrap();
assert_eq!(tabs.len(), designcraft_doc::MAX_TAB_STOPS);
assert!(tabs.iter().all(|t| t.position.is_finite() && t.leader.chars().count() <= designcraft_doc::MAX_TAB_LEADER));
assert!(tabs.windows(2).all(|w| w[0].position < w[1].position), "sorted");
assert_eq!((tabs[0].position, tabs[99].position), (0.0, 297.0), "the first stops by position");
// A list within the limits, unsorted and with a char stop on `.`, imports without a warning.
let fine = format!(
r#"<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging"><Story Self="s1">
<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/[No paragraph style]">
<Properties><TabList type="list">{}{}<ListItem type="record"><Alignment type="enumeration">CharacterAlign</Alignment><AlignmentCharacter type="string">.</AlignmentCharacter><Leader type="string"></Leader><Position type="unit">40</Position></ListItem></TabList></Properties>
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>Few tabs</Content></CharacterStyleRange>
</ParagraphStyleRange></Story></idPkg:Story>"#,
item("90", "."),
item("12", "")
);
let report = import_idml_report(&fixture_with_story(&fine), &|_| None).unwrap();
assert!(report.warnings.is_empty(), "{:?}", report.warnings);
}