1use crate::objects::{Dictionary, Object};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum TransitionStyle {
11 Split,
13 Blinds,
15 Box,
17 Wipe,
19 Dissolve,
21 Glitter,
23 Replace,
25 Fly,
27 Push,
29 Cover,
31 Uncover,
33 Fade,
35}
36
37impl TransitionStyle {
38 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#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum TransitionDimension {
60 Horizontal,
62 Vertical,
64}
65
66impl TransitionDimension {
67 pub fn to_pdf_name(&self) -> &'static str {
69 match self {
70 TransitionDimension::Horizontal => "H",
71 TransitionDimension::Vertical => "V",
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
78pub enum TransitionMotion {
79 Inward,
81 Outward,
83}
84
85impl TransitionMotion {
86 pub fn to_pdf_name(&self) -> &'static str {
88 match self {
89 TransitionMotion::Inward => "I",
90 TransitionMotion::Outward => "O",
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq)]
97pub enum TransitionDirection {
98 LeftToRight,
100 BottomToTop,
102 RightToLeft,
104 TopToBottom,
106 TopLeftToBottomRight,
108 Custom(u16),
110}
111
112impl TransitionDirection {
113 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#[derive(Debug, Clone)]
128pub struct PageTransition {
129 pub style: TransitionStyle,
131 pub duration: Option<f32>,
133 pub dimension: Option<TransitionDimension>,
135 pub motion: Option<TransitionMotion>,
137 pub direction: Option<TransitionDirection>,
139 pub scale: Option<f32>,
141 pub area: Option<[f32; 4]>, }
144
145impl PageTransition {
146 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 pub fn with_duration(mut self, duration: f32) -> Self {
161 self.duration = Some(duration.max(0.0));
162 self
163 }
164
165 pub fn with_dimension(mut self, dimension: TransitionDimension) -> Self {
167 self.dimension = Some(dimension);
168 self
169 }
170
171 pub fn with_motion(mut self, motion: TransitionMotion) -> Self {
173 self.motion = Some(motion);
174 self
175 }
176
177 pub fn with_direction(mut self, direction: TransitionDirection) -> Self {
179 self.direction = Some(direction);
180 self
181 }
182
183 pub fn with_scale(mut self, scale: f32) -> Self {
185 self.scale = Some(scale.clamp(0.01, 100.0));
186 self
187 }
188
189 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 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 pub fn split(dimension: TransitionDimension, motion: TransitionMotion) -> Self {
238 PageTransition::new(TransitionStyle::Split)
239 .with_dimension(dimension)
240 .with_motion(motion)
241 }
242
243 pub fn blinds(dimension: TransitionDimension) -> Self {
245 PageTransition::new(TransitionStyle::Blinds).with_dimension(dimension)
246 }
247
248 pub fn box_transition(motion: TransitionMotion) -> Self {
250 PageTransition::new(TransitionStyle::Box).with_motion(motion)
251 }
252
253 pub fn wipe(direction: TransitionDirection) -> Self {
255 PageTransition::new(TransitionStyle::Wipe).with_direction(direction)
256 }
257
258 pub fn dissolve() -> Self {
260 PageTransition::new(TransitionStyle::Dissolve)
261 }
262
263 pub fn glitter(direction: TransitionDirection) -> Self {
265 PageTransition::new(TransitionStyle::Glitter).with_direction(direction)
266 }
267
268 pub fn replace() -> Self {
270 PageTransition::new(TransitionStyle::Replace)
271 }
272
273 pub fn fly(direction: TransitionDirection) -> Self {
275 PageTransition::new(TransitionStyle::Fly).with_direction(direction)
276 }
277
278 pub fn push(direction: TransitionDirection) -> Self {
280 PageTransition::new(TransitionStyle::Push).with_direction(direction)
281 }
282
283 pub fn cover(direction: TransitionDirection) -> Self {
285 PageTransition::new(TransitionStyle::Cover).with_direction(direction)
286 }
287
288 pub fn uncover(direction: TransitionDirection) -> Self {
290 PageTransition::new(TransitionStyle::Uncover).with_direction(direction)
291 }
292
293 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); }
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 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); assert_eq!(transition.scale, Some(0.01));
475
476 let transition = PageTransition::fly(TransitionDirection::LeftToRight).with_scale(200.0); assert_eq!(transition.scale, Some(100.0));
478 }
479}