From 61d14799fe97d0a278a598baa05890afb4f76b80 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 07:48:15 -0500 Subject: [PATCH] Write paragraph box formatting through shared IDML model --- crates/idml/src/model/mod.rs | 6 +- crates/idml/src/model/resources.rs | 124 +++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/crates/idml/src/model/mod.rs b/crates/idml/src/model/mod.rs index 0cd6ff593..924a9fbe6 100644 --- a/crates/idml/src/model/mod.rs +++ b/crates/idml/src/model/mod.rs @@ -29,9 +29,9 @@ pub use resources::{ CjkCharacterValues, ColorDefinition, ColorGroup, ColorGroupDefinition, ColorGroupMember, ColorSpace, CompositeFontDefinition, CompositeFontEntryDefinition, EpubExportTagMap, FontDefinition, FontFamilyDefinition, FontsPart, GradientDefinition, GradientStop, GradientStopDefinition, GradientStopValue, GraphicPart, GrepStyleValue, Ink, InkDefinition, MojikumiRow, NestedLineStyleValue, NestedStyleValue, - NestedStyleValues, OpenTypeFeatureValues, ParagraphListValues, StrokeDefinition, StrokeStyle, StrokeStyleKind, Style, StyleGroup, - StyleGroupWriter, StyleKind, StyleNode, Swatch, SwatchKind, TabStopValues, TintDefinition, color_groups, inks, named, preference, stroke_styles, - style_groups, swatches, + NestedStyleValues, OpenTypeFeatureValues, ParagraphBoxValues, ParagraphListValues, StrokeDefinition, StrokeStyle, StrokeStyleKind, Style, + StyleGroup, StyleGroupWriter, StyleKind, StyleNode, Swatch, SwatchKind, TabStopValues, TintDefinition, color_groups, inks, named, preference, + stroke_styles, style_groups, swatches, }; pub use spread::{ ClippingPathValues, Graphic, GraphicChild, GraphicKind, GraphicLayerOptions, Guide, ItemChild, ItemKind, Link, MarginPreference, Page, PageChild, diff --git a/crates/idml/src/model/resources.rs b/crates/idml/src/model/resources.rs index 752dd88dd..f89f35f67 100644 --- a/crates/idml/src/model/resources.rs +++ b/crates/idml/src/model/resources.rs @@ -33,6 +33,130 @@ pub struct ParagraphListValues { pub bullet_font_style: Option, } +/// Paragraph border and shading values after the caller resolves model colours and enums. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ParagraphBoxValues { + pub shading_on: Option, + pub shading_color: Option, + pub shading_tint: Option, + pub shading_offsets: Option<[f64; 4]>, + pub shading_overprint: Option, + pub border_on: Option, + pub border_color: Option, + pub border_tint: Option, + pub border_weights: Option<[f64; 4]>, + pub border_offsets: Option<[f64; 4]>, + pub border_type: Option, + pub border_cap: Option, + pub border_join: Option, + pub border_corners: Option<[(String, f64); 4]>, + pub border_width: Option, + pub border_top: Option, + pub border_bottom: Option, + pub border_display_if_splits: Option, + pub border_merge: Option, + pub border_gap_color: Option, + pub border_gap_tint: Option, + pub border_overprint: Option, + pub border_gap_overprint: Option, + pub shading_width: Option, + pub shading_top: Option, + pub shading_bottom: Option, + pub shading_clip: Option, + pub shading_nonprinting: Option, + pub shading_corners: Option<[(String, f64); 4]>, +} + +impl ParagraphBoxValues { + /// Writes IDML attributes and properties for paragraph borders and shading. + pub fn to_parts(&self) -> (Vec<(String, String)>, Vec) { + use crate::value::num; + + let mut attrs = Vec::new(); + let mut props = Vec::new(); + push_boolean(&mut attrs, "ParagraphShadingOn", self.shading_on); + if let Some(color) = &self.shading_color { + props.push(property("ParagraphShadingColor", "object", color.clone())); + } + push_number(&mut attrs, "ParagraphShadingTint", self.shading_tint); + push_boolean(&mut attrs, "ParagraphShadingOverprint", self.shading_overprint); + const SIDES: [&str; 4] = ["Top", "Left", "Bottom", "Right"]; + if let Some(offsets) = self.shading_offsets { + for (side, value) in SIDES.iter().zip(offsets) { + push_number(&mut attrs, &format!("ParagraphShading{side}Offset"), Some(value)); + } + } + push_boolean(&mut attrs, "ParagraphBorderOn", self.border_on); + if let Some(color) = &self.border_color { + props.push(property("ParagraphBorderColor", "object", color.clone())); + } + push_number(&mut attrs, "ParagraphBorderTint", self.border_tint); + push_boolean(&mut attrs, "ParagraphBorderOverprint", self.border_overprint); + if let Some(weights) = self.border_weights { + for (side, value) in SIDES.iter().zip(weights) { + push_number(&mut attrs, &format!("ParagraphBorder{side}LineWeight"), Some(value)); + } + } + if let Some(offsets) = self.border_offsets { + for (side, value) in SIDES.iter().zip(offsets) { + push_number(&mut attrs, &format!("ParagraphBorder{side}Offset"), Some(value)); + } + } + if let Some(kind) = &self.border_type { + props.push(property("ParagraphBorderType", "object", kind.clone())); + } + if let Some(cap) = &self.border_cap { + attrs.push(("ParagraphBorderStrokeEndCap".into(), cap.clone())); + } + if let Some(join) = &self.border_join { + attrs.push(("ParagraphBorderStrokeEndJoin".into(), join.clone())); + } + for (prefix, corners) in [("ParagraphBorder", &self.border_corners), ("ParagraphShading", &self.shading_corners)] { + if let Some(corners) = corners { + const CORNERS: [&str; 4] = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"]; + for ((shape, radius), corner) in corners.iter().zip(CORNERS) { + attrs.push((format!("{prefix}{corner}CornerOption"), shape.clone())); + attrs.push((format!("{prefix}{corner}CornerRadius"), num(*radius))); + } + } + } + for (key, value) in [ + ("ParagraphBorderWidth", &self.border_width), + ("ParagraphBorderTopOrigin", &self.border_top), + ("ParagraphBorderBottomOrigin", &self.border_bottom), + ("ParagraphShadingWidth", &self.shading_width), + ("ParagraphShadingTopOrigin", &self.shading_top), + ("ParagraphShadingBottomOrigin", &self.shading_bottom), + ] { + if let Some(value) = value { + attrs.push((key.to_string(), value.clone())); + } + } + push_boolean(&mut attrs, "ParagraphBorderDisplayIfSplits", self.border_display_if_splits); + push_boolean(&mut attrs, "MergeConsecutiveParaBorders", self.border_merge); + if let Some(color) = &self.border_gap_color { + props.push(property("ParagraphBorderGapColor", "object", color.clone())); + } + push_number(&mut attrs, "ParagraphBorderGapTint", self.border_gap_tint); + push_boolean(&mut attrs, "ParagraphBorderGapOverprint", self.border_gap_overprint); + push_boolean(&mut attrs, "ParagraphShadingClipToFrame", self.shading_clip); + push_boolean(&mut attrs, "ParagraphShadingSuppressPrinting", self.shading_nonprinting); + (attrs, props) + } +} + +fn push_boolean(attrs: &mut Vec<(String, String)>, key: &str, value: Option) { + if let Some(value) = value { + attrs.push((key.to_string(), if value { "true" } else { "false" }.to_string())); + } +} + +fn push_number(attrs: &mut Vec<(String, String)>, key: &str, value: Option) { + if let Some(value) = value { + attrs.push((key.to_string(), crate::value::num(value))); + } +} + impl ParagraphListValues { pub fn from_el(el: &El) -> Self { let values = Values::of(el);