From 04abab951f2f641908ad031c7352faa6bea76451 Mon Sep 17 00:00:00 2001 From: Sheroy Cooper <59650384+CooperSheroy@users.noreply.github.com> Date: Wed, 5 Aug 2026 01:11:22 +0530 Subject: [PATCH] Support password-protected PDF item JSON extraction (#245) --- src/bin/pdf2md.rs | 37 ++++++++++++++++++++++++++++++++----- src/extractor/mod.rs | 24 ++++++++++++++++++++---- src/lib.rs | 2 +- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/bin/pdf2md.rs b/src/bin/pdf2md.rs index 3c0e365..1d5e82a 100644 --- a/src/bin/pdf2md.rs +++ b/src/bin/pdf2md.rs @@ -2,8 +2,8 @@ use pdf_inspector::extractor::ItemType; use pdf_inspector::{ - extract_text_with_positions_pages, process_pdf_with_options, LayoutComplexity, PdfOptions, - PdfType, ProcessMode, TextItem, + extract_text_with_positions_pages_with_password, process_pdf_with_options, LayoutComplexity, + PdfOptions, PdfType, ProcessMode, TextItem, }; use std::collections::HashSet; use std::env; @@ -103,9 +103,18 @@ fn format_items_json(items: &[TextItem]) -> String { ) } +fn extract_items_json( + pdf_path: &str, + page_filter: Option<&HashSet>, + password: Option<&str>, +) -> Result { + extract_text_with_positions_pages_with_password(pdf_path, page_filter, password) + .map(|items| format_items_json(&items)) +} + #[cfg(test)] mod tests { - use super::format_items_json; + use super::{extract_items_json, format_items_json}; use pdf_inspector::extractor::ItemType; use pdf_inspector::TextItem; @@ -137,6 +146,24 @@ mod tests { assert!(json.contains(r#""item_type":"text""#)); assert!(json.contains(r#""mcid":7"#)); } + + #[test] + fn items_json_uses_supplied_pdf_password() { + let path = "tests/fixtures/encrypted-secret123.pdf"; + + let without_password = extract_items_json(path, None, None); + assert!( + without_password.is_err(), + "encrypted fixture unexpectedly extracted without a password" + ); + + let json = extract_items_json(path, None, Some("secret123")) + .expect("correct password should decrypt positioned text"); + assert!( + json.contains("Procurement"), + "decrypted item JSON should contain fixture text, got {json}" + ); + } } /// Parse a page specification like "1,3,5-10,20" into a HashSet of page numbers. @@ -257,8 +284,8 @@ fn main() { }); if items_json_output { - match extract_text_with_positions_pages(pdf_path, page_filter.as_ref()) { - Ok(items) => println!("{}", format_items_json(&items)), + match extract_items_json(pdf_path, page_filter.as_ref(), password.as_deref()) { + Ok(json) => println!("{}", json), Err(e) => { println!(r#"{{"error":"{}"}}"#, json_escape(&e.to_string())); process::exit(1); diff --git a/src/extractor/mod.rs b/src/extractor/mod.rs index f38f115..e5387ac 100644 --- a/src/extractor/mod.rs +++ b/src/extractor/mod.rs @@ -84,17 +84,33 @@ pub fn extract_text_with_positions_pages>( path: P, page_filter: Option<&HashSet>, ) -> Result, PdfError> { - let (items, _rects, _lines) = extract_text_with_positions_and_rects(path, page_filter)?; + let (items, _rects, _lines) = + extract_text_with_positions_and_rects_with_password(path, page_filter, None)?; Ok(items) } -/// Extract text with positions and rectangles from a file. -pub(crate) fn extract_text_with_positions_and_rects>( +/// Extract text with positions from a file, limited to specific pages and +/// decrypting with `password` when the PDF is encrypted. +/// +/// `page_filter` is an optional set of 1-indexed page numbers to process. +/// When `None`, all pages are processed. +pub fn extract_text_with_positions_pages_with_password>( path: P, page_filter: Option<&HashSet>, + password: Option<&str>, +) -> Result, PdfError> { + let (items, _rects, _lines) = + extract_text_with_positions_and_rects_with_password(path, page_filter, password)?; + Ok(items) +} + +pub(crate) fn extract_text_with_positions_and_rects_with_password>( + path: P, + page_filter: Option<&HashSet>, + password: Option<&str>, ) -> Result { crate::validate_pdf_file(&path)?; - let (doc, _) = crate::load_document_from_path(&path)?; + let (doc, _) = crate::load_document_from_path_with_password(&path, password)?; let font_cmaps = FontCMaps::from_doc(&doc); let (extraction, _thresholds, _gid_pages) = extract_positioned_text_from_doc(&doc, &font_cmaps, page_filter)?; diff --git a/src/lib.rs b/src/lib.rs index 2b3121f..e65785b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ pub use detector::{ }; pub use extractor::{ extract_text, extract_text_with_positions, extract_text_with_positions_mem, - extract_text_with_positions_pages, + extract_text_with_positions_pages, extract_text_with_positions_pages_with_password, }; pub use markdown::{ to_markdown, to_markdown_from_items, to_markdown_from_items_with_rects,