Skip to main content

oxidize_pdf/
page_transitions.rs

1//! Page transitions for presentations in PDF documents
2//!
3//! Page transitions control the visual effect when transitioning between pages
4//! in presentation mode (full screen). These are defined in ISO 32000-1:2008.
5
6use crate::objects::{Dictionary, Object};
7
8/// Page transition styles defined in ISO 32000-1
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum TransitionStyle {
11    /// Split transition
12    Split,
13    /// Blinds transition  
14    Blinds,
15    /// Box transition
16    Box,
17    /// Wipe transition
18    Wipe,
19    /// Dissolve transition
20    Dissolve,
21    /// Glitter transition
22    Glitter,
23    /// Replace transition (default)
24    Replace,
25    /// Fly transition  
26    Fly,
27    /// Push transition
28    Push,
29    /// Cover transition
30    Cover,
31    /// Uncover transition
32    Uncover,
33    /// Fade transition
34    Fade,
35}
36
37impl TransitionStyle {
38    /// Convert to PDF name
39    pub fn to_pdf_name(&self) -> &'static str {
40        match self {
41            TransitionStyle::Split => "Split",
42            TransitionStyle::Blinds => "Blinds",
43            TransitionStyle::Box => "Box",
44            TransitionStyle::Wipe => "Wipe",
45            TransitionStyle::Dissolve => "Dissolve",
46            TransitionStyle::Glitter => "Glitter",
47            TransitionStyle::Replace => "Replace",
48            TransitionStyle::Fly => "Fly",
49            TransitionStyle::Push => "Push",
50            TransitionStyle::Cover => "Cover",
51            TransitionStyle::Uncover => "Uncover",
52            TransitionStyle::Fade => "Fade",
53        }
54    }
55}
56
57/// Transition dimension for applicable styles
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum TransitionDimension {
60    /// Horizontal direction
61    Horizontal,
62    /// Vertical direction
63    Vertical,
64}
65
66impl TransitionDimension {
67    /// Convert to PDF name
68    pub fn to_pdf_name(&self) -> &'static str {
69        match self {
70            TransitionDimension::Horizontal => "H",
71            TransitionDimension::Vertical => "V",
72        }
73    }
74}
75
76/// Motion direction for applicable transition styles
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub enum TransitionMotion {
79    /// Inward motion
80    Inward,
81    /// Outward motion
82    Outward,
83}
84
85impl TransitionMotion {
86    /// Convert to PDF name
87    pub fn to_pdf_name(&self) -> &'static str {
88        match self {
89            TransitionMotion::Inward => "I",
90            TransitionMotion::Outward => "O",
91        }
92    }
93}
94
95/// Direction angle for glitter and fly transitions
96#[derive(Debug, Clone, Copy, PartialEq)]
97pub enum TransitionDirection {
98    /// Left to right (0 degrees)
99    LeftToRight,
100    /// Bottom to top (90 degrees)  
101    BottomToTop,
102    /// Right to left (180 degrees)
103    RightToLeft,
104    /// Top to bottom (270 degrees)
105    TopToBottom,
106    /// Top-left to bottom-right (315 degrees)
107    TopLeftToBottomRight,
108    /// Custom angle in degrees (0-360)
109    Custom(u16),
110}
111
112impl TransitionDirection {
113    /// Convert to PDF angle value
114    pub fn to_pdf_angle(&self) -> u16 {
115        match self {
116            TransitionDirection::LeftToRight => 0,
117            TransitionDirection::BottomToTop => 90,
118            TransitionDirection::RightToLeft => 180,
119            TransitionDirection::TopToBottom => 270,
120            TransitionDirection::TopLeftToBottomRight => 315,
121            TransitionDirection::Custom(angle) => *angle % 360,
122        }
123    }
124}
125
126/// Page transition definition
127#[derive(Debug, Clone)]
128pub struct PageTransition {
129    /// Transition style
130    pub style: TransitionStyle,
131    /// Duration in seconds
132    pub duration: Option<f32>,
133    /// Dimension (for applicable styles)
134    pub dimension: Option<TransitionDimension>,
135    /// Motion direction (for applicable styles)
136    pub motion: Option<TransitionMotion>,
137    /// Direction angle (for glitter and fly)
138    pub direction: Option<TransitionDirection>,
139    /// Scale factor (for fly transitions)
140    pub scale: Option<f32>,
141    /// Rectangular area (for fly transitions)
142    pub area: Option<[f32; 4]>, // [x, y, width, height]
143}
144
145impl PageTransition {
146    /// Create a new page transition with the specified style
147    pub fn new(style: TransitionStyle) -> Self {
148        PageTransition {
149            style,
150            duration: None,
151            dimension: None,
152            motion: None,
153            direction: None,
154            scale: None,
155            area: None,
156        }
157    }
158
159    /// Set transition duration in seconds
160    pub fn with_duration(mut self, duration: f32) -> Self {
161        self.duration = Some(duration.max(0.0));
162        self
163    }
164
165    /// Set transition dimension (for Split, Blinds styles)
166    pub fn with_dimension(mut self, dimension: TransitionDimension) -> Self {
167        self.dimension = Some(dimension);
168        self
169    }
170
171    /// Set transition motion (for Split, Box styles)
172    pub fn with_motion(mut self, motion: TransitionMotion) -> Self {
173        self.motion = Some(motion);
174        self
175    }
176
177    /// Set transition direction (for Wipe, Glitter, Fly styles)
178    pub fn with_direction(mut self, direction: TransitionDirection) -> Self {
179        self.direction = Some(direction);
180        self
181    }
182
183    /// Set scale factor (for Fly style)
184    pub fn with_scale(mut self, scale: f32) -> Self {
185        self.scale = Some(scale.clamp(0.01, 100.0));
186        self
187    }
188
189    /// Set rectangular area (for Fly style)
190    pub fn with_area(mut self, x: f32, y: f32, width: f32, height: f32) -> Self {
191        self.area = Some([x, y, width, height]);
192        self
193    }
194
195    /// Convert to PDF dictionary
196    pub fn to_dict(&self) -> Dictionary {
197        let mut dict = Dictionary::new();
198        dict.set("Type", Object::Name("Trans".to_string()));
199        dict.set("S", Object::Name(self.style.to_pdf_name().to_string()));
200
201        if let Some(duration) = self.duration {
202            dict.set("D", Object::Real(duration as f64));
203        }
204
205        if let Some(dimension) = self.dimension {
206            dict.set("Dm", Object::Name(dimension.to_pdf_name().to_string()));
207        }
208
209        if let Some(motion) = self.motion {
210            dict.set("M", Object::Name(motion.to_pdf_name().to_string()));
211        }
212
213        if let Some(direction) = self.direction {
214            dict.set("Di", Object::Integer(direction.to_pdf_angle() as i64));
215        }
216
217        if let Some(scale) = self.scale {
218            dict.set("SS", Object::Real(scale as f64));
219        }
220
221        if let Some(area) = self.area {
222            let area_array = vec![
223                Object::Real(area[0] as f64),
224                Object::Real(area[1] as f64),
225                Object::Real(area[2] as f64),
226                Object::Real(area[3] as f64),
227            ];
228            dict.set("B", Object::Array(area_array));
229        }
230
231        dict
232    }
233
234    // Convenience constructors for common transitions
235
236    /// Split transition (horizontal or vertical)
237    pub fn split(dimension: TransitionDimension, motion: TransitionMotion) -> Self {
238        PageTransition::new(TransitionStyle::Split)
239            .with_dimension(dimension)
240            .with_motion(motion)
241    }
242
243    /// Blinds transition (horizontal or vertical)
244    pub fn blinds(dimension: TransitionDimension) -> Self {
245        PageTransition::new(TransitionStyle::Blinds).with_dimension(dimension)
246    }
247
248    /// Box transition (inward or outward)
249    pub fn box_transition(motion: TransitionMotion) -> Self {
250        PageTransition::new(TransitionStyle::Box).with_motion(motion)
251    }
252
253    /// Wipe transition with direction
254    pub fn wipe(direction: TransitionDirection) -> Self {
255        PageTransition::new(TransitionStyle::Wipe).with_direction(direction)
256    }
257
258    /// Dissolve transition
259    pub fn dissolve() -> Self {
260        PageTransition::new(TransitionStyle::Dissolve)
261    }
262
263    /// Glitter transition with direction
264    pub fn glitter(direction: TransitionDirection) -> Self {
265        PageTransition::new(TransitionStyle::Glitter).with_direction(direction)
266    }
267
268    /// Replace transition (no effect)
269    pub fn replace() -> Self {
270        PageTransition::new(TransitionStyle::Replace)
271    }
272
273    /// Fly transition with direction and optional scale
274    pub fn fly(direction: TransitionDirection) -> Self {
275        PageTransition::new(TransitionStyle::Fly).with_direction(direction)
276    }
277
278    /// Push transition with direction
279    pub fn push(direction: TransitionDirection) -> Self {
280        PageTransition::new(TransitionStyle::Push).with_direction(direction)
281    }
282
283    /// Cover transition with direction
284    pub fn cover(direction: TransitionDirection) -> Self {
285        PageTransition::new(TransitionStyle::Cover).with_direction(direction)
286    }
287
288    /// Uncover transition with direction
289    pub fn uncover(direction: TransitionDirection) -> Self {
290        PageTransition::new(TransitionStyle::Uncover).with_direction(direction)
291    }
292
293    /// Fade transition
294    pub fn fade() -> Self {
295        PageTransition::new(TransitionStyle::Fade)
296    }
297}
298
299impl Default for PageTransition {
300    fn default() -> Self {
301        PageTransition::replace()
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_transition_style_names() {
311        assert_eq!(TransitionStyle::Split.to_pdf_name(), "Split");
312        assert_eq!(TransitionStyle::Blinds.to_pdf_name(), "Blinds");
313        assert_eq!(TransitionStyle::Box.to_pdf_name(), "Box");
314        assert_eq!(TransitionStyle::Wipe.to_pdf_name(), "Wipe");
315        assert_eq!(TransitionStyle::Dissolve.to_pdf_name(), "Dissolve");
316        assert_eq!(TransitionStyle::Glitter.to_pdf_name(), "Glitter");
317        assert_eq!(TransitionStyle::Replace.to_pdf_name(), "Replace");
318        assert_eq!(TransitionStyle::Fly.to_pdf_name(), "Fly");
319        assert_eq!(TransitionStyle::Push.to_pdf_name(), "Push");
320        assert_eq!(TransitionStyle::Cover.to_pdf_name(), "Cover");
321        assert_eq!(TransitionStyle::Uncover.to_pdf_name(), "Uncover");
322        assert_eq!(TransitionStyle::Fade.to_pdf_name(), "Fade");
323    }
324
325    #[test]
326    fn test_transition_dimension_names() {
327        assert_eq!(TransitionDimension::Horizontal.to_pdf_name(), "H");
328        assert_eq!(TransitionDimension::Vertical.to_pdf_name(), "V");
329    }
330
331    #[test]
332    fn test_transition_motion_names() {
333        assert_eq!(TransitionMotion::Inward.to_pdf_name(), "I");
334        assert_eq!(TransitionMotion::Outward.to_pdf_name(), "O");
335    }
336
337    #[test]
338    fn test_transition_direction_angles() {
339        assert_eq!(TransitionDirection::LeftToRight.to_pdf_angle(), 0);
340        assert_eq!(TransitionDirection::BottomToTop.to_pdf_angle(), 90);
341        assert_eq!(TransitionDirection::RightToLeft.to_pdf_angle(), 180);
342        assert_eq!(TransitionDirection::TopToBottom.to_pdf_angle(), 270);
343        assert_eq!(
344            TransitionDirection::TopLeftToBottomRight.to_pdf_angle(),
345            315
346        );
347        assert_eq!(TransitionDirection::Custom(45).to_pdf_angle(), 45);
348        assert_eq!(TransitionDirection::Custom(450).to_pdf_angle(), 90); // 450 % 360 = 90
349    }
350
351    #[test]
352    fn test_basic_transition() {
353        let transition = PageTransition::new(TransitionStyle::Dissolve);
354        let dict = transition.to_dict();
355
356        assert_eq!(dict.get("Type"), Some(&Object::Name("Trans".to_string())));
357        assert_eq!(dict.get("S"), Some(&Object::Name("Dissolve".to_string())));
358    }
359
360    #[test]
361    fn test_transition_with_duration() {
362        let transition = PageTransition::dissolve().with_duration(2.5);
363        let dict = transition.to_dict();
364
365        assert_eq!(dict.get("D"), Some(&Object::Real(2.5)));
366    }
367
368    #[test]
369    fn test_split_transition() {
370        let transition =
371            PageTransition::split(TransitionDimension::Horizontal, TransitionMotion::Inward);
372        let dict = transition.to_dict();
373
374        assert_eq!(dict.get("S"), Some(&Object::Name("Split".to_string())));
375        assert_eq!(dict.get("Dm"), Some(&Object::Name("H".to_string())));
376        assert_eq!(dict.get("M"), Some(&Object::Name("I".to_string())));
377    }
378
379    #[test]
380    fn test_wipe_transition_with_direction() {
381        let transition = PageTransition::wipe(TransitionDirection::LeftToRight);
382        let dict = transition.to_dict();
383
384        assert_eq!(dict.get("S"), Some(&Object::Name("Wipe".to_string())));
385        assert_eq!(dict.get("Di"), Some(&Object::Integer(0)));
386    }
387
388    #[test]
389    fn test_fly_transition_with_scale() {
390        let transition = PageTransition::fly(TransitionDirection::BottomToTop)
391            .with_scale(1.5)
392            .with_area(100.0, 100.0, 200.0, 200.0);
393        let dict = transition.to_dict();
394
395        assert_eq!(dict.get("S"), Some(&Object::Name("Fly".to_string())));
396        assert_eq!(dict.get("Di"), Some(&Object::Integer(90)));
397        assert_eq!(dict.get("SS"), Some(&Object::Real(1.5)));
398
399        if let Some(Object::Array(area)) = dict.get("B") {
400            assert_eq!(area.len(), 4);
401        } else {
402            panic!("Expected area array");
403        }
404    }
405
406    #[test]
407    fn test_convenience_constructors() {
408        // Test all convenience constructors
409        assert!(matches!(
410            PageTransition::split(TransitionDimension::Horizontal, TransitionMotion::Inward).style,
411            TransitionStyle::Split
412        ));
413        assert!(matches!(
414            PageTransition::blinds(TransitionDimension::Vertical).style,
415            TransitionStyle::Blinds
416        ));
417        assert!(matches!(
418            PageTransition::box_transition(TransitionMotion::Outward).style,
419            TransitionStyle::Box
420        ));
421        assert!(matches!(
422            PageTransition::wipe(TransitionDirection::LeftToRight).style,
423            TransitionStyle::Wipe
424        ));
425        assert!(matches!(
426            PageTransition::dissolve().style,
427            TransitionStyle::Dissolve
428        ));
429        assert!(matches!(
430            PageTransition::glitter(TransitionDirection::TopToBottom).style,
431            TransitionStyle::Glitter
432        ));
433        assert!(matches!(
434            PageTransition::replace().style,
435            TransitionStyle::Replace
436        ));
437        assert!(matches!(
438            PageTransition::fly(TransitionDirection::RightToLeft).style,
439            TransitionStyle::Fly
440        ));
441        assert!(matches!(
442            PageTransition::push(TransitionDirection::BottomToTop).style,
443            TransitionStyle::Push
444        ));
445        assert!(matches!(
446            PageTransition::cover(TransitionDirection::TopToBottom).style,
447            TransitionStyle::Cover
448        ));
449        assert!(matches!(
450            PageTransition::uncover(TransitionDirection::LeftToRight).style,
451            TransitionStyle::Uncover
452        ));
453        assert!(matches!(
454            PageTransition::fade().style,
455            TransitionStyle::Fade
456        ));
457    }
458
459    #[test]
460    fn test_default_transition() {
461        let transition = PageTransition::default();
462        assert!(matches!(transition.style, TransitionStyle::Replace));
463    }
464
465    #[test]
466    fn test_duration_bounds() {
467        let transition = PageTransition::dissolve().with_duration(-1.0);
468        assert_eq!(transition.duration, Some(0.0));
469    }
470
471    #[test]
472    fn test_scale_bounds() {
473        let transition = PageTransition::fly(TransitionDirection::LeftToRight).with_scale(0.001); // Too small
474        assert_eq!(transition.scale, Some(0.01));
475
476        let transition = PageTransition::fly(TransitionDirection::LeftToRight).with_scale(200.0); // Too large
477        assert_eq!(transition.scale, Some(100.0));
478    }
479}