spectre_pdf 1.0.0

Native Rust PDF extraction engine: text, markdown for RAG, AcroForm widgets, image decoding, and encrypted PDFs. Lazy parser, persistent Document handle, no C dependencies.
Documentation
//! PDF metadata extraction.

use crate::structure::decode_pdf_string;
use spectre_parse::Object as SpObject;
use std::collections::HashMap;

/// Encrypted PDFs allowed: page count + pdf_version come from the trailer
/// (plaintext) and `/Info` is plaintext in most encrypted documents.
pub fn extract_metadata_impl(pdf_bytes: &[u8]) -> Result<HashMap<String, String>, String> {
    let doc = crate::document::open_sp_with_password(pdf_bytes, b"")
        .map_err(|e| format!("Failed to load PDF: {e}"))?;
    let mut meta = HashMap::new();
    meta.insert("pages".to_string(), doc.get_pages().len().to_string());
    meta.insert("pdf_version".to_string(), doc.version().to_string());

    let Some(SpObject::Reference(info_id)) = doc.trailer().get_optional(b"Info") else {
        return Ok(meta);
    };
    let Ok(info_dict) = doc.get_dictionary(*info_id) else {
        return Ok(meta);
    };

    for (pdf_key, meta_key) in [
        ("Title", "title"),
        ("Author", "author"),
        ("Subject", "subject"),
        ("Keywords", "keywords"),
        ("Creator", "creator"),
        ("Producer", "producer"),
        ("CreationDate", "creation_date"),
        ("ModDate", "mod_date"),
    ] {
        if let Some(val) = info_dict.get_optional(pdf_key.as_bytes()) {
            if let Some(s) = object_to_string(val) {
                meta.insert(meta_key.to_string(), s);
            }
        }
    }

    Ok(meta)
}

fn object_to_string(obj: &SpObject) -> Option<String> {
    match obj {
        SpObject::String(bytes, _) => Some(decode_pdf_string(bytes).trim().to_string()),
        SpObject::Name(name) => std::str::from_utf8(name).ok().map(|s| s.to_string()),
        SpObject::Integer(n) => Some(n.to_string()),
        _ => None,
    }
}