use crate::blocks::{ExtractOptions, OcrMode};
use crate::document::PdfDocument;
use crate::error::{EngineError, Result};
#[cfg(feature = "ocr-cli")]
use crate::render::RenderOptions;
impl PdfDocument {
#[cfg(feature = "ocr-cli")]
pub fn searchable_copy<E: pdf_ocr::OcrEngine>(
&self,
options: &ExtractOptions,
engine: &E,
) -> Result<PdfDocument> {
options.validate(self.page_count())?;
if options.ocr == OcrMode::Never {
return Err(EngineError::ocr_failure(
"searchable_copy with ocr=Never: nothing would be recognized",
));
}
Ok(self.searchable_copy_inner(options, engine)?.0)
}
pub(crate) fn ocr_copy_if_requested(
&self,
options: &ExtractOptions,
) -> Result<Option<PdfDocument>> {
#[cfg(feature = "ocr-cli")]
{
if options.ocr == OcrMode::Never {
return Ok(None);
}
if self.ocr_window_pages(options).is_empty() {
return Ok(None);
}
let engine = match pdf_ocr::TesseractCliEngine::new(&options.ocr_language) {
Ok(engine) => engine,
Err(e) if e.is_no_binary() && options.ocr == OcrMode::Auto => {
return Ok(None);
}
Err(e) => return Err(EngineError::ocr_engine_failure(e)),
};
let (copy, changed) = self.searchable_copy_inner(options, &engine)?;
Ok(changed.then_some(copy))
}
#[cfg(not(feature = "ocr-cli"))]
{
match options.ocr {
OcrMode::Never | OcrMode::Auto => Ok(None),
OcrMode::Force => Err(EngineError::ocr_failure(
"OCR was forced but this build has no OCR engine: it looks \
for the tesseract binary (enable the `ocr-cli` feature)",
)),
}
}
}
#[cfg(feature = "ocr-cli")]
fn ocr_window_pages(&self, options: &ExtractOptions) -> Vec<usize> {
(0..self.page_count())
.filter(|page| options.holds(*page))
.collect()
}
#[cfg(feature = "ocr-cli")]
fn searchable_copy_inner<E: pdf_ocr::OcrEngine>(
&self,
options: &ExtractOptions,
engine: &E,
) -> Result<(PdfDocument, bool)> {
let window = self.ocr_window_pages(options);
let source: &[u8] = self.pdf().data().as_ref();
if window.is_empty() {
let copy = PdfDocument::open(source.to_vec())
.map_err(|e| EngineError::ocr_failure(format!("reopen OCR copy: {e}")))?;
return Ok((copy, false));
}
let original = lopdf::Document::load_mem(source)
.map_err(|e| EngineError::ocr_failure(format!("read document for OCR: {e}")))?;
let mut working = original.clone();
let mut changed = false;
if options.ocr == OcrMode::Force {
changed |= strip_window_text(&mut working, &window)?;
}
let pages: Vec<u32> = window.iter().map(|page| *page as u32 + 1).collect();
let config = pdf_ocr::OcrConfig {
dpi: options.ocr_dpi,
pages,
..pdf_ocr::OcrConfig::default()
};
let report = pdf_ocr::make_searchable(&mut working, engine, &config, |_, page, dpi| {
self.render_page_rgb(page - 1, dpi)
})
.map_err(|e| EngineError::ocr_failure(e.to_string()))?;
changed |= report.total_words > 0;
let bytes = incremental_bytes(source, &original, &working)?;
let mut copy = PdfDocument::open(bytes)
.map_err(|e| EngineError::ocr_failure(format!("reopen OCR copy: {e}")))?;
copy.adopt_settings(self);
Ok((copy, changed))
}
#[cfg(feature = "ocr-cli")]
fn render_page_rgb(
&self,
index: u32,
dpi: u32,
) -> std::result::Result<(Vec<u8>, u32, u32), String> {
let options = RenderOptions {
dpi: f64::from(dpi),
..RenderOptions::default()
};
let rendered = self
.render_page(index as usize, &options)
.map_err(|e| e.to_string())?;
let mut rgb = Vec::with_capacity(rendered.width as usize * rendered.height as usize * 3);
for px in rendered.pixels.chunks_exact(4) {
rgb.extend_from_slice(&px[..3]);
}
Ok((rgb, rendered.width, rendered.height))
}
}
#[cfg(feature = "ocr-cli")]
fn incremental_bytes(
source: &[u8],
original: &lopdf::Document,
working: &lopdf::Document,
) -> Result<Vec<u8>> {
let mut incremental =
lopdf::IncrementalDocument::create_from(source.to_vec(), original.clone());
for (object_id, object) in &working.objects {
let changed = match original.objects.get(object_id) {
Some(previous) => previous != object,
None => true,
};
if changed {
incremental
.new_document
.set_object(*object_id, object.clone());
}
}
incremental.new_document.version = working.version.clone();
for (key, value) in &working.trailer {
if key != b"Prev" && key != b"Size" {
incremental
.new_document
.trailer
.set(key.clone(), value.clone());
}
}
incremental.new_document.max_id = working.max_id;
let mut bytes = Vec::with_capacity(source.len() + 64 * 1024);
incremental
.save_to(&mut bytes)
.map_err(|e| EngineError::ocr_failure(format!("write OCR copy: {e}")))?;
Ok(bytes)
}
#[cfg(feature = "ocr-cli")]
fn strip_window_text(doc: &mut lopdf::Document, pages: &[usize]) -> Result<bool> {
let ids = doc.get_pages();
let mut changed = false;
for &page in pages {
let number = page as u32 + 1;
let Some(&page_id) = ids.get(&number) else {
continue;
};
changed |= strip_page_text(doc, page_id)?;
}
Ok(changed)
}
#[cfg(feature = "ocr-cli")]
fn strip_page_text(doc: &mut lopdf::Document, page_id: lopdf::ObjectId) -> Result<bool> {
let content = doc.get_page_content(page_id);
let mut decoded = match lopdf::content::Content::decode(&content) {
Ok(decoded) => decoded,
Err(_) => return Ok(false),
};
let before = decoded.operations.len();
decoded
.operations
.retain(|op| !matches!(op.operator.as_str(), "Tj" | "TJ" | "'" | "\""));
if decoded.operations.len() == before {
return Ok(false);
}
let encoded = decoded
.encode()
.map_err(|e| EngineError::ocr_failure(format!("write stripped page for OCR: {e}")))?;
let stream_id = doc.add_object(lopdf::Object::Stream(lopdf::Stream::new(
lopdf::dictionary! {},
encoded,
)));
if let Ok(lopdf::Object::Dictionary(d)) = doc.get_object_mut(page_id) {
d.set("Contents", lopdf::Object::Reference(stream_id));
}
Ok(true)
}