Skip to main content

rpptx_layout/
lib.rs

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