From f0e63b333dd0ff85f6c498d5fa1c31e127cb223e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 10:01:16 -0800 Subject: [PATCH] IDML: a style without a category flag takes it from its based-on style An object style that gives neither EnableStrokeAndCornerOptions nor corners of its own takes the category from the style it is based on, and one without EnableTextFrameGeneralOptions does the same for its text frame options. A style based on [None] still sets neither, and [None] keeps the corners it sets itself. --- crates/idml/src/import/styles.rs | 40 +++++++++++++++++--------- crates/idml/src/tests/object_styles.rs | 37 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/crates/idml/src/import/styles.rs b/crates/idml/src/import/styles.rs index e2e11e979..023ab2d40 100644 --- a/crates/idml/src/import/styles.rs +++ b/crates/idml/src/import/styles.rs @@ -199,8 +199,9 @@ impl<'r> Importer<'r> { .flatten(); let levels = based_on_levels(e, &objects_by_self); // Text Frame General Options: each attribute from the nearest style whose - // `TextFramePreference` sets it, among the styles that don't turn the category off. - let text_frame = if e.get("EnableTextFrameGeneralOptions") == Some("true") { + // `TextFramePreference` sets it, among the styles that don't turn the category off. A + // style without the category flag takes it from the style it is based on. + let text_frame = if category_on(&levels, "EnableTextFrameGeneralOptions", |_| false) { let prefs: Vec<&El> = levels .iter() .filter(|l| l.get("EnableTextFrameGeneralOptions") != Some("false")) @@ -211,7 +212,7 @@ impl<'r> Importer<'r> { None }; let effects = self.style_effects(e); - let corners = style_corners(e, &levels); + let corners = style_corners(&levels); let apply_next_style = paragraph_style.is_some() && e.get("ApplyNextParagraphStyle") == Some("true"); let s = ObjectStyle { name: name.clone(), based_on, fill, stroke, paragraph_style, apply_next_style, text_frame, effects, corners }; match self.styles.object.iter_mut().find(|o| o.name == name) { @@ -412,21 +413,34 @@ impl<'r> Importer<'r> { } } -/// Object style `e`'s corner options (Stroke & Corner Options), by name, when it sets them. A style -/// that sets some corners, or turns the category on, takes the others from the styles in `levels` -/// (`e` and the styles it is based on) that don't turn the category off. -fn style_corners(e: &El, levels: &[&El]) -> Option { - match e.get("EnableStrokeAndCornerOptions") { - Some("false") => return None, - Some("true") => {} - _ => { - corners(e, "")?; - } +/// The corner options (Stroke & Corner Options) of the object style that starts `levels` (the +/// style, then the styles it is based on), by name, when it sets them. A style that sets some +/// corners, or turns the category on (itself or through the style it is based on), takes the +/// others from the styles in `levels` that don't turn the category off. +fn style_corners(levels: &[&El]) -> Option { + if !category_on(levels, "EnableStrokeAndCornerOptions", |l| corners(l, "").is_some()) { + return None; } let on: Vec<&El> = levels.iter().copied().filter(|l| l.get("EnableStrokeAndCornerOptions") != Some("false")).collect(); corners_in(&on, "").map(|c| CornerOptions { corners: c }) } +/// Whether the style chain `levels` (a style, then the styles it is based on) turns the category +/// whose flag is `flag` on. The nearest style that gives the flag decides; a style without it +/// turns the category on when `own` finds settings of its own. A `[None]` after the first style +/// ends the chain, as a `BasedOn` of `[None]` names no style. +fn category_on(levels: &[&El], flag: &str, own: impl Fn(&El) -> bool) -> bool { + let none = |l: &&El| l.get("Self") == Some("ObjectStyle/$ID/[None]"); + for l in levels.iter().take(1).chain(levels.iter().skip(1).take_while(|l| !none(l))) { + match l.get(flag) { + Some(v) => return v == "true", + None if own(l) => return true, + None => {} + } + } + false +} + /// Object style `e`, then the styles it is based on, nearest first (cycle-safe, at most 16). A /// `BasedOn` may name a style by its `Self` id or by its bare name. fn based_on_levels<'e>(e: &'e El, objects_by_self: &HashMap<&str, &'e El>) -> Vec<&'e El> { diff --git a/crates/idml/src/tests/object_styles.rs b/crates/idml/src/tests/object_styles.rs index 6226d4a74..52cfd3e20 100644 --- a/crates/idml/src/tests/object_styles.rs +++ b/crates/idml/src/tests/object_styles.rs @@ -363,6 +363,43 @@ fn a_style_turning_corners_on_inherits_all_four() { assert_eq!((c.map(|c| c.shape), c.map(|c| c.size)), ([CornerShape::Rounded; 4], [17.0; 4])); } +/// A style with no Stroke & Corner Options or Text Frame General Options flag and no settings of +/// its own takes each category from the style it is based on: on, off, or (for a style based on +/// `[None]`) not set. +#[test] +fn a_style_without_category_flags_takes_them_from_its_based_on_style() { + let styles = r#" + $ID/[None] + + + + ObjectStyle/Round + + + + ObjectStyle/Round + + + ObjectStyle/Framed + + + ObjectStyle/Off + + + $ID/[None] + "#; + let d = fixture(&format!("{ROUND}{styles}"), &[]); + let style = |n: &str| d.styles.object_style(n).cloned().unwrap(); + let c = style("RoundKid").corners.expect("RoundKid inherits Round's corners").corners; + assert_eq!((c.map(|c| c.shape), c.map(|c| c.size)), ([CornerShape::Rounded; 4], [17.0; 4])); + assert_eq!(style("FramedKid").text_frame.map(|t| t.columns), Some(3), "FramedKid inherits Framed's text frame options"); + for n in ["OffKid", "Bare"] { + assert!(style(n).corners.is_none() && style(n).text_frame.is_none(), "{n}: {:?}", style(n)); + } + assert!(style(designcraft_doc::NO_OBJECT_STYLE).corners.is_some(), "[None] keeps the corners it sets itself"); +} + /// An object style giving its corners only with the legacy all-corners `CornerOption` and /// `CornerRadius` keeps them, so applying it to another frame rounds it. #[test]