1use crate::core::escape_xml;
6
7#[derive(Clone, Debug, Copy, PartialEq, Eq)]
9pub enum VideoFormat {
10 Mp4,
11 Wmv,
12 Avi,
13 Mov,
14 Mkv,
15 Webm,
16 M4v,
17}
18
19impl VideoFormat {
20 pub fn mime_type(&self) -> &'static str {
22 match self {
23 VideoFormat::Mp4 => "video/mp4",
24 VideoFormat::Wmv => "video/x-ms-wmv",
25 VideoFormat::Avi => "video/x-msvideo",
26 VideoFormat::Mov => "video/quicktime",
27 VideoFormat::Mkv => "video/x-matroska",
28 VideoFormat::Webm => "video/webm",
29 VideoFormat::M4v => "video/x-m4v",
30 }
31 }
32
33 pub fn extension(&self) -> &'static str {
35 match self {
36 VideoFormat::Mp4 => "mp4",
37 VideoFormat::Wmv => "wmv",
38 VideoFormat::Avi => "avi",
39 VideoFormat::Mov => "mov",
40 VideoFormat::Mkv => "mkv",
41 VideoFormat::Webm => "webm",
42 VideoFormat::M4v => "m4v",
43 }
44 }
45
46 pub fn from_extension(ext: &str) -> Option<Self> {
48 match ext.to_lowercase().as_str() {
49 "mp4" => Some(VideoFormat::Mp4),
50 "wmv" => Some(VideoFormat::Wmv),
51 "avi" => Some(VideoFormat::Avi),
52 "mov" => Some(VideoFormat::Mov),
53 "mkv" => Some(VideoFormat::Mkv),
54 "webm" => Some(VideoFormat::Webm),
55 "m4v" => Some(VideoFormat::M4v),
56 _ => None,
57 }
58 }
59}
60
61#[derive(Clone, Debug, Copy, PartialEq, Eq)]
63pub enum AudioFormat {
64 Mp3,
65 Wav,
66 Wma,
67 M4a,
68 Ogg,
69 Flac,
70 Aac,
71}
72
73impl AudioFormat {
74 pub fn mime_type(&self) -> &'static str {
76 match self {
77 AudioFormat::Mp3 => "audio/mpeg",
78 AudioFormat::Wav => "audio/wav",
79 AudioFormat::Wma => "audio/x-ms-wma",
80 AudioFormat::M4a => "audio/mp4",
81 AudioFormat::Ogg => "audio/ogg",
82 AudioFormat::Flac => "audio/flac",
83 AudioFormat::Aac => "audio/aac",
84 }
85 }
86
87 pub fn extension(&self) -> &'static str {
89 match self {
90 AudioFormat::Mp3 => "mp3",
91 AudioFormat::Wav => "wav",
92 AudioFormat::Wma => "wma",
93 AudioFormat::M4a => "m4a",
94 AudioFormat::Ogg => "ogg",
95 AudioFormat::Flac => "flac",
96 AudioFormat::Aac => "aac",
97 }
98 }
99
100 pub fn from_extension(ext: &str) -> Option<Self> {
102 match ext.to_lowercase().as_str() {
103 "mp3" => Some(AudioFormat::Mp3),
104 "wav" => Some(AudioFormat::Wav),
105 "wma" => Some(AudioFormat::Wma),
106 "m4a" => Some(AudioFormat::M4a),
107 "ogg" => Some(AudioFormat::Ogg),
108 "flac" => Some(AudioFormat::Flac),
109 "aac" => Some(AudioFormat::Aac),
110 _ => None,
111 }
112 }
113}
114
115#[derive(Clone, Debug)]
117pub struct VideoOptions {
118 pub auto_play: bool,
120 pub loop_playback: bool,
122 pub hide_when_stopped: bool,
124 pub muted: bool,
126 pub start_time: Option<u32>,
128 pub end_time: Option<u32>,
130 pub volume: u32,
132}
133
134impl Default for VideoOptions {
135 fn default() -> Self {
136 VideoOptions {
137 auto_play: false,
138 loop_playback: false,
139 hide_when_stopped: false,
140 muted: false,
141 start_time: None,
142 end_time: None,
143 volume: 100,
144 }
145 }
146}
147
148impl VideoOptions {
149 pub fn auto_play() -> Self {
151 VideoOptions {
152 auto_play: true,
153 ..Default::default()
154 }
155 }
156
157 pub fn with_loop(mut self, loop_playback: bool) -> Self {
159 self.loop_playback = loop_playback;
160 self
161 }
162
163 pub fn with_muted(mut self, muted: bool) -> Self {
165 self.muted = muted;
166 self
167 }
168
169 pub fn with_volume(mut self, volume: u32) -> Self {
171 self.volume = volume.min(100);
172 self
173 }
174
175 pub fn with_start_time(mut self, ms: u32) -> Self {
177 self.start_time = Some(ms);
178 self
179 }
180
181 pub fn with_end_time(mut self, ms: u32) -> Self {
183 self.end_time = Some(ms);
184 self
185 }
186}
187
188#[derive(Clone, Debug)]
190pub struct AudioOptions {
191 pub auto_play: bool,
193 pub loop_playback: bool,
195 pub hide_during_show: bool,
197 pub play_across_slides: bool,
199 pub volume: u32,
201}
202
203impl Default for AudioOptions {
204 fn default() -> Self {
205 AudioOptions {
206 auto_play: false,
207 loop_playback: false,
208 hide_during_show: false,
209 play_across_slides: false,
210 volume: 100,
211 }
212 }
213}
214
215impl AudioOptions {
216 pub fn auto_play() -> Self {
218 AudioOptions {
219 auto_play: true,
220 ..Default::default()
221 }
222 }
223
224 pub fn with_loop(mut self, loop_playback: bool) -> Self {
226 self.loop_playback = loop_playback;
227 self
228 }
229
230 pub fn with_play_across_slides(mut self, play: bool) -> Self {
232 self.play_across_slides = play;
233 self
234 }
235
236 pub fn with_volume(mut self, volume: u32) -> Self {
238 self.volume = volume.min(100);
239 self
240 }
241}
242
243#[derive(Clone, Debug)]
245pub struct Video {
246 pub source: String,
248 pub format: VideoFormat,
250 pub x: u32,
252 pub y: u32,
254 pub width: u32,
256 pub height: u32,
258 pub options: VideoOptions,
260 pub poster: Option<String>,
262 pub alt_text: Option<String>,
264}
265
266impl Video {
267 pub fn new(source: &str, format: VideoFormat, x: u32, y: u32, width: u32, height: u32) -> Self {
269 Video {
270 source: source.to_string(),
271 format,
272 x,
273 y,
274 width,
275 height,
276 options: VideoOptions::default(),
277 poster: None,
278 alt_text: None,
279 }
280 }
281
282 pub fn from_file(path: &str, x: u32, y: u32, width: u32, height: u32) -> Option<Self> {
284 let ext = path.rsplit('.').next()?;
285 let format = VideoFormat::from_extension(ext)?;
286 Some(Self::new(path, format, x, y, width, height))
287 }
288
289 pub fn with_options(mut self, options: VideoOptions) -> Self {
291 self.options = options;
292 self
293 }
294
295 pub fn with_poster(mut self, poster: &str) -> Self {
297 self.poster = Some(poster.to_string());
298 self
299 }
300
301 pub fn with_alt_text(mut self, alt: &str) -> Self {
303 self.alt_text = Some(alt.to_string());
304 self
305 }
306}
307
308#[derive(Clone, Debug)]
310pub struct Audio {
311 pub source: String,
313 pub format: AudioFormat,
315 pub x: u32,
317 pub y: u32,
319 pub width: u32,
321 pub height: u32,
323 pub options: AudioOptions,
325 pub alt_text: Option<String>,
327}
328
329impl Audio {
330 pub fn new(source: &str, format: AudioFormat, x: u32, y: u32, width: u32, height: u32) -> Self {
332 Audio {
333 source: source.to_string(),
334 format,
335 x,
336 y,
337 width,
338 height,
339 options: AudioOptions::default(),
340 alt_text: None,
341 }
342 }
343
344 pub fn from_file(path: &str, x: u32, y: u32, width: u32, height: u32) -> Option<Self> {
346 let ext = path.rsplit('.').next()?;
347 let format = AudioFormat::from_extension(ext)?;
348 Some(Self::new(path, format, x, y, width, height))
349 }
350
351 pub fn with_options(mut self, options: AudioOptions) -> Self {
353 self.options = options;
354 self
355 }
356
357 pub fn with_alt_text(mut self, alt: &str) -> Self {
359 self.alt_text = Some(alt.to_string());
360 self
361 }
362}
363
364pub fn generate_video_xml(video: &Video, shape_id: usize, video_r_id: &str, _image_r_id: &str) -> String {
366 let alt_text = video.alt_text.as_deref().unwrap_or("Video");
367
368 format!(
369 r#"<p:pic>
370<p:nvPicPr>
371<p:cNvPr id="{}" name="Video {}" descr="{}">
372<a:hlinkClick r:id="" action="ppaction://media"/>
373</p:cNvPr>
374<p:cNvPicPr>
375<a:picLocks noChangeAspect="1"/>
376</p:cNvPicPr>
377<p:nvPr>
378<a:videoFile r:link="{}"/>
379<p:extLst>
380<p:ext uri="{{DAA4B4D4-6D71-4841-9C94-3DE7FCFB9230}}">
381<p14:media xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" r:embed="{}"/>
382</p:ext>
383</p:extLst>
384</p:nvPr>
385</p:nvPicPr>
386<p:blipFill>
387<a:blip r:embed="{}"/>
388<a:stretch>
389<a:fillRect/>
390</a:stretch>
391</p:blipFill>
392<p:spPr>
393<a:xfrm>
394<a:off x="{}" y="{}"/>
395<a:ext cx="{}" cy="{}"/>
396</a:xfrm>
397<a:prstGeom prst="rect">
398<a:avLst/>
399</a:prstGeom>
400</p:spPr>
401</p:pic>"#,
402 shape_id, shape_id, escape_xml(alt_text),
403 video_r_id, video_r_id, video_r_id,
404 video.x, video.y, video.width, video.height
405 )
406}
407
408pub fn generate_audio_xml(audio: &Audio, shape_id: usize, audio_r_id: &str) -> String {
410 let alt_text = audio.alt_text.as_deref().unwrap_or("Audio");
411
412 format!(
413 r#"<p:pic>
414<p:nvPicPr>
415<p:cNvPr id="{}" name="Audio {}" descr="{}">
416<a:hlinkClick r:id="" action="ppaction://media"/>
417</p:cNvPr>
418<p:cNvPicPr>
419<a:picLocks noChangeAspect="1"/>
420</p:cNvPicPr>
421<p:nvPr>
422<a:audioFile r:link="{}"/>
423<p:extLst>
424<p:ext uri="{{DAA4B4D4-6D71-4841-9C94-3DE7FCFB9230}}">
425<p14:media xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" r:embed="{}"/>
426</p:ext>
427</p:extLst>
428</p:nvPr>
429</p:nvPicPr>
430<p:blipFill>
431<a:blip r:embed="{}"/>
432<a:stretch>
433<a:fillRect/>
434</a:stretch>
435</p:blipFill>
436<p:spPr>
437<a:xfrm>
438<a:off x="{}" y="{}"/>
439<a:ext cx="{}" cy="{}"/>
440</a:xfrm>
441<a:prstGeom prst="rect">
442<a:avLst/>
443</a:prstGeom>
444</p:spPr>
445</p:pic>"#,
446 shape_id, shape_id, escape_xml(alt_text),
447 audio_r_id, audio_r_id, audio_r_id,
448 audio.x, audio.y, audio.width, audio.height
449 )
450}
451
452pub fn video_content_type(format: VideoFormat) -> String {
454 format!(
455 r#"<Default Extension="{}" ContentType="{}"/>"#,
456 format.extension(),
457 format.mime_type()
458 )
459}
460
461pub fn audio_content_type(format: AudioFormat) -> String {
463 format!(
464 r#"<Default Extension="{}" ContentType="{}"/>"#,
465 format.extension(),
466 format.mime_type()
467 )
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_video_format_mime() {
476 assert_eq!(VideoFormat::Mp4.mime_type(), "video/mp4");
477 assert_eq!(VideoFormat::Wmv.mime_type(), "video/x-ms-wmv");
478 }
479
480 #[test]
481 fn test_video_format_extension() {
482 assert_eq!(VideoFormat::Mp4.extension(), "mp4");
483 assert_eq!(VideoFormat::from_extension("mp4"), Some(VideoFormat::Mp4));
484 }
485
486 #[test]
487 fn test_audio_format_mime() {
488 assert_eq!(AudioFormat::Mp3.mime_type(), "audio/mpeg");
489 assert_eq!(AudioFormat::Wav.mime_type(), "audio/wav");
490 }
491
492 #[test]
493 fn test_audio_format_extension() {
494 assert_eq!(AudioFormat::Mp3.extension(), "mp3");
495 assert_eq!(AudioFormat::from_extension("mp3"), Some(AudioFormat::Mp3));
496 }
497
498 #[test]
499 fn test_video_options() {
500 let opts = VideoOptions::auto_play()
501 .with_loop(true)
502 .with_volume(80);
503 assert!(opts.auto_play);
504 assert!(opts.loop_playback);
505 assert_eq!(opts.volume, 80);
506 }
507
508 #[test]
509 fn test_audio_options() {
510 let opts = AudioOptions::auto_play()
511 .with_play_across_slides(true);
512 assert!(opts.auto_play);
513 assert!(opts.play_across_slides);
514 }
515
516 #[test]
517 fn test_video_from_file() {
518 let video = Video::from_file("test.mp4", 0, 0, 1000000, 750000);
519 assert!(video.is_some());
520 let video = video.unwrap();
521 assert_eq!(video.format, VideoFormat::Mp4);
522 }
523
524 #[test]
525 fn test_audio_from_file() {
526 let audio = Audio::from_file("test.mp3", 0, 0, 500000, 500000);
527 assert!(audio.is_some());
528 let audio = audio.unwrap();
529 assert_eq!(audio.format, AudioFormat::Mp3);
530 }
531
532 #[test]
533 fn test_video_builder() {
534 let video = Video::new("video.mp4", VideoFormat::Mp4, 0, 0, 1000000, 750000)
535 .with_options(VideoOptions::auto_play())
536 .with_alt_text("My Video");
537 assert!(video.options.auto_play);
538 assert_eq!(video.alt_text, Some("My Video".to_string()));
539 }
540
541 #[test]
542 fn test_generate_video_xml() {
543 let video = Video::new("video.mp4", VideoFormat::Mp4, 0, 0, 1000000, 750000);
544 let xml = generate_video_xml(&video, 1, "rId1", "rId2");
545 assert!(xml.contains("p:pic"));
546 assert!(xml.contains("videoFile"));
547 }
548
549 #[test]
550 fn test_generate_audio_xml() {
551 let audio = Audio::new("audio.mp3", AudioFormat::Mp3, 0, 0, 500000, 500000);
552 let xml = generate_audio_xml(&audio, 1, "rId1");
553 assert!(xml.contains("p:pic"));
554 assert!(xml.contains("audioFile"));
555 }
556}