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 RichMediaActivation {
#[default]
Explicit,
PageVisible,
PageOpen,
}
impl RichMediaActivation {
pub fn pdf_name(&self) -> &'static str {
match self {
RichMediaActivation::Explicit => "XA",
RichMediaActivation::PageVisible => "PV",
RichMediaActivation::PageOpen => "PO",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RichMediaDeactivation {
Explicit,
#[default]
PageNotVisible,
PageClose,
}
impl RichMediaDeactivation {
pub fn pdf_name(&self) -> &'static str {
match self {
RichMediaDeactivation::Explicit => "XD",
RichMediaDeactivation::PageNotVisible => "PI",
RichMediaDeactivation::PageClose => "PC",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RichMediaWindow {
#[default]
Embedded,
Floating,
FullScreen,
}
impl RichMediaWindow {
pub fn pdf_name(&self) -> &'static str {
match self {
RichMediaWindow::Embedded => "Embedded",
RichMediaWindow::Floating => "Windowed",
RichMediaWindow::FullScreen => "Fullscreen",
}
}
}
#[derive(Debug, Clone)]
pub struct RichMediaAsset {
pub name: String,
pub data: Vec<u8>,
pub mime_type: Option<String>,
}
impl RichMediaAsset {
pub fn new(name: impl Into<String>, data: Vec<u8>) -> Self {
Self {
name: name.into(),
data,
mime_type: None,
}
}
pub fn video(name: impl Into<String>, data: Vec<u8>) -> Self {
Self {
name: name.into(),
data,
mime_type: Some("video/mp4".to_string()),
}
}
pub fn swf(name: impl Into<String>, data: Vec<u8>) -> Self {
Self {
name: name.into(),
data,
mime_type: Some("application/x-shockwave-flash".to_string()),
}
}
pub fn with_mime_type(mut self, mime: impl Into<String>) -> Self {
self.mime_type = Some(mime.into());
self
}
pub fn build(&self, file_spec_ref: Option<ObjectRef>) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Name".to_string(), Object::String(self.name.as_bytes().to_vec()));
if let Some(ref_obj) = file_spec_ref {
let mut ef = HashMap::new();
ef.insert("F".to_string(), Object::Reference(ref_obj));
dict.insert("EF".to_string(), Object::Dictionary(ef));
}
if let Some(ref mime) = self.mime_type {
dict.insert("Subtype".to_string(), Object::String(mime.as_bytes().to_vec()));
}
dict
}
}
#[derive(Debug, Clone)]
pub struct RichMediaContent {
pub assets: Vec<RichMediaAsset>,
pub configuration_name: Option<String>,
}
impl Default for RichMediaContent {
fn default() -> Self {
Self::new()
}
}
impl RichMediaContent {
pub fn new() -> Self {
Self {
assets: Vec::new(),
configuration_name: None,
}
}
pub fn add_asset(mut self, asset: RichMediaAsset) -> Self {
self.assets.push(asset);
self
}
pub fn with_configuration(mut self, name: impl Into<String>) -> Self {
self.configuration_name = Some(name.into());
self
}
pub fn build(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("RichMediaContent".to_string()));
let mut assets = HashMap::new();
let names: Vec<Object> = self
.assets
.iter()
.flat_map(|a| {
vec![
Object::String(a.name.as_bytes().to_vec()),
Object::Dictionary(a.build(None)),
]
})
.collect();
if !names.is_empty() {
assets.insert("Names".to_string(), Object::Array(names));
}
dict.insert("Assets".to_string(), Object::Dictionary(assets));
if self.configuration_name.is_some() {
let mut config = HashMap::new();
config.insert("Type".to_string(), Object::Name("RichMediaConfiguration".to_string()));
config.insert("Subtype".to_string(), Object::Name("Video".to_string()));
if let Some(ref name) = self.configuration_name {
config.insert("Name".to_string(), Object::String(name.as_bytes().to_vec()));
}
dict.insert(
"Configurations".to_string(),
Object::Array(vec![Object::Dictionary(config)]),
);
}
dict
}
}
#[derive(Debug, Clone)]
pub struct RichMediaSettings {
pub activation: RichMediaActivation,
pub deactivation: RichMediaDeactivation,
pub window: RichMediaWindow,
pub toolbar: bool,
pub transparent: bool,
}
impl Default for RichMediaSettings {
fn default() -> Self {
Self {
activation: RichMediaActivation::default(),
deactivation: RichMediaDeactivation::default(),
window: RichMediaWindow::default(),
toolbar: true,
transparent: false,
}
}
}
impl RichMediaSettings {
pub fn new() -> Self {
Self::default()
}
pub fn with_activation(mut self, activation: RichMediaActivation) -> Self {
self.activation = activation;
self
}
pub fn with_deactivation(mut self, deactivation: RichMediaDeactivation) -> Self {
self.deactivation = deactivation;
self
}
pub fn with_window(mut self, window: RichMediaWindow) -> Self {
self.window = window;
self
}
pub fn with_toolbar(mut self, toolbar: bool) -> Self {
self.toolbar = toolbar;
self
}
pub fn with_transparent(mut self, transparent: bool) -> Self {
self.transparent = transparent;
self
}
pub fn build(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("RichMediaSettings".to_string()));
let mut activation = HashMap::new();
activation.insert("Type".to_string(), Object::Name("RichMediaActivation".to_string()));
activation
.insert("Condition".to_string(), Object::Name(self.activation.pdf_name().to_string()));
let mut presentation = HashMap::new();
presentation.insert("Type".to_string(), Object::Name("RichMediaPresentation".to_string()));
presentation.insert("Style".to_string(), Object::Name(self.window.pdf_name().to_string()));
presentation.insert("Toolbar".to_string(), Object::Boolean(self.toolbar));
presentation.insert("Transparent".to_string(), Object::Boolean(self.transparent));
if matches!(self.window, RichMediaWindow::Floating) {
let mut window = HashMap::new();
window.insert("Type".to_string(), Object::Name("RichMediaWindow".to_string()));
let mut width = HashMap::new();
width.insert("Default".to_string(), Object::Real(400.0));
width.insert("Min".to_string(), Object::Real(200.0));
width.insert("Max".to_string(), Object::Real(800.0));
window.insert("Width".to_string(), Object::Dictionary(width));
let mut height = HashMap::new();
height.insert("Default".to_string(), Object::Real(300.0));
height.insert("Min".to_string(), Object::Real(150.0));
height.insert("Max".to_string(), Object::Real(600.0));
window.insert("Height".to_string(), Object::Dictionary(height));
presentation.insert("Window".to_string(), Object::Dictionary(window));
}
activation.insert("Presentation".to_string(), Object::Dictionary(presentation));
dict.insert("Activation".to_string(), Object::Dictionary(activation));
let mut deactivation = HashMap::new();
deactivation.insert("Type".to_string(), Object::Name("RichMediaDeactivation".to_string()));
deactivation.insert(
"Condition".to_string(),
Object::Name(self.deactivation.pdf_name().to_string()),
);
dict.insert("Deactivation".to_string(), Object::Dictionary(deactivation));
dict
}
}
#[derive(Debug, Clone)]
pub struct RichMediaAnnotation {
pub rect: Rect,
pub content: RichMediaContent,
pub settings: RichMediaSettings,
pub title: Option<String>,
pub flags: AnnotationFlags,
pub contents: Option<String>,
}
impl RichMediaAnnotation {
pub fn new(rect: Rect, content: RichMediaContent) -> Self {
Self {
rect,
content,
settings: RichMediaSettings::default(),
title: None,
flags: AnnotationFlags::printable(),
contents: None,
}
}
pub fn video(rect: Rect, name: impl Into<String>, data: Vec<u8>) -> Self {
let content = RichMediaContent::new().add_asset(RichMediaAsset::video(name, data));
Self::new(rect, content)
}
pub fn with_settings(mut self, settings: RichMediaSettings) -> Self {
self.settings = settings;
self
}
pub fn with_activation(mut self, activation: RichMediaActivation) -> Self {
self.settings.activation = activation;
self
}
pub fn with_deactivation(mut self, deactivation: RichMediaDeactivation) -> Self {
self.settings.deactivation = deactivation;
self
}
pub fn with_window(mut self, window: RichMediaWindow) -> Self {
self.settings.window = window;
self
}
pub fn with_toolbar(mut self, toolbar: bool) -> Self {
self.settings.toolbar = toolbar;
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
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 add_asset(mut self, asset: RichMediaAsset) -> Self {
self.content.assets.push(asset);
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("RichMedia".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()));
}
dict.insert(
"NM".to_string(),
Object::String(
format!("RichMedia_{}", self.rect.x as i32)
.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()));
}
dict.insert("RichMediaContent".to_string(), Object::Dictionary(self.content.build()));
dict.insert("RichMediaSettings".to_string(), Object::Dictionary(self.settings.build()));
dict
}
pub fn content(&self) -> &RichMediaContent {
&self.content
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_richmedia_activation() {
assert_eq!(RichMediaActivation::Explicit.pdf_name(), "XA");
assert_eq!(RichMediaActivation::PageVisible.pdf_name(), "PV");
assert_eq!(RichMediaActivation::PageOpen.pdf_name(), "PO");
}
#[test]
fn test_richmedia_deactivation() {
assert_eq!(RichMediaDeactivation::Explicit.pdf_name(), "XD");
assert_eq!(RichMediaDeactivation::PageNotVisible.pdf_name(), "PI");
assert_eq!(RichMediaDeactivation::PageClose.pdf_name(), "PC");
}
#[test]
fn test_richmedia_window() {
assert_eq!(RichMediaWindow::Embedded.pdf_name(), "Embedded");
assert_eq!(RichMediaWindow::Floating.pdf_name(), "Windowed");
assert_eq!(RichMediaWindow::FullScreen.pdf_name(), "Fullscreen");
}
#[test]
fn test_richmedia_asset_new() {
let asset = RichMediaAsset::new("test.mp4", vec![1, 2, 3]);
assert_eq!(asset.name, "test.mp4");
assert_eq!(asset.data, vec![1, 2, 3]);
assert!(asset.mime_type.is_none());
}
#[test]
fn test_richmedia_asset_video() {
let asset = RichMediaAsset::video("demo.mp4", vec![]);
assert_eq!(asset.name, "demo.mp4");
assert_eq!(asset.mime_type, Some("video/mp4".to_string()));
}
#[test]
fn test_richmedia_asset_swf() {
let asset = RichMediaAsset::swf("player.swf", vec![]);
assert_eq!(asset.name, "player.swf");
assert_eq!(asset.mime_type, Some("application/x-shockwave-flash".to_string()));
}
#[test]
fn test_richmedia_content_new() {
let content = RichMediaContent::new();
assert!(content.assets.is_empty());
assert!(content.configuration_name.is_none());
}
#[test]
fn test_richmedia_content_builder() {
let content = RichMediaContent::new()
.add_asset(RichMediaAsset::video("video1.mp4", vec![]))
.add_asset(RichMediaAsset::video("video2.mp4", vec![]))
.with_configuration("VideoPlayer");
assert_eq!(content.assets.len(), 2);
assert_eq!(content.configuration_name, Some("VideoPlayer".to_string()));
}
#[test]
fn test_richmedia_content_build() {
let content = RichMediaContent::new()
.add_asset(RichMediaAsset::video("test.mp4", vec![]))
.with_configuration("Player");
let dict = content.build();
assert_eq!(dict.get("Type"), Some(&Object::Name("RichMediaContent".to_string())));
assert!(dict.contains_key("Assets"));
assert!(dict.contains_key("Configurations"));
}
#[test]
fn test_richmedia_settings_default() {
let settings = RichMediaSettings::default();
assert!(matches!(settings.activation, RichMediaActivation::Explicit));
assert!(matches!(settings.deactivation, RichMediaDeactivation::PageNotVisible));
assert!(matches!(settings.window, RichMediaWindow::Embedded));
assert!(settings.toolbar);
assert!(!settings.transparent);
}
#[test]
fn test_richmedia_settings_builder() {
let settings = RichMediaSettings::new()
.with_activation(RichMediaActivation::PageVisible)
.with_deactivation(RichMediaDeactivation::PageClose)
.with_window(RichMediaWindow::Floating)
.with_toolbar(false)
.with_transparent(true);
assert!(matches!(settings.activation, RichMediaActivation::PageVisible));
assert!(matches!(settings.deactivation, RichMediaDeactivation::PageClose));
assert!(matches!(settings.window, RichMediaWindow::Floating));
assert!(!settings.toolbar);
assert!(settings.transparent);
}
#[test]
fn test_richmedia_settings_build() {
let settings = RichMediaSettings::new();
let dict = settings.build();
assert_eq!(dict.get("Type"), Some(&Object::Name("RichMediaSettings".to_string())));
assert!(dict.contains_key("Activation"));
assert!(dict.contains_key("Deactivation"));
}
#[test]
fn test_richmedia_annotation_new() {
let rect = Rect::new(72.0, 400.0, 320.0, 240.0);
let content = RichMediaContent::new();
let annot = RichMediaAnnotation::new(rect, content);
assert_eq!(annot.rect.x, 72.0);
assert!(annot.content.assets.is_empty());
}
#[test]
fn test_richmedia_annotation_video() {
let rect = Rect::new(100.0, 300.0, 400.0, 300.0);
let annot = RichMediaAnnotation::video(rect, "demo.mp4", vec![0u8; 100]);
assert_eq!(annot.content.assets.len(), 1);
assert_eq!(annot.content.assets[0].name, "demo.mp4");
}
#[test]
fn test_richmedia_annotation_builder() {
let rect = Rect::new(72.0, 400.0, 320.0, 240.0);
let annot = RichMediaAnnotation::video(rect, "video.mp4", vec![])
.with_title("Video Player")
.with_activation(RichMediaActivation::PageVisible)
.with_window(RichMediaWindow::Embedded)
.with_toolbar(true)
.with_contents("A demonstration video");
assert_eq!(annot.title, Some("Video Player".to_string()));
assert!(matches!(annot.settings.activation, RichMediaActivation::PageVisible));
assert!(matches!(annot.settings.window, RichMediaWindow::Embedded));
assert!(annot.settings.toolbar);
assert_eq!(annot.contents, Some("A demonstration video".to_string()));
}
#[test]
fn test_richmedia_annotation_add_asset() {
let rect = Rect::new(72.0, 400.0, 320.0, 240.0);
let annot = RichMediaAnnotation::new(rect, RichMediaContent::new())
.add_asset(RichMediaAsset::video("video1.mp4", vec![]))
.add_asset(RichMediaAsset::video("video2.mp4", vec![]));
assert_eq!(annot.content.assets.len(), 2);
}
#[test]
fn test_richmedia_annotation_build() {
let rect = Rect::new(72.0, 400.0, 320.0, 240.0);
let annot =
RichMediaAnnotation::video(rect, "test.mp4", vec![1, 2, 3]).with_title("Test Video");
let dict = annot.build(&[]);
assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("RichMedia".to_string())));
assert!(dict.contains_key("Rect"));
assert!(dict.contains_key("T")); assert!(dict.contains_key("RichMediaContent"));
assert!(dict.contains_key("RichMediaSettings"));
}
}