Refactor idml for strict Clippy checks

This commit is contained in:
Dylan M. Taylor
2026-10-28 11:39:26 +00:00
parent 0104122f06
commit be9b8f360e
24 changed files with 953 additions and 758 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Affine, Color, DOM_VERSION, El, Node, PKG_NS, escape_id, num};
pub(crate) fn uid(n: u64) -> String {
format!("u{n:x}")
+1 -1
View File
@@ -1,5 +1,5 @@
use super::common::{anchor_self, bool_s, p, uid, with_props, xref_format_self};
use super::*;
use super::{CHAR_BUILTINS, DOM_VERSION, Document, El, Ex, HashSet, PARA_BUILTINS, PKG_NS, document_with_pi, escape_id, names, num};
impl<'a> Ex<'a> {
// ---------- designmap ----------
+1 -1
View File
@@ -1,6 +1,6 @@
use super::common::{bool_s, p, pct, pkg_root};
use super::spread::margin_el;
use super::*;
use super::{El, Ex, PARA_BUILTINS, document, escape_id, names, num, pt, st};
impl<'a> Ex<'a> {
// ---------- Fonts.xml ----------
+1 -1
View File
@@ -1,5 +1,5 @@
use super::common::{bool_s, color_value, pct, pkg_root};
use super::*;
use super::{Color, ColorType, El, Ex, GradientKind, SwatchValue, document, escape_id, num, swatch};
// Retained IDML attributes have unprefixed ASCII names. Native JSON can be
// edited independently: never let its map keys become XML syntax or Self IDs.
+56 -49
View File
@@ -1,5 +1,8 @@
use super::common::{affine_s, bool_s, p, pct, uid, with_props};
use super::*;
use super::{
Affine, ColumnsKind, Content, El, Ex, HashSet, Item, Node, OBJECT_BUILTINS, PathData, Shape, TextFrameOptions, base64_encode, escape_id, names,
num, pt,
};
impl<'a> Ex<'a> {
pub(super) fn item_el(&mut self, it: &Item, xf: Affine) -> El {
@@ -99,54 +102,7 @@ impl<'a> Ex<'a> {
props.push(path_geometry(&it.path));
}
let mut el = with_props(el, props);
// Transparency.
// An item whose style sets effects writes the effects it has turned off too, so that on
// import it doesn't take the style's.
let styled = self.d.styles.object_effects(os).is_some();
let ds = &it.effects.drop_shadow;
let (gf, df) = (&it.effects.gradient_feather, &it.effects.directional_feather);
if it.opacity < 1.0 || it.blend != designcraft_color::BlendMode::Normal || ds.on || it.effects.feather > 0.0 || gf.on || df.on || styled {
let mut t = El::new("TransparencySetting");
t.push(El::new("BlendingSetting").attr("BlendMode", names::blend_out(it.blend)).attr("Opacity", pct(it.opacity as f64)));
for c in self.effect_els(&it.effects, styled) {
t.push(c);
}
if df.on {
let [top, left, bottom, right] = df.widths;
t.push(
El::new("DirectionalFeatherSetting")
.attr("Applied", "true")
.attr("LeftWidth", num(left))
.attr("RightWidth", num(right))
.attr("TopWidth", num(top))
.attr("BottomWidth", num(bottom)),
);
}
if gf.on {
let mut g = El::new("GradientFeatherSetting").attr("Applied", "true").attr("Type", if gf.radial { "Radial" } else { "Linear" });
if let Some([x0, y0, x1, y1]) = gf.vector {
// Gradient Feather tool vector: start, length and angle in the item's space.
g.set("Angle", num((-(y1 - y0)).atan2(x1 - x0).to_degrees()));
g.set("Length", num((x1 - x0).hypot(y1 - y0)));
g.set("GradientStart", format!("{} {}", num(x0), num(y0)));
} else {
g.set("Angle", num(gf.angle));
}
let id = uid(it.id.0);
for (i, (opacity, location)) in [(gf.start, 0.0), (gf.end, 100.0)].into_iter().enumerate() {
let mut s = El::new("OpacityGradientStop")
.attr("Self", format!("{id}TransparencySetting1GradientFeatherSetting1OpacityGradientStop{i}"))
.attr("Opacity", pct(opacity as f64))
.attr("Location", num(location));
if i > 0 {
s.set("Midpoint", "50");
}
g.push(s);
}
t.push(g);
}
el.push(t);
}
self.append_transparency(&mut el, it, os);
if let Content::Text(t) = &it.content {
el.push(text_frame_pref(&t.options, &self.sw(&t.options.column_rule_color)));
if let Some(g) = t.options.baseline_grid {
@@ -198,6 +154,57 @@ impl<'a> Ex<'a> {
el
}
fn append_transparency(&self, el: &mut El, it: &Item, os: &str) {
// Transparency.
// An item whose style sets effects writes the effects it has turned off too, so that on
// import it doesn't take the style's.
let styled = self.d.styles.object_effects(os).is_some();
let ds = &it.effects.drop_shadow;
let (gf, df) = (&it.effects.gradient_feather, &it.effects.directional_feather);
if it.opacity < 1.0 || it.blend != designcraft_color::BlendMode::Normal || ds.on || it.effects.feather > 0.0 || gf.on || df.on || styled {
let mut t = El::new("TransparencySetting");
t.push(El::new("BlendingSetting").attr("BlendMode", names::blend_out(it.blend)).attr("Opacity", pct(it.opacity as f64)));
for c in self.effect_els(&it.effects, styled) {
t.push(c);
}
if df.on {
let [top, left, bottom, right] = df.widths;
t.push(
El::new("DirectionalFeatherSetting")
.attr("Applied", "true")
.attr("LeftWidth", num(left))
.attr("RightWidth", num(right))
.attr("TopWidth", num(top))
.attr("BottomWidth", num(bottom)),
);
}
if gf.on {
let mut g = El::new("GradientFeatherSetting").attr("Applied", "true").attr("Type", if gf.radial { "Radial" } else { "Linear" });
if let Some([x0, y0, x1, y1]) = gf.vector {
// Gradient Feather tool vector: start, length and angle in the item's space.
g.set("Angle", num((-(y1 - y0)).atan2(x1 - x0).to_degrees()));
g.set("Length", num((x1 - x0).hypot(y1 - y0)));
g.set("GradientStart", format!("{} {}", num(x0), num(y0)));
} else {
g.set("Angle", num(gf.angle));
}
let id = uid(it.id.0);
for (i, (opacity, location)) in [(gf.start, 0.0), (gf.end, 100.0)].into_iter().enumerate() {
let mut s = El::new("OpacityGradientStop")
.attr("Self", format!("{id}TransparencySetting1GradientFeatherSetting1OpacityGradientStop{i}"))
.attr("Opacity", pct(opacity as f64))
.attr("Location", num(location));
if i > 0 {
s.set("Midpoint", "50");
}
g.push(s);
}
t.push(g);
}
el.push(t);
}
}
/// The `TransparencySetting` children for `e`'s drop shadow and feather; `all` also writes the
/// effects that are off (`Mode="None"`), as an object style that sets effects does.
pub(super) fn effect_els(&self, e: &designcraft_doc::Effects, all: bool) -> Vec<El> {
+1 -1
View File
@@ -1,5 +1,5 @@
use super::common::{bool_s, uid};
use super::*;
use super::{Affine, Document, El, Ex, PageSide, Spread, names, num};
/// The IDML spread origin in our spread coordinates.
pub(crate) fn spread_origin(sp: &Spread, facing: bool) -> (f64, f64) {
+305 -279
View File
@@ -1,6 +1,9 @@
use super::common::{anchor_self, bool_s, change_type, topic_self, uid, with_props, xref_format_self};
use super::graphics::graphic_layer_attribute_name;
use super::*;
use super::{
BTreeMap, CHAR_BUILTINS, CharFormat, El, Ex, ListType, NO_CHAR_ID, Node, PARA_BUILTINS, ParaAttrs, ParaFormat, Story, TOC_STYLE_ID, names, num,
st,
};
impl<'a> Ex<'a> {
/// `<GraphicLayerOption>` of a placed graphic: its layer attributes and the nested layer tree.
@@ -189,295 +192,318 @@ impl<'a> Ex<'a> {
el
}
/// `ParagraphStyleRange`s of a story (or a table cell's story).
pub(super) fn story_paras(&mut self, s: &Story) -> Vec<El> {
let mut paras = Vec::new();
let ranges = s.para_ranges();
let n = ranges.len();
for (pi, r) in ranges.into_iter().enumerate() {
let pf = s.paras.get(pi).cloned().unwrap_or_default();
let mut psr = self.psr_el(&pf);
let last = pi + 1 == n;
let ends_with_break = s.text.get(r.clone()).and_then(|t| t.chars().next_back()).is_some_and(st::is_break_char);
// Character runs intersecting the paragraph.
let mut segs: Vec<(std::ops::Range<usize>, CharFormat)> = Vec::new();
for (rr, f) in s.runs() {
let a = rr.start.max(r.start);
let b = rr.end.min(r.end);
if a < b {
segs.push((a..b, f.clone()));
}
/// Write an index reference and record any cross-reference it declares.
fn index_reference(&mut self, s: &Story, k: usize) -> Option<El> {
if let Some(r) = s.index_refs.get(k).filter(|r| !r.topics.is_empty()) {
use designcraft_doc::index::IndexRange as R;
for n in 1..=r.topics.len() {
// A shared topic has one key: keep the first reference's
// value (even empty), rather than inventing a merged key.
self.index_topics.entry(r.topics[..n].to_vec()).or_insert_with(|| r.sort.get(n - 1).cloned().unwrap_or_default());
}
if segs.is_empty() {
let f = s.char_format_at(r.start).clone();
segs.push((r.start..r.start, f));
let (kind, limit) = match &r.range {
R::CurrentPage => (Some("CurrentPage"), None),
R::ToEndOfStory => (Some("ToEndOfStory"), None),
R::NextParagraphs(n) => (Some("ForNextNParagraphs"), Some(*n)),
R::SuppressPageRange => (Some("SuppressPageNumbers"), None),
R::See(t) => {
self.index_xrefs.push((r.topics.clone(), "See", t.clone()));
(None, None)
}
R::SeeAlso(t) => {
self.index_xrefs.push((r.topics.clone(), "SeeAlso", t.clone()));
(Some("CurrentPage"), None)
}
};
if let Some(kind) = kind {
let mut el = El::new("PageReference")
.attr("Self", self.fresh())
.attr("PageReferenceType", kind)
.attr("ReferencedTopic", topic_self(&r.topics))
.attr("Id", k + 1);
if let Some(n) = limit {
el.set("PageReferenceLimit", n);
}
return Some(el);
}
let nseg = segs.len();
for (k, (rr, f)) in segs.into_iter().enumerate() {
let seg_start = rr.start;
let text = &s.text[rr];
// Split at break characters.
let mut cur = String::new();
let mut pending: Vec<Node> = Vec::new();
let mut csrs: Vec<El> = Vec::new();
let flush_text = |cur: &mut String, pending: &mut Vec<Node>| {
if !cur.is_empty() {
pending.push(Node::Text(std::mem::take(cur)));
}
};
let flush_content = |pending: &mut Vec<Node>, out: &mut Vec<Node>| {
if !pending.is_empty() {
let mut c = El::new("Content");
c.children = std::mem::take(pending);
out.push(Node::El(c));
}
};
let mut out: Vec<Node> = Vec::new();
for (ci, ch) in text.char_indices() {
let ace = match ch {
st::PAGE_NUMBER | st::NEXT_PAGE_NUMBER | st::PREV_PAGE_NUMBER => Some("18"),
st::SECTION_MARKER => Some("19"),
st::INDENT_HERE => Some("7"),
st::RIGHT_INDENT_TAB => Some("8"),
_ => None,
};
let brk = match ch {
st::COLUMN_BREAK => Some("NextColumn"),
st::FRAME_BREAK => Some("NextFrame"),
st::PAGE_BREAK => Some("NextPage"),
st::ODD_PAGE_BREAK => Some("NextOddPage"),
st::EVEN_PAGE_BREAK => Some("NextEvenPage"),
_ => None,
};
if let Some(code) = ace {
flush_text(&mut cur, &mut pending);
pending.push(Node::Pi("ACE".into(), code.into()));
} else if ch == st::TABLE_ANCHOR {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if let Some(t) = s.para_table(pi) {
out.push(Node::El(self.table_el(t)));
}
} else if ch == designcraft_doc::ANCHOR_MARK {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
let k = s.text[..seg_start + ci].matches(designcraft_doc::ANCHOR_MARK).count();
if let Some(a) = s.anchors.get(k) {
out.push(Node::El(
El::new("HyperlinkTextDestination")
.attr("Self", anchor_self(a.id))
.attr("Name", &a.name)
.attr("Hidden", "false")
.attr("DestinationUniqueKey", a.id & 0xFFFF_FFFF),
));
}
} else if ch == designcraft_doc::OBJECT_MARK {
// An anchored object: the item inside the range with its anchoring settings.
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
let k = s.text[..seg_start + ci].matches(designcraft_doc::OBJECT_MARK).count();
if let Some(o) = s.objects.get(k) {
let mut e = self.item_el(&o.item, o.item.xf);
use designcraft_doc::anchored::AnchorAlign as A;
let setting = match &o.position {
designcraft_doc::AnchorPosition::Inline { y_offset } => {
El::new("AnchoredObjectSetting").attr("AnchoredPosition", "InlinePosition").attr("AnchorYoffset", num(*y_offset))
}
designcraft_doc::AnchorPosition::AboveLine { align, space_before, space_after } => El::new("AnchoredObjectSetting")
.attr("AnchoredPosition", "AboveLine")
.attr(
"HorizontalAlignment",
match align {
A::Left => "LeftAlign",
A::Center => "CenterAlign",
A::Right => "RightAlign",
},
)
.attr("AnchorSpaceAbove", num(*space_before))
.attr("AnchorYoffset", num(*space_after)),
designcraft_doc::AnchorPosition::Custom {
x_relative,
y_relative,
x_offset,
y_offset,
object_point,
ref_point,
keep_within_column,
spine_relative,
} => El::new("AnchoredObjectSetting")
.attr("AnchoredPosition", "Anchored")
.attr("AnchorPoint", names::anchor_out(*object_point))
.attr("HorizontalReferencePoint", names::anchor_rel_out(*x_relative, false))
.attr("VerticalReferencePoint", names::anchor_rel_out(*y_relative, true))
.attr("HorizontalAlignment", ["LeftAlign", "CenterAlign", "RightAlign"][(*ref_point % 3) as usize])
.attr("VerticalAlignment", ["TopAlign", "CenterAlign", "BottomAlign"][(*ref_point / 3).min(2) as usize])
// IDML measures a left-aligned object's X offset leftwards.
.attr("AnchorXoffset", num(if *ref_point % 3 == 0 { 0.0 - *x_offset } else { *x_offset }))
.attr("AnchorYoffset", num(*y_offset))
.attr("PinPosition", bool_s(*keep_within_column))
.attr("SpineRelative", bool_s(*spine_relative)),
};
e.push(setting);
out.push(Node::El(e));
}
} else if ch == designcraft_doc::INDEX_MARK {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
let k = s.text[..seg_start + ci].matches(designcraft_doc::INDEX_MARK).count();
if let Some(r) = s.index_refs.get(k).filter(|r| !r.topics.is_empty()) {
use designcraft_doc::index::IndexRange as R;
for n in 1..=r.topics.len() {
// A shared topic has one key: keep the first reference's
// value (even empty), rather than inventing a merged key.
self.index_topics.entry(r.topics[..n].to_vec()).or_insert_with(|| r.sort.get(n - 1).cloned().unwrap_or_default());
}
let (kind, limit) = match &r.range {
R::CurrentPage => (Some("CurrentPage"), None),
R::ToEndOfStory => (Some("ToEndOfStory"), None),
R::NextParagraphs(n) => (Some("ForNextNParagraphs"), Some(*n)),
R::SuppressPageRange => (Some("SuppressPageNumbers"), None),
R::See(t) => {
self.index_xrefs.push((r.topics.clone(), "See", t.clone()));
(None, None)
}
R::SeeAlso(t) => {
self.index_xrefs.push((r.topics.clone(), "SeeAlso", t.clone()));
(Some("CurrentPage"), None)
}
};
if let Some(kind) = kind {
let mut el = El::new("PageReference")
.attr("Self", self.fresh())
.attr("PageReferenceType", kind)
.attr("ReferencedTopic", topic_self(&r.topics))
.attr("Id", k + 1);
if let Some(n) = limit {
el.set("PageReferenceLimit", n);
}
out.push(Node::El(el));
}
}
} else if ch == designcraft_doc::XREF_MARK {
// A source wrapping its current text (InDesign regenerates it on update).
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if !out.is_empty() {
let mut c = self.csr_el(&f);
c.children.append(&mut out);
csrs.push(c);
}
let k = s.text[..seg_start + ci].matches(designcraft_doc::XREF_MARK).count();
if let Some(x) = s.xrefs.get(k) {
let me = self.fresh();
let fmt = self.d.xref_formats.iter().position(|ff| ff.name == x.format).map_or("n".to_string(), xref_format_self);
let text = self.d.xref_values(x.target).map_or("??".into(), |mut v| {
v.page = "?".into();
designcraft_doc::xref::expand(self.d.xref_format(&x.format).map_or("<fullPara />", |ff| ff.definition.as_str()), &v)
});
let mut c = self.csr_el(&f);
let mut content = El::new("Content");
content.children.push(Node::Text(text));
c.push(content);
let mut src = El::new("CrossReferenceSource")
.attr("Self", &me)
.attr("AppliedFormat", fmt)
.attr("Name", format!("Cross-Reference {}", self.xref_sources.len() + 1))
.attr("Hidden", "false");
src.push(c);
csrs.push(src);
self.xref_sources.push((me, x.target));
}
} else if let Some(vi) = designcraft_doc::vars::var_index(ch) {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if let Some(v) = self.d.text_variables.get(vi) {
let result = self.d.variable_value(vi, None).unwrap_or_else(|| format!("{}{}", v.before, v.after));
out.push(Node::El(
El::new("TextVariableInstance")
.attr("Self", self.fresh())
.attr("Name", &v.name)
.attr("ResultText", result)
.attr("AssociatedTextVariable", format!("dTextVariablen{}", v.name)),
));
}
} else if ch == designcraft_doc::NOTE_MARK {
// Editorial notes aren't written to IDML yet.
} else if ch == designcraft_doc::ENDNOTE_REF {
// Endnotes aren't written to IDML yet: the endnote frame keeps their text
// (numbers included) and the references are left out.
} else if ch == designcraft_doc::FOOTNOTE_REF {
// The reference: its own range carrying the reference position.
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if !out.is_empty() {
let mut c = self.csr_el(&f);
c.children.append(&mut out);
csrs.push(c);
}
if let Some(n) = s.notes.get(s.notes_before(seg_start + ci)) {
let mut c = self.csr_el(&f);
let pos = self.d.footnote_options.ref_position;
if pos != designcraft_doc::Position::Normal {
c.set("Position", names::position_out(pos));
}
c.push(self.footnote_el(&n.text));
csrs.push(c);
}
} else if let Some(bt) = brk {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if !out.is_empty() {
let mut c = self.csr_el(&f);
c.children.append(&mut out);
csrs.push(c);
}
let mut c = self.csr_el(&f);
c.set("ParagraphBreakType", bt);
c.push(El::new("Br"));
csrs.push(c);
} else {
cur.push(ch);
}
}
None
}
fn anchored_object(&mut self, s: &Story, k: usize) -> Option<El> {
if let Some(o) = s.objects.get(k) {
let mut e = self.item_el(&o.item, o.item.xf);
use designcraft_doc::anchored::AnchorAlign as A;
let setting = match &o.position {
designcraft_doc::AnchorPosition::Inline { y_offset } => {
El::new("AnchoredObjectSetting").attr("AnchoredPosition", "InlinePosition").attr("AnchorYoffset", num(*y_offset))
}
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
let is_last_seg = k + 1 == nseg;
// A break character that ends the paragraph was written as its `Br`.
let para_br = is_last_seg && !last && !ends_with_break;
if !out.is_empty() || csrs.is_empty() || para_br {
let mut c = self.csr_el(&f);
c.children.extend(out);
csrs.push(c);
}
// Tracked changes wrap the text they mark.
if let Some(kind) = change_type(f.over.change) {
for c in csrs.iter_mut().filter(|c| c.name == "CharacterStyleRange" && !c.children.is_empty()) {
designcraft_doc::AnchorPosition::AboveLine { align, space_before, space_after } => El::new("AnchoredObjectSetting")
.attr("AnchoredPosition", "AboveLine")
.attr(
"HorizontalAlignment",
match align {
A::Left => "LeftAlign",
A::Center => "CenterAlign",
A::Right => "RightAlign",
},
)
.attr("AnchorSpaceAbove", num(*space_before))
.attr("AnchorYoffset", num(*space_after)),
designcraft_doc::AnchorPosition::Custom {
x_relative,
y_relative,
x_offset,
y_offset,
object_point,
ref_point,
keep_within_column,
spine_relative,
} => El::new("AnchoredObjectSetting")
.attr("AnchoredPosition", "Anchored")
.attr("AnchorPoint", names::anchor_out(*object_point))
.attr("HorizontalReferencePoint", names::anchor_rel_out(*x_relative, false))
.attr("VerticalReferencePoint", names::anchor_rel_out(*y_relative, true))
.attr("HorizontalAlignment", ["LeftAlign", "CenterAlign", "RightAlign"][(*ref_point % 3) as usize])
.attr("VerticalAlignment", ["TopAlign", "CenterAlign", "BottomAlign"][(*ref_point / 3).min(2) as usize])
// IDML measures a left-aligned object's X offset leftwards.
.attr("AnchorXoffset", num(if *ref_point % 3 == 0 { 0.0 - *x_offset } else { *x_offset }))
.attr("AnchorYoffset", num(*y_offset))
.attr("PinPosition", bool_s(*keep_within_column))
.attr("SpineRelative", bool_s(*spine_relative)),
};
e.push(setting);
return Some(e);
}
None
}
fn apply_tracked_changes(&mut self, s: &Story, f: &CharFormat, csrs: &mut [El], para_br: bool, para_end: usize) {
// Tracked changes wrap the text they mark.
if let Some(kind) = change_type(f.over.change) {
for c in csrs.iter_mut().filter(|c| c.name == "CharacterStyleRange" && !c.children.is_empty()) {
let mut ch = El::new("Change").attr("Self", self.fresh()).attr("ChangeType", kind);
ch.children = std::mem::take(&mut c.children);
c.push(ch);
}
}
if para_br && let Some(c) = csrs.last_mut() {
let nl = s.runs().find(|(rr, _)| rr.contains(&para_end)).and_then(|(_, nf)| change_type(nf.over.change));
match nl {
None => c.push(El::new("Br")),
Some(kind) => match c.children.last_mut() {
Some(Node::El(ch)) if ch.name == "Change" && ch.get("ChangeType") == Some(kind) => ch.push(El::new("Br")),
_ => {
let mut ch = El::new("Change").attr("Self", self.fresh()).attr("ChangeType", kind);
ch.children = std::mem::take(&mut c.children);
ch.push(El::new("Br"));
c.push(ch);
}
},
}
}
}
/// `ParagraphStyleRange`s of a story (or a table cell's story).
pub(super) fn story_paras(&mut self, s: &Story) -> Vec<El> {
let ranges = s.para_ranges();
let n = ranges.len();
ranges.into_iter().enumerate().map(|(pi, r)| self.story_para(s, pi, r, pi + 1 == n)).collect()
}
fn paragraph_runs(s: &Story, r: &std::ops::Range<usize>) -> Vec<(std::ops::Range<usize>, CharFormat)> {
// Character runs intersecting the paragraph.
let mut segs: Vec<(std::ops::Range<usize>, CharFormat)> = Vec::new();
for (rr, f) in s.runs() {
let a = rr.start.max(r.start);
let b = rr.end.min(r.end);
if a < b {
segs.push((a..b, f.clone()));
}
}
if segs.is_empty() {
let f = s.char_format_at(r.start).clone();
segs.push((r.start..r.start, f));
}
segs
}
fn story_para(&mut self, s: &Story, pi: usize, r: std::ops::Range<usize>, last: bool) -> El {
let pf = s.paras.get(pi).cloned().unwrap_or_default();
let mut psr = self.psr_el(&pf);
let ends_with_break = s.text.get(r.clone()).and_then(|t| t.chars().next_back()).is_some_and(st::is_break_char);
let segs = Self::paragraph_runs(s, &r);
let nseg = segs.len();
for (k, (rr, f)) in segs.into_iter().enumerate() {
let seg_start = rr.start;
let text = &s.text[rr];
// Split at break characters.
let mut cur = String::new();
let mut pending: Vec<Node> = Vec::new();
let mut csrs: Vec<El> = Vec::new();
let flush_text = |cur: &mut String, pending: &mut Vec<Node>| {
if !cur.is_empty() {
pending.push(Node::Text(std::mem::take(cur)));
}
if para_br && let Some(c) = csrs.last_mut() {
let nl = s.runs().find(|(rr, _)| rr.contains(&r.end)).and_then(|(_, nf)| change_type(nf.over.change));
match nl {
None => c.push(El::new("Br")),
Some(kind) => match c.children.last_mut() {
Some(Node::El(ch)) if ch.name == "Change" && ch.get("ChangeType") == Some(kind) => ch.push(El::new("Br")),
_ => {
let mut ch = El::new("Change").attr("Self", self.fresh()).attr("ChangeType", kind);
ch.push(El::new("Br"));
c.push(ch);
}
},
};
let flush_content = |pending: &mut Vec<Node>, out: &mut Vec<Node>| {
if !pending.is_empty() {
let mut c = El::new("Content");
c.children = std::mem::take(pending);
out.push(Node::El(c));
}
};
let mut out: Vec<Node> = Vec::new();
for (ci, ch) in text.char_indices() {
let ace = match ch {
st::PAGE_NUMBER | st::NEXT_PAGE_NUMBER | st::PREV_PAGE_NUMBER => Some("18"),
st::SECTION_MARKER => Some("19"),
st::INDENT_HERE => Some("7"),
st::RIGHT_INDENT_TAB => Some("8"),
_ => None,
};
let brk = match ch {
st::COLUMN_BREAK => Some("NextColumn"),
st::FRAME_BREAK => Some("NextFrame"),
st::PAGE_BREAK => Some("NextPage"),
st::ODD_PAGE_BREAK => Some("NextOddPage"),
st::EVEN_PAGE_BREAK => Some("NextEvenPage"),
_ => None,
};
if let Some(code) = ace {
flush_text(&mut cur, &mut pending);
pending.push(Node::Pi("ACE".into(), code.into()));
} else if ch == st::TABLE_ANCHOR {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if let Some(t) = s.para_table(pi) {
out.push(Node::El(self.table_el(t)));
}
}
for c in csrs {
psr.push(c);
} else if ch == designcraft_doc::ANCHOR_MARK {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
let k = s.text[..seg_start + ci].matches(designcraft_doc::ANCHOR_MARK).count();
if let Some(a) = s.anchors.get(k) {
out.push(Node::El(
El::new("HyperlinkTextDestination")
.attr("Self", anchor_self(a.id))
.attr("Name", &a.name)
.attr("Hidden", "false")
.attr("DestinationUniqueKey", a.id & 0xFFFF_FFFF),
));
}
} else if ch == designcraft_doc::OBJECT_MARK {
// An anchored object: the item inside the range with its anchoring settings.
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
let k = s.text[..seg_start + ci].matches(designcraft_doc::OBJECT_MARK).count();
if let Some(el) = self.anchored_object(s, k) {
out.push(Node::El(el));
}
} else if ch == designcraft_doc::INDEX_MARK {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
let k = s.text[..seg_start + ci].matches(designcraft_doc::INDEX_MARK).count();
if let Some(el) = self.index_reference(s, k) {
out.push(Node::El(el));
}
} else if ch == designcraft_doc::XREF_MARK {
// A source wrapping its current text (InDesign regenerates it on update).
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if !out.is_empty() {
let mut c = self.csr_el(&f);
c.children.append(&mut out);
csrs.push(c);
}
let k = s.text[..seg_start + ci].matches(designcraft_doc::XREF_MARK).count();
if let Some(x) = s.xrefs.get(k) {
let me = self.fresh();
let fmt = self.d.xref_formats.iter().position(|ff| ff.name == x.format).map_or("n".to_string(), xref_format_self);
let text = self.d.xref_values(x.target).map_or("??".into(), |mut v| {
v.page = "?".into();
designcraft_doc::xref::expand(self.d.xref_format(&x.format).map_or("<fullPara />", |ff| ff.definition.as_str()), &v)
});
let mut c = self.csr_el(&f);
let mut content = El::new("Content");
content.children.push(Node::Text(text));
c.push(content);
let mut src = El::new("CrossReferenceSource")
.attr("Self", &me)
.attr("AppliedFormat", fmt)
.attr("Name", format!("Cross-Reference {}", self.xref_sources.len() + 1))
.attr("Hidden", "false");
src.push(c);
csrs.push(src);
self.xref_sources.push((me, x.target));
}
} else if let Some(vi) = designcraft_doc::vars::var_index(ch) {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if let Some(v) = self.d.text_variables.get(vi) {
let result = self.d.variable_value(vi, None).unwrap_or_else(|| format!("{}{}", v.before, v.after));
out.push(Node::El(
El::new("TextVariableInstance")
.attr("Self", self.fresh())
.attr("Name", &v.name)
.attr("ResultText", result)
.attr("AssociatedTextVariable", format!("dTextVariablen{}", v.name)),
));
}
} else if ch == designcraft_doc::NOTE_MARK {
// Editorial notes aren't written to IDML yet.
} else if ch == designcraft_doc::ENDNOTE_REF {
// Endnotes aren't written to IDML yet: the endnote frame keeps their text
// (numbers included) and the references are left out.
} else if ch == designcraft_doc::FOOTNOTE_REF {
// The reference: its own range carrying the reference position.
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if !out.is_empty() {
let mut c = self.csr_el(&f);
c.children.append(&mut out);
csrs.push(c);
}
if let Some(n) = s.notes.get(s.notes_before(seg_start + ci)) {
let mut c = self.csr_el(&f);
let pos = self.d.footnote_options.ref_position;
if pos != designcraft_doc::Position::Normal {
c.set("Position", names::position_out(pos));
}
c.push(self.footnote_el(&n.text));
csrs.push(c);
}
} else if let Some(bt) = brk {
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
if !out.is_empty() {
let mut c = self.csr_el(&f);
c.children.append(&mut out);
csrs.push(c);
}
let mut c = self.csr_el(&f);
c.set("ParagraphBreakType", bt);
c.push(El::new("Br"));
csrs.push(c);
} else {
cur.push(ch);
}
}
paras.push(psr);
flush_text(&mut cur, &mut pending);
flush_content(&mut pending, &mut out);
let is_last_seg = k + 1 == nseg;
// A break character that ends the paragraph was written as its `Br`.
let para_br = is_last_seg && !last && !ends_with_break;
if !out.is_empty() || csrs.is_empty() || para_br {
let mut c = self.csr_el(&f);
c.children.extend(out);
csrs.push(c);
}
self.apply_tracked_changes(s, &f, &mut csrs, para_br, r.end);
for c in csrs {
psr.push(c);
}
}
paras
psr
}
pub(super) fn psr_el(&mut self, pf: &ParaFormat) -> El {
+136 -127
View File
@@ -1,7 +1,9 @@
use super::common::{bool_s, p, pct, pkg_root, with_props};
use super::item::{Group, text_frame_pref};
use super::story::with_list_tab;
use super::*;
use super::{
CHAR_BUILTINS, El, Ex, NO_PARA_ID, OBJECT_BUILTINS, PARA_BUILTINS, Rule, TOC_STYLE_ID, TOC_STYLE_NAME, document, escape_id, names, num, st,
};
impl<'a> Ex<'a> {
// ---------- Styles.xml ----------
@@ -72,132 +74,8 @@ impl<'a> Ex<'a> {
pg.emit(&mut rp, "ParagraphStyleGroup", "");
root.push(rp);
// Cell and table styles.
let mut cells = El::new("RootCellStyleGroup")
.attr("Self", self.fresh())
.child(El::new("CellStyle").attr("Self", "CellStyle/$ID/[None]").attr("AppliedParagraphStyle", NO_PARA_ID).attr("Name", "$ID/[None]"));
for cs in styles.cell.iter().filter(|c| c.name != designcraft_doc::NO_CELL_STYLE) {
let mut el = El::new("CellStyle")
.attr("Self", names::style_self("CellStyle", names::CELL_BUILTINS, &cs.name))
.attr("Name", names::style_name_out(names::CELL_BUILTINS, &cs.name));
if let Some(f) = &cs.fill {
el.set("FillColor", self.sw(f));
}
if let Some(t) = cs.fill_tint {
el.set("FillTint", pct(t as f64));
}
if let Some(i) = cs.insets {
for (k, v) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().zip(i) {
el.set(k, num(v));
}
}
for (key, value) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().zip(cs.inset_overrides) {
if let Some(value) = value {
el.set(key, num(value));
}
}
if let Some(vj) = cs.vj {
el.set("VerticalJustification", names::vj_out(vj));
}
if let Some(fb) = cs.first_baseline {
el.set("FirstBaselineOffset", names::first_baseline_out(fb));
}
if let Some(min) = cs.first_baseline_min {
el.set("MinimumFirstBaselineOffset", num(min));
}
if let Some(st) = &cs.stroke {
for side in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"] {
self.cell_stroke_attrs(&mut el, side, st);
}
}
for (side, attrs) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().zip(&cs.strokes) {
self.partial_cell_stroke_attrs(&mut el, side, attrs);
}
if let Some(ps) = &cs.paragraph_style {
el.set("AppliedParagraphStyle", names::style_self("ParagraphStyle", PARA_BUILTINS, ps));
}
let base = cs.based_on.as_deref().unwrap_or(designcraft_doc::NO_CELL_STYLE);
let based_on = if base == designcraft_doc::NO_CELL_STYLE {
p("BasedOn", "string", "$ID/[None]")
} else {
p("BasedOn", "object", names::style_self("CellStyle", names::CELL_BUILTINS, base))
};
cells = cells.child(with_props(el, vec![based_on]));
}
root.push(cells);
let mut tables = El::new("RootTableStyleGroup").attr("Self", self.fresh());
if !styles.table.iter().any(|s| s.name == designcraft_doc::NO_TABLE_STYLE) {
tables.push(El::new("TableStyle").attr("Self", "TableStyle/$ID/[No table style]").attr("Name", "$ID/[No table style]"));
}
if !styles.table.iter().any(|s| s.name == designcraft_doc::BASIC_TABLE) {
tables.push(with_props(
El::new("TableStyle").attr("Self", "TableStyle/$ID/[Basic Table]").attr("Name", "$ID/[Basic Table]"),
vec![p("BasedOn", "string", "$ID/[No table style]")],
));
}
for ts in &styles.table {
let mut el = El::new("TableStyle")
.attr("Self", names::style_self("TableStyle", names::TABLE_BUILTINS, &ts.name))
.attr("Name", names::style_name_out(names::TABLE_BUILTINS, &ts.name));
for (k, v) in [
("HeaderRegionCellStyle", &ts.header),
("BodyRegionCellStyle", &ts.body),
("FooterRegionCellStyle", &ts.footer),
("LeftColumnRegionCellStyle", &ts.left_column),
("RightColumnRegionCellStyle", &ts.right_column),
] {
if let Some(n) = v {
el.set(k, names::style_self("CellStyle", names::CELL_BUILTINS, n));
}
}
for (key, value) in [
("HeaderRegionSameAsBodyRegion", ts.header_same_as_body),
("FooterRegionSameAsBodyRegion", ts.footer_same_as_body),
("LeftColumnRegionSameAsBodyRegion", ts.left_column_same_as_body),
("RightColumnRegionSameAsBodyRegion", ts.right_column_same_as_body),
] {
if let Some(value) = value {
el.set(key, value);
}
}
if let Some(b) = &ts.border {
for side in ["Top", "Left", "Bottom", "Right"] {
self.cell_stroke_attrs(&mut el, &format!("{side}Border"), b);
}
}
for (side, attrs) in ["TopBorder", "LeftBorder", "BottomBorder", "RightBorder"].iter().zip(&ts.borders) {
self.partial_cell_stroke_attrs(&mut el, side, attrs);
}
if let Some(a) = &ts.alt_rows {
el.set("StartRowFillColor", self.sw(&a.first_color));
el.set("StartRowFillCount", a.first);
el.set("StartRowFillTint", pct(a.first_tint as f64));
el.set("EndRowFillColor", self.sw(&a.next_color));
el.set("EndRowFillCount", a.next);
el.set("EndRowFillTint", pct(a.next_tint as f64));
}
self.partial_alt_fill_attrs(&mut el, "Row", &ts.row_fills);
self.partial_alt_fill_attrs(&mut el, "Column", &ts.column_fills);
self.partial_alt_stroke_attrs(&mut el, "Row", &ts.row_strokes);
self.partial_alt_stroke_attrs(&mut el, "Column", &ts.column_strokes);
if let Some(v) = ts.space_before {
el.set("SpaceBefore", num(v));
}
if let Some(v) = ts.space_after {
el.set("SpaceAfter", num(v));
}
let mut props = Vec::new();
if ts.name != designcraft_doc::NO_TABLE_STYLE {
let base = ts.based_on.as_deref().unwrap_or(designcraft_doc::NO_TABLE_STYLE);
props.push(if base == designcraft_doc::NO_TABLE_STYLE {
p("BasedOn", "string", "$ID/[No table style]")
} else {
p("BasedOn", "object", names::style_self("TableStyle", names::TABLE_BUILTINS, base))
});
}
tables = tables.child(with_props(el, props));
}
root.push(tables);
self.append_cell_styles(&mut root, &styles);
self.append_table_styles(&mut root, &styles);
// Named numbered lists.
for l in &self.d.settings.lists {
root.push(
@@ -281,6 +159,137 @@ impl<'a> Ex<'a> {
document(&root)
}
fn append_cell_styles(&mut self, root: &mut El, styles: &designcraft_doc::Styles) {
let mut cells = El::new("RootCellStyleGroup")
.attr("Self", self.fresh())
.child(El::new("CellStyle").attr("Self", "CellStyle/$ID/[None]").attr("AppliedParagraphStyle", NO_PARA_ID).attr("Name", "$ID/[None]"));
for cs in styles.cell.iter().filter(|c| c.name != designcraft_doc::NO_CELL_STYLE) {
let mut el = El::new("CellStyle")
.attr("Self", names::style_self("CellStyle", names::CELL_BUILTINS, &cs.name))
.attr("Name", names::style_name_out(names::CELL_BUILTINS, &cs.name));
if let Some(f) = &cs.fill {
el.set("FillColor", self.sw(f));
}
if let Some(t) = cs.fill_tint {
el.set("FillTint", pct(t as f64));
}
if let Some(i) = cs.insets {
for (k, v) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().zip(i) {
el.set(k, num(v));
}
}
for (key, value) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().zip(cs.inset_overrides) {
if let Some(value) = value {
el.set(key, num(value));
}
}
if let Some(vj) = cs.vj {
el.set("VerticalJustification", names::vj_out(vj));
}
if let Some(fb) = cs.first_baseline {
el.set("FirstBaselineOffset", names::first_baseline_out(fb));
}
if let Some(min) = cs.first_baseline_min {
el.set("MinimumFirstBaselineOffset", num(min));
}
if let Some(st) = &cs.stroke {
for side in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"] {
self.cell_stroke_attrs(&mut el, side, st);
}
}
for (side, attrs) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().zip(&cs.strokes) {
self.partial_cell_stroke_attrs(&mut el, side, attrs);
}
if let Some(ps) = &cs.paragraph_style {
el.set("AppliedParagraphStyle", names::style_self("ParagraphStyle", PARA_BUILTINS, ps));
}
let base = cs.based_on.as_deref().unwrap_or(designcraft_doc::NO_CELL_STYLE);
let based_on = if base == designcraft_doc::NO_CELL_STYLE {
p("BasedOn", "string", "$ID/[None]")
} else {
p("BasedOn", "object", names::style_self("CellStyle", names::CELL_BUILTINS, base))
};
cells = cells.child(with_props(el, vec![based_on]));
}
root.push(cells);
}
fn append_table_styles(&mut self, root: &mut El, styles: &designcraft_doc::Styles) {
let mut tables = El::new("RootTableStyleGroup").attr("Self", self.fresh());
if !styles.table.iter().any(|s| s.name == designcraft_doc::NO_TABLE_STYLE) {
tables.push(El::new("TableStyle").attr("Self", "TableStyle/$ID/[No table style]").attr("Name", "$ID/[No table style]"));
}
if !styles.table.iter().any(|s| s.name == designcraft_doc::BASIC_TABLE) {
tables.push(with_props(
El::new("TableStyle").attr("Self", "TableStyle/$ID/[Basic Table]").attr("Name", "$ID/[Basic Table]"),
vec![p("BasedOn", "string", "$ID/[No table style]")],
));
}
for ts in &styles.table {
let mut el = El::new("TableStyle")
.attr("Self", names::style_self("TableStyle", names::TABLE_BUILTINS, &ts.name))
.attr("Name", names::style_name_out(names::TABLE_BUILTINS, &ts.name));
for (k, v) in [
("HeaderRegionCellStyle", &ts.header),
("BodyRegionCellStyle", &ts.body),
("FooterRegionCellStyle", &ts.footer),
("LeftColumnRegionCellStyle", &ts.left_column),
("RightColumnRegionCellStyle", &ts.right_column),
] {
if let Some(n) = v {
el.set(k, names::style_self("CellStyle", names::CELL_BUILTINS, n));
}
}
for (key, value) in [
("HeaderRegionSameAsBodyRegion", ts.header_same_as_body),
("FooterRegionSameAsBodyRegion", ts.footer_same_as_body),
("LeftColumnRegionSameAsBodyRegion", ts.left_column_same_as_body),
("RightColumnRegionSameAsBodyRegion", ts.right_column_same_as_body),
] {
if let Some(value) = value {
el.set(key, value);
}
}
if let Some(b) = &ts.border {
for side in ["Top", "Left", "Bottom", "Right"] {
self.cell_stroke_attrs(&mut el, &format!("{side}Border"), b);
}
}
for (side, attrs) in ["TopBorder", "LeftBorder", "BottomBorder", "RightBorder"].iter().zip(&ts.borders) {
self.partial_cell_stroke_attrs(&mut el, side, attrs);
}
if let Some(a) = &ts.alt_rows {
el.set("StartRowFillColor", self.sw(&a.first_color));
el.set("StartRowFillCount", a.first);
el.set("StartRowFillTint", pct(a.first_tint as f64));
el.set("EndRowFillColor", self.sw(&a.next_color));
el.set("EndRowFillCount", a.next);
el.set("EndRowFillTint", pct(a.next_tint as f64));
}
self.partial_alt_fill_attrs(&mut el, "Row", &ts.row_fills);
self.partial_alt_fill_attrs(&mut el, "Column", &ts.column_fills);
self.partial_alt_stroke_attrs(&mut el, "Row", &ts.row_strokes);
self.partial_alt_stroke_attrs(&mut el, "Column", &ts.column_strokes);
if let Some(v) = ts.space_before {
el.set("SpaceBefore", num(v));
}
if let Some(v) = ts.space_after {
el.set("SpaceAfter", num(v));
}
let mut props = Vec::new();
if ts.name != designcraft_doc::NO_TABLE_STYLE {
let base = ts.based_on.as_deref().unwrap_or(designcraft_doc::NO_TABLE_STYLE);
props.push(if base == designcraft_doc::NO_TABLE_STYLE {
p("BasedOn", "string", "$ID/[No table style]")
} else {
p("BasedOn", "object", names::style_self("TableStyle", names::TABLE_BUILTINS, base))
});
}
tables = tables.child(with_props(el, props));
}
root.push(tables);
}
/// The document's table of contents as a TOC style; its story names it in `AppliedTOCStyle`.
fn toc_style_el(&mut self, toc: &designcraft_doc::Toc) -> El {
let para = |n: &str| names::style_self("ParagraphStyle", PARA_BUILTINS, n);
+1 -1
View File
@@ -1,5 +1,5 @@
use super::common::{bool_s, pct, uid};
use super::*;
use super::{Affine, CharFormat, Content, El, Ex, Item, ParaFormat, Shape, names, num};
impl<'a> Ex<'a> {
pub(super) fn cell_stroke_attrs(&self, el: &mut El, prefix: &str, s: &designcraft_doc::CellStroke) {
+251 -205
View File
@@ -1,5 +1,26 @@
use super::common::{bool_s, p, pct};
use super::*;
use super::{CHAR_BUILTINS, CharAttrs, El, Ex, ListType, ParaAttrs, SpanColumns, escape_id, names, num, st};
// Optional paragraph values are omitted when inherited from the style.
macro_rules! n {
($a:expr, $el:expr, $f:ident, $k:literal) => {
if let Some(v) = $a.$f {
$el.set($k, num(v as f64));
}
};
($a:expr, $el:expr, $f:ident, $k:literal, pct) => {
if let Some(v) = $a.$f {
$el.set($k, pct(v as f64));
}
};
}
macro_rules! b {
($a:expr, $el:expr, $f:ident, $k:literal) => {
if let Some(v) = $a.$f {
$el.set($k, bool_s(v));
}
};
}
impl<'a> Ex<'a> {
pub(super) fn char_attrs(&mut self, el: &mut El, props: &mut Vec<El>, a: &CharAttrs) {
@@ -21,6 +42,93 @@ impl<'a> Ex<'a> {
if let Some(v) = a.character_alignment {
el.set("CharacterAlignment", crate::cjk::alignment_out(v));
}
self.char_font_attrs(el, props, a);
if let Some(v) = &a.fill {
el.set("FillColor", self.sw(v));
}
if let Some(v) = a.fill_tint {
el.set("FillTint", pct(v as f64));
}
if let Some(v) = &a.stroke {
el.set("StrokeColor", self.sw(v));
}
if let Some(v) = a.stroke_tint {
el.set("StrokeTint", pct(v as f64));
}
if let Some(v) = a.stroke_weight {
el.set("StrokeWeight", num(v));
}
if let Some(v) = a.stroke_behind_fill {
el.set("StrokeAlignment", if v { "OutsideAlignment" } else { "CenterAlignment" });
}
if let Some(v) = a.capitalization {
el.set("Capitalization", names::caps_out(v));
}
if let Some(v) = a.position {
el.set("Position", names::position_out(v));
}
if let Some(v) = a.underline {
el.set("Underline", bool_s(v));
}
if let Some(v) = a.strikethrough {
el.set("StrikeThru", bool_s(v));
}
if let Some(v) = a.ligatures {
el.set("Ligatures", bool_s(v));
}
self.char_decoration_attrs(el, a);
if let Some(list) = &a.otf_features {
for (tag, _, attr) in designcraft_doc::otf::TOGGLES {
el.set(attr, bool_s(designcraft_doc::otf::is_on(list, tag)));
}
let fig = designcraft_doc::otf::figures(list);
if let Some(f) = designcraft_doc::otf::FIGURES.iter().find(|f| f.0 == fig) {
el.set("OTFFigureStyle", f.3);
}
el.set("OTFStylisticSets", designcraft_doc::otf::stylistic_sets(list).to_string());
}
if let Some(v) = &a.glyph_form {
el.set("GlyphForm", v);
}
if let Some(v) = a.no_break {
el.set("NoBreak", bool_s(v));
}
self.char_arabic_attrs(el, a);
self.char_cjk_layout_attrs(el, props, a);
self.cjk_settings_chars(el, props, a);
if let Some(list) = &a.conditions {
el.set("AppliedConditions", list.iter().map(|c| format!("Condition/{}", escape_id(c))).collect::<Vec<_>>().join(" "));
}
if let Some(v) = &a.language {
el.set("AppliedLanguage", format!("$ID/{v}"));
}
}
fn char_arabic_attrs(&self, el: &mut El, a: &CharAttrs) {
if let Some(v) = a.digits {
el.set("DigitsType", names::digits_out(v));
}
if let Some(v) = a.character_direction {
el.set("CharacterDirection", crate::arabic::direction_out(v));
}
if let Some(v) = a.allow_kashidas {
el.set("Kashidas", if v { "DefaultKashidas" } else { "KashidasOff" });
}
if let Some(v) = a.diacritic_position {
el.set("DiacriticPosition", crate::arabic::diacritic_out(v));
}
if let Some(v) = a.diacritic_x_offset {
el.set("XOffsetDiacritic", num(v));
}
if let Some(v) = a.diacritic_y_offset {
el.set("YOffsetDiacritic", num(v));
}
if let Some(v) = &a.positional_form {
el.set("PositionalForm", v);
}
}
fn char_font_attrs(&mut self, el: &mut El, props: &mut Vec<El>, a: &CharAttrs) {
if let Some(f) = &a.font_family {
if let Some(composite) = self.d.styles.composite_font(f) {
props.push(p("AppliedFont", "object", format!("CompositeFont/{}", escape_id(&composite.name))));
@@ -63,39 +171,9 @@ impl<'a> Ex<'a> {
if let Some(v) = a.skew {
el.set("Skew", num(v));
}
if let Some(v) = &a.fill {
el.set("FillColor", self.sw(v));
}
if let Some(v) = a.fill_tint {
el.set("FillTint", pct(v as f64));
}
if let Some(v) = &a.stroke {
el.set("StrokeColor", self.sw(v));
}
if let Some(v) = a.stroke_tint {
el.set("StrokeTint", pct(v as f64));
}
if let Some(v) = a.stroke_weight {
el.set("StrokeWeight", num(v));
}
if let Some(v) = a.stroke_behind_fill {
el.set("StrokeAlignment", if v { "OutsideAlignment" } else { "CenterAlignment" });
}
if let Some(v) = a.capitalization {
el.set("Capitalization", names::caps_out(v));
}
if let Some(v) = a.position {
el.set("Position", names::position_out(v));
}
if let Some(v) = a.underline {
el.set("Underline", bool_s(v));
}
if let Some(v) = a.strikethrough {
el.set("StrikeThru", bool_s(v));
}
if let Some(v) = a.ligatures {
el.set("Ligatures", bool_s(v));
}
}
fn char_decoration_attrs(&self, el: &mut El, a: &CharAttrs) {
for (k, w, o, c, t) in [
("Underline", a.underline_weight, a.underline_offset, &a.underline_color, a.underline_tint),
("StrikeThru", a.strikethrough_weight, a.strikethrough_offset, &a.strikethrough_color, a.strikethrough_tint),
@@ -113,43 +191,9 @@ impl<'a> Ex<'a> {
el.set(&format!("{k}Tint"), num(t as f64 * 100.0));
}
}
if let Some(list) = &a.otf_features {
for (tag, _, attr) in designcraft_doc::otf::TOGGLES {
el.set(attr, bool_s(designcraft_doc::otf::is_on(list, tag)));
}
let fig = designcraft_doc::otf::figures(list);
if let Some(f) = designcraft_doc::otf::FIGURES.iter().find(|f| f.0 == fig) {
el.set("OTFFigureStyle", f.3);
}
el.set("OTFStylisticSets", designcraft_doc::otf::stylistic_sets(list).to_string());
}
if let Some(v) = &a.glyph_form {
el.set("GlyphForm", v);
}
if let Some(v) = a.no_break {
el.set("NoBreak", bool_s(v));
}
if let Some(v) = a.digits {
el.set("DigitsType", names::digits_out(v));
}
if let Some(v) = a.character_direction {
el.set("CharacterDirection", crate::arabic::direction_out(v));
}
if let Some(v) = a.allow_kashidas {
el.set("Kashidas", if v { "DefaultKashidas" } else { "KashidasOff" });
}
if let Some(v) = a.diacritic_position {
el.set("DiacriticPosition", crate::arabic::diacritic_out(v));
}
if let Some(v) = a.diacritic_x_offset {
el.set("XOffsetDiacritic", num(v));
}
if let Some(v) = a.diacritic_y_offset {
el.set("YOffsetDiacritic", num(v));
}
if let Some(v) = &a.positional_form {
el.set("PositionalForm", v);
}
}
fn char_cjk_layout_attrs(&mut self, el: &mut El, props: &mut Vec<El>, a: &CharAttrs) {
if let Some(r) = a.ruby.as_ref() {
el.set("RubyFlag", if r.is_empty() { "0" } else { "1" });
el.set("RubyString", r.as_str());
@@ -196,66 +240,9 @@ impl<'a> Ex<'a> {
if let Some(v) = a.warichu_chars_after_break {
el.set("WarichuCharsAfterBreak", v);
}
self.cjk_settings_chars(el, props, a);
if let Some(list) = &a.conditions {
el.set("AppliedConditions", list.iter().map(|c| format!("Condition/{}", escape_id(c))).collect::<Vec<_>>().join(" "));
}
if let Some(v) = &a.language {
el.set("AppliedLanguage", format!("$ID/{v}"));
}
}
/// Write paragraph attributes `a`, set over the resolved paragraph style `parent` (`None` for the
/// root style).
pub(super) fn para_attrs(&mut self, el: &mut El, props: &mut Vec<El>, a: &ParaAttrs, parent: Option<&str>) {
if let Some(v) = &a.mojikumi {
props.push(p("Mojikumi", if v.starts_with("MojikumiTable/") { "object" } else { "enumeration" }, v));
}
if let Some(v) = &a.kinsoku_type {
el.set("KinsokuType", v);
}
if let Some(v) = a.kinsoku_hang {
el.set(
"KinsokuHangType",
match v {
designcraft_doc::cjk::KinsokuHang::None => "None",
designcraft_doc::cjk::KinsokuHang::Regular => "KinsokuHangRegular",
designcraft_doc::cjk::KinsokuHang::Force => "KinsokuHangForce",
},
);
}
if let Some(v) = a.bunri_kinshi {
el.set("BunriKinshi", bool_s(v));
}
if let Some(v) = a.rensuuji {
el.set("Rensuuji", bool_s(v));
}
if let Some(v) = a.treat_ideographic_space_as_space {
el.set("TreatIdeographicSpaceAsSpace", bool_s(v));
}
cjk_settings_para(el, a);
if let Some(Some(k)) = &a.kinsoku {
props.push(p(
"KinsokuSet",
if k.name.is_empty() { "enumeration" } else { "object" },
if k.name.is_empty() { "Nothing".into() } else { format!("KinsokuTable/{}", escape_id(&k.name)) },
));
}
if let Some(n) = &a.list_name {
el.set(
"AppliedNumberingList",
if n.is_empty() { "NumberingList/$ID/[Default]".to_string() } else { format!("NumberingList/{}", escape_id(n)) },
);
}
if let Some(v) = a.start_at {
match v {
Some(n) => {
el.set("NumberingContinue", "false");
el.set("NumberingStartAt", n);
}
None => el.set("NumberingContinue", "true"),
}
}
fn para_nested_styles(&self, props: &mut Vec<El>, a: &ParaAttrs, parent: Option<&str>) {
// Nested and GREP styles: property lists of records. The drop cap's character style is the
// nested style list's leading "through 1 Dropcap" entry (IDML has no attribute for it): a
// drop cap style set here (`[None]` included) restyles that entry, and a real one is
@@ -319,25 +306,9 @@ impl<'a> Ex<'a> {
}
props.push(l);
}
macro_rules! n {
($f:ident, $k:literal) => {
if let Some(v) = a.$f {
el.set($k, num(v as f64));
}
};
($f:ident, $k:literal, pct) => {
if let Some(v) = a.$f {
el.set($k, pct(v as f64));
}
};
}
macro_rules! b {
($f:ident, $k:literal) => {
if let Some(v) = a.$f {
el.set($k, bool_s(v));
}
};
}
}
fn para_alignment_attrs(&self, el: &mut El, a: &ParaAttrs, parent: Option<&str>) {
if let Some(v) = a.align {
el.set("Justification", names::align_out(v));
}
@@ -356,20 +327,20 @@ impl<'a> Ex<'a> {
if let Some(Some(v)) = a.paragraph_kashida_width {
el.set("ParagraphKashidaWidth", num(v));
}
n!(left_indent, "LeftIndent");
n!(right_indent, "RightIndent");
n!(first_line_indent, "FirstLineIndent");
n!(last_line_indent, "LastLineIndent");
n!(space_before, "SpaceBefore");
n!(space_after, "SpaceAfter");
n!(a, el, left_indent, "LeftIndent");
n!(a, el, right_indent, "RightIndent");
n!(a, el, first_line_indent, "FirstLineIndent");
n!(a, el, last_line_indent, "LastLineIndent");
n!(a, el, space_before, "SpaceBefore");
n!(a, el, space_after, "SpaceAfter");
match a.same_para_style_spacing {
Some(Some(v)) => el.set("SameParaStyleSpacing", num(v)),
// Explicit Ignore: must be written so it still overrides a based-on style's value.
Some(None) => el.set("SameParaStyleSpacing", "Nothing"),
None => {}
}
n!(drop_cap_lines, "DropCapLines");
n!(drop_cap_chars, "DropCapCharacters");
n!(a, el, drop_cap_lines, "DropCapLines");
n!(a, el, drop_cap_chars, "DropCapCharacters");
if a.drop_cap_align_left.is_some() || a.drop_cap_scale_descenders.is_some() {
let d = parent.map(|name| self.d.styles.resolve_para_style(name).0).unwrap_or_default();
let bits = u32::from(a.drop_cap_align_left.unwrap_or(d.drop_cap_align_left))
@@ -403,40 +374,46 @@ impl<'a> Ex<'a> {
},
);
}
b!(hyphenate, "Hyphenation");
n!(hyph_min_word, "HyphenateWordsLongerThan");
n!(hyph_after_first, "HyphenateAfterFirst");
n!(hyph_before_last, "HyphenateBeforeLast");
n!(hyph_limit, "HyphenateLadderLimit");
n!(hyph_zone, "HyphenationZone");
b!(hyph_capitalized, "HyphenateCapitalizedWords");
b!(hyph_last_word, "HyphenateLastWord");
b!(hyph_across_column, "HyphenateAcrossColumns");
}
fn para_flow_attrs(&self, el: &mut El, a: &ParaAttrs) {
b!(a, el, hyphenate, "Hyphenation");
n!(a, el, hyph_min_word, "HyphenateWordsLongerThan");
n!(a, el, hyph_after_first, "HyphenateAfterFirst");
n!(a, el, hyph_before_last, "HyphenateBeforeLast");
n!(a, el, hyph_limit, "HyphenateLadderLimit");
n!(a, el, hyph_zone, "HyphenationZone");
b!(a, el, hyph_capitalized, "HyphenateCapitalizedWords");
b!(a, el, hyph_last_word, "HyphenateLastWord");
b!(a, el, hyph_across_column, "HyphenateAcrossColumns");
if let Some(v) = a.hyph_weight {
el.set("HyphenWeight", num((v * 10.0).round()));
}
n!(word_space_min, "MinimumWordSpacing", pct);
n!(word_space_desired, "DesiredWordSpacing", pct);
n!(word_space_max, "MaximumWordSpacing", pct);
n!(letter_space_min, "MinimumLetterSpacing", pct);
n!(letter_space_desired, "DesiredLetterSpacing", pct);
n!(letter_space_max, "MaximumLetterSpacing", pct);
n!(glyph_scale_min, "MinimumGlyphScaling", pct);
n!(glyph_scale_desired, "DesiredGlyphScaling", pct);
n!(glyph_scale_max, "MaximumGlyphScaling", pct);
n!(auto_leading, "AutoLeading", pct);
n!(a, el, word_space_min, "MinimumWordSpacing", pct);
n!(a, el, word_space_desired, "DesiredWordSpacing", pct);
n!(a, el, word_space_max, "MaximumWordSpacing", pct);
n!(a, el, letter_space_min, "MinimumLetterSpacing", pct);
n!(a, el, letter_space_desired, "DesiredLetterSpacing", pct);
n!(a, el, letter_space_max, "MaximumLetterSpacing", pct);
n!(a, el, glyph_scale_min, "MinimumGlyphScaling", pct);
n!(a, el, glyph_scale_desired, "DesiredGlyphScaling", pct);
n!(a, el, glyph_scale_max, "MaximumGlyphScaling", pct);
n!(a, el, auto_leading, "AutoLeading", pct);
if let Some(v) = a.single_word_justify {
el.set("SingleWordJustification", names::align_out(v));
}
n!(keep_with_next, "KeepWithNext");
b!(keep_lines_together, "KeepLinesTogether");
b!(ignore_optical_margin, "IgnoreEdgeAlignment");
b!(keep_all_lines, "KeepAllLinesTogether");
n!(keep_first, "KeepFirstLines");
n!(keep_last, "KeepLastLines");
n!(a, el, keep_with_next, "KeepWithNext");
b!(a, el, keep_lines_together, "KeepLinesTogether");
b!(a, el, ignore_optical_margin, "IgnoreEdgeAlignment");
b!(a, el, keep_all_lines, "KeepAllLinesTogether");
n!(a, el, keep_first, "KeepFirstLines");
n!(a, el, keep_last, "KeepLastLines");
if let Some(v) = a.start_paragraph {
el.set("StartParagraph", names::start_para_out(v));
}
}
fn para_columns_and_list_attrs(&mut self, el: &mut El, props: &mut Vec<El>, a: &ParaAttrs) {
if let Some(sc) = a.span_columns {
match sc {
SpanColumns::Single => el.set("SpanColumnType", "SingleColumn"),
@@ -450,8 +427,8 @@ impl<'a> Ex<'a> {
}
}
}
n!(split_inside_gutter, "SplitColumnInsideGutter");
n!(split_outside_gutter, "SplitColumnOutsideGutter");
n!(a, el, split_inside_gutter, "SplitColumnInsideGutter");
n!(a, el, split_outside_gutter, "SplitColumnOutsideGutter");
if let Some(r) = &a.rule_above {
self.rule(el, props, "RuleAbove", r);
}
@@ -497,22 +474,37 @@ impl<'a> Ex<'a> {
if let Some(b) = a.balance_ragged {
props.push(p("BalanceRaggedLines", "enumeration", if b { "FullyBalanced" } else { "NoBalancing" }));
}
b!(shading_on, "ParagraphShadingOn");
}
fn para_box_corners(el: &mut El, a: &ParaAttrs) {
const CORNERS: [&str; 4] = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"];
for (prefix, corners) in [("ParagraphBorder", a.border_corners), ("ParagraphShading", a.shading_corners)] {
if let Some(cs) = corners {
for (c, k) in cs.iter().zip(CORNERS) {
el.set(&format!("{prefix}{k}CornerOption"), names::corner_out(c.shape));
el.set(&format!("{prefix}{k}CornerRadius"), num(c.size));
}
}
}
}
fn para_box_attrs(&self, el: &mut El, props: &mut Vec<El>, a: &ParaAttrs) {
b!(a, el, shading_on, "ParagraphShadingOn");
if let Some(c) = &a.shading_color {
props.push(p("ParagraphShadingColor", "object", self.sw(c)));
}
n!(shading_tint, "ParagraphShadingTint", pct);
n!(a, el, shading_tint, "ParagraphShadingTint", pct);
const SIDES: [&str; 4] = ["Top", "Left", "Bottom", "Right"];
if let Some(o) = a.shading_offsets {
for (i, side) in SIDES.iter().enumerate() {
el.set(&format!("ParagraphShading{side}Offset"), num(o[i]));
}
}
b!(border_on, "ParagraphBorderOn");
b!(a, el, border_on, "ParagraphBorderOn");
if let Some(c) = &a.border_color {
props.push(p("ParagraphBorderColor", "object", self.sw(c)));
}
n!(border_tint, "ParagraphBorderTint", pct);
n!(a, el, border_tint, "ParagraphBorderTint", pct);
if let Some(w) = a.border_weights {
for (i, side) in SIDES.iter().enumerate() {
el.set(&format!("ParagraphBorder{side}LineWeight"), num(w[i]));
@@ -532,15 +524,7 @@ impl<'a> Ex<'a> {
if let Some(j) = a.border_join {
el.set("ParagraphBorderStrokeEndJoin", names::join_out(j));
}
const CORNERS: [&str; 4] = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"];
for (prefix, corners) in [("ParagraphBorder", a.border_corners), ("ParagraphShading", a.shading_corners)] {
if let Some(cs) = corners {
for (c, k) in cs.iter().zip(CORNERS) {
el.set(&format!("{prefix}{k}CornerOption"), names::corner_out(c.shape));
el.set(&format!("{prefix}{k}CornerRadius"), num(c.size));
}
}
}
Self::para_box_corners(el, a);
if let Some(w) = a.border_width {
el.set("ParagraphBorderWidth", names::box_width_out(w));
}
@@ -550,12 +534,12 @@ impl<'a> Ex<'a> {
if let Some(b) = a.border_bottom {
el.set("ParagraphBorderBottomOrigin", names::box_bottom_out(b));
}
b!(border_display_if_splits, "ParagraphBorderDisplayIfSplits");
b!(border_merge, "MergeConsecutiveParaBorders");
b!(a, el, border_display_if_splits, "ParagraphBorderDisplayIfSplits");
b!(a, el, border_merge, "MergeConsecutiveParaBorders");
if let Some(c) = &a.border_gap_color {
props.push(p("ParagraphBorderGapColor", "object", self.sw(c)));
}
n!(border_gap_tint, "ParagraphBorderGapTint", pct);
n!(a, el, border_gap_tint, "ParagraphBorderGapTint", pct);
if let Some(w) = a.shading_width {
el.set("ParagraphShadingWidth", names::box_width_out(w));
}
@@ -565,11 +549,14 @@ impl<'a> Ex<'a> {
if let Some(b) = a.shading_bottom {
el.set("ParagraphShadingBottomOrigin", names::box_bottom_out(b));
}
b!(shading_clip, "ParagraphShadingClipToFrame");
b!(shading_nonprinting, "ParagraphShadingSuppressPrinting");
n!(span_space_before, "SpanColumnMinSpaceBefore");
n!(span_space_after, "SpanColumnMinSpaceAfter");
n!(list_level, "NumberingLevel");
b!(a, el, shading_clip, "ParagraphShadingClipToFrame");
b!(a, el, shading_nonprinting, "ParagraphShadingSuppressPrinting");
}
fn para_list_attrs(&self, el: &mut El, props: &mut Vec<El>, a: &ParaAttrs) {
n!(a, el, span_space_before, "SpanColumnMinSpaceBefore");
n!(a, el, span_space_after, "SpanColumnMinSpaceAfter");
n!(a, el, list_level, "NumberingLevel");
for (k, style) in [("BulletsCharacterStyle", &a.bullet_char_style), ("NumberingCharacterStyle", &a.number_char_style)] {
if let Some(st) = style {
props.push(p(k, "object", names::style_self("CharacterStyle", CHAR_BUILTINS, st)));
@@ -581,7 +568,7 @@ impl<'a> Ex<'a> {
if let Some(v) = a.number_align {
el.set("NumberingAlignment", names::list_align_out(v));
}
b!(restart_numbers, "NumberingApplyRestartPolicy");
b!(a, el, restart_numbers, "NumberingApplyRestartPolicy");
if let Some(level) = a.restart_after_level {
let (policy, level) = if level == 0 { ("AnyPreviousLevel", 0) } else { ("SpecificPreviousLevel", level) };
props.push(
@@ -598,6 +585,65 @@ impl<'a> Ex<'a> {
props.push(p("BulletsFontStyle", "string", f.clone()));
}
}
/// Write paragraph attributes `a`, set over the resolved paragraph style `parent` (`None` for the
/// root style).
pub(super) fn para_attrs(&mut self, el: &mut El, props: &mut Vec<El>, a: &ParaAttrs, parent: Option<&str>) {
if let Some(v) = &a.mojikumi {
props.push(p("Mojikumi", if v.starts_with("MojikumiTable/") { "object" } else { "enumeration" }, v));
}
if let Some(v) = &a.kinsoku_type {
el.set("KinsokuType", v);
}
if let Some(v) = a.kinsoku_hang {
el.set(
"KinsokuHangType",
match v {
designcraft_doc::cjk::KinsokuHang::None => "None",
designcraft_doc::cjk::KinsokuHang::Regular => "KinsokuHangRegular",
designcraft_doc::cjk::KinsokuHang::Force => "KinsokuHangForce",
},
);
}
if let Some(v) = a.bunri_kinshi {
el.set("BunriKinshi", bool_s(v));
}
if let Some(v) = a.rensuuji {
el.set("Rensuuji", bool_s(v));
}
if let Some(v) = a.treat_ideographic_space_as_space {
el.set("TreatIdeographicSpaceAsSpace", bool_s(v));
}
cjk_settings_para(el, a);
if let Some(Some(k)) = &a.kinsoku {
props.push(p(
"KinsokuSet",
if k.name.is_empty() { "enumeration" } else { "object" },
if k.name.is_empty() { "Nothing".into() } else { format!("KinsokuTable/{}", escape_id(&k.name)) },
));
}
if let Some(n) = &a.list_name {
el.set(
"AppliedNumberingList",
if n.is_empty() { "NumberingList/$ID/[Default]".to_string() } else { format!("NumberingList/{}", escape_id(n)) },
);
}
if let Some(v) = a.start_at {
match v {
Some(n) => {
el.set("NumberingContinue", "false");
el.set("NumberingStartAt", n);
}
None => el.set("NumberingContinue", "true"),
}
}
self.para_nested_styles(props, a, parent);
self.para_alignment_attrs(el, a, parent);
self.para_flow_attrs(el, a);
self.para_columns_and_list_attrs(el, props, a);
self.para_box_attrs(el, props, a);
self.para_list_attrs(el, props, a);
}
}
/// Auto tate-chu-yoko, Japanese composition and grid settings of a paragraph range or style.
+14 -1
View File
@@ -284,7 +284,7 @@ impl<'r> Importer<'r> {
self.next_id
}
fn run(&mut self, _root: &El, top: &[El]) -> Result<()> {
fn read_preferences(&mut self, top: &[El]) {
// Preferences first (page size, facing pages).
for e in top {
match e.local() {
@@ -341,6 +341,9 @@ impl<'r> Importer<'r> {
_ => {}
}
}
}
fn read_composite_fonts(&mut self, top: &[El]) {
for e in top.iter().filter(|e| e.local() == "CompositeFont") {
let name =
e.get("Name").map(str::to_string).unwrap_or_else(|| unescape_id(e.get("Self").unwrap_or("").trim_start_matches("CompositeFont/")));
@@ -365,6 +368,9 @@ impl<'r> Importer<'r> {
.collect();
self.styles.composite_fonts.push(designcraft_doc::cjk::CompositeFont { name, entries });
}
}
fn read_cjk_tables(&mut self, top: &[El]) -> Result<()> {
for e in top.iter().filter(|e| e.local() == "KinsokuTable") {
if e.get("CantBeginLineChars").is_some() || e.get("CantEndLineChars").is_some() {
self.kinsoku.insert(
@@ -394,6 +400,13 @@ impl<'r> Importer<'r> {
overrides,
});
}
Ok(())
}
fn run(&mut self, _root: &El, top: &[El]) -> Result<()> {
self.read_preferences(top);
self.read_composite_fonts(top);
self.read_cjk_tables(top)?;
self.graphics(top);
self.styles(top);
if let Some(e) = top.iter().find(|e| e.local() == "FootnoteOption") {
+1 -1
View File
@@ -1,5 +1,5 @@
use super::values::tint;
use super::*;
use super::{BaselineGrid, El, GridRelative, Importer, PARA_BUILTINS, names};
/// IDML stores a sort key on each topic, rather than on its page references.
#[derive(Clone, Default)]
+1 -1
View File
@@ -1,5 +1,5 @@
use super::values::tint;
use super::*;
use super::{Columns, ColumnsKind, El, Margins, TextFrameOptions, TextWrap, names};
/// Drop-shadow angle (degrees) from IDML offsets (inverse of export's mapping).
/// A `TextWrapPreference` element (of a page item or a placed graphic).
+17 -11
View File
@@ -2,7 +2,10 @@ use super::frame::{Prefs, frame_grid_relative, text_frame_options, text_wrap};
use super::object_style::INDESIGN_CORNER_SIZE;
use super::package::uri_to_path;
use super::values::{nums, parse_affine, parse_point, tint};
use super::*;
use super::{
Affine, Anchor, Arc, Asset, AssetId, Content, Corner, CornerOptions, El, Fill, Importer, Item, ItemId, OBJECT_BUILTINS, PathData, Point, Shape,
Story, StoryId, Stroke, SubPath, TextFrame, TextFrameOptions, base64_decode, names, sniff_image, swatch, unescape_id,
};
/// The viewBox size of an SVG whose root has a viewBox but no width/height (percentages count as
/// none): InDesign places such a file at one viewBox unit to the point.
@@ -177,7 +180,11 @@ impl<'r> Importer<'r> {
self.ctx.threads.push((id, story_self, "n".into(), "n".into()));
}
}
// Content.
self.read_content(e, tag, id, &mut it);
Some(it)
}
fn read_content(&mut self, e: &El, tag: &str, id: ItemId, item: &mut Item) {
match tag {
"TextFrame" => {
let story_self = e.get("ParentStory").unwrap_or("n").to_string();
@@ -206,7 +213,7 @@ impl<'r> Importer<'r> {
relative_to: g.get("BaselineFrameGridRelativeOption").map(frame_grid_relative).unwrap_or_default(),
});
}
it.content = Content::Text(TextFrame { story, options });
item.content = Content::Text(TextFrame { story, options });
self.ctx.threads.push((
id,
story_self,
@@ -221,18 +228,18 @@ impl<'r> Importer<'r> {
kids.push(Arc::new(k));
}
}
it.content = Content::Group { items: kids };
it.fill = Fill::none();
it.stroke = Stroke::none();
item.content = Content::Group { items: kids };
item.fill = Fill::none();
item.stroke = Stroke::none();
}
_ => {
if let Some(g) = e.elements().find(|c| matches!(c.local(), "Image" | "PDF" | "EPS" | "ImportedPage" | "WMF" | "PICT" | "SVG")) {
it.content = self.graphic(g);
item.content = self.graphic(g);
let fill = self.image_fill(g);
if let Content::Graphic(gr) = &mut it.content {
if let Content::Graphic(gr) = &mut item.content {
gr.fill = fill;
}
if let (Some(ff), Content::Graphic(gr)) = (self.with_style(e, "FrameFittingOption"), &mut it.content) {
if let (Some(ff), Content::Graphic(gr)) = (self.with_style(e, "FrameFittingOption"), &mut item.content) {
gr.crop = [ff.num("TopCrop"), ff.num("LeftCrop"), ff.num("BottomCrop"), ff.num("RightCrop")].map(|v| v.unwrap_or(0.0));
gr.fit_align = ff.get("FittingAlignment").map_or(4, names::anchor_in);
if ff.get("AutoFit") == Some("true") {
@@ -251,12 +258,11 @@ impl<'r> Importer<'r> {
.map(Arc::new)
.collect();
if !kids.is_empty() {
it.content = Content::Group { items: kids };
item.content = Content::Group { items: kids };
}
}
}
}
Some(it)
}
/// The swatch applied to a placed image (`FillColor`, `FillTint`): it colours a greyscale or
+1 -1
View File
@@ -1,5 +1,5 @@
use super::values::{finite, tint};
use super::*;
use super::{Corner, DirectionalFeather, DropShadow, Effects, El, GradientFeather, HashMap, Importer, Node, Stroke, names};
/// InDesign's corner size where a file gives a corner shape without one.
pub(super) const INDESIGN_CORNER_SIZE: f64 = 12.0;
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{HashMap, IdmlError, Read, Result};
/// Most entries an IDML package may hold.
const MAX_ENTRIES: usize = 100_000;
+3 -1
View File
@@ -1,6 +1,8 @@
use super::frame::{fit_columns, margins_of};
use super::values::{nums, parse_affine};
use super::*;
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
+4 -1
View File
@@ -1,5 +1,8 @@
use super::text_attrs::{ace_char, clear_story_drop_cap_styles, inherits_drop_cap_style};
use super::*;
use super::{
Arc, BTreeMap, CharAttrs, CharFormat, CharRun, El, HashMap, HashSet, Importer, Item, ItemId, ParaFormat, Story, StoryId, Table, names, st,
};
use crate::xml::Node;
impl<'r> Importer<'r> {
// ---------- stories ----------
+4 -1
View File
@@ -1,6 +1,9 @@
use super::table::uniform_cell_stroke;
use super::values::tint;
use super::*;
use super::{
CHAR_BUILTINS, CharacterStyle, El, Fill, Importer, OBJECT_BUILTINS, ObjectStyle, PARA_BUILTINS, ParagraphStyle, Rule, names, st, swatch,
unescape_id,
};
impl<'r> Importer<'r> {
// ---------- styles ----------
+4 -1
View File
@@ -1,6 +1,9 @@
use super::frame::{Prefs, text_frame_options};
use super::values::{lab_to_color, nums};
use super::*;
use super::{
Color, ColorType, El, Gradient, GradientKind, GradientStop, HashMap, HashSet, Importer, Styles, Swatch, SwatchValue, TextFrameOptions, names,
swatch,
};
impl<'r> Importer<'r> {
// ---------- swatches ----------
+1 -1
View File
@@ -1,5 +1,5 @@
use super::values::{span, tint};
use super::*;
use super::{CellRange, El, Importer, RowHeightMode, StoryId, Table, names, swatch};
/// Preserve the legacy uniform field only when all four sides explicitly specify every
/// property. Partial records must stay sparse so they can inherit from a parent style.
+72 -61
View File
@@ -1,7 +1,10 @@
use super::object_style::corners;
use super::story::MAX_STORY_NESTING;
use super::values::tint;
use super::*;
use super::{
Arc, CharAttrs, El, GridAlign, HashSet, IdmlError, Importer, Kerning, ListType, ParaAttrs, Result, SpanColumns, Story, Styles, TabStop, names,
st, unescape_id,
};
/// Whether sanitizing a file's tab list dropped, moved or shortened a stop. Sorting, and the `.`
/// a char stop with no character aligns on, change nothing.
@@ -213,49 +216,7 @@ impl<'r> Importer<'r> {
Some("true") => a.start_at = Some(None),
_ => {}
}
// Nested and GREP styles.
if let Some(l) = e.prop_el("AllNestedStyles") {
let v: Vec<designcraft_doc::NestedStyle> = l
.find_all("ListItem")
.map(|it| {
let t = |k: &str| it.find(k).map(|x| x.text_content()).unwrap_or_default();
let d = it.find("Delimiter");
designcraft_doc::NestedStyle {
style: self.listed_char_style_ref(&t("AppliedCharacterStyle")),
through: t("Inclusive").trim() != "false",
count: t("Repetition").trim().parse().unwrap_or(1),
until: names::nested_until_in(d.and_then(|d| d.get("type")).unwrap_or("enumeration"), &t("Delimiter")),
}
})
.collect();
// IDML has no drop cap style attribute: the drop cap's character style is the list's
// leading "through 1 Dropcap" nested style.
a.drop_cap_style = v.first().filter(|ns| ns.is_drop_cap()).map(|ns| ns.style.clone());
a.nested_styles = Some(v);
}
if let Some(l) = e.prop_el("AllNestedLineStyles") {
let v: Vec<designcraft_doc::NestedLineStyle> = l
.find_all("ListItem")
.map(|it| {
let t = |k: &str| it.find(k).map(|x| x.text_content()).unwrap_or_default();
designcraft_doc::NestedLineStyle {
style: self.listed_char_style_ref(&t("AppliedCharacterStyle")),
lines: t("LineCount").trim().parse().unwrap_or(1),
}
})
.collect();
a.nested_line_styles = Some(v);
}
if let Some(l) = e.prop_el("AllGREPStyles") {
let v: Vec<designcraft_doc::GrepStyle> = l
.find_all("ListItem")
.map(|it| {
let t = |k: &str| it.find(k).map(|x| x.text_content()).unwrap_or_default();
designcraft_doc::GrepStyle { style: self.listed_char_style_ref(&t("AppliedCharacterStyle")), pattern: t("GrepExpression") }
})
.collect();
a.grep_styles = Some(v);
}
self.read_nested_styles(e, &mut a);
a.space_after = e.num("SpaceAfter");
// A number, or an enumeration (Ignore) that turns it off even over a based-on style's value.
a.same_para_style_spacing = e.prop("SameParaStyleSpacing").map(|v| v.trim().parse::<f64>().ok().filter(|v| v.is_finite()));
@@ -320,23 +281,7 @@ impl<'r> Importer<'r> {
a.split_outside_gutter = e.num("SplitColumnOutsideGutter").filter(|v| v.is_finite());
a.rule_above = self.rule(e, "RuleAbove");
a.rule_below = self.rule(e, "RuleBelow");
if let Some(tl) = e.prop_el("TabList") {
let mut tabs = Vec::new();
for li in tl.find_all("ListItem") {
let get = |k: &str| li.find(k).map(|x| x.text_content());
tabs.push(TabStop {
position: get("Position").and_then(|v| v.trim().parse::<f64>().ok()).filter(|v| v.is_finite()).unwrap_or(0.0),
align: names::tab_align_in(get("Alignment").unwrap_or_default().trim()),
leader: get("Leader").unwrap_or_default(),
align_on: get("AlignmentCharacter").filter(|c| c != ".").unwrap_or_default(),
});
}
let sanitized = TabStop::sanitized_list(tabs.clone());
if tab_list_repaired(&tabs, &sanitized) {
self.repaired_tab_lists = self.repaired_tab_lists.saturating_add(1);
}
a.tabs = Some(sanitized);
}
a.tabs = self.read_tabs(e);
a.list_type = e.prop("BulletsAndNumberingListType").map(|v| match v.trim() {
"BulletList" => ListType::Bullets,
"NumberedList" => ListType::Numbers,
@@ -417,6 +362,72 @@ impl<'r> Importer<'r> {
}
a
}
fn read_nested_styles(&self, e: &El, attrs: &mut ParaAttrs) {
if let Some(list) = e.prop_el("AllNestedStyles") {
let styles: Vec<designcraft_doc::NestedStyle> = list
.find_all("ListItem")
.map(|item| {
let text = |key: &str| item.find(key).map(|element| element.text_content()).unwrap_or_default();
let delimiter = item.find("Delimiter");
designcraft_doc::NestedStyle {
style: self.listed_char_style_ref(&text("AppliedCharacterStyle")),
through: text("Inclusive").trim() != "false",
count: text("Repetition").trim().parse().unwrap_or(1),
until: names::nested_until_in(delimiter.and_then(|element| element.get("type")).unwrap_or("enumeration"), &text("Delimiter")),
}
})
.collect();
// IDML has no drop cap style attribute: the drop cap's character style is the list's
// leading "through 1 Dropcap" nested style.
attrs.drop_cap_style = styles.first().filter(|style| style.is_drop_cap()).map(|style| style.style.clone());
attrs.nested_styles = Some(styles);
}
if let Some(list) = e.prop_el("AllNestedLineStyles") {
let styles = list
.find_all("ListItem")
.map(|item| {
let text = |key: &str| item.find(key).map(|element| element.text_content()).unwrap_or_default();
designcraft_doc::NestedLineStyle {
style: self.listed_char_style_ref(&text("AppliedCharacterStyle")),
lines: text("LineCount").trim().parse().unwrap_or(1),
}
})
.collect();
attrs.nested_line_styles = Some(styles);
}
if let Some(list) = e.prop_el("AllGREPStyles") {
let styles = list
.find_all("ListItem")
.map(|item| {
let text = |key: &str| item.find(key).map(|element| element.text_content()).unwrap_or_default();
designcraft_doc::GrepStyle { style: self.listed_char_style_ref(&text("AppliedCharacterStyle")), pattern: text("GrepExpression") }
})
.collect();
attrs.grep_styles = Some(styles);
}
}
fn read_tabs(&mut self, e: &El) -> Option<Vec<TabStop>> {
let list = e.prop_el("TabList")?;
let tabs: Vec<_> = list
.find_all("ListItem")
.map(|item| {
let get = |key: &str| item.find(key).map(|element| element.text_content());
TabStop {
position: get("Position").and_then(|value| value.trim().parse::<f64>().ok()).filter(|value| value.is_finite()).unwrap_or(0.0),
align: names::tab_align_in(get("Alignment").unwrap_or_default().trim()),
leader: get("Leader").unwrap_or_default(),
align_on: get("AlignmentCharacter").filter(|character| character != ".").unwrap_or_default(),
}
})
.collect();
let sanitized = TabStop::sanitized_list(tabs.clone());
if tab_list_repaired(&tabs, &sanitized) {
self.repaired_tab_lists = self.repaired_tab_lists.saturating_add(1);
}
Some(sanitized)
}
}
pub(super) fn ace_char(v: &str) -> Option<char> {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Affine, Color, El, Point, lab};
/// An XMP date property, written as an element or an attribute.
pub(super) fn xmp_date(xmp: &str, key: &str) -> Option<i64> {
+75 -9
View File
@@ -733,6 +733,15 @@ fn kanji_page_numbers() {
#[test]
fn imports_hand_written_fixture() {
let d = import_idml_with(&fixture(), &|_| None).unwrap();
assert_fixture_settings_and_pages(&d);
assert_fixture_sections_and_layers(&d);
assert_fixture_swatches(&d);
assert_fixture_styles(&d);
assert_fixture_text(&d);
assert_fixture_items(&d);
}
fn assert_fixture_settings_and_pages(d: &Document) {
assert_eq!(d.title, "Fixture");
assert!(d.settings.facing_pages);
assert_eq!((d.settings.page_width, d.settings.page_height), (500.0, 700.0));
@@ -748,12 +757,18 @@ fn imports_hand_written_fixture() {
assert_eq!(sp.pages[0].parent, Some(d.parents[0].id));
let info = d.parents[0].parent.as_ref().unwrap();
assert_eq!((info.prefix.as_str(), info.name.as_str()), ("A", "Parent"));
}
fn assert_fixture_sections_and_layers(d: &Document) {
// Sections.
assert_eq!(d.page_name(0), "v");
assert_eq!(d.page_name(1), "vi");
// Layers.
assert_eq!(d.layers[0].name, "Art");
assert_eq!(d.layers[0].color, [255, 0, 0]);
}
fn assert_fixture_swatches(d: &Document) {
// Swatches.
assert!(matches!(d.swatch("Brand").unwrap().value, SwatchValue::Color { color_type: designcraft_color::ColorType::Spot, .. }));
assert!(matches!(&d.swatch("Brand 50%").unwrap().value, SwatchValue::Tint { base, tint } if base == "Brand" && *tint == 0.5));
@@ -767,6 +782,9 @@ fn imports_hand_written_fixture() {
let mr = &d.parents[0].items[0];
assert_eq!(mr.fill.swatch, "C=10 M=20 Y=30 K=40");
assert_eq!(mr.bounds(), Rect::new(20.0, 20.0, 100.0, 50.0));
}
fn assert_fixture_styles(d: &Document) {
// Styles.
let body = d.styles.para("Text/Body").expect("grouped style");
assert_eq!(body.based_on.as_deref(), Some(st::BASIC_PARAGRAPH));
@@ -774,6 +792,9 @@ fn imports_hand_written_fixture() {
assert_eq!(body.chars.leading, Some(designcraft_doc::Leading::Points(13.0)));
assert_eq!(body.para.align, Some(designcraft_doc::Align::LeftJustified));
assert_eq!(d.styles.char_style("Strong").unwrap().chars.font_style.as_deref(), Some("Bold"));
}
fn assert_fixture_text(d: &Document) {
// Frames, threads, text.
let story = d.stories.values().next().unwrap();
assert_eq!(story.text, format!("Hello & bold\nPage {}\tend{}\nafter", st::PAGE_NUMBER, st::FRAME_BREAK));
@@ -791,6 +812,10 @@ fn imports_hand_written_fixture() {
let o = &f1.text_frame().unwrap().options;
assert_eq!((o.columns, o.gutter, o.inset), (2, 8.0, [1.0, 2.0, 3.0, 4.0]));
assert_eq!(d.item(story.frames[1]).unwrap().bounds(), Rect::new(550.0, 50.0, 650.0, 150.0));
}
fn assert_fixture_items(d: &Document) {
let sp = &d.spreads[0];
// Group child keeps its own transform relative to the group.
let g = sp.items.iter().find(|i| i.shape == Shape::Group).unwrap();
assert_eq!(g.bounds(), Rect::new(510.0, 370.0, 560.0, 420.0));
@@ -2431,7 +2456,20 @@ fn fixture_with_table_styles(styles: &str, story: &str) -> Vec<u8> {
fn table_style_cascade_preserves_sparse_edges_and_local_overrides() {
let d = import_idml(&fixture_with_table_styles(CASCADE_STYLES, CASCADE_STORY)).unwrap();
d.check().unwrap();
let table = d.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
let table = first_table(&d);
assert_cascade_table_options(table);
assert_cascade_header_cell(table);
assert_cascade_explicit_cell(table);
assert_cascade_local_cell(table);
assert_cascade_body_cells(table);
assert_cascade_round_trip(&d, table);
}
fn first_table(doc: &Document) -> &designcraft_doc::Table {
doc.stories.values().flat_map(|s| s.tables.values()).next().unwrap()
}
fn assert_cascade_table_options(table: &designcraft_doc::Table) {
assert_eq!((table.nrows(), table.ncols()), (2, 3));
assert_eq!(table.style, "Table");
assert_eq!(table.options.space_before, 19.0);
@@ -2440,6 +2478,9 @@ fn table_style_cascade_preserves_sparse_edges_and_local_overrides() {
let fills = table.options.alt_rows.as_ref().unwrap();
assert_eq!((fills.first, fills.next, fills.first_tint), (1, 1, 0.2));
assert_eq!(fills.first_color, "Brand");
}
fn assert_cascade_header_cell(table: &designcraft_doc::Table) {
let header = table.cell(0, 0).unwrap();
assert_eq!(header.fill, designcraft_color::swatch::PAPER, "header takes precedence over left region");
assert!(header.style.is_empty(), "a table region does not become AppliedCellStyle");
@@ -2453,6 +2494,9 @@ fn table_style_cascade_preserves_sparse_edges_and_local_overrides() {
assert_eq!(header.border_overrides, [true; 4]);
assert_eq!(header.stroke_defined, [true; 4]);
assert_eq!(header.stroke_priorities, [0; 4]);
}
fn assert_cascade_explicit_cell(table: &designcraft_doc::Table) {
let explicit = table.cell(0, 1).unwrap();
assert_eq!(explicit.fill, "Brand");
assert_eq!(explicit.fill_tint, 0.75);
@@ -2461,6 +2505,9 @@ fn table_style_cascade_preserves_sparse_edges_and_local_overrides() {
assert_eq!(explicit.strokes[0].color, designcraft_color::swatch::NONE);
assert_eq!(explicit.strokes[3].weight, 5.0);
assert_eq!(explicit.strokes[3].tint, 0.3);
}
fn assert_cascade_local_cell(table: &designcraft_doc::Table) {
let local = table.cell(0, 2).unwrap();
assert_eq!(local.fill, designcraft_color::swatch::NONE);
assert_eq!(local.fill_tint, 0.0);
@@ -2470,13 +2517,18 @@ fn table_style_cascade_preserves_sparse_edges_and_local_overrides() {
assert_eq!(local.strokes[3].color, designcraft_color::swatch::NONE);
assert_eq!(local.stroke_priorities, [0, 0, 0, 7]);
assert_eq!(local.text.paras[0].style, st::BASIC_PARAGRAPH, "explicit paragraph style wins");
}
fn assert_cascade_body_cells(table: &designcraft_doc::Table) {
assert_eq!(table.cell(1, 0).unwrap().fill, designcraft_color::swatch::BLACK);
assert_eq!(table.cell(1, 0).unwrap().strokes[1].weight, 0.0);
assert_eq!(table.cell(1, 1).unwrap().fill_tint, 0.5);
assert_eq!(table.cell(1, 2).unwrap().fill, "Brand");
}
let back = import_idml(&export_idml(&d)).unwrap();
let roundtrip = back.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
fn assert_cascade_round_trip(d: &Document, table: &designcraft_doc::Table) {
let back = import_idml(&export_idml(d)).unwrap();
let roundtrip = first_table(&back);
for (before, after) in table.cells.iter().zip(&roundtrip.cells) {
assert_eq!(after.strokes, before.strokes);
assert_eq!(after.border_overrides, before.border_overrides);
@@ -3000,14 +3052,22 @@ fn kenten_shatai_and_grid_settings_import_and_round_trip() {
#[test]
fn ruby_settings_import_from_idml_and_round_trip() {
use designcraft_doc::cjk_settings::AdornmentOverprint;
use designcraft_doc::ruby::{RubyAlignment, RubyOverhang, RubyParentSpacing, RubyPosition, RubyType};
// InDesign's names: RubyFlag is a number, colours and fonts are properties.
let story = r#"<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging"><Story Self="s1">
<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/[No paragraph style]">
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]" RubyFlag="1" RubyString="かん じ" RubyType="PerCharacterRuby" RubyAlignment="RubyCenter" RubyPosition="BelowLeft" RubyYOffset="-1.4173228" RubyFontSize="-1" RubyXScale="90" RubyParentOverhangAmount="RubyOverhangNoLimit" RubyParentSpacing="RubyParentBothSides" RubyAutoAlign="false" RubyAutoScaling="true" RubyParentScalingPercent="40" RubyAutoTcyDigits="2" RubyTint="-1" RubyOverprintFill="Auto"><Properties><RubyFont type="string">$ID/</RubyFont><RubyFontStyle type="enumeration">Nothing</RubyFontStyle><RubyFill type="string">Text Color</RubyFill></Properties><Content>漢字</Content></CharacterStyleRange>
</ParagraphStyleRange></Story></idPkg:Story>"#;
let doc = import_idml(&fixture_with_story(story)).unwrap();
let imported = import_idml(&fixture_with_story(story)).unwrap();
assert_imported_ruby_settings(&imported);
let doc = ruby_round_trip_document();
let back = import_idml(&export_idml(&doc)).unwrap();
assert_ruby_round_trip(&doc, &back);
}
fn assert_imported_ruby_settings(doc: &Document) {
use designcraft_doc::cjk_settings::AdornmentOverprint;
use designcraft_doc::ruby::{RubyAlignment, RubyOverhang, RubyParentSpacing, RubyPosition, RubyType};
let a = doc.stories.values().find(|s| s.text.contains("漢字")).unwrap().runs().next().unwrap().1.over.clone();
assert_eq!(a.ruby.as_deref(), Some("かん じ"));
assert_eq!(a.ruby_type, Some(RubyType::PerCharacter));
@@ -3022,8 +3082,11 @@ fn ruby_settings_import_from_idml_and_round_trip() {
assert_eq!(a.ruby_auto_tcy_digits, Some(2));
assert_eq!((a.ruby_font.as_deref(), a.ruby_font_style.as_deref(), a.ruby_fill.as_deref()), (Some(""), Some(""), Some("")));
assert_eq!((a.ruby_fill_tint, a.ruby_overprint_fill), (Some(None), Some(AdornmentOverprint::Auto)));
}
// Every setting survives export and import.
fn ruby_round_trip_document() -> Document {
use designcraft_doc::cjk_settings::AdornmentOverprint;
use designcraft_doc::ruby::{RubyAlignment, RubyOverhang, RubyParentSpacing, RubyPosition, RubyType};
let mut d = Document::new(&NewDocument::default());
let lid = d.default_layer();
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(72.0, 72.0, 300.0, 200.0), lid, "漢字", ParaFormat::default()).unwrap();
@@ -3058,8 +3121,11 @@ fn ruby_settings_import_from_idml_and_round_trip() {
o.ruby_overprint_fill = Some(AdornmentOverprint::On);
o.ruby_overprint_stroke = Some(AdornmentOverprint::Off);
});
let a = d.stories[&sid].runs().next().unwrap().1.over.clone();
let back = import_idml(&export_idml(&d)).unwrap();
d
}
fn assert_ruby_round_trip(d: &Document, back: &Document) {
let a = d.stories.values().find(|s| s.text.contains("漢字")).unwrap().runs().next().unwrap().1.over.clone();
let b = back.stories.values().find(|s| s.text.contains("漢字")).unwrap().runs().next().unwrap().1.over.clone();
macro_rules! same {
($($f:ident),*) => { $( assert_eq!(a.$f, b.$f, stringify!($f)); )* };