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