Import IDML page and spread values through model

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 04:48:32 -05:00
1 parent 210d190f8d
commit ababbf02ac
3 files changed
+42 -64

No files matched your search

Generated
+10 -10
View File
@@ -1225,10 +1225,10 @@ dependencies = [
[[package]]
name = "craft-idml"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=356ddacd1aac03c285b90a0596a7320ddf5eebb5#356ddacd1aac03c285b90a0596a7320ddf5eebb5"
source = "git+https://github.com/storytold/craft-libs?rev=584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca#584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca"
dependencies = [
"craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=356ddacd1aac03c285b90a0596a7320ddf5eebb5)",
"craft-zip 0.1.0 (git+https://github.com/storytold/craft-libs?rev=356ddacd1aac03c285b90a0596a7320ddf5eebb5)",
"craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca)",
"craft-zip 0.1.0 (git+https://github.com/storytold/craft-libs?rev=584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca)",
"thiserror 2.0.21",
]
@@ -1578,7 +1578,7 @@ dependencies = [
[[package]]
name = "craft-xml"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=356ddacd1aac03c285b90a0596a7320ddf5eebb5#356ddacd1aac03c285b90a0596a7320ddf5eebb5"
source = "git+https://github.com/storytold/craft-libs?rev=40525913633942f84b06d4837a8d90534a55d63f#40525913633942f84b06d4837a8d90534a55d63f"
dependencies = [
"quick-xml 0.41.0",
"thiserror 2.0.21",
@@ -1587,7 +1587,7 @@ dependencies = [
[[package]]
name = "craft-xml"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=40525913633942f84b06d4837a8d90534a55d63f#40525913633942f84b06d4837a8d90534a55d63f"
source = "git+https://github.com/storytold/craft-libs?rev=584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca#584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca"
dependencies = [
"quick-xml 0.41.0",
"thiserror 2.0.21",
@@ -1633,7 +1633,7 @@ dependencies = [
[[package]]
name = "craft-zip"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=356ddacd1aac03c285b90a0596a7320ddf5eebb5#356ddacd1aac03c285b90a0596a7320ddf5eebb5"
source = "git+https://github.com/storytold/craft-libs?rev=584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca#584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca"
dependencies = [
"flate2",
"thiserror 2.0.21",
@@ -1914,7 +1914,7 @@ dependencies = [
"craft-prepress",
"craft-print",
"craft-svg 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895)",
"craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=356ddacd1aac03c285b90a0596a7320ddf5eebb5)",
"craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca)",
"craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=f23b0f33f0d00b157476533a43cbbcfd901aa1f1)",
"designcraft-color",
"designcraft-compose",
@@ -1998,8 +1998,8 @@ dependencies = [
name = "designcraft-idml"
version = "0.20.1"
dependencies = [
"craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=356ddacd1aac03c285b90a0596a7320ddf5eebb5)",
"craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=356ddacd1aac03c285b90a0596a7320ddf5eebb5)",
"craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca)",
"craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca)",
"designcraft-color",
"designcraft-compose",
"designcraft-doc",
@@ -2116,7 +2116,7 @@ version = "0.20.1"
dependencies = [
"craft-docx",
"craft-file-formats",
"craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=356ddacd1aac03c285b90a0596a7320ddf5eebb5)",
"craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca)",
"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-document-bundle = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", 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 = "356ddacd1aac03c285b90a0596a7320ddf5eebb5", version = "=0.1.0" }
craft-idml = { git = "https://github.com/storytold/craft-libs", rev = "584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca", 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/dylanmtaylor/craft-libs", rev = "2e670cf9377b523112f3fe4b409399772c745895", version = "=0.1.0", features = ["tree-render", "tree-text"] }
craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "356ddacd1aac03c285b90a0596a7320ddf5eebb5", version = "=0.1.0" }
craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "584e00bf1f7e812e4fd204bc9b077c8a96e0d1ca", 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" }
+30 -52
View File
@@ -1,29 +1,8 @@
use super::frame::{fit_columns, margins_of};
use super::values::{nums, parse_affine};
use super::{
Affine, Arc, Columns, El, Guide, Importer, ItemId, Margins, Orientation, Page, PageId, PageSide, ParentInfo, Point, Rect, Spread, SpreadId, names,
};
/// View › Rotate Spread from a `Spread` element's `ItemTransform`: quarter turns clockwise on
/// screen (IDML y points down, so `0 1 -1 0` is one clockwise turn). The spread's items and
/// pages stay in unrotated spread coordinates, so only the rotation part is read. Anything other
/// than an exact quarter turn (identity, scale, other angles, malformed numbers) is no rotation.
fn spread_view_rotation(s: Option<&str>) -> u8 {
let parsed: std::result::Result<Vec<f64>, _> = s.unwrap_or("").split_whitespace().take(7).map(str::parse::<f64>).collect();
let Some([a, b, c, d, _, _]) = parsed.ok().and_then(|v| <[f64; 6]>::try_from(v).ok()) else { return 0 };
let is = |x: f64, y: f64| (x - y).abs() < 1e-6;
let quarter = |m: [f64; 4]| is(a, m[0]) && is(b, m[1]) && is(c, m[2]) && is(d, m[3]);
if quarter([0.0, 1.0, -1.0, 0.0]) {
1
} else if quarter([-1.0, 0.0, 0.0, -1.0]) {
2
} else if quarter([0.0, -1.0, 1.0, 0.0]) {
3
} else {
0
}
}
impl<'r> Importer<'r> {
// ---------- spreads and items ----------
@@ -55,22 +34,28 @@ impl<'r> Importer<'r> {
}
/// The page rectangles of a spread element, in IDML spread coordinates.
fn page_rects<'a>(&self, e: &'a El) -> Vec<(Rect, &'a El)> {
let mut pages: Vec<(Rect, &'a El)> = Vec::new();
for p in e.find_all("Page") {
let gb = nums(p.get("GeometricBounds").unwrap_or(""));
let (t, l, bt, r) =
if gb.len() == 4 { (gb[0], gb[1], gb[2], gb[3]) } else { (0.0, 0.0, self.settings.page_height, self.settings.page_width) };
let xf = parse_affine(p.get("ItemTransform"));
fn page_rects(&self, spread: &craft_idml::model::Spread) -> Vec<(Rect, craft_idml::model::Page)> {
let mut pages: Vec<(Rect, craft_idml::model::Page)> = Vec::new();
for p in spread.pages() {
let gb = p.geometric_bounds().unwrap_or([0.0, 0.0, self.settings.page_height, self.settings.page_width]);
let [t, l, bt, r] = gb;
let xf = Affine::new(p.item_transform());
let a = xf * Point::new(l, t);
let c = xf * Point::new(r, bt);
pages.push((Rect::from_points(a, c), p));
pages.push((Rect::from_points(a, c), p.clone()));
}
pages
}
/// A page's margins and columns, and whether they are local (differ from its parent page's).
fn page_margins(&self, p: &El, r: &Rect, master: bool, parent: Option<SpreadId>, (side, index): (PageSide, usize)) -> (Margins, Columns, bool) {
fn page_margins(
&self,
p: &craft_idml::model::Page,
r: &Rect,
master: bool,
parent: Option<SpreadId>,
(side, index): (PageSide, usize),
) -> (Margins, Columns, bool) {
// A document page's margins are local when they differ from its parent page's; a
// page without its own takes the parent page's.
let from_parent = if master {
@@ -81,7 +66,8 @@ impl<'r> Importer<'r> {
.and_then(|sp| sp.pages.get(sp.parent_page_at(side, index)?))
.map(|q| (q.margins, q.columns.clone()))
};
let (margins, columns) = match (p.find("MarginPreference"), &from_parent) {
let preference = p.margin_preference().map(|v| crate::xml::from_craft(v.element("MarginPreference")));
let (margins, columns) = match (preference.as_ref(), &from_parent) {
(Some(m), _) => {
let (margins, columns) = margins_of(m);
(margins, fit_columns(columns, &margins, r.width()))
@@ -97,9 +83,10 @@ impl<'r> Importer<'r> {
}
pub(super) fn spread(&mut self, e: &El, id: SpreadId, master: bool) -> Spread {
let model_spread = craft_idml::model::Spread::from_el(&crate::xml::to_craft(e));
let facing = self.settings.facing_pages;
// Page rectangles in IDML spread coordinates.
let pages = self.page_rects(e);
let pages = self.page_rects(&model_spread);
let min_x = pages.iter().map(|(r, _)| r.x0).fold(f64::INFINITY, f64::min);
let min_y = pages.iter().map(|(r, _)| r.y0).fold(f64::INFINITY, f64::min);
let (min_x, min_y) = if min_x.is_finite() { (min_x, min_y) } else { (0.0, 0.0) };
@@ -107,13 +94,13 @@ impl<'r> Importer<'r> {
// The spine: between a left and a right page (else the spread's left edge).
let spine = pages.iter().map(|(r, _)| r.x0 - min_x).filter(|x| *x > 1e-6).fold(f64::INFINITY, f64::min);
let spine = if spine.is_finite() && facing { spine } else { 0.0 };
let show = e.get("ShowMasterItems") != Some("false");
let show = model_spread.show_master_items();
let first_abs: usize = self.spreads.iter().map(|s| s.pages.len()).sum();
let view_rotation = if master { 0 } else { spread_view_rotation(e.get("ItemTransform")) };
let view_rotation = model_spread.view_rotation();
let mut out_pages = Vec::new();
for (i, (r, p)) in pages.iter().enumerate() {
let pid = PageId(self.alloc());
if let Some(s) = p.get("Self")
if let Some(s) = p.self_id()
&& !master
{
self.page_index.insert(s.to_string(), first_abs + i);
@@ -125,7 +112,7 @@ impl<'r> Importer<'r> {
} else {
PageSide::Right
};
let parent = p.get("AppliedMaster").and_then(|m| self.parent_ids.get(m)).copied();
let parent = p.applied_master().and_then(|m| self.parent_ids.get(m)).copied();
let (margins, columns, local_margins) = self.page_margins(p, r, master, parent, (side, i));
out_pages.push(Page {
id: pid,
@@ -138,20 +125,15 @@ impl<'r> Importer<'r> {
parent,
side,
overridden: vec![],
guides: self.guides_of(
&craft_idml::model::Page::from_el(&crate::xml::to_craft(p)),
Rect::new(r.x0 - min_x, r.y0 - min_y, r.x1 - min_x, r.y1 - min_y),
spine,
),
guides: self.guides_of(p, Rect::new(r.x0 - min_x, r.y0 - min_y, r.x1 - min_x, r.y1 - min_y), spine),
show_parent_items: show,
liquid: names::liquid_rule_in(p.get("LayoutRule").unwrap_or("Off")),
liquid: names::liquid_rule_in(p.layout_rule()),
transition: None,
view_rotation: if i == 0 { view_rotation } else { 0 },
show_parent_overlay: false,
});
}
let overrides: Vec<Vec<String>> =
pages.iter().map(|(_, p)| p.get("OverrideList").unwrap_or("").split_whitespace().map(str::to_string).collect()).collect();
let overrides: Vec<Vec<String>> = pages.iter().map(|(_, p)| p.override_list().into_iter().map(str::to_string).collect()).collect();
let mut items = Vec::new();
for c in e.elements() {
if let Some(it) = self.item(c, shift) {
@@ -162,15 +144,11 @@ impl<'r> Importer<'r> {
pg.overridden = ov.iter().filter_map(|s| self.item_ids.get(s)).copied().filter(|i| self.is_parent_item(*i)).collect();
}
let parent = if master {
let prefix =
e.get("NamePrefix").map(str::to_string).unwrap_or_else(|| e.get("Name").unwrap_or("A").split('-').next().unwrap_or("A").to_string());
let name = e.get("BaseName").map(str::to_string).unwrap_or_else(|| {
let n = e.get("Name").unwrap_or("Parent");
n.split_once('-').map(|(_, b)| b.to_string()).unwrap_or_else(|| n.to_string())
});
let prefix = model_spread.parent_prefix();
let name = model_spread.base_name();
// A parent spread has one base: its first page's. Pages that disagree lose theirs.
let applied: Vec<Option<SpreadId>> =
pages.iter().map(|(_, p)| p.get("AppliedMaster").and_then(|m| self.parent_ids.get(m)).copied()).collect();
pages.iter().map(|(_, p)| p.applied_master().and_then(|m| self.parent_ids.get(m)).copied()).collect();
let label = format!("{prefix}-{name}");
if applied.windows(2).any(|w| w[0] != w[1]) {
self.warnings.push(format!("parent {label}: its pages are based on different parents; only the first page's is kept"));
@@ -189,7 +167,7 @@ impl<'r> Importer<'r> {
p.parent = None;
}
}
Spread { id, pages: out_pages, items, parent, allow_shuffle: e.get("AllowPageShuffle") != Some("false") }
Spread { id, pages: out_pages, items, parent, allow_shuffle: model_spread.allow_shuffle() }
}
pub(super) fn is_parent_item(&self, id: ItemId) -> bool {