1use crate::document::Page;
5use crate::object::{Dict, Object};
6use crate::source::AsyncObjectSource;
7use crate::tree::{resolved_dict, Entries};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum TransitionStyle {
13 Split,
15 Blinds,
17 Box,
19 Wipe,
21 Dissolve,
23 Glitter,
25 #[default]
27 Replace,
28 Fly,
30 Push,
32 Cover,
34 Uncover,
36 Fade,
38}
39
40impl TransitionStyle {
41 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub enum Dimension {
64 #[default]
66 Horizontal,
67 Vertical,
69}
70
71impl Dimension {
72 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub enum Motion {
85 #[default]
87 Inward,
88 Outward,
90}
91
92impl Motion {
93 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum TransitionDirection {
107 Angle(u32),
110 None,
112}
113
114impl Default for TransitionDirection {
115 fn default() -> TransitionDirection {
116 TransitionDirection::Angle(0)
117 }
118}
119
120#[derive(Debug, Clone, PartialEq)]
123pub struct Transition {
124 pub style: TransitionStyle,
126 pub duration: f64,
128 pub dimension: Dimension,
130 pub motion: Motion,
132 pub direction: TransitionDirection,
134 pub scale: f64,
136 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#[derive(Debug, Clone, PartialEq)]
157pub struct Presentation {
158 pub duration: Option<f64>,
161 pub transition: Option<Transition>,
163}
164
165pub 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
195async 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 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 #[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 #[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}