use crate::object::{Object, ObjectRef};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct EmbeddedFile {
pub name: String,
pub data: Vec<u8>,
pub description: Option<String>,
pub mime_type: Option<String>,
pub creation_date: Option<String>,
pub modification_date: Option<String>,
pub af_relationship: Option<AFRelationship>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AFRelationship {
Source,
Data,
Alternative,
Supplement,
EncryptedPayload,
FormData,
Schema,
Unspecified,
}
impl AFRelationship {
pub fn pdf_name(&self) -> &'static str {
match self {
AFRelationship::Source => "Source",
AFRelationship::Data => "Data",
AFRelationship::Alternative => "Alternative",
AFRelationship::Supplement => "Supplement",
AFRelationship::EncryptedPayload => "EncryptedPayload",
AFRelationship::FormData => "FormData",
AFRelationship::Schema => "Schema",
AFRelationship::Unspecified => "Unspecified",
}
}
}
impl EmbeddedFile {
pub fn new(name: impl Into<String>, data: Vec<u8>) -> Self {
Self {
name: name.into(),
data,
description: None,
mime_type: None,
creation_date: None,
modification_date: None,
af_relationship: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
pub fn with_creation_date(mut self, date: impl Into<String>) -> Self {
self.creation_date = Some(date.into());
self
}
pub fn with_modification_date(mut self, date: impl Into<String>) -> Self {
self.modification_date = Some(date.into());
self
}
pub fn with_af_relationship(mut self, relationship: AFRelationship) -> Self {
self.af_relationship = Some(relationship);
self
}
pub fn size(&self) -> usize {
self.data.len()
}
pub fn build_stream_dict(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("EmbeddedFile".to_string()));
if let Some(ref mime) = self.mime_type {
dict.insert("Subtype".to_string(), Object::Name(mime.replace('/', "#2F")));
}
let mut params = HashMap::new();
params.insert("Size".to_string(), Object::Integer(self.data.len() as i64));
if let Some(ref creation) = self.creation_date {
params.insert(
"CreationDate".to_string(),
Object::String(
format!("D:{}", creation.replace(['-', ':', 'T', 'Z'], "")).into_bytes(),
),
);
}
if let Some(ref modification) = self.modification_date {
params.insert(
"ModDate".to_string(),
Object::String(
format!("D:{}", modification.replace(['-', ':', 'T', 'Z'], "")).into_bytes(),
),
);
}
let checksum = md5_hash(&self.data);
params.insert("CheckSum".to_string(), Object::String(checksum));
dict.insert("Params".to_string(), Object::Dictionary(params));
dict
}
pub fn build_filespec(&self, embedded_stream_ref: ObjectRef) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("Filespec".to_string()));
dict.insert("F".to_string(), Object::String(self.name.as_bytes().to_vec()));
dict.insert("UF".to_string(), Object::String(encode_utf16_be(&self.name)));
if let Some(ref desc) = self.description {
dict.insert("Desc".to_string(), Object::String(desc.as_bytes().to_vec()));
}
let mut ef_dict = HashMap::new();
ef_dict.insert("F".to_string(), Object::Reference(embedded_stream_ref));
ef_dict.insert("UF".to_string(), Object::Reference(embedded_stream_ref));
dict.insert("EF".to_string(), Object::Dictionary(ef_dict));
if let Some(relationship) = self.af_relationship {
dict.insert(
"AFRelationship".to_string(),
Object::Name(relationship.pdf_name().to_string()),
);
}
dict
}
}
#[derive(Debug, Default)]
pub struct EmbeddedFilesBuilder {
files: Vec<EmbeddedFile>,
}
impl EmbeddedFilesBuilder {
pub fn new() -> Self {
Self { files: Vec::new() }
}
pub fn add_file(&mut self, file: EmbeddedFile) -> &mut Self {
self.files.push(file);
self
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn files(&self) -> &[EmbeddedFile] {
&self.files
}
pub fn into_files(self) -> Vec<EmbeddedFile> {
self.files
}
pub fn build_names_array(&self, filespec_refs: &[(String, ObjectRef)]) -> Object {
let mut names_array = Vec::new();
let mut sorted_refs: Vec<_> = filespec_refs.iter().collect();
sorted_refs.sort_by(|a, b| a.0.cmp(&b.0));
for (name, ref_) in sorted_refs {
names_array.push(Object::String(name.as_bytes().to_vec()));
names_array.push(Object::Reference(*ref_));
}
Object::Array(names_array)
}
pub fn build_embedded_files_dict(
&self,
filespec_refs: &[(String, ObjectRef)],
) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Names".to_string(), self.build_names_array(filespec_refs));
dict
}
}
fn md5_hash(data: &[u8]) -> Vec<u8> {
use md5::{Digest, Md5};
let mut hasher = Md5::new();
hasher.update(data);
hasher.finalize().to_vec()
}
fn encode_utf16_be(s: &str) -> Vec<u8> {
let mut result = vec![0xFE, 0xFF]; for c in s.encode_utf16() {
result.push((c >> 8) as u8);
result.push((c & 0xFF) as u8);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_embedded_file_new() {
let file = EmbeddedFile::new("test.txt", b"Hello, World!".to_vec());
assert_eq!(file.name, "test.txt");
assert_eq!(file.size(), 13);
assert!(file.description.is_none());
assert!(file.mime_type.is_none());
}
#[test]
fn test_embedded_file_builder() {
let file = EmbeddedFile::new("data.csv", b"a,b,c".to_vec())
.with_description("Test CSV file")
.with_mime_type("text/csv")
.with_creation_date("2024-01-15T10:30:00Z")
.with_af_relationship(AFRelationship::Data);
assert_eq!(file.description, Some("Test CSV file".to_string()));
assert_eq!(file.mime_type, Some("text/csv".to_string()));
assert_eq!(file.af_relationship, Some(AFRelationship::Data));
}
#[test]
fn test_af_relationship_pdf_name() {
assert_eq!(AFRelationship::Source.pdf_name(), "Source");
assert_eq!(AFRelationship::Data.pdf_name(), "Data");
assert_eq!(AFRelationship::Alternative.pdf_name(), "Alternative");
}
#[test]
fn test_embedded_files_builder() {
let mut builder = EmbeddedFilesBuilder::new();
assert!(builder.is_empty());
builder.add_file(EmbeddedFile::new("file1.txt", b"content1".to_vec()));
builder.add_file(EmbeddedFile::new("file2.txt", b"content2".to_vec()));
assert_eq!(builder.len(), 2);
assert!(!builder.is_empty());
}
#[test]
fn test_build_stream_dict() {
let file = EmbeddedFile::new("test.txt", b"Hello".to_vec()).with_mime_type("text/plain");
let dict = file.build_stream_dict();
assert!(dict.contains_key("Type"));
assert!(dict.contains_key("Subtype"));
assert!(dict.contains_key("Params"));
if let Some(Object::Dictionary(params)) = dict.get("Params") {
assert!(params.contains_key("Size"));
assert!(params.contains_key("CheckSum"));
} else {
panic!("Params should be a dictionary");
}
}
#[test]
fn test_encode_utf16_be() {
let result = encode_utf16_be("test");
assert_eq!(&result[0..2], &[0xFE, 0xFF]); assert!(result.len() > 2);
}
#[test]
fn test_md5_hash() {
let hash = md5_hash(b"test data");
assert_eq!(hash.len(), 16);
let hash2 = md5_hash(b"test data");
assert_eq!(hash, hash2);
let hash3 = md5_hash(b"different data");
assert_ne!(hash, hash3);
}
}