Skip to main content

pdfboss_core/
presentation.rs

1//! Presentations (ISO 32000-1 §12.4.4): a page's display duration and its
2//! transition dictionary, read as data with the table's defaults filled in.
3
4use crate::document::Page;
5use crate::object::{Dict, Object};
6use crate::source::AsyncObjectSource;
7use crate::tree::{resolved_dict, Entries};
8
9/// The transition styles of ISO 32000-1 Table 162: how a presentation
10/// reveals the page it moves to.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum TransitionStyle {
13    /// Two lines sweep across the screen, along `Dimension` and `Motion`.
14    Split,
15    /// Evenly spaced lines sweep in the same direction, along `Dimension`.
16    Blinds,
17    /// A rectangle sweeps inward from the edges or outward from the centre.
18    Box,
19    /// One line sweeps from one edge to the other, in `TransitionDirection`.
20    Wipe,
21    /// The old page dissolves into the new one.
22    Dissolve,
23    /// A dissolve that sweeps across the page as a band.
24    Glitter,
25    /// `/R`: the new page replaces the old one with no effect, the default.
26    #[default]
27    Replace,
28    /// PDF 1.5: changes fly in or out, scaled from `Transition::scale`.
29    Fly,
30    /// PDF 1.5: the new page pushes the old one off the screen.
31    Push,
32    /// PDF 1.5: the new page slides over the old one.
33    Cover,
34    /// PDF 1.5: the old page slides off, uncovering the new one.
35    Uncover,
36    /// PDF 1.5: the new page fades in through the old one.
37    Fade,
38}
39
40impl TransitionStyle {
41    /// The style `/S` names; `None` for a name the table does not list.
42    pub fn from_name(name: &str) -> Option<TransitionStyle> {
43        Some(match name {
44            "Split" => TransitionStyle::Split,
45            "Blinds" => TransitionStyle::Blinds,
46            "Box" => TransitionStyle::Box,
47            "Wipe" => TransitionStyle::Wipe,
48            "Dissolve" => TransitionStyle::Dissolve,
49            "Glitter" => TransitionStyle::Glitter,
50            "R" => TransitionStyle::Replace,
51            "Fly" => TransitionStyle::Fly,
52            "Push" => TransitionStyle::Push,
53            "Cover" => TransitionStyle::Cover,
54            "Uncover" => TransitionStyle::Uncover,
55            "Fade" => TransitionStyle::Fade,
56            _ => return None,
57        })
58    }
59}
60
61/// `/Dm`: the dimension a Split or Blinds transition moves along.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub enum Dimension {
64    /// `/H`, the default.
65    #[default]
66    Horizontal,
67    /// `/V`.
68    Vertical,
69}
70
71impl Dimension {
72    /// The dimension `/Dm` names; `None` for any other name.
73    pub fn from_name(name: &str) -> Option<Dimension> {
74        match name {
75            "H" => Some(Dimension::Horizontal),
76            "V" => Some(Dimension::Vertical),
77            _ => None,
78        }
79    }
80}
81
82/// `/M`: the direction of motion of a Split, Box or Fly transition.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub enum Motion {
85    /// `/I`: inward from the edges of the page, the default.
86    #[default]
87    Inward,
88    /// `/O`: outward from the centre of the page.
89    Outward,
90}
91
92impl Motion {
93    /// The motion `/M` names; `None` for any other name.
94    pub fn from_name(name: &str) -> Option<Motion> {
95        match name {
96            "I" => Some(Motion::Inward),
97            "O" => Some(Motion::Outward),
98            _ => None,
99        }
100    }
101}
102
103/// `/Di`: the direction a Wipe, Glitter, Fly, Cover, Uncover or Push
104/// transition moves in.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum TransitionDirection {
107    /// Degrees counterclockwise from left to right: 0, 90, 180, 270 or 315
108    /// in the table, 0 being the default.
109    Angle(u32),
110    /// The name `None`, meaningful for a Fly transition whose scale is not 1.
111    None,
112}
113
114impl Default for TransitionDirection {
115    fn default() -> TransitionDirection {
116        TransitionDirection::Angle(0)
117    }
118}
119
120/// A transition dictionary (ISO 32000-1 §12.4.4, Table 162) with every
121/// absent entry at its default.
122#[derive(Debug, Clone, PartialEq)]
123pub struct Transition {
124    /// `/S`, `Replace` when absent or not a listed style.
125    pub style: TransitionStyle,
126    /// `/D`: the effect's duration in seconds, 1 when absent.
127    pub duration: f64,
128    /// `/Dm`, horizontal when absent.
129    pub dimension: Dimension,
130    /// `/M`, inward when absent.
131    pub motion: Motion,
132    /// `/Di`, left to right when absent.
133    pub direction: TransitionDirection,
134    /// `/SS`: the starting or ending scale of a Fly transition, 1 when absent.
135    pub scale: f64,
136    /// `/B`: whether a Fly transition's area is rectangular and opaque.
137    pub opaque: bool,
138}
139
140impl Default for Transition {
141    fn default() -> Transition {
142        Transition {
143            style: TransitionStyle::default(),
144            duration: 1.0,
145            dimension: Dimension::default(),
146            motion: Motion::default(),
147            direction: TransitionDirection::default(),
148            scale: 1.0,
149            opaque: false,
150        }
151    }
152}
153
154/// How a page takes part in a presentation (ISO 32000-1 §12.4.4): its
155/// display duration and the transition that reveals it.
156#[derive(Debug, Clone, PartialEq)]
157pub struct Presentation {
158    /// `/Dur`: the seconds the page shows before the presentation advances
159    /// on its own; `None` leaves the page waiting for the user.
160    pub duration: Option<f64>,
161    /// `/Trans`: the transition into this page; `None` without one.
162    pub transition: Option<Transition>,
163}
164
165/// The presentation entries of `page`: `None` when it carries neither
166/// `/Dur` nor a `/Trans` dictionary, so a plain page is no slide.
167///
168/// Covers ISO 32000-1 §12.4.4.
169pub async fn presentation_with<S: AsyncObjectSource>(src: &S, page: &Page) -> Option<Presentation> {
170    let dict = page.dict();
171    let duration = match dict.get("Dur") {
172        Some(value) => src
173            .resolve(value)
174            .await
175            .ok()
176            .and_then(|value| value.as_f64()),
177        None => None,
178    };
179    let transition = match dict.get("Trans") {
180        Some(value) => match resolved_dict(src, value).await {
181            Some(trans) => Some(transition_from(src, &trans).await),
182            None => None,
183        },
184        None => None,
185    };
186    if duration.is_none() && transition.is_none() {
187        return None;
188    }
189    Some(Presentation {
190        duration,
191        transition,
192    })
193}
194
195/// Table 162 with defaults: an entry that is absent, or a name the table
196/// does not list, takes the default.
197async fn transition_from<S: AsyncObjectSource>(src: &S, dict: &Dict) -> Transition {
198    let entries = Entries { src, dict };
199    let defaults = Transition::default();
200    let direction = match entries.value("Di").await {
201        Some(Object::Name(name)) if name.0 == "None" => TransitionDirection::None,
202        Some(value) => value
203            .as_f64()
204            .map(|degrees| TransitionDirection::Angle(degrees as u32))
205            .unwrap_or_default(),
206        None => TransitionDirection::default(),
207    };
208    Transition {
209        style: entries
210            .named("S", TransitionStyle::from_name)
211            .await
212            .unwrap_or_default(),
213        duration: entries
214            .value("D")
215            .await
216            .and_then(|value| value.as_f64())
217            .unwrap_or(defaults.duration),
218        dimension: entries
219            .named("Dm", Dimension::from_name)
220            .await
221            .unwrap_or_default(),
222        motion: entries
223            .named("M", Motion::from_name)
224            .await
225            .unwrap_or_default(),
226        direction,
227        scale: entries
228            .value("SS")
229            .await
230            .and_then(|value| value.as_f64())
231            .unwrap_or(defaults.scale),
232        opaque: entries.flag("B").await.unwrap_or(defaults.opaque),
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::document::Document;
240    use pdfboss_testkit::PdfBuilder;
241
242    /// A one-page document whose page carries `page_extra`.
243    fn doc_with(page_extra: &str) -> Document {
244        let mut b = PdfBuilder::new();
245        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
246        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
247        b.object(
248            3,
249            &format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] {page_extra} >>"),
250        );
251        Document::load(b.build(1)).unwrap()
252    }
253
254    fn presentation(page_extra: &str) -> Option<Presentation> {
255        let doc = doc_with(page_extra);
256        doc.presentation(&doc.page(0).unwrap())
257    }
258
259    /// The clause's EXAMPLE: a five-second page reached through a
260    /// 3.5-second vertical split moving outward; the entries the example
261    /// leaves out take Table 162's defaults.
262    // Covers ISO 32000-1 §12.4.4.
263    #[test]
264    fn reads_the_duration_and_the_transition_with_the_tables_defaults() {
265        let shown =
266            presentation("/Dur 5 /Trans << /Type /Trans /D 3.5 /S /Split /Dm /V /M /O >>").unwrap();
267        assert_eq!(shown.duration, Some(5.0));
268        assert_eq!(
269            shown.transition,
270            Some(Transition {
271                style: TransitionStyle::Split,
272                duration: 3.5,
273                dimension: Dimension::Vertical,
274                motion: Motion::Outward,
275                direction: TransitionDirection::Angle(0),
276                scale: 1.0,
277                opaque: false,
278            })
279        );
280        let fly = presentation("/Trans << /S /Fly /Di /None /SS 0.5 /B true >>").unwrap();
281        assert_eq!(fly.duration, None);
282        let fly = fly.transition.unwrap();
283        assert_eq!(fly.style, TransitionStyle::Fly);
284        assert_eq!(fly.direction, TransitionDirection::None);
285        assert_eq!((fly.scale, fly.opaque), (0.5, true));
286        let wipe = presentation("/Trans << /S /Wipe /Di 270 >>")
287            .unwrap()
288            .transition
289            .unwrap();
290        assert_eq!(wipe.style, TransitionStyle::Wipe);
291        assert_eq!(wipe.direction, TransitionDirection::Angle(270));
292        assert_eq!(
293            presentation("/Trans << >>").unwrap().transition,
294            Some(Transition::default())
295        );
296        assert_eq!(Transition::default().style, TransitionStyle::Replace);
297        assert_eq!(Transition::default().duration, 1.0);
298    }
299
300    /// A page with neither entry is no slide; `/Dur` alone times the page
301    /// without a transition; a `/Trans` that is no dictionary reads as no
302    /// transition; an unknown style reads as the effect-free default.
303    // Covers ISO 32000-1 §12.4.4.
304    #[test]
305    fn pages_without_the_entries_have_no_presentation_and_odd_values_fall_back() {
306        assert_eq!(presentation(""), None);
307        assert_eq!(
308            presentation("/Dur 7"),
309            Some(Presentation {
310                duration: Some(7.0),
311                transition: None,
312            })
313        );
314        assert_eq!(presentation("/Trans 5"), None);
315        assert_eq!(
316            presentation("/Dur 2 /Trans 5"),
317            Some(Presentation {
318                duration: Some(2.0),
319                transition: None,
320            })
321        );
322        let odd = presentation("/Trans << /S /Sparkle /Dm /Q /M /Z /Di 45 >>")
323            .unwrap()
324            .transition
325            .unwrap();
326        assert_eq!(odd.style, TransitionStyle::Replace);
327        assert_eq!(odd.dimension, Dimension::Horizontal);
328        assert_eq!(odd.motion, Motion::Inward);
329        assert_eq!(odd.direction, TransitionDirection::Angle(45));
330    }
331}