use rpdfium_core::{Name, ObjectId, PdfSource};
use rpdfium_parser::{Object, ObjectStore};
use crate::error::{DocError, DocResult};
#[derive(Debug, Clone)]
pub struct AnnotationAppearance {
pub normal: Option<ObjectId>,
pub rollover: Option<ObjectId>,
pub down: Option<ObjectId>,
}
pub fn extract_appearance<S: PdfSource>(
ap_obj: &Object,
store: &ObjectStore<S>,
) -> DocResult<AnnotationAppearance> {
let resolved = store
.deep_resolve(ap_obj)
.map_err(|e| DocError::Parser(e.to_string()))?;
let ap_dict = resolved.as_dict().ok_or(DocError::UnexpectedType)?;
let normal = extract_sub_appearance(ap_dict.get(&Name::n()), store);
let rollover = extract_sub_appearance(ap_dict.get(&Name::r()), store);
let down = extract_sub_appearance(ap_dict.get(&Name::d()), store);
Ok(AnnotationAppearance {
normal,
rollover,
down,
})
}
fn extract_sub_appearance<S: PdfSource>(
obj: Option<&Object>,
store: &ObjectStore<S>,
) -> Option<ObjectId> {
let entry = obj?;
if let Some(id) = entry.as_reference() {
return Some(id);
}
let resolved = store.deep_resolve(entry).ok()?;
if let Some(state_dict) = resolved.as_dict() {
for value in state_dict.values() {
if let Some(id) = value.as_reference() {
return Some(id);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use rpdfium_core::error::ObjectId as CoreObjectId;
use std::collections::HashMap;
fn build_store() -> ObjectStore<Vec<u8>> {
let pdf = build_minimal_pdf();
ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
}
fn build_minimal_pdf() -> Vec<u8> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 3\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
pdf
}
#[test]
fn test_extract_appearance_with_direct_refs() {
let store = build_store();
let mut ap_dict = HashMap::new();
ap_dict.insert(Name::n(), Object::Reference(CoreObjectId::new(1, 0)));
ap_dict.insert(Name::r(), Object::Reference(CoreObjectId::new(2, 0)));
let ap_obj = Object::Dictionary(ap_dict);
let result = extract_appearance(&ap_obj, &store).unwrap();
assert_eq!(result.normal, Some(CoreObjectId::new(1, 0)));
assert_eq!(result.rollover, Some(CoreObjectId::new(2, 0)));
assert!(result.down.is_none());
}
#[test]
fn test_extract_appearance_with_state_dict() {
let store = build_store();
let mut state_dict = HashMap::new();
state_dict.insert(
Name::from("Yes"),
Object::Reference(CoreObjectId::new(1, 0)),
);
let mut ap_dict = HashMap::new();
ap_dict.insert(Name::n(), Object::Dictionary(state_dict));
let ap_obj = Object::Dictionary(ap_dict);
let result = extract_appearance(&ap_obj, &store).unwrap();
assert!(result.normal.is_some());
}
#[test]
fn test_extract_appearance_empty() {
let store = build_store();
let ap_dict: HashMap<Name, Object> = HashMap::new();
let ap_obj = Object::Dictionary(ap_dict);
let result = extract_appearance(&ap_obj, &store).unwrap();
assert!(result.normal.is_none());
assert!(result.rollover.is_none());
assert!(result.down.is_none());
}
}