1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use pdf_graphics::Size;
5use pdf_objects::{
6 PageInfo, ParsedDocument, PdfResult, parse_pdf, parse_pdf_with_certificate,
7 parse_pdf_with_password,
8};
9use pdf_redact::{ApplyReport, apply_redactions};
10use pdf_targets::{NormalizedRedactionPlan, normalize_plan};
11use pdf_text::{ExtractedPageText, TextItem, analyze_page_text, search_page_text};
12use pdf_writer::save_document;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PageSize {
18 pub width: f64,
20 pub height: f64,
22}
23
24impl From<Size> for PageSize {
25 fn from(value: Size) -> Self {
26 Self {
27 width: value.width,
28 height: value.height,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PageText {
36 pub page_index: usize,
38 pub text: String,
40 pub items: Vec<TextItem>,
42}
43
44pub struct PdfDocument {
46 parsed: ParsedDocument,
47 text_cache: Mutex<HashMap<usize, Arc<ExtractedPageText>>>,
53}
54
55impl PdfDocument {
56 pub fn open(bytes: &[u8]) -> PdfResult<Self> {
61 Ok(Self::with_parsed(parse_pdf(bytes)?))
62 }
63
64 pub fn open_with_password(bytes: &[u8], password: &[u8]) -> PdfResult<Self> {
70 Ok(Self::with_parsed(parse_pdf_with_password(bytes, password)?))
71 }
72
73 pub fn open_with_certificate(
84 bytes: &[u8],
85 cert_der: &[u8],
86 private_key_der: &[u8],
87 ) -> PdfResult<Self> {
88 Ok(Self::with_parsed(parse_pdf_with_certificate(
89 bytes,
90 cert_der,
91 private_key_der,
92 )?))
93 }
94
95 fn with_parsed(parsed: ParsedDocument) -> Self {
96 Self {
97 parsed,
98 text_cache: Mutex::new(HashMap::new()),
99 }
100 }
101
102 pub fn page_count(&self) -> usize {
104 self.parsed.pages.len()
105 }
106
107 pub fn page_size(&self, page_index: usize) -> PdfResult<PageSize> {
109 let page = self
110 .parsed
111 .pages
112 .get(page_index)
113 .ok_or(pdf_objects::PdfError::InvalidPageIndex(page_index))?;
114 Ok(page.page_box.size().into())
115 }
116
117 pub fn extract_text(&self, page_index: usize) -> PdfResult<PageText> {
119 let extracted = self.cached_page_text(page_index)?;
120 Ok(PageText {
121 page_index,
122 text: extracted.text.clone(),
123 items: extracted.items.clone(),
124 })
125 }
126
127 pub fn search_text(&self, page_index: usize, query: &str) -> PdfResult<Vec<TextMatch>> {
129 let extracted = self.cached_page_text(page_index)?;
130 Ok(search_page_text(&extracted, query))
131 }
132
133 pub fn apply_redactions(&mut self, plan: pdf_targets::RedactionPlan) -> PdfResult<ApplyReport> {
135 let normalized = self.normalize_plan(plan)?;
136 let report = apply_redactions(&mut self.parsed.file, &mut self.parsed.pages, &normalized)?;
137 if let Ok(mut cache) = self.text_cache.lock() {
140 cache.clear();
141 }
142 Ok(report)
143 }
144
145 fn cached_page_text(&self, page_index: usize) -> PdfResult<Arc<ExtractedPageText>> {
146 if let Ok(cache) = self.text_cache.lock() {
147 if let Some(entry) = cache.get(&page_index) {
148 return Ok(entry.clone());
149 }
150 }
151 let page = self.get_page(page_index)?;
152 let extracted = Arc::new(analyze_page_text(&self.parsed.file, page_index, page)?);
153 if let Ok(mut cache) = self.text_cache.lock() {
154 cache.insert(page_index, extracted.clone());
155 }
156 Ok(extracted)
157 }
158
159 pub fn save(&self) -> PdfResult<Vec<u8>> {
161 Ok(save_document(&self.parsed.file))
162 }
163
164 fn normalize_plan(
165 &self,
166 plan: pdf_targets::RedactionPlan,
167 ) -> PdfResult<NormalizedRedactionPlan> {
168 let sizes = self
169 .parsed
170 .pages
171 .iter()
172 .map(|page| page.page_box.size())
173 .collect::<Vec<_>>();
174 normalize_plan(plan, &sizes)
175 }
176
177 fn get_page(&self, page_index: usize) -> PdfResult<&PageInfo> {
178 self.parsed
179 .pages
180 .get(page_index)
181 .ok_or(pdf_objects::PdfError::InvalidPageIndex(page_index))
182 }
183}
184
185pub use pdf_objects::PdfError;
186pub use pdf_targets::{FillColor, RedactionMode, RedactionPlan, RedactionTarget};
187pub use pdf_text::TextMatch;