Skip to main content

open_redact_pdf/
api.rs

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/// Normalized page size in PDF user-space units.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PageSize {
18    /// Page width after crop-box translation and page rotation normalization.
19    pub width: f64,
20    /// Page height after crop-box translation and page rotation normalization.
21    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/// Extracted text content and geometry for a single page.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PageText {
36    /// Zero-based page index.
37    pub page_index: usize,
38    /// Human-readable page text assembled from the supported extraction subset.
39    pub text: String,
40    /// Structured text items with bounding geometry suitable for later authoring tools.
41    pub items: Vec<TextItem>,
42}
43
44/// Parsed PDF document handle used for inspection, redaction, and save.
45pub struct PdfDocument {
46    parsed: ParsedDocument,
47    /// Per-page cached results of [`analyze_page_text`]. Successive
48    /// `extract_text` / `search_text` calls on the same page reuse the
49    /// cached extraction instead of walking the content stream again;
50    /// `apply_redactions` clears the cache since the underlying content
51    /// streams are about to be rewritten.
52    text_cache: Mutex<HashMap<usize, Arc<ExtractedPageText>>>,
53}
54
55impl PdfDocument {
56    /// Opens an unencrypted PDF from raw bytes, or an encrypted PDF
57    /// whose user password is empty. For encrypted PDFs that require a
58    /// user- or owner-supplied password, use
59    /// [`PdfDocument::open_with_password`].
60    pub fn open(bytes: &[u8]) -> PdfResult<Self> {
61        Ok(Self::with_parsed(parse_pdf(bytes)?))
62    }
63
64    /// Opens an encrypted PDF from raw bytes using the supplied password.
65    /// The password is tried first as the user password, then as the
66    /// owner password; if neither authenticates, the function returns
67    /// [`pdf_objects::PdfError::InvalidPassword`]. For unencrypted
68    /// documents the password is ignored.
69    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    /// Opens an Adobe.PubSec-encrypted PDF using the recipient's X.509
74    /// certificate and matching RSA private key, both DER-encoded
75    /// (PKCS#8 for the private key, the standard form returned by most
76    /// browser key-management APIs). Returns
77    /// [`pdf_objects::PdfError::InvalidPassword`] when no recipient blob
78    /// in the PDF unwraps with the supplied private key. For
79    /// password-encrypted or unencrypted documents this returns
80    /// [`pdf_objects::PdfError::Unsupported`] — use
81    /// [`PdfDocument::open_with_password`] / [`PdfDocument::open`]
82    /// respectively.
83    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    /// Returns the number of pages in the parsed document.
103    pub fn page_count(&self) -> usize {
104        self.parsed.pages.len()
105    }
106
107    /// Returns the normalized page size for a zero-based page index.
108    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    /// Extracts page text and geometry for the current supported subset.
118    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    /// Searches page text in visual glyph order and returns page-space match geometry.
128    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    /// Applies a redaction plan in place to the opened document.
134    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        // Content streams have been rewritten, so any cached extraction
138        // from before the plan applied is stale.
139        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    /// Saves the current document state as a new deterministic full-save PDF.
160    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;