Delegate IDML object style merging to craft-idml

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 08:56:24 -05:00
1 parent 75eeeca16f
commit a92db95301
6 files changed
+40 -159

No files matched your search

Generated
+9 -9
View File
@@ -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",
+2 -2
View File
@@ -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" }
+1 -1
View File
@@ -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;
+6 -3
View File
@@ -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::<Vec<_>>();
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::<Vec<_>>();
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") {
+20 -143
View File
@@ -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<El> {
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<String>) -> 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<El> {
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<El> = 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<Effects> {
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::<Vec<_>>()
std::iter::once(e).chain(self.object_style_chain(e).iter()).map(crate::xml::to_craft).collect::<Vec<_>>()
} 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.
+2 -1
View File
@@ -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 {