use pdfrum_common::{Limits, PageIndex};
use pdfrum_object::{Array, Dict, Name, ObjRef, Object, Resolve, names};
use crate::{attach, doc::EditDoc, error::Error};
type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Relationship {
Source,
Data,
Alternative,
Supplement,
EncryptedPayload,
FormData,
Schema,
#[default]
Unspecified,
}
impl Relationship {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Source => "Source",
Self::Data => "Data",
Self::Alternative => "Alternative",
Self::Supplement => "Supplement",
Self::EncryptedPayload => "EncryptedPayload",
Self::FormData => "FormData",
Self::Schema => "Schema",
Self::Unspecified => "Unspecified",
}
}
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Option<Relationship> {
match bytes {
b"Source" => Some(Self::Source),
b"Data" => Some(Self::Data),
b"Alternative" => Some(Self::Alternative),
b"Supplement" => Some(Self::Supplement),
b"EncryptedPayload" => Some(Self::EncryptedPayload),
b"FormData" => Some(Self::FormData),
b"Schema" => Some(Self::Schema),
b"Unspecified" => Some(Self::Unspecified),
_ => None,
}
}
}
pub fn associate_file_with_document(
dest: &mut EditDoc<'_>,
limits: &Limits,
index: usize,
relationship: Relationship,
) -> Result<bool> {
let Some(spec_ref) = tag_relationship(dest, limits, index, relationship)? else {
return Ok(false);
};
let Some(root) = dest.base().trailer().reference(names::ROOT) else {
return Err(Error::NoDestinationCatalog);
};
let Some(mut catalog) = dict_at(dest, root) else {
return Err(Error::NoDestinationCatalog);
};
let af = push_unique(dest, &catalog, spec_ref);
catalog.insert(af_key(), Object::Array(af));
dest.replace(root, Object::Dict(catalog));
Ok(true)
}
pub fn associate_file_with_page(
dest: &mut EditDoc<'_>,
limits: &Limits,
page: impl Into<PageIndex>,
index: usize,
relationship: Relationship,
) -> Result<bool> {
let page = page.into();
let Some((page_ref, page_dict, _)) = dest.page_state(page)? else {
return Err(Error::InlinePage(page));
};
let Some(spec_ref) = tag_relationship(dest, limits, index, relationship)? else {
return Ok(false);
};
let mut page_dict = page_dict;
let af = push_unique(dest, &page_dict, spec_ref);
page_dict.insert(af_key(), Object::Array(af));
dest.replace(page_ref, Object::Dict(page_dict));
Ok(true)
}
pub fn document_associated_files(
dest: &EditDoc<'_>,
limits: &Limits,
) -> Result<Vec<(usize, Relationship)>> {
let Some(root) = dest.base().trailer().reference(names::ROOT) else {
return Err(Error::NoDestinationCatalog);
};
let Some(catalog) = dict_at(dest, root) else {
return Err(Error::NoDestinationCatalog);
};
associated_in(dest, limits, &catalog)
}
pub fn page_associated_files(
dest: &EditDoc<'_>,
limits: &Limits,
page: impl Into<PageIndex>,
) -> Result<Vec<(usize, Relationship)>> {
let page = page.into();
let Some((_, page_dict, _)) = dest.page_state(page)? else {
return Err(Error::InlinePage(page));
};
associated_in(dest, limits, &page_dict)
}
fn associated_in(
dest: &EditDoc<'_>,
limits: &Limits,
holder: &Dict,
) -> Result<Vec<(usize, Relationship)>> {
let Some(af) = holder.array(&af_key(), dest) else {
return Ok(Vec::new());
};
let specs = attachment_spec_refs(dest, limits)?;
let mut out = Vec::new();
for position in 0..af.len() {
let Some(reference) = af.reference_at(position) else {
continue;
};
let Some(index) = specs.iter().position(|spec| *spec == Some(reference)) else {
continue;
};
let relationship = dest
.fetch(reference)
.ok()
.and_then(|object| object.as_dict().cloned())
.and_then(|spec| {
spec.raw(&relationship_key())
.and_then(Object::as_name)
.cloned()
})
.and_then(|name| Relationship::from_bytes(name.as_bytes()))
.unwrap_or_default();
out.push((index, relationship));
}
Ok(out)
}
fn tag_relationship(
dest: &mut EditDoc<'_>,
limits: &Limits,
index: usize,
relationship: Relationship,
) -> Result<Option<ObjRef>> {
let Some(spec_ref) = attach::attachment_spec_ref(dest, limits, index)? else {
return Ok(None);
};
let Some(mut spec) = dict_at(dest, spec_ref) else {
return Ok(None);
};
spec.insert(
relationship_key(),
Object::Name(Name::from(relationship.as_str().as_bytes())),
);
dest.replace(spec_ref, Object::Dict(spec));
Ok(Some(spec_ref))
}
fn attachment_spec_refs(dest: &EditDoc<'_>, limits: &Limits) -> Result<Vec<Option<ObjRef>>> {
let count = attach::attachment_entries(dest, limits)?.len();
(0..count)
.map(|index| attach::attachment_spec_ref(dest, limits, index))
.collect()
}
fn push_unique(dest: &EditDoc<'_>, holder: &Dict, spec_ref: ObjRef) -> Array {
let mut af: Array = holder
.array(&af_key(), dest)
.map(|existing| existing.iter().cloned().collect())
.unwrap_or_default();
let already = af
.iter()
.any(|entry| matches!(entry, Object::Ref(reference) if *reference == spec_ref));
if !already {
af.push(Object::Ref(spec_ref));
}
af
}
fn dict_at(dest: &EditDoc<'_>, reference: ObjRef) -> Option<Dict> {
dest.fetch(reference)
.ok()
.and_then(|object| object.as_dict().cloned())
}
fn af_key() -> Name {
Name::from("AF")
}
fn relationship_key() -> Name {
Name::from("AFRelationship")
}