ppt_rs/generator/
media.rs

1//! Media embedding support for PPTX (video and audio)
2//!
3//! Provides types and XML generation for embedding videos and audio files.
4
5use crate::core::escape_xml;
6
7/// Video format types
8#[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    /// Get MIME type
21    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    /// Get file extension
34    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    /// Detect format from file extension
47    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/// Audio format types
62#[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    /// Get MIME type
75    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    /// Get file extension
88    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    /// Detect format from file extension
101    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/// Video playback options
116#[derive(Clone, Debug)]
117pub struct VideoOptions {
118    /// Auto-play when slide is shown
119    pub auto_play: bool,
120    /// Loop playback
121    pub loop_playback: bool,
122    /// Hide when not playing
123    pub hide_when_stopped: bool,
124    /// Mute audio
125    pub muted: bool,
126    /// Start time in milliseconds
127    pub start_time: Option<u32>,
128    /// End time in milliseconds
129    pub end_time: Option<u32>,
130    /// Volume (0-100)
131    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    /// Create with auto-play enabled
150    pub fn auto_play() -> Self {
151        VideoOptions {
152            auto_play: true,
153            ..Default::default()
154        }
155    }
156
157    /// Set loop playback
158    pub fn with_loop(mut self, loop_playback: bool) -> Self {
159        self.loop_playback = loop_playback;
160        self
161    }
162
163    /// Set muted
164    pub fn with_muted(mut self, muted: bool) -> Self {
165        self.muted = muted;
166        self
167    }
168
169    /// Set volume (0-100)
170    pub fn with_volume(mut self, volume: u32) -> Self {
171        self.volume = volume.min(100);
172        self
173    }
174
175    /// Set start time in milliseconds
176    pub fn with_start_time(mut self, ms: u32) -> Self {
177        self.start_time = Some(ms);
178        self
179    }
180
181    /// Set end time in milliseconds
182    pub fn with_end_time(mut self, ms: u32) -> Self {
183        self.end_time = Some(ms);
184        self
185    }
186}
187
188/// Audio playback options
189#[derive(Clone, Debug)]
190pub struct AudioOptions {
191    /// Auto-play when slide is shown
192    pub auto_play: bool,
193    /// Loop playback
194    pub loop_playback: bool,
195    /// Hide icon during playback
196    pub hide_during_show: bool,
197    /// Play across slides
198    pub play_across_slides: bool,
199    /// Volume (0-100)
200    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    /// Create with auto-play enabled
217    pub fn auto_play() -> Self {
218        AudioOptions {
219            auto_play: true,
220            ..Default::default()
221        }
222    }
223
224    /// Set loop playback
225    pub fn with_loop(mut self, loop_playback: bool) -> Self {
226        self.loop_playback = loop_playback;
227        self
228    }
229
230    /// Set play across slides
231    pub fn with_play_across_slides(mut self, play: bool) -> Self {
232        self.play_across_slides = play;
233        self
234    }
235
236    /// Set volume (0-100)
237    pub fn with_volume(mut self, volume: u32) -> Self {
238        self.volume = volume.min(100);
239        self
240    }
241}
242
243/// Video element
244#[derive(Clone, Debug)]
245pub struct Video {
246    /// Video file path or URL
247    pub source: String,
248    /// Video format
249    pub format: VideoFormat,
250    /// Position X in EMU
251    pub x: u32,
252    /// Position Y in EMU
253    pub y: u32,
254    /// Width in EMU
255    pub width: u32,
256    /// Height in EMU
257    pub height: u32,
258    /// Playback options
259    pub options: VideoOptions,
260    /// Poster image (thumbnail)
261    pub poster: Option<String>,
262    /// Alt text
263    pub alt_text: Option<String>,
264}
265
266impl Video {
267    /// Create a new video element
268    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    /// Create from file path (auto-detect format)
283    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    /// Set playback options
290    pub fn with_options(mut self, options: VideoOptions) -> Self {
291        self.options = options;
292        self
293    }
294
295    /// Set poster image
296    pub fn with_poster(mut self, poster: &str) -> Self {
297        self.poster = Some(poster.to_string());
298        self
299    }
300
301    /// Set alt text
302    pub fn with_alt_text(mut self, alt: &str) -> Self {
303        self.alt_text = Some(alt.to_string());
304        self
305    }
306}
307
308/// Audio element
309#[derive(Clone, Debug)]
310pub struct Audio {
311    /// Audio file path or URL
312    pub source: String,
313    /// Audio format
314    pub format: AudioFormat,
315    /// Position X in EMU (for icon)
316    pub x: u32,
317    /// Position Y in EMU (for icon)
318    pub y: u32,
319    /// Icon width in EMU
320    pub width: u32,
321    /// Icon height in EMU
322    pub height: u32,
323    /// Playback options
324    pub options: AudioOptions,
325    /// Alt text
326    pub alt_text: Option<String>,
327}
328
329impl Audio {
330    /// Create a new audio element
331    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    /// Create from file path (auto-detect format)
345    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    /// Set playback options
352    pub fn with_options(mut self, options: AudioOptions) -> Self {
353        self.options = options;
354        self
355    }
356
357    /// Set alt text
358    pub fn with_alt_text(mut self, alt: &str) -> Self {
359        self.alt_text = Some(alt.to_string());
360        self
361    }
362}
363
364/// Generate video XML for slide
365pub 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
408/// Generate audio XML for slide
409pub 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
452/// Generate content type for video
453pub fn video_content_type(format: VideoFormat) -> String {
454    format!(
455        r#"<Default Extension="{}" ContentType="{}"/>"#,
456        format.extension(),
457        format.mime_type()
458    )
459}
460
461/// Generate content type for audio
462pub 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}