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