Skip to main content

rpptx_layout/
lib.rs

1//! PresentationML inheritance resolution and shape-tree flattening.
2
3use std::collections::HashMap;
4
5use oxml_chart::CT_ChartSpace;
6use oxml_layout::{
7    Diagnostic, Effect, GroupElement, MediaId, Paint, Path, Point, Rect, Stroke, Transform,
8};
9
10mod context;
11mod font;
12mod style;
13mod text;
14pub mod timeline;
15
16pub use context::{
17    BackgroundContent, BackgroundSource, EffectiveBackground, FlattenedItem, FlattenedSource,
18    ResolveCtx, inherited_xfrm,
19};
20pub use style::{EffectiveShapeStyle, ResolveError};
21pub use text::{EffectiveListStyle, EffectiveTextProperties};
22
23/// Content-addressed media identifiers kept separate by relationship scope.
24#[derive(Clone, Debug, Default, Eq, PartialEq)]
25pub struct ScopedMediaIds {
26    pub slide: HashMap<String, MediaId>,
27    pub layout: HashMap<String, MediaId>,
28    pub master: HashMap<String, MediaId>,
29    /// Package content type for each content-addressed media item.
30    pub media_content_types: HashMap<MediaId, String>,
31}
32
33impl ScopedMediaIds {
34    /// Look up an embedded relationship only in its producing part's scope.
35    pub fn get(&self, source: FlattenedSource, relationship_id: &str) -> Option<MediaId> {
36        match source {
37            FlattenedSource::Slide => &self.slide,
38            FlattenedSource::Layout => &self.layout,
39            FlattenedSource::Master => &self.master,
40            FlattenedSource::Background => return None,
41        }
42        .get(relationship_id)
43        .copied()
44    }
45
46    /// Return the package content type for a relationship-resolved media item.
47    pub fn content_type(&self, source: FlattenedSource, relationship_id: &str) -> Option<&str> {
48        let media_id = self.get(source, relationship_id)?;
49        self.media_content_types.get(&media_id).map(String::as_str)
50    }
51}
52
53/// Stable render rejections kept separate by relationship scope.
54#[derive(Clone, Debug, Default, Eq, PartialEq)]
55pub struct ScopedMediaFailures {
56    pub slide: HashMap<String, String>,
57    pub layout: HashMap<String, String>,
58    pub master: HashMap<String, String>,
59}
60
61impl ScopedMediaFailures {
62    /// Look up a rejected embedded relationship only in its producing part's scope.
63    pub fn get(&self, source: FlattenedSource, relationship_id: &str) -> Option<&str> {
64        match source {
65            FlattenedSource::Slide => &self.slide,
66            FlattenedSource::Layout => &self.layout,
67            FlattenedSource::Master => &self.master,
68            FlattenedSource::Background => return None,
69        }
70        .get(relationship_id)
71        .map(String::as_str)
72    }
73}
74
75/// One chart relationship target projected during package assembly.
76#[derive(Clone, Debug, PartialEq)]
77pub enum ChartResource {
78    Parsed(Box<CT_ChartSpace>),
79    External(String),
80    MissingTarget(String),
81    Invalid(String),
82}
83
84/// Parsed chart resources kept separate by their producing part's scope.
85#[derive(Clone, Debug, Default, PartialEq)]
86pub struct ScopedChartResources {
87    pub slide: HashMap<String, ChartResource>,
88    pub layout: HashMap<String, ChartResource>,
89    pub master: HashMap<String, ChartResource>,
90}
91
92impl ScopedChartResources {
93    /// Look up a chart relationship only in its producing part's scope.
94    pub fn get(&self, source: FlattenedSource, relationship_id: &str) -> Option<&ChartResource> {
95        match source {
96            FlattenedSource::Slide => &self.slide,
97            FlattenedSource::Layout => &self.layout,
98            FlattenedSource::Master => &self.master,
99            FlattenedSource::Background => return None,
100        }
101        .get(relationship_id)
102    }
103}
104
105/// External hyperlink targets kept separate by relationship scope.
106#[derive(Clone, Debug, Default, Eq, PartialEq)]
107pub struct ScopedHyperlinkTargets {
108    pub slide: HashMap<String, String>,
109    pub layout: HashMap<String, String>,
110    pub master: HashMap<String, String>,
111}
112
113impl ScopedHyperlinkTargets {
114    /// Look up an external hyperlink only in its producing part's scope.
115    pub fn get(&self, source: FlattenedSource, relationship_id: &str) -> Option<&str> {
116        match source {
117            FlattenedSource::Slide => &self.slide,
118            FlattenedSource::Layout => &self.layout,
119            FlattenedSource::Master => &self.master,
120            FlattenedSource::Background => return None,
121        }
122        .get(relationship_id)
123        .map(String::as_str)
124    }
125}
126
127/// An owned slide with every renderer-visible value resolved.
128#[derive(Clone, Debug, PartialEq)]
129pub struct ResolvedSlide {
130    /// Slide width and height in typographic points.
131    pub size: (f64, f64),
132    pub background: Option<ResolvedBackground>,
133    /// Failed semantic member resolutions, including members omitted without bounds.
134    /// This count is independent of drawable fallback shapes.
135    pub unresolved_member_count: usize,
136    /// Shapes in final draw order.
137    pub shapes: Vec<ResolvedShape>,
138    pub diagnostics: Vec<Diagnostic>,
139}
140
141/// Paragraph base directions parallel to one resolved slide's shape and text-body order.
142pub type ResolvedSlideTextDirections = Vec<Vec<Vec<oxml_layout::TextDirection>>>;
143
144/// One concrete slide background ready for renderer lowering.
145#[derive(Clone, Debug, PartialEq)]
146pub enum ResolvedBackground {
147    Paint(Paint),
148    Image(ResolvedImage),
149}
150
151/// One renderer-facing shape with no OOXML model types or part-tree lifetimes.
152#[derive(Clone, Debug, PartialEq)]
153pub struct ResolvedShape {
154    /// Affine mapping from this shape's coordinate space through all parent groups.
155    pub group_transform: Transform,
156    pub bounds: Rect,
157    pub rotation_deg: f64,
158    pub flip_h: bool,
159    pub flip_v: bool,
160    pub geometry: ResolvedGeometry,
161    pub fill: Option<Paint>,
162    pub image_fill: Option<ResolvedImage>,
163    pub line: Option<Stroke>,
164    pub head_end: Option<ResolvedLineEnd>,
165    pub tail_end: Option<ResolvedLineEnd>,
166    pub shadow: Option<Effect>,
167    pub content: ResolvedContent,
168    /// Stable fallback category when the renderer cannot reproduce the source.
169    pub unsupported: Option<&'static str>,
170}
171
172/// One source-neutral line endpoint ready for renderer geometry lowering.
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub struct ResolvedLineEnd {
175    pub kind: ResolvedLineEndKind,
176    pub width: ResolvedLineEndSize,
177    pub length: ResolvedLineEndSize,
178}
179
180/// A visible line endpoint kind. DrawingML `none` resolves to no endpoint.
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum ResolvedLineEndKind {
183    Triangle,
184    Stealth,
185    Diamond,
186    Oval,
187    Arrow,
188}
189
190/// A line endpoint width or length multiplier category.
191#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
192pub enum ResolvedLineEndSize {
193    Small,
194    #[default]
195    Medium,
196    Large,
197}
198
199/// Concrete geometry ready for the renderer, or a visible bounds fallback.
200#[derive(Clone, Debug, PartialEq)]
201pub enum ResolvedGeometry {
202    Rectangle,
203    Custom {
204        paths: Vec<Path>,
205        text_rect: Option<Rect>,
206    },
207    BoundsFallback,
208}
209
210/// Owned content attached to one resolved shape.
211#[derive(Clone, Debug, PartialEq)]
212pub enum ResolvedContent {
213    None,
214    Text(ResolvedTextBody),
215    Image(ResolvedImage),
216    Table(ResolvedTable),
217    Group(GroupElement),
218}
219
220/// One resolved image shared by shape pictures and slide backgrounds.
221#[derive(Clone, Debug, PartialEq)]
222pub struct ResolvedImage {
223    pub media: MediaId,
224    pub src_rect: Option<CropRect>,
225    pub placement: ResolvedImagePlacement,
226    pub dpi: Option<f64>,
227    pub rotate_with_shape: bool,
228    /// Effective picture opacity from `a:alphaModFix`, from transparent to opaque.
229    pub opacity: f64,
230}
231
232/// Destination placement for one resolved picture.
233#[derive(Clone, Debug, PartialEq)]
234pub enum ResolvedImagePlacement {
235    Stretch { fill_rect: Option<CropRect> },
236    Tile(ResolvedTilePlacement),
237}
238
239impl Default for ResolvedImagePlacement {
240    fn default() -> Self {
241        Self::Stretch { fill_rect: None }
242    }
243}
244
245/// Concrete tile placement with all DrawingML defaults applied.
246#[derive(Clone, Copy, Debug, PartialEq)]
247pub struct ResolvedTilePlacement {
248    pub translation: Point,
249    pub scale_x: f64,
250    pub scale_y: f64,
251    pub flip: ResolvedTileFlip,
252    pub alignment: ResolvedRectAlignment,
253}
254
255/// Alternating tile reflection axes.
256#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
257pub enum ResolvedTileFlip {
258    #[default]
259    None,
260    Horizontal,
261    Vertical,
262    Both,
263}
264
265/// The nine DrawingML rectangle alignment positions.
266#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
267pub enum ResolvedRectAlignment {
268    #[default]
269    TopLeft,
270    Top,
271    TopRight,
272    Left,
273    Center,
274    Right,
275    BottomLeft,
276    Bottom,
277    BottomRight,
278}
279
280/// Picture cropping as fractions of the source dimensions.
281#[derive(Clone, Copy, Debug, Default, PartialEq)]
282pub struct CropRect {
283    pub left: f64,
284    pub top: f64,
285    pub right: f64,
286    pub bottom: f64,
287}
288
289/// Text-box geometry and paragraphs after inheritance.
290#[derive(Clone, Debug, PartialEq)]
291pub struct ResolvedTextBody {
292    pub insets: TextInsets,
293    pub anchor: TextAnchor,
294    pub wrap: bool,
295    pub vertical: TextDirection,
296    pub space_first_last_paragraph: bool,
297    pub autofit: ResolvedAutofit,
298    pub paragraphs: Vec<ResolvedParagraph>,
299}
300
301/// Text-box edge insets in points.
302#[derive(Clone, Copy, Debug, Default, PartialEq)]
303pub struct TextInsets {
304    pub left: f64,
305    pub top: f64,
306    pub right: f64,
307    pub bottom: f64,
308}
309
310#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
311pub enum TextAnchor {
312    #[default]
313    Top,
314    Center,
315    Bottom,
316    Justified,
317    Distributed,
318}
319
320#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
321pub enum TextDirection {
322    #[default]
323    Horizontal,
324    Vertical,
325    Vertical270,
326    EastAsianVertical,
327    MongolianVertical,
328    WordArtVertical,
329    WordArtVerticalRtl,
330}
331
332#[derive(Clone, Copy, Debug, Default, PartialEq)]
333pub enum ResolvedAutofit {
334    #[default]
335    None,
336    Shape,
337    Normal {
338        font_scale: Option<f64>,
339        line_spacing_reduction: Option<f64>,
340    },
341}
342
343/// One paragraph with concrete layout and text-run values.
344#[derive(Clone, Debug, Default, PartialEq)]
345pub struct ResolvedParagraph {
346    pub level: u8,
347    pub left_margin: f64,
348    pub right_margin: f64,
349    pub indent: f64,
350    pub alignment: ParagraphAlignment,
351    pub line_spacing: Option<ResolvedTextSpacing>,
352    pub space_before: Option<ResolvedTextSpacing>,
353    pub space_after: Option<ResolvedTextSpacing>,
354    pub bullet: Option<ResolvedBullet>,
355    pub end_style: ResolvedRunStyle,
356    pub runs: Vec<ResolvedTextRun>,
357}
358
359#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
360pub enum ParagraphAlignment {
361    #[default]
362    Left,
363    Center,
364    Right,
365    Justified,
366    Distributed,
367}
368
369/// Concrete paragraph spacing as a font-relative fraction or typographic points.
370#[derive(Clone, Debug, PartialEq)]
371pub enum ResolvedTextSpacing {
372    Percent(f64),
373    Points(f64),
374}
375
376/// Concrete bullet size as a font-relative fraction or typographic points.
377#[derive(Clone, Debug, PartialEq)]
378pub enum ResolvedBulletSize {
379    Percent(f64),
380    Points(f64),
381}
382
383#[derive(Clone, Debug, PartialEq)]
384pub enum ResolvedBullet {
385    Character {
386        character: String,
387        font: Option<String>,
388        color: Option<oxml_layout::Color>,
389        size: Option<ResolvedBulletSize>,
390    },
391    AutoNumber {
392        scheme: String,
393        start_at: u32,
394        font: Option<String>,
395        color: Option<oxml_layout::Color>,
396        size: Option<ResolvedBulletSize>,
397    },
398}
399
400/// Ordered text, explicit breaks, and fields in one paragraph.
401#[derive(Clone, Debug, PartialEq)]
402pub enum ResolvedTextRun {
403    Text {
404        text: String,
405        style: ResolvedRunStyle,
406    },
407    Break,
408    Field {
409        text: String,
410        field_type: Option<String>,
411        style: ResolvedRunStyle,
412    },
413}
414
415/// Concrete renderer-visible properties of one text run.
416#[derive(Clone, Debug, Default, PartialEq)]
417pub struct ResolvedRunStyle {
418    pub font_size: Option<f64>,
419    pub bold: bool,
420    pub italic: bool,
421    pub all_caps: bool,
422    pub small_caps: bool,
423    /// The producing run language used for selected-face shaping.
424    pub language: Option<String>,
425    pub highlight: Option<oxml_layout::Color>,
426    pub underline: bool,
427    pub strike: bool,
428    pub spacing: Option<f64>,
429    pub baseline: Option<f64>,
430    pub fill: Option<Paint>,
431    pub latin_typeface: Option<String>,
432    pub east_asian_typeface: Option<String>,
433    pub complex_script_typeface: Option<String>,
434    pub symbol_typeface: Option<String>,
435    /// Direct external URI resolved in the run's producing relationship scope.
436    pub hyperlink_url: Option<String>,
437}
438
439/// One table with concrete dimensions and owned cell text.
440#[derive(Clone, Debug, Default, PartialEq)]
441pub struct ResolvedTable {
442    pub right_to_left: bool,
443    pub column_widths: Vec<f64>,
444    pub rows: Vec<ResolvedTableRow>,
445    /// The table style's background, painted over the whole table box
446    /// beneath the cell fills.
447    pub background: Option<Paint>,
448}
449
450#[derive(Clone, Debug, Default, PartialEq)]
451pub struct ResolvedTableRow {
452    pub height: f64,
453    pub cells: Vec<ResolvedTableCell>,
454}
455
456#[derive(Clone, Debug, PartialEq)]
457pub struct ResolvedTableCell {
458    pub text: Option<ResolvedTextBody>,
459    pub fill: Option<Paint>,
460    pub margins: TextInsets,
461    pub left: Option<ResolvedTableBorder>,
462    pub right: Option<ResolvedTableBorder>,
463    pub top: Option<ResolvedTableBorder>,
464    pub bottom: Option<ResolvedTableBorder>,
465    pub row_span: u32,
466    pub grid_span: u32,
467    pub horizontal_merge: bool,
468    pub vertical_merge: bool,
469}
470
471impl Default for ResolvedTableCell {
472    fn default() -> Self {
473        Self {
474            text: None,
475            fill: None,
476            margins: TextInsets::default(),
477            left: None,
478            right: None,
479            top: None,
480            bottom: None,
481            row_span: 1,
482            grid_span: 1,
483            horizontal_merge: false,
484            vertical_merge: false,
485        }
486    }
487}
488
489/// One table edge plus its resolved style-precedence rank.
490#[derive(Clone, Debug, Default, PartialEq)]
491pub struct ResolvedTableBorder {
492    pub stroke: Option<Stroke>,
493    pub priority: u8,
494}