From a92db95301c4239ab98e13f0938424d909df2b1b Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 08:56:24 -0500 Subject: [PATCH] Delegate IDML object style merging to craft-idml --- Cargo.lock | 18 +-- Cargo.toml | 4 +- crates/idml/src/import.rs | 2 +- crates/idml/src/import/item.rs | 9 +- crates/idml/src/import/object_style.rs | 163 +++---------------------- crates/idml/src/import/story.rs | 3 +- 6 files changed, 40 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66b6116cd..eb45ed884 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1242,10 +1242,10 @@ dependencies = [ [[package]] name = "craft-idml" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=7aca8fe26#7aca8fe26798806266fa09187908e50a614ff303" +source = "git+https://github.com/storytold/craft-libs?rev=4f6ae3aa2#4f6ae3aa2d2c89361c09b735de55ed9c976527c3" dependencies = [ - "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=7aca8fe26)", - "craft-zip 0.1.0 (git+https://github.com/storytold/craft-libs?rev=7aca8fe26)", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=4f6ae3aa2)", + "craft-zip 0.1.0 (git+https://github.com/storytold/craft-libs?rev=4f6ae3aa2)", "thiserror 2.0.21", ] @@ -1656,7 +1656,7 @@ dependencies = [ [[package]] name = "craft-xml" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=7aca8fe26#7aca8fe26798806266fa09187908e50a614ff303" +source = "git+https://github.com/storytold/craft-libs?rev=4f6ae3aa2#4f6ae3aa2d2c89361c09b735de55ed9c976527c3" dependencies = [ "quick-xml 0.41.0", "thiserror 2.0.21", @@ -1720,7 +1720,7 @@ dependencies = [ [[package]] name = "craft-zip" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=7aca8fe26#7aca8fe26798806266fa09187908e50a614ff303" +source = "git+https://github.com/storytold/craft-libs?rev=4f6ae3aa2#4f6ae3aa2d2c89361c09b735de55ed9c976527c3" dependencies = [ "flate2", "thiserror 2.0.21", @@ -2021,7 +2021,7 @@ dependencies = [ "craft-prepress", "craft-print", "craft-svg 0.1.0 (git+https://github.com/storytold/craft-libs?rev=cc23ffd4d26f3074808e2187d332564817624f8f)", - "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=7aca8fe26)", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=4f6ae3aa2)", "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=f23b0f33f0d00b157476533a43cbbcfd901aa1f1)", "designcraft-color", "designcraft-compose", @@ -2103,9 +2103,9 @@ name = "designcraft-idml" version = "0.20.1" dependencies = [ "craft-file-formats", - "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=7aca8fe26)", + "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=4f6ae3aa2)", "craft-svg 0.1.0 (git+https://github.com/storytold/craft-libs?rev=cc23ffd4d26f3074808e2187d332564817624f8f)", - "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=7aca8fe26)", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=4f6ae3aa2)", "designcraft-color", "designcraft-compose", "designcraft-doc", @@ -2222,7 +2222,7 @@ version = "0.20.1" dependencies = [ "craft-docx", "craft-file-formats", - "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=7aca8fe26)", + "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=4f6ae3aa2)", "craft-rtf", "craft-wordmodel", "craft-xlsx", diff --git a/Cargo.toml b/Cargo.toml index 9cd572e6c..f00d1362b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,10 @@ craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "4052591363 craft-designcraft-format = { git = "https://github.com/storytold/craft-libs", rev = "93d0998383aa150fa50a2ffbf49ef37786e37eca", version = "=0.1.0" } craft-eps = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-epub = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } -craft-idml = { git = "https://github.com/storytold/craft-libs", rev = "7aca8fe26", version = "=0.1.0" } +craft-idml = { git = "https://github.com/storytold/craft-libs", rev = "4f6ae3aa2", version = "=0.1.0" } craft-indd = { git = "https://github.com/storytold/craft-libs", rev = "47bf5650b0d503b97bfb5e841746c4957897d11f", version = "=0.1.0" } craft-svg = { git = "https://github.com/storytold/craft-libs", rev = "cc23ffd4d26f3074808e2187d332564817624f8f", version = "=0.1.0", features = ["tree-render", "tree-text"] } -craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "7aca8fe26", version = "=0.1.0" } +craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "4f6ae3aa2", version = "=0.1.0" } craft-xml-dtd = { package = "craft-xml", git = "https://github.com/storytold/craft-libs", rev = "f23b0f33f0d00b157476533a43cbbcfd901aa1f1", version = "=0.1.0" } craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-xlsx = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 52a0aa48e..60beb360c 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -17,7 +17,7 @@ use designcraft_geom::corners::{Corner, CornerOptions}; use designcraft_geom::{Affine, Anchor, PathData, Point, Rect, SubPath}; use crate::names::{self, CHAR_BUILTINS, OBJECT_BUILTINS, PARA_BUILTINS, unescape_id}; -use crate::xml::{El, Node}; +use crate::xml::El; use crate::{IdmlError, Result, sniff_image}; mod document; diff --git a/crates/idml/src/import/item.rs b/crates/idml/src/import/item.rs index 3662d00e7..e092408c2 100644 --- a/crates/idml/src/import/item.rs +++ b/crates/idml/src/import/item.rs @@ -45,7 +45,8 @@ impl<'r> Importer<'r> { .unwrap_or_else(|| designcraft_doc::NO_OBJECT_STYLE.into()); // Fill and stroke. if tag != "Group" { - let fill_elements = std::iter::once(e).chain(self.object_style_chain(e)).map(crate::xml::to_craft).collect::>(); + let style_chain = self.object_style_chain(e); + let fill_elements = std::iter::once(e).chain(style_chain.iter()).map(crate::xml::to_craft).collect::>(); let fill_refs: Vec<_> = fill_elements.iter().collect(); let fill_values = craft_idml::model::FillValues::from_elements(&fill_refs); if let Some(f) = fill_values.color.as_deref() { @@ -87,7 +88,8 @@ impl<'r> Importer<'r> { // Corners: each corner's shape and radius come from the item, then its object style chain; // at each level a per-corner attribute wins over the legacy all-corners one. if tag != "Group" { - let levels: Vec<&El> = std::iter::once(e).chain(self.object_style_chain(e)).collect(); + let style_chain = self.object_style_chain(e); + let levels: Vec<&El> = std::iter::once(e).chain(style_chain.iter()).collect(); let corner_option = |n: &str| levels.iter().find_map(|s| craft_idml::model::Values::of(&crate::xml::to_craft(s)).corner_values(n).option); let corner_radius = |n: &str| levels.iter().find_map(|s| craft_idml::model::Values::of(&crate::xml::to_craft(s)).corner_values(n).radius); let mut corners = CornerOptions::default(); @@ -163,7 +165,8 @@ impl<'r> Importer<'r> { } }; // Options the frame doesn't set come from its object style chain. - let prefs: Vec<&El> = std::iter::once(e).chain(self.object_style_chain(e)).filter_map(|s| s.find("TextFramePreference")).collect(); + let style_chain = self.object_style_chain(e); + let prefs: Vec<&El> = std::iter::once(e).chain(style_chain.iter()).filter_map(|s| s.find("TextFramePreference")).collect(); let mut options = text_frame_options(&prefs); // `prefs` borrows the object styles, so the rule colour's swatch is resolved after it. if let Some(r) = Prefs(&prefs).prop("ColumnRuleStrokeColor") { diff --git a/crates/idml/src/import/object_style.rs b/crates/idml/src/import/object_style.rs index a261569f9..a4a8a426d 100644 --- a/crates/idml/src/import/object_style.rs +++ b/crates/idml/src/import/object_style.rs @@ -1,71 +1,28 @@ use super::values::{finite, tint}; -use super::{DirectionalFeather, DropShadow, Effects, El, GradientFeather, HashMap, Importer, Node, Stroke, names}; +use super::{DirectionalFeather, DropShadow, Effects, El, GradientFeather, Importer, Stroke, names}; impl<'r> Importer<'r> { /// An item's object style chain: its applied style, the styles that one is based on, then /// `[None]` (cycle-safe). - pub(super) fn object_style_chain(&self, e: &El) -> Vec<&El> { - let mut chain = self.object_style_bases(e.get("AppliedObjectStyle").map(str::to_string)); - if let Some(none) = self.object_els.get("ObjectStyle/$ID/[None]") - && !chain.iter().any(|c| std::ptr::eq(*c, none)) - { - chain.push(none); - } - chain + pub(super) fn object_style_chain(&self, e: &El) -> Vec { + let model = crate::xml::to_craft(e); + let styles: Vec<_> = self.object_els.values().map(crate::xml::to_craft).collect(); + let values = craft_idml::model::Values::of(&model); + let reference = values.get("AppliedObjectStyle"); + craft_idml::model::ObjectStyleStack::from_styles(&styles, reference, true).styles().iter().cloned().map(crate::xml::from_craft).collect() } /// Object style `id` and the styles it is based on, nearest first (cycle-safe, at most 16). - fn object_style_bases(&self, id: Option) -> Vec<&El> { - let mut chain: Vec<&El> = Vec::new(); - let mut os = id; - while let Some(id) = os { - let Some(s) = self.object_els.get(&id) else { break }; - if chain.len() >= 16 || chain.iter().any(|c| std::ptr::eq(*c, s)) { - break; - } - chain.push(s); - os = s.prop("BasedOn").map(|b| { - let b = b.trim(); - if b.starts_with("ObjectStyle/") { b.to_string() } else { format!("ObjectStyle/{}", names::escape_id(b)) } - }); - } - chain - } - /// The item's child element `name` laid over the same element of each style in its object style /// chain: attributes and nested elements merge one by one, nearer levels winning. IDML writes on an /// item only the values that differ from its style. Effects whose category the applied style turns /// off (`ObjectStyle*EffectsCategorySettings`) come from the item alone. pub(super) fn with_style(&self, e: &El, name: &str) -> Option { - let chain = self.object_style_chain(e); - let category = match name { - "TransparencySetting" => Some("ObjectStyleObjectEffectsCategorySettings"), - "StrokeTransparencySetting" => Some("ObjectStyleStrokeEffectsCategorySettings"), - "FillTransparencySetting" => Some("ObjectStyleFillEffectsCategorySettings"), - "ContentTransparencySetting" => Some("ObjectStyleContentEffectsCategorySettings"), - _ => None, - }; - let enabled = |effect: &str| { - let Some(flag) = category.and_then(|_| effect_flag(effect)) else { return true }; - chain.iter().find_map(|s| category.and_then(|c| s.find(c)).and_then(|c| c.get(flag))) != Some("false") - }; - let mut out: Option = None; - for s in chain.iter().rev() { - let Some(c) = s.find(name) else { continue }; - let mut c = c.clone(); - c.children.retain(|n| !matches!(n, Node::El(x) if !enabled(x.local()))); - match &mut out { - Some(o) => overlay(o, &c, 0), - None => out = Some(c), - } - } - if let Some(c) = e.find(name) { - match &mut out { - Some(o) => overlay(o, c, 0), - None => out = Some(c.clone()), - } - } - out + let model = crate::xml::to_craft(e); + let styles: Vec<_> = self.object_els.values().map(crate::xml::to_craft).collect(); + let values = craft_idml::model::Values::of(&model); + let reference = values.get("AppliedObjectStyle"); + craft_idml::model::ObjectStyleStack::from_styles(&styles, reference, true).merged_child(&model, name).map(crate::xml::from_craft) } /// Reads the drop shadow, feather, gradient feather and directional feather of a @@ -116,28 +73,14 @@ impl<'r> Importer<'r> { /// on (without category settings: when its setting is on), from the nearest style in the /// BasedOn chain that has the setting. `None` when the style sets neither. pub(super) fn style_effects(&mut self, e: &El) -> Option { - let mut chain = self.object_style_bases(e.get("Self").map(str::to_string)); - if chain.is_empty() { - chain.push(e); - } - let category = chain.iter().find_map(|s| s.find("ObjectStyleObjectEffectsCategorySettings")).cloned(); - let setting = |name: &str| chain.iter().find_map(|s| s.find("TransparencySetting")?.find(name)).cloned(); - let mut t = El::new("TransparencySetting"); - let mut any = false; - for (name, flag) in [("DropShadowSetting", "EnableDropShadow"), ("FeatherSetting", "EnableFeather")] { - let el = setting(name); - let on = match &category { - Some(c) => c.get(flag) == Some("true"), - None => el.as_ref().and_then(|x| x.get("Mode")).is_some_and(|m| m != "None"), - }; - any |= on; - if let (true, Some(el)) = (on, el) { - t.push(el); - } - } - if !any { - return None; + let model = crate::xml::to_craft(e); + let mut styles: Vec<_> = self.object_els.values().map(crate::xml::to_craft).collect(); + let reference = craft_idml::model::Values::of(&model).self_id().map(str::to_owned); + if !styles.iter().any(|style| craft_idml::model::Values::of(style).self_id() == reference.as_deref()) { + styles.push(model.clone()); } + let t = craft_idml::model::ObjectStyleStack::from_styles(&styles, reference.as_deref(), false).enabled_shadow_feather()?; + let t = crate::xml::from_craft(t); let mut fx = Effects::default(); self.effects_in(&t, &mut fx); Some(fx) @@ -145,7 +88,7 @@ impl<'r> Importer<'r> { pub(super) fn stroke_from(&mut self, e: &El, item: Option<&El>) -> Stroke { let model_elements = if item.is_some() { - std::iter::once(e).chain(self.object_style_chain(e)).map(crate::xml::to_craft).collect::>() + std::iter::once(e).chain(self.object_style_chain(e).iter()).map(crate::xml::to_craft).collect::>() } else { vec![crate::xml::to_craft(e)] }; @@ -194,72 +137,6 @@ impl<'r> Importer<'r> { } } -/// The object style category flag that turns an effect element on or off. -fn effect_flag(effect: &str) -> Option<&'static str> { - Some(match effect { - "BlendingSetting" => "EnableTransparency", - "DropShadowSetting" => "EnableDropShadow", - "FeatherSetting" => "EnableFeather", - "InnerShadowSetting" => "EnableInnerShadow", - "OuterGlowSetting" => "EnableOuterGlow", - "InnerGlowSetting" => "EnableInnerGlow", - "BevelAndEmbossSetting" => "EnableBevelEmboss", - "SatinSetting" => "EnableSatin", - "DirectionalFeatherSetting" => "EnableDirectionalFeather", - "GradientFeatherSetting" => "EnableGradientFeather", - _ => return None, - }) -} - -/// Lays `top` over `base`: attributes one by one, child elements matched by name and merged the same -/// way, text replaced. A property given in one form (attribute or `Properties` child) replaces the -/// other form in `base`. -fn overlay(base: &mut El, top: &El, depth: usize) { - if depth > 32 { - *base = top.clone(); - return; - } - for (k, v) in &top.attrs { - base.set(k, v); - if let Some(Node::El(p)) = base.children.iter_mut().find(|n| matches!(n, Node::El(p) if p.local() == "Properties")) { - p.children.retain(|n| !matches!(n, Node::El(x) if x.local() == k)); - } - } - if let Some(p) = top.find("Properties") { - let names: Vec<&str> = p.elements().map(El::local).collect(); - base.attrs.retain(|(k, _)| !names.contains(&k.as_str())); - } - let has_text = top.children.iter().any(|n| matches!(n, Node::Text(t) | Node::CData(t) if !t.trim().is_empty())); - if has_text { - base.children.retain(|n| !matches!(n, Node::Text(_) | Node::CData(_))); - } - // The k-th child of a name merges with the k-th child of that name in `base` (repeated elements - // such as gradient stops pair up in order). - let mut seen: HashMap<&str, usize> = HashMap::new(); - for n in &top.children { - match n { - Node::El(c) => { - let k = seen.entry(c.local()).or_insert(0); - let slot = base - .children - .iter_mut() - .filter_map(|b| match b { - Node::El(b) if b.local() == c.local() => Some(b), - _ => None, - }) - .nth(*k); - *k += 1; - match slot { - Some(b) => overlay(b, c, depth + 1), - None => base.children.push(n.clone()), - } - } - Node::Text(_) | Node::CData(_) if has_text => base.children.push(n.clone()), - _ => {} - } - } -} - /// A `GradientFeatherSetting`. The model holds two opacity stops at the ends of the gradient: the /// first and last `OpacityGradientStop` give them; with a linear vector, their `Location`s move its /// ends. diff --git a/crates/idml/src/import/story.rs b/crates/idml/src/import/story.rs index de764b877..63350d866 100644 --- a/crates/idml/src/import/story.rs +++ b/crates/idml/src/import/story.rs @@ -307,7 +307,8 @@ impl<'r> Importer<'r> { use designcraft_doc::anchored::AnchorAlign as A; // Each setting the item leaves unset comes from its object style chain, // then from the IDML defaults. - let levels: Vec<&El> = std::iter::once(&c).chain(self.object_style_chain(&c)).collect(); + let style_chain = self.object_style_chain(&c); + let levels: Vec<&El> = std::iter::once(&c).chain(style_chain.iter()).collect(); let s = |k: &str| { let set = levels.iter().filter_map(|l| l.find("AnchoredObjectSetting")).find_map(|x| x.get(k)); set.unwrap_or(match k {