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 TemporalAccess {
#[default]
Random,
Sequential,
}
impl TemporalAccess {
pub fn pdf_name(&self) -> &'static str {
match self {
TemporalAccess::Random => "Random",
TemporalAccess::Sequential => "Sequential",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MediaPermissions {
pub temp_file_access: bool,
}
impl MediaPermissions {
pub fn new() -> Self {
Self {
temp_file_access: true,
}
}
pub fn build(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("MediaPermissions".to_string()));
dict.insert(
"TF".to_string(),
Object::String(if self.temp_file_access {
"TEMPACCESS".as_bytes().to_vec()
} else {
"TEMPNEVER".as_bytes().to_vec()
}),
);
dict
}
}
#[derive(Debug, Clone)]
pub struct MediaClip {
pub filename: String,
pub data: Vec<u8>,
pub content_type: Option<String>,
pub alt_text: Option<String>,
pub temporal_access: TemporalAccess,
pub permissions: MediaPermissions,
}
impl MediaClip {
pub fn new(filename: impl Into<String>, data: Vec<u8>) -> Self {
Self {
filename: filename.into(),
data,
content_type: None,
alt_text: None,
temporal_access: TemporalAccess::default(),
permissions: MediaPermissions::default(),
}
}
pub fn with_mime_type(mut self, mime: impl Into<String>) -> Self {
self.content_type = Some(mime.into());
self
}
pub fn with_alt_text(mut self, text: impl Into<String>) -> Self {
self.alt_text = Some(text.into());
self
}
pub fn with_temporal_access(mut self, access: TemporalAccess) -> Self {
self.temporal_access = access;
self
}
pub fn build(&self, file_spec_ref: Option<ObjectRef>) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("MediaClip".to_string()));
dict.insert("S".to_string(), Object::Name("MCD".to_string()));
dict.insert("N".to_string(), Object::String(self.filename.as_bytes().to_vec()));
if let Some(ref_obj) = file_spec_ref {
dict.insert("D".to_string(), Object::Reference(ref_obj));
}
if let Some(ref ct) = self.content_type {
dict.insert("CT".to_string(), Object::String(ct.as_bytes().to_vec()));
}
if let Some(ref alt) = self.alt_text {
dict.insert(
"Alt".to_string(),
Object::Array(vec![
Object::String(Vec::new()), Object::String(alt.as_bytes().to_vec()),
]),
);
}
dict.insert("P".to_string(), Object::Dictionary(self.permissions.build()));
dict
}
}
#[derive(Debug, Clone)]
pub struct MediaPlayParams {
pub volume: f32,
pub auto_play: bool,
pub repeat_count: u32,
pub show_controls: bool,
pub bg_color: Option<(u8, u8, u8)>,
pub bg_opacity: Option<f32>,
pub window_type: WindowType,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum WindowType {
#[default]
Annotation,
Floating,
FullScreen,
Hidden,
}
impl WindowType {
pub fn pdf_value(&self) -> i64 {
match self {
WindowType::Annotation => 3, WindowType::Floating => 0, WindowType::FullScreen => 1, WindowType::Hidden => 2, }
}
}
impl Default for MediaPlayParams {
fn default() -> Self {
Self {
volume: 1.0,
auto_play: true,
repeat_count: 1,
show_controls: true,
bg_color: None,
bg_opacity: None,
window_type: WindowType::default(),
}
}
}
impl MediaPlayParams {
pub fn new() -> Self {
Self::default()
}
pub fn with_volume(mut self, volume: f32) -> Self {
self.volume = volume.clamp(0.0, 1.0);
self
}
pub fn with_auto_play(mut self, auto: bool) -> Self {
self.auto_play = auto;
self
}
pub fn with_repeat(mut self, count: u32) -> Self {
self.repeat_count = count;
self
}
pub fn with_controls(mut self, show: bool) -> Self {
self.show_controls = show;
self
}
pub fn with_background(mut self, r: u8, g: u8, b: u8) -> Self {
self.bg_color = Some((r, g, b));
self
}
pub fn with_window_type(mut self, wtype: WindowType) -> Self {
self.window_type = wtype;
self
}
pub fn build(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("MediaPlayParams".to_string()));
if (self.volume - 1.0).abs() > 0.001 {
dict.insert("V".to_string(), Object::Integer((self.volume * 100.0) as i64));
}
if !self.auto_play {
dict.insert("A".to_string(), Object::Boolean(false));
}
if self.repeat_count != 1 {
dict.insert("RC".to_string(), Object::Real(self.repeat_count as f64));
}
let mut be = HashMap::new();
if !self.show_controls {
be.insert("C".to_string(), Object::Boolean(false));
}
if !be.is_empty() {
dict.insert("BE".to_string(), Object::Dictionary(be));
}
if let Some((r, g, b)) = self.bg_color {
let mut bg = HashMap::new();
bg.insert(
"C".to_string(),
Object::Array(vec![
Object::Real(r as f64 / 255.0),
Object::Real(g as f64 / 255.0),
Object::Real(b as f64 / 255.0),
]),
);
if let Some(opacity) = self.bg_opacity {
bg.insert("O".to_string(), Object::Real(opacity as f64));
}
dict.insert("BG".to_string(), Object::Dictionary(bg));
}
if self.window_type != WindowType::Annotation {
let mut fw = HashMap::new();
fw.insert("W".to_string(), Object::Integer(self.window_type.pdf_value()));
dict.insert("F".to_string(), Object::Dictionary(fw));
}
dict
}
}
#[derive(Debug, Clone)]
pub struct MediaRendition {
pub clip: MediaClip,
pub play_params: MediaPlayParams,
pub name: Option<String>,
}
impl MediaRendition {
pub fn new(clip: MediaClip) -> Self {
Self {
clip,
play_params: MediaPlayParams::default(),
name: None,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_play_params(mut self, params: MediaPlayParams) -> Self {
self.play_params = params;
self
}
pub fn with_volume(mut self, volume: f32) -> Self {
self.play_params.volume = volume.clamp(0.0, 1.0);
self
}
pub fn with_repeat(mut self, count: u32) -> Self {
self.play_params.repeat_count = count;
self
}
pub fn build(&self, clip_ref: Option<ObjectRef>) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("Rendition".to_string()));
dict.insert("S".to_string(), Object::Name("MR".to_string()));
if let Some(ref name) = self.name {
dict.insert("N".to_string(), Object::String(name.as_bytes().to_vec()));
}
if let Some(ref_obj) = clip_ref {
dict.insert("C".to_string(), Object::Reference(ref_obj));
}
let play_dict = self.play_params.build();
if !play_dict.is_empty() {
dict.insert("P".to_string(), Object::Dictionary(play_dict));
}
dict
}
pub fn clip(&self) -> &MediaClip {
&self.clip
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RenditionOperation {
#[default]
Play,
Stop,
Pause,
Resume,
PlayAssociate,
}
impl RenditionOperation {
pub fn pdf_value(&self) -> i64 {
match self {
RenditionOperation::Play => 0,
RenditionOperation::Stop => 1,
RenditionOperation::Pause => 2,
RenditionOperation::Resume => 3,
RenditionOperation::PlayAssociate => 4,
}
}
}
#[derive(Debug, Clone)]
pub struct ScreenAnnotation {
pub rect: Rect,
pub rendition: MediaRendition,
pub title: Option<String>,
pub flags: AnnotationFlags,
pub contents: Option<String>,
pub appearance: Option<Vec<u8>>,
pub operation: RenditionOperation,
}
impl ScreenAnnotation {
pub fn new(rect: Rect, rendition: MediaRendition) -> Self {
Self {
rect,
rendition,
title: None,
flags: AnnotationFlags::printable(),
contents: None,
appearance: None,
operation: RenditionOperation::default(),
}
}
pub fn from_clip(rect: Rect, clip: MediaClip) -> Self {
Self::new(rect, MediaRendition::new(clip))
}
pub fn video(rect: Rect, filename: impl Into<String>, data: Vec<u8>) -> Self {
let clip = MediaClip::new(filename, data).with_mime_type("video/mp4");
Self::from_clip(rect, clip)
}
pub fn audio(rect: Rect, filename: impl Into<String>, data: Vec<u8>) -> Self {
let clip = MediaClip::new(filename, data).with_mime_type("audio/mpeg");
Self::from_clip(rect, clip)
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_rendition_name(mut self, name: impl Into<String>) -> Self {
self.rendition = self.rendition.with_name(name);
self
}
pub fn with_volume(mut self, volume: f32) -> Self {
self.rendition.play_params.volume = volume.clamp(0.0, 1.0);
self
}
pub fn with_repeat(mut self, count: u32) -> Self {
self.rendition.play_params.repeat_count = count;
self
}
pub fn with_auto_play(mut self, auto: bool) -> Self {
self.rendition.play_params.auto_play = auto;
self
}
pub fn with_controls(mut self, show: bool) -> Self {
self.rendition.play_params.show_controls = show;
self
}
pub fn with_window_type(mut self, wtype: WindowType) -> Self {
self.rendition.play_params.window_type = wtype;
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 with_operation(mut self, op: RenditionOperation) -> Self {
self.operation = op;
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("Screen".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.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 mut mk = HashMap::new();
mk.insert(
"BG".to_string(),
Object::Array(vec![
Object::Real(0.9), Object::Real(0.9),
Object::Real(0.9),
]),
);
dict.insert("MK".to_string(), Object::Dictionary(mk));
let mut action = HashMap::new();
action.insert("S".to_string(), Object::Name("Rendition".to_string()));
action.insert("OP".to_string(), Object::Integer(self.operation.pdf_value()));
dict.insert("A".to_string(), Object::Dictionary(action));
dict
}
pub fn rendition(&self) -> &MediaRendition {
&self.rendition
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_media_clip_new() {
let data = vec![0u8; 1000];
let clip = MediaClip::new("video.mp4", data.clone());
assert_eq!(clip.filename, "video.mp4");
assert_eq!(clip.data, data);
assert!(clip.content_type.is_none());
}
#[test]
fn test_media_clip_builder() {
let clip = MediaClip::new("audio.mp3", vec![])
.with_mime_type("audio/mpeg")
.with_alt_text("Background music")
.with_temporal_access(TemporalAccess::Sequential);
assert_eq!(clip.content_type, Some("audio/mpeg".to_string()));
assert_eq!(clip.alt_text, Some("Background music".to_string()));
assert!(matches!(clip.temporal_access, TemporalAccess::Sequential));
}
#[test]
fn test_media_clip_build() {
let clip = MediaClip::new("test.mp4", vec![1, 2, 3]).with_mime_type("video/mp4");
let dict = clip.build(None);
assert_eq!(dict.get("Type"), Some(&Object::Name("MediaClip".to_string())));
assert_eq!(dict.get("S"), Some(&Object::Name("MCD".to_string())));
assert!(dict.contains_key("N"));
assert!(dict.contains_key("CT"));
}
#[test]
fn test_media_play_params_default() {
let params = MediaPlayParams::default();
assert_eq!(params.volume, 1.0);
assert!(params.auto_play);
assert_eq!(params.repeat_count, 1);
assert!(params.show_controls);
assert!(matches!(params.window_type, WindowType::Annotation));
}
#[test]
fn test_media_play_params_builder() {
let params = MediaPlayParams::new()
.with_volume(0.5)
.with_auto_play(false)
.with_repeat(3)
.with_controls(false)
.with_background(255, 0, 0)
.with_window_type(WindowType::FullScreen);
assert_eq!(params.volume, 0.5);
assert!(!params.auto_play);
assert_eq!(params.repeat_count, 3);
assert!(!params.show_controls);
assert_eq!(params.bg_color, Some((255, 0, 0)));
assert!(matches!(params.window_type, WindowType::FullScreen));
}
#[test]
fn test_media_play_params_build() {
let params = MediaPlayParams::new().with_volume(0.7).with_repeat(0);
let dict = params.build();
assert!(dict.contains_key("V")); assert!(dict.contains_key("RC")); }
#[test]
fn test_media_rendition_new() {
let clip = MediaClip::new("video.mp4", vec![]);
let rendition = MediaRendition::new(clip);
assert!(rendition.name.is_none());
assert_eq!(rendition.play_params.volume, 1.0);
}
#[test]
fn test_media_rendition_builder() {
let clip = MediaClip::new("video.mp4", vec![]);
let rendition = MediaRendition::new(clip)
.with_name("Demo Video")
.with_volume(0.8)
.with_repeat(2);
assert_eq!(rendition.name, Some("Demo Video".to_string()));
assert_eq!(rendition.play_params.volume, 0.8);
assert_eq!(rendition.play_params.repeat_count, 2);
}
#[test]
fn test_media_rendition_build() {
let clip = MediaClip::new("test.mp4", vec![]);
let rendition = MediaRendition::new(clip).with_name("Test");
let dict = rendition.build(None);
assert_eq!(dict.get("Type"), Some(&Object::Name("Rendition".to_string())));
assert_eq!(dict.get("S"), Some(&Object::Name("MR".to_string())));
assert!(dict.contains_key("N"));
}
#[test]
fn test_screen_annotation_new() {
let rect = Rect::new(72.0, 400.0, 320.0, 240.0);
let clip = MediaClip::new("video.mp4", vec![]);
let rendition = MediaRendition::new(clip);
let screen = ScreenAnnotation::new(rect, rendition);
assert_eq!(screen.rect.x, 72.0);
assert_eq!(screen.rect.width, 320.0);
assert!(screen.title.is_none());
}
#[test]
fn test_screen_annotation_video() {
let rect = Rect::new(100.0, 300.0, 640.0, 480.0);
let screen = ScreenAnnotation::video(rect, "demo.mp4", vec![0u8; 100]);
assert_eq!(screen.rendition.clip.filename, "demo.mp4");
assert_eq!(screen.rendition.clip.content_type, Some("video/mp4".to_string()));
}
#[test]
fn test_screen_annotation_audio() {
let rect = Rect::new(100.0, 700.0, 200.0, 50.0);
let screen = ScreenAnnotation::audio(rect, "music.mp3", vec![]);
assert_eq!(screen.rendition.clip.content_type, Some("audio/mpeg".to_string()));
}
#[test]
fn test_screen_annotation_builder() {
let rect = Rect::new(72.0, 400.0, 320.0, 240.0);
let screen = ScreenAnnotation::video(rect, "video.mp4", vec![])
.with_title("Video Player")
.with_rendition_name("Main Video")
.with_volume(0.5)
.with_repeat(0)
.with_auto_play(false)
.with_controls(true)
.with_contents("Demo video content");
assert_eq!(screen.title, Some("Video Player".to_string()));
assert_eq!(screen.rendition.name, Some("Main Video".to_string()));
assert_eq!(screen.rendition.play_params.volume, 0.5);
assert_eq!(screen.rendition.play_params.repeat_count, 0);
assert!(!screen.rendition.play_params.auto_play);
assert!(screen.rendition.play_params.show_controls);
assert_eq!(screen.contents, Some("Demo video content".to_string()));
}
#[test]
fn test_screen_annotation_build() {
let rect = Rect::new(72.0, 400.0, 320.0, 240.0);
let screen = ScreenAnnotation::video(rect, "test.mp4", vec![]).with_title("Test Video");
let dict = screen.build(&[]);
assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Screen".to_string())));
assert!(dict.contains_key("Rect"));
assert!(dict.contains_key("T")); assert!(dict.contains_key("A")); assert!(dict.contains_key("MK")); }
#[test]
fn test_window_type_values() {
assert_eq!(WindowType::Floating.pdf_value(), 0);
assert_eq!(WindowType::FullScreen.pdf_value(), 1);
assert_eq!(WindowType::Hidden.pdf_value(), 2);
assert_eq!(WindowType::Annotation.pdf_value(), 3);
}
#[test]
fn test_rendition_operation_values() {
assert_eq!(RenditionOperation::Play.pdf_value(), 0);
assert_eq!(RenditionOperation::Stop.pdf_value(), 1);
assert_eq!(RenditionOperation::Pause.pdf_value(), 2);
assert_eq!(RenditionOperation::Resume.pdf_value(), 3);
assert_eq!(RenditionOperation::PlayAssociate.pdf_value(), 4);
}
#[test]
fn test_temporal_access_names() {
assert_eq!(TemporalAccess::Random.pdf_name(), "Random");
assert_eq!(TemporalAccess::Sequential.pdf_name(), "Sequential");
}
}