Handle encrypted PDFs with empty user password retry

Map lopdf's Unimplemented("encrypted...") error to PdfError::Encrypted
instead of falling through to PdfError::Parse. Retry all Document::load
calls with an empty password when encryption is detected, so
owner-password-only PDFs can be opened. If the retry also fails, the
user now sees a clear "PDF is encrypted" message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-02-23 10:25:42 -08:00
co-authored by Claude Opus 4.6
parent 1bacf34bd0
commit 433132d195
3 changed files with 68 additions and 8 deletions
+28 -4
View File
@@ -98,11 +98,23 @@ pub fn detect_pdf_type_with_config<P: AsRef<Path>>(
crate::validate_pdf_file(&path)?;
// First, load metadata only (fast operation)
let metadata = Document::load_metadata(&path)?;
let metadata = match Document::load_metadata(&path) {
Ok(m) => m,
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
Document::load_metadata_with_password(&path, "")?
}
Err(e) => return Err(e.into()),
};
// Then load the full document for content inspection
// We use filtered loading to skip heavy objects we don't need
let doc = Document::load(&path)?;
let doc = match Document::load(&path) {
Ok(d) => d,
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
Document::load_with_password(&path, "")?
}
Err(e) => return Err(e.into()),
};
detect_from_document(&doc, metadata.page_count, &config)
}
@@ -120,10 +132,22 @@ pub fn detect_pdf_type_mem_with_config(
crate::validate_pdf_bytes(buffer)?;
// Load metadata first (fast)
let metadata = Document::load_metadata_mem(buffer)?;
let metadata = match Document::load_metadata_mem(buffer) {
Ok(m) => m,
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
Document::load_metadata_mem_with_password(buffer, "")?
}
Err(e) => return Err(e.into()),
};
// Load document for inspection
let doc = Document::load_mem(buffer)?;
let doc = match Document::load_mem(buffer) {
Ok(d) => d,
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
Document::load_mem_with_password(buffer, "")?
}
Err(e) => return Err(e.into()),
};
detect_from_document(&doc, metadata.page_count, &config)
}
+28 -4
View File
@@ -33,14 +33,26 @@ pub use layout::group_into_lines;
/// Extract text from PDF file as plain string
pub fn extract_text<P: AsRef<Path>>(path: P) -> Result<String, PdfError> {
crate::validate_pdf_file(&path)?;
let doc = Document::load(path)?;
let doc = match Document::load(&path) {
Ok(d) => d,
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
Document::load_with_password(&path, "")?
}
Err(e) => return Err(e.into()),
};
extract_text_from_doc(&doc)
}
/// Extract text from PDF memory buffer
pub fn extract_text_mem(buffer: &[u8]) -> Result<String, PdfError> {
crate::validate_pdf_bytes(buffer)?;
let doc = Document::load_mem(buffer)?;
let doc = match Document::load_mem(buffer) {
Ok(d) => d,
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
Document::load_mem_with_password(buffer, "")?
}
Err(e) => return Err(e.into()),
};
extract_text_from_doc(&doc)
}
@@ -76,7 +88,13 @@ pub(crate) fn extract_text_with_positions_and_rects<P: AsRef<Path>>(
page_filter: Option<&HashSet<u32>>,
) -> Result<(Vec<TextItem>, Vec<PdfRect>), PdfError> {
crate::validate_pdf_file(&path)?;
let doc = Document::load(path)?;
let doc = match Document::load(&path) {
Ok(d) => d,
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
Document::load_with_password(&path, "")?
}
Err(e) => return Err(e.into()),
};
let font_cmaps = FontCMaps::from_doc(&doc);
extract_positioned_text_from_doc(&doc, &font_cmaps, page_filter)
}
@@ -101,7 +119,13 @@ pub(crate) fn extract_text_with_positions_mem_and_rects(
page_filter: Option<&HashSet<u32>>,
) -> Result<(Vec<TextItem>, Vec<PdfRect>), PdfError> {
crate::validate_pdf_bytes(buffer)?;
let doc = Document::load_mem(buffer)?;
let doc = match Document::load_mem(buffer) {
Ok(d) => d,
Err(ref e) if crate::is_encrypted_lopdf_error(e) => {
Document::load_mem_with_password(buffer, "")?
}
Err(e) => return Err(e.into()),
};
let font_cmaps = FontCMaps::from_doc(&doc);
extract_positioned_text_from_doc(&doc, &font_cmaps, page_filter)
}
+12
View File
@@ -500,6 +500,7 @@ impl From<lopdf::Error> for PdfError {
| lopdf::Error::InvalidPassword
| lopdf::Error::AlreadyEncrypted
| lopdf::Error::UnsupportedSecurityHandler(_) => PdfError::Encrypted,
lopdf::Error::Unimplemented(msg) if msg.contains("encrypted") => PdfError::Encrypted,
lopdf::Error::Parse(ref pe) if pe.to_string().contains("invalid file header") => {
PdfError::NotAPdf("invalid PDF file header".to_string())
}
@@ -514,6 +515,17 @@ impl From<lopdf::Error> for PdfError {
}
}
/// Check whether a `lopdf::Error` represents an encryption-related failure.
pub(crate) fn is_encrypted_lopdf_error(e: &lopdf::Error) -> bool {
matches!(
e,
lopdf::Error::Decryption(_)
| lopdf::Error::InvalidPassword
| lopdf::Error::AlreadyEncrypted
| lopdf::Error::UnsupportedSecurityHandler(_)
) || matches!(e, lopdf::Error::Unimplemented(msg) if msg.contains("encrypted"))
}
// ---------------------------------------------------------------------------
// PDF validation helpers
// ---------------------------------------------------------------------------