use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use pdf_graphics::Size;
use pdf_objects::{PageInfo, ParsedDocument, PdfResult, parse_pdf, parse_pdf_with_password};
use pdf_redact::{ApplyReport, apply_redactions};
use pdf_targets::{NormalizedRedactionPlan, normalize_plan};
use pdf_text::{ExtractedPageText, TextItem, analyze_page_text, search_page_text};
use pdf_writer::save_document;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageSize {
pub width: f64,
pub height: f64,
}
impl From<Size> for PageSize {
fn from(value: Size) -> Self {
Self {
width: value.width,
height: value.height,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageText {
pub page_index: usize,
pub text: String,
pub items: Vec<TextItem>,
}
pub struct PdfDocument {
parsed: ParsedDocument,
text_cache: Mutex<HashMap<usize, Arc<ExtractedPageText>>>,
}
impl PdfDocument {
pub fn open(bytes: &[u8]) -> PdfResult<Self> {
Ok(Self::with_parsed(parse_pdf(bytes)?))
}
pub fn open_with_password(bytes: &[u8], password: &[u8]) -> PdfResult<Self> {
Ok(Self::with_parsed(parse_pdf_with_password(bytes, password)?))
}
fn with_parsed(parsed: ParsedDocument) -> Self {
Self {
parsed,
text_cache: Mutex::new(HashMap::new()),
}
}
pub fn page_count(&self) -> usize {
self.parsed.pages.len()
}
pub fn page_size(&self, page_index: usize) -> PdfResult<PageSize> {
let page = self
.parsed
.pages
.get(page_index)
.ok_or(pdf_objects::PdfError::InvalidPageIndex(page_index))?;
Ok(page.page_box.size().into())
}
pub fn extract_text(&self, page_index: usize) -> PdfResult<PageText> {
let extracted = self.cached_page_text(page_index)?;
Ok(PageText {
page_index,
text: extracted.text.clone(),
items: extracted.items.clone(),
})
}
pub fn search_text(&self, page_index: usize, query: &str) -> PdfResult<Vec<TextMatch>> {
let extracted = self.cached_page_text(page_index)?;
Ok(search_page_text(&extracted, query))
}
pub fn apply_redactions(&mut self, plan: pdf_targets::RedactionPlan) -> PdfResult<ApplyReport> {
let normalized = self.normalize_plan(plan)?;
let report = apply_redactions(&mut self.parsed.file, &mut self.parsed.pages, &normalized)?;
if let Ok(mut cache) = self.text_cache.lock() {
cache.clear();
}
Ok(report)
}
fn cached_page_text(&self, page_index: usize) -> PdfResult<Arc<ExtractedPageText>> {
if let Ok(cache) = self.text_cache.lock() {
if let Some(entry) = cache.get(&page_index) {
return Ok(entry.clone());
}
}
let page = self.get_page(page_index)?;
let extracted = Arc::new(analyze_page_text(&self.parsed.file, page_index, page)?);
if let Ok(mut cache) = self.text_cache.lock() {
cache.insert(page_index, extracted.clone());
}
Ok(extracted)
}
pub fn save(&self) -> PdfResult<Vec<u8>> {
Ok(save_document(&self.parsed.file))
}
fn normalize_plan(
&self,
plan: pdf_targets::RedactionPlan,
) -> PdfResult<NormalizedRedactionPlan> {
let sizes = self
.parsed
.pages
.iter()
.map(|page| page.page_box.size())
.collect::<Vec<_>>();
normalize_plan(plan, &sizes)
}
fn get_page(&self, page_index: usize) -> PdfResult<&PageInfo> {
self.parsed
.pages
.get(page_index)
.ok_or(pdf_objects::PdfError::InvalidPageIndex(page_index))
}
}
pub use pdf_objects::PdfError;
pub use pdf_targets::{FillColor, RedactionMode, RedactionPlan, RedactionTarget};
pub use pdf_text::TextMatch;