use crate::annotation_types::AnnotationFlags;
use crate::geometry::Rect;
use crate::object::{Object, ObjectRef};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum MoviePlayMode {
#[default]
Once,
Open,
Repeat,
Palindrome,
}
impl MoviePlayMode {
pub fn pdf_name(&self) -> &'static str {
match self {
MoviePlayMode::Once => "Once",
MoviePlayMode::Open => "Open",
MoviePlayMode::Repeat => "Repeat",
MoviePlayMode::Palindrome => "Palindrome",
}
}
}
#[derive(Debug, Clone)]
pub struct MovieActivation {
pub start: Option<i64>,
pub duration: Option<i64>,
pub rate: f32,
pub volume: f32,
pub show_controls: bool,
pub mode: MoviePlayMode,
pub synchronous: bool,
pub fws_scale: Option<(f32, f32)>,
pub fws_position: Option<(f32, f32)>,
}
impl Default for MovieActivation {
fn default() -> Self {
Self {
start: None,
duration: None,
rate: 1.0,
volume: 1.0,
show_controls: true,
mode: MoviePlayMode::default(),
synchronous: false,
fws_scale: None,
fws_position: None,
}
}
}
impl MovieActivation {
pub fn new() -> Self {
Self::default()
}
pub fn with_start(mut self, start: i64) -> Self {
self.start = Some(start);
self
}
pub fn with_duration(mut self, duration: i64) -> Self {
self.duration = Some(duration);
self
}
pub fn with_rate(mut self, rate: f32) -> Self {
self.rate = rate;
self
}
pub fn with_volume(mut self, volume: f32) -> Self {
self.volume = volume.clamp(0.0, 1.0);
self
}
pub fn with_controls(mut self, show: bool) -> Self {
self.show_controls = show;
self
}
pub fn with_mode(mut self, mode: MoviePlayMode) -> Self {
self.mode = mode;
self
}
pub fn with_synchronous(mut self, sync: bool) -> Self {
self.synchronous = sync;
self
}
pub fn build(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
if let Some(start) = self.start {
dict.insert("Start".to_string(), Object::Integer(start));
}
if let Some(duration) = self.duration {
dict.insert("Duration".to_string(), Object::Integer(duration));
}
if (self.rate - 1.0).abs() > 0.001 {
dict.insert("Rate".to_string(), Object::Real(self.rate as f64));
}
if (self.volume - 1.0).abs() > 0.001 {
dict.insert("Volume".to_string(), Object::Real(self.volume as f64));
}
if !self.show_controls {
dict.insert("ShowControls".to_string(), Object::Boolean(false));
}
if self.mode != MoviePlayMode::Once {
dict.insert("Mode".to_string(), Object::Name(self.mode.pdf_name().to_string()));
}
if self.synchronous {
dict.insert("Synchronous".to_string(), Object::Boolean(true));
}
if let Some((x_scale, y_scale)) = self.fws_scale {
dict.insert(
"FWScale".to_string(),
Object::Array(vec![Object::Real(x_scale as f64), Object::Real(y_scale as f64)]),
);
}
if let Some((x, y)) = self.fws_position {
dict.insert(
"FWPosition".to_string(),
Object::Array(vec![Object::Real(x as f64), Object::Real(y as f64)]),
);
}
dict
}
}
#[derive(Debug, Clone)]
pub struct MovieData {
pub filename: String,
pub data: Vec<u8>,
pub width: Option<u32>,
pub height: Option<u32>,
pub rotation: u16,
pub poster: Option<Vec<u8>>,
}
impl MovieData {
pub fn new(filename: impl Into<String>, data: Vec<u8>) -> Self {
Self {
filename: filename.into(),
data,
width: None,
height: None,
rotation: 0,
poster: None,
}
}
pub fn with_aspect(mut self, width: u32, height: u32) -> Self {
self.width = Some(width);
self.height = Some(height);
self
}
pub fn with_rotation(mut self, rotation: u16) -> Self {
self.rotation = rotation;
self
}
pub fn with_poster(mut self, poster: Vec<u8>) -> Self {
self.poster = Some(poster);
self
}
pub fn build_movie_dict(&self, file_spec_ref: ObjectRef) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("F".to_string(), Object::Reference(file_spec_ref));
if let (Some(w), Some(h)) = (self.width, self.height) {
dict.insert(
"Aspect".to_string(),
Object::Array(vec![Object::Integer(w as i64), Object::Integer(h as i64)]),
);
}
if self.rotation != 0 {
dict.insert("Rotate".to_string(), Object::Integer(self.rotation as i64));
}
if self.poster.is_some() {
dict.insert("Poster".to_string(), Object::Boolean(true));
}
dict
}
}
#[derive(Debug, Clone)]
pub struct MovieAnnotation {
pub rect: Rect,
pub movie: MovieData,
pub title: Option<String>,
pub activation: MovieActivation,
pub border: bool,
pub flags: AnnotationFlags,
pub contents: Option<String>,
}
impl MovieAnnotation {
pub fn new(rect: Rect, filename: impl Into<String>, data: Vec<u8>) -> Self {
Self {
rect,
movie: MovieData::new(filename, data),
title: None,
activation: MovieActivation::default(),
border: true,
flags: AnnotationFlags::printable(),
contents: None,
}
}
pub fn from_movie_data(rect: Rect, movie: MovieData) -> Self {
Self {
rect,
movie,
title: None,
activation: MovieActivation::default(),
border: true,
flags: AnnotationFlags::printable(),
contents: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_aspect(mut self, width: u32, height: u32) -> Self {
self.movie = self.movie.with_aspect(width, height);
self
}
pub fn with_rotation(mut self, rotation: u16) -> Self {
self.movie = self.movie.with_rotation(rotation);
self
}
pub fn with_activation(mut self, activation: MovieActivation) -> Self {
self.activation = activation;
self
}
pub fn with_volume(mut self, volume: f32) -> Self {
self.activation.volume = volume.clamp(0.0, 1.0);
self
}
pub fn with_mode(mut self, mode: MoviePlayMode) -> Self {
self.activation.mode = mode;
self
}
pub fn with_border(mut self, border: bool) -> Self {
self.border = border;
self
}
pub fn with_contents(mut self, contents: impl Into<String>) -> Self {
self.contents = Some(contents.into());
self
}
pub fn with_flags(mut self, flags: AnnotationFlags) -> Self {
self.flags = flags;
self
}
pub fn build(&self, _page_refs: &[ObjectRef]) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("Annot".to_string()));
dict.insert("Subtype".to_string(), Object::Name("Movie".to_string()));
dict.insert(
"Rect".to_string(),
Object::Array(vec![
Object::Real(self.rect.x as f64),
Object::Real(self.rect.y as f64),
Object::Real((self.rect.x + self.rect.width) as f64),
Object::Real((self.rect.y + self.rect.height) as f64),
]),
);
if let Some(ref title) = self.title {
dict.insert("T".to_string(), Object::String(title.as_bytes().to_vec()));
}
if !self.border {
dict.insert(
"Border".to_string(),
Object::Array(vec![Object::Integer(0), Object::Integer(0), Object::Integer(0)]),
);
}
if self.flags.bits() != 0 {
dict.insert("F".to_string(), Object::Integer(self.flags.bits() as i64));
}
if let Some(ref contents) = self.contents {
dict.insert("Contents".to_string(), Object::String(contents.as_bytes().to_vec()));
}
let activation_dict = self.activation.build();
if !activation_dict.is_empty() {
dict.insert("A".to_string(), Object::Dictionary(activation_dict));
}
dict
}
pub fn movie_data(&self) -> &MovieData {
&self.movie
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_movie_data_new() {
let data = vec![0u8; 1000];
let movie = MovieData::new("video.mov", data.clone());
assert_eq!(movie.filename, "video.mov");
assert_eq!(movie.data, data);
assert!(movie.width.is_none());
assert!(movie.height.is_none());
assert_eq!(movie.rotation, 0);
}
#[test]
fn test_movie_data_builder() {
let movie = MovieData::new("test.mp4", vec![])
.with_aspect(1920, 1080)
.with_rotation(90);
assert_eq!(movie.width, Some(1920));
assert_eq!(movie.height, Some(1080));
assert_eq!(movie.rotation, 90);
}
#[test]
fn test_movie_activation_default() {
let activation = MovieActivation::default();
assert_eq!(activation.rate, 1.0);
assert_eq!(activation.volume, 1.0);
assert!(activation.show_controls);
assert!(matches!(activation.mode, MoviePlayMode::Once));
assert!(!activation.synchronous);
}
#[test]
fn test_movie_activation_builder() {
let activation = MovieActivation::new()
.with_start(100)
.with_duration(5000)
.with_rate(1.5)
.with_volume(0.7)
.with_controls(false)
.with_mode(MoviePlayMode::Repeat);
assert_eq!(activation.start, Some(100));
assert_eq!(activation.duration, Some(5000));
assert_eq!(activation.rate, 1.5);
assert_eq!(activation.volume, 0.7);
assert!(!activation.show_controls);
assert!(matches!(activation.mode, MoviePlayMode::Repeat));
}
#[test]
fn test_movie_activation_build() {
let activation = MovieActivation::new()
.with_rate(2.0)
.with_mode(MoviePlayMode::Palindrome);
let dict = activation.build();
assert!(dict.contains_key("Rate"));
assert_eq!(dict.get("Mode"), Some(&Object::Name("Palindrome".to_string())));
}
#[test]
fn test_movie_annotation_new() {
let rect = Rect::new(72.0, 500.0, 320.0, 240.0);
let annot = MovieAnnotation::new(rect, "video.mov", vec![0u8; 100]);
assert_eq!(annot.rect.x, 72.0);
assert_eq!(annot.rect.width, 320.0);
assert_eq!(annot.movie.filename, "video.mov");
assert!(annot.border);
}
#[test]
fn test_movie_annotation_builder() {
let rect = Rect::new(100.0, 400.0, 640.0, 480.0);
let annot = MovieAnnotation::new(rect, "demo.avi", vec![])
.with_title("Demo Video")
.with_aspect(640, 480)
.with_volume(0.5)
.with_mode(MoviePlayMode::Repeat)
.with_border(false)
.with_contents("A demonstration video");
assert_eq!(annot.title, Some("Demo Video".to_string()));
assert_eq!(annot.movie.width, Some(640));
assert_eq!(annot.movie.height, Some(480));
assert_eq!(annot.activation.volume, 0.5);
assert!(matches!(annot.activation.mode, MoviePlayMode::Repeat));
assert!(!annot.border);
assert_eq!(annot.contents, Some("A demonstration video".to_string()));
}
#[test]
fn test_movie_annotation_build() {
let rect = Rect::new(72.0, 500.0, 320.0, 240.0);
let annot = MovieAnnotation::new(rect, "video.mov", vec![])
.with_title("Test Movie")
.with_border(false);
let dict = annot.build(&[]);
assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Movie".to_string())));
assert!(dict.contains_key("Rect"));
assert!(dict.contains_key("T")); assert!(dict.contains_key("Border")); }
#[test]
fn test_play_mode_names() {
assert_eq!(MoviePlayMode::Once.pdf_name(), "Once");
assert_eq!(MoviePlayMode::Open.pdf_name(), "Open");
assert_eq!(MoviePlayMode::Repeat.pdf_name(), "Repeat");
assert_eq!(MoviePlayMode::Palindrome.pdf_name(), "Palindrome");
}
}