use std::{fs::File, path::Path};
use lopdf::{Document, FilterFunc, LoadOptions};
use memmap2::{Mmap, MmapOptions};
use crate::{
filter::normalize_filter_names_for_lopdf_load, range::PageRangeError, PdfOpsError, Result,
};
pub(crate) fn load_document(path: &Path, password: Option<&str>) -> Result<Document> {
let mmap = map_file(path)?;
let document = Document::load_mem_with_options(
&mmap,
load_options(password, Some(normalize_filter_names_for_lopdf_load)),
)
.map_err(|err| decorate_load_error(err, path))?;
ensure_decrypted(&document, path)?;
if document.page_iter().next().is_none() {
return Err(PdfOpsError::Range(PageRangeError::NoPages));
}
Ok(document)
}
pub(crate) fn load_options(password: Option<&str>, filter: Option<FilterFunc>) -> LoadOptions {
LoadOptions {
password: password.map(str::to_owned),
filter,
strict: false,
}
}
pub(crate) fn decorate_load_error(err: lopdf::Error, path: &Path) -> PdfOpsError {
match err {
lopdf::Error::InvalidPassword => {
PdfOpsError::Password(format!("invalid password for {}", path.display()))
}
err => PdfOpsError::Pdf(err),
}
}
pub(crate) fn upgrade_damaged_xref_error(err: lopdf::Error, path: &Path) -> PdfOpsError {
let xref_class = match &err {
lopdf::Error::Xref(_)
| lopdf::Error::ObjectIdMismatch
| lopdf::Error::IndirectObject { .. } => true,
lopdf::Error::Parse(inner) => {
let message = inner.to_string();
message.contains("trailer")
|| message.contains("cross reference")
|| message.contains("end of input")
}
_ => false,
};
if xref_class {
PdfOpsError::InvalidStructure(format!(
"{}: damaged cross-reference table or trailer ({err}); automatic repair \
failed — a dedicated repair tool (e.g. `qpdf file.pdf repaired.pdf`) may \
still recover it",
path.display()
))
} else {
decorate_load_error(err, path)
}
}
pub(crate) fn ensure_decrypted(document: &Document, path: &Path) -> Result<()> {
if document.is_encrypted() {
return Err(PdfOpsError::Password(format!(
"{} is encrypted and requires a password; retry with --password",
path.display()
)));
}
Ok(())
}
pub(crate) fn map_file(path: &Path) -> Result<Mmap> {
let file = File::open(path)?;
Ok(unsafe { MmapOptions::new().map(&file)? })
}