mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Delegate IDML object style merging to craft-idml
This commit is contained in:
6 files changed
+40
-159
No files matched your search
Generated
+9
-9
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user