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,
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    /// Shapes in final draw order.
134    pub shapes: Vec<ResolvedShape>,
135    pub diagnostics: Vec<Diagnostic>,
136}
137
138/// Paragraph base directions parallel to one resolved slide's shape and text-body order.
139pub type ResolvedSlideTextDirections = Vec<Vec<Vec<oxml_layout::TextDirection>>>;
140
141/// One concrete slide background ready for renderer lowering.
142#[derive(Clone, Debug, PartialEq)]
143pub enum ResolvedBackground {
144    Paint(Paint),
145    Image(ResolvedImage),
146}
147
148/// One renderer-facing shape with no OOXML model types or part-tree lifetimes.
149#[derive(Clone, Debug, PartialEq)]
150pub struct ResolvedShape {
151    /// Affine mapping from this shape's coordinate space through all parent groups.
152    pub group_transform: Transform,
153    pub bounds: Rect,
154    pub rotation_deg: f64,
155    pub flip_h: bool,
156    pub flip_v: bool,
157    pub geometry: ResolvedGeometry,
158    pub fill: Option<Paint>,
159    pub image_fill: Option<ResolvedImage>,
160    pub line: Option<Stroke>,
161    pub head_end: Option<ResolvedLineEnd>,
162    pub tail_end: Option<ResolvedLineEnd>,
163    pub shadow: Option<Effect>,
164    pub content: ResolvedContent,
165    /// Stable fallback category when the renderer cannot reproduce the source.
166    pub unsupported: Option<&'static str>,
167}
168
169/// One source-neutral line endpoint ready for renderer geometry lowering.
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub struct ResolvedLineEnd {
172    pub kind: ResolvedLineEndKind,
173    pub width: ResolvedLineEndSize,
174    pub length: ResolvedLineEndSize,
175}
176
177/// A visible line endpoint kind. DrawingML `none` resolves to no endpoint.
178#[derive(Clone, Copy, Debug, Eq, PartialEq)]
179pub enum ResolvedLineEndKind {
180    Triangle,
181    Stealth,
182    Diamond,
183    Oval,
184    Arrow,
185}
186
187/// A line endpoint width or length multiplier category.
188#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
189pub enum ResolvedLineEndSize {
190    Small,
191    #[default]
192    Medium,
193    Large,
194}
195
196/// Concrete geometry ready for the renderer, or a visible bounds fallback.
197#[derive(Clone, Debug, PartialEq)]
198pub enum ResolvedGeometry {
199    Rectangle,
200    Custom {
201        paths: Vec<Path>,
202        text_rect: Option<Rect>,
203    },
204    BoundsFallback,
205}
206
207/// Owned content attached to one resolved shape.
208#[derive(Clone, Debug, PartialEq)]
209pub enum ResolvedContent {
210    None,
211    Text(ResolvedTextBody),
212    Image(ResolvedImage),
213    Table(ResolvedTable),
214    Group(GroupElement),
215}
216
217/// One resolved image shared by shape pictures and slide backgrounds.
218#[derive(Clone, Debug, PartialEq)]
219pub struct ResolvedImage {
220    pub media: MediaId,
221    pub src_rect: Option<CropRect>,
222    pub placement: ResolvedImagePlacement,
223    pub dpi: Option<f64>,
224    pub rotate_with_shape: bool,
225    /// Effective picture opacity from `a:alphaModFix`, from transparent to opaque.
226    pub opacity: f64,
227}
228
229/// Destination placement for one resolved picture.
230#[derive(Clone, Debug, PartialEq)]
231pub enum ResolvedImagePlacement {
232    Stretch { fill_rect: Option<CropRect> },
233    Tile(ResolvedTilePlacement),
234}
235
236impl Default for ResolvedImagePlacement {
237    fn default() -> Self {
238        Self::Stretch { fill_rect: None }
239    }
240}
241
242/// Concrete tile placement with all DrawingML defaults applied.
243#[derive(Clone, Copy, Debug, PartialEq)]
244pub struct ResolvedTilePlacement {
245    pub translation: Point,
246    pub scale_x: f64,
247    pub scale_y: f64,
248    pub flip: ResolvedTileFlip,
249    pub alignment: ResolvedRectAlignment,
250}
251
252/// Alternating tile reflection axes.
253#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
254pub enum ResolvedTileFlip {
255    #[default]
256    None,
257    Horizontal,
258    Vertical,
259    Both,
260}
261
262/// The nine DrawingML rectangle alignment positions.
263#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
264pub enum ResolvedRectAlignment {
265    #[default]
266    TopLeft,
267    Top,
268    TopRight,
269    Left,
270    Center,
271    Right,
272    BottomLeft,
273    Bottom,
274    BottomRight,
275}
276
277/// Picture cropping as fractions of the source dimensions.
278#[derive(Clone, Copy, Debug, Default, PartialEq)]
279pub struct CropRect {
280    pub left: f64,
281    pub top: f64,
282    pub right: f64,
283    pub bottom: f64,
284}
285
286/// Text-box geometry and paragraphs after inheritance.
287#[derive(Clone, Debug, PartialEq)]
288pub struct ResolvedTextBody {
289    pub insets: TextInsets,
290    pub anchor: TextAnchor,
291    pub wrap: bool,
292    pub vertical: TextDirection,
293    pub space_first_last_paragraph: bool,
294    pub autofit: ResolvedAutofit,
295    pub paragraphs: Vec<ResolvedParagraph>,
296}
297
298/// Text-box edge insets in points.
299#[derive(Clone, Copy, Debug, Default, PartialEq)]
300pub struct TextInsets {
301    pub left: f64,
302    pub top: f64,
303    pub right: f64,
304    pub bottom: f64,
305}
306
307#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
308pub enum TextAnchor {
309    #[default]
310    Top,
311    Center,
312    Bottom,
313    Justified,
314    Distributed,
315}
316
317#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
318pub enum TextDirection {
319    #[default]
320    Horizontal,
321    Vertical,
322    Vertical270,
323    EastAsianVertical,
324    MongolianVertical,
325    WordArtVertical,
326    WordArtVerticalRtl,
327}
328
329#[derive(Clone, Copy, Debug, Default, PartialEq)]
330pub enum ResolvedAutofit {
331    #[default]
332    None,
333    Shape,
334    Normal {
335        font_scale: Option<f64>,
336        line_spacing_reduction: Option<f64>,
337    },
338}
339
340/// One paragraph with concrete layout and text-run values.
341#[derive(Clone, Debug, Default, PartialEq)]
342pub struct ResolvedParagraph {
343    pub level: u8,
344    pub left_margin: f64,
345    pub right_margin: f64,
346    pub indent: f64,
347    pub alignment: ParagraphAlignment,
348    pub line_spacing: Option<ResolvedTextSpacing>,
349    pub space_before: Option<ResolvedTextSpacing>,
350    pub space_after: Option<ResolvedTextSpacing>,
351    pub bullet: Option<ResolvedBullet>,
352    pub end_style: ResolvedRunStyle,
353    pub runs: Vec<ResolvedTextRun>,
354}
355
356#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
357pub enum ParagraphAlignment {
358    #[default]
359    Left,
360    Center,
361    Right,
362    Justified,
363    Distributed,
364}
365
366/// Concrete paragraph spacing as a font-relative fraction or typographic points.
367#[derive(Clone, Debug, PartialEq)]
368pub enum ResolvedTextSpacing {
369    Percent(f64),
370    Points(f64),
371}
372
373/// Concrete bullet size as a font-relative fraction or typographic points.
374#[derive(Clone, Debug, PartialEq)]
375pub enum ResolvedBulletSize {
376    Percent(f64),
377    Points(f64),
378}
379
380#[derive(Clone, Debug, PartialEq)]
381pub enum ResolvedBullet {
382    Character {
383        character: String,
384        font: Option<String>,
385        color: Option<oxml_layout::Color>,
386        size: Option<ResolvedBulletSize>,
387    },
388    AutoNumber {
389        scheme: String,
390        start_at: u32,
391        font: Option<String>,
392        color: Option<oxml_layout::Color>,
393        size: Option<ResolvedBulletSize>,
394    },
395}
396
397/// Ordered text, explicit breaks, and fields in one paragraph.
398#[derive(Clone, Debug, PartialEq)]
399pub enum ResolvedTextRun {
400    Text {
401        text: String,
402        style: ResolvedRunStyle,
403    },
404    Break,
405    Field {
406        text: String,
407        field_type: Option<String>,
408        style: ResolvedRunStyle,
409    },
410}
411
412/// Concrete renderer-visible properties of one text run.
413#[derive(Clone, Debug, Default, PartialEq)]
414pub struct ResolvedRunStyle {
415    pub font_size: Option<f64>,
416    pub bold: bool,
417    pub italic: bool,
418    pub all_caps: bool,
419    pub underline: bool,
420    pub strike: bool,
421    pub spacing: Option<f64>,
422    pub baseline: Option<f64>,
423    pub fill: Option<Paint>,
424    pub latin_typeface: Option<String>,
425    pub east_asian_typeface: Option<String>,
426    pub complex_script_typeface: Option<String>,
427    pub symbol_typeface: Option<String>,
428    /// Direct external URI resolved in the run's producing relationship scope.
429    pub hyperlink_url: Option<String>,
430}
431
432/// One table with concrete dimensions and owned cell text.
433#[derive(Clone, Debug, Default, PartialEq)]
434pub struct ResolvedTable {
435    pub right_to_left: bool,
436    pub column_widths: Vec<f64>,
437    pub rows: Vec<ResolvedTableRow>,
438}
439
440#[derive(Clone, Debug, Default, PartialEq)]
441pub struct ResolvedTableRow {
442    pub height: f64,
443    pub cells: Vec<ResolvedTableCell>,
444}
445
446#[derive(Clone, Debug, PartialEq)]
447pub struct ResolvedTableCell {
448    pub text: Option<ResolvedTextBody>,
449    pub fill: Option<Paint>,
450    pub margins: TextInsets,
451    pub left: Option<ResolvedTableBorder>,
452    pub right: Option<ResolvedTableBorder>,
453    pub top: Option<ResolvedTableBorder>,
454    pub bottom: Option<ResolvedTableBorder>,
455    pub row_span: u32,
456    pub grid_span: u32,
457    pub horizontal_merge: bool,
458    pub vertical_merge: bool,
459}
460
461impl Default for ResolvedTableCell {
462    fn default() -> Self {
463        Self {
464            text: None,
465            fill: None,
466            margins: TextInsets::default(),
467            left: None,
468            right: None,
469            top: None,
470            bottom: None,
471            row_span: 1,
472            grid_span: 1,
473            horizontal_merge: false,
474            vertical_merge: false,
475        }
476    }
477}
478
479/// One table edge plus its resolved style-precedence rank.
480#[derive(Clone, Debug, Default, PartialEq)]
481pub struct ResolvedTableBorder {
482    pub stroke: Option<Stroke>,
483    pub priority: u8,
484}