use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::Path;
use std::sync::Mutex;
use image::GenericImageView;
use image::DynamicImage;
use pdfium_render::prelude::{PdfRenderConfig, Pdfium};
static PDFIUM_LOCK: Mutex<PdfiumBindState> = Mutex::new(PdfiumBindState::Unbound);
enum PdfiumBindState {
Unbound,
Ready,
Failed,
}
pub(crate) fn create_thumbnail<P>(path: P, width: u32, height: u32) -> anyhow::Result<DynamicImage>
where
P: AsRef<Path>,
{
let path = path.as_ref().to_path_buf();
with_pdfium(|pdfium| render_first_page(pdfium, &path, width, height))
.ok_or_else(|| anyhow::anyhow!("pdfium 渲染失败(库绑定失败或渲染出错/panic)"))
}
pub(crate) fn probe_page_size(path: &Path) -> Option<(u32, u32)> {
with_pdfium(|pdfium| {
let document = pdfium.load_pdf_from_file(path, None)?;
let first_page = document.pages().first()?;
let width = first_page.width().value.round() as u32;
let height = first_page.height().value.round() as u32;
Ok((width, height))
})
}
fn with_pdfium<T>(task: impl FnOnce(&Pdfium) -> anyhow::Result<T>) -> Option<T> {
let mut state = PDFIUM_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
if matches!(*state, PdfiumBindState::Unbound) {
*state = if bind_once() {
PdfiumBindState::Ready
} else {
PdfiumBindState::Failed
};
}
if matches!(*state, PdfiumBindState::Failed) {
return None;
}
catch_unwind(AssertUnwindSafe(|| {
let pdfium = Pdfium::default();
task(&pdfium).ok()
}))
.ok()
.flatten()
}
fn bind_once() -> bool {
let bind_result = catch_unwind(|| {
Pdfium::bind_to_library(Pdfium::pdfium_platform_library_name_at_path("./"))
.or_else(|_| Pdfium::bind_to_system_library())
});
match bind_result {
Ok(Ok(_)) => true,
Ok(Err(_)) => false,
Err(_) => catch_unwind(|| {
let _ = Pdfium::default();
})
.is_ok(),
}
}
fn render_first_page(
pdfium: &Pdfium,
path: &Path,
width: u32,
height: u32,
) -> anyhow::Result<DynamicImage> {
let document = pdfium.load_pdf_from_file(path, None)?;
let render_config = PdfRenderConfig::new();
let first_page = document.pages().first()?;
let img = first_page.render_with_config(&render_config)?.as_image()?;
Ok(img.thumbnail(width, height))
}
pub fn render_pdf_pages(path: &Path, max_pages: usize) -> Option<Vec<DynamicImage>> {
render_pdf_pages_with_limit(path, max_pages, None)
}
pub fn render_pdf_pages_for_ocr(
path: &Path,
max_pages: usize,
scale: f32,
) -> Option<Vec<DynamicImage>> {
let scale = scale.max(1.0);
render_pdf_pages_with_limit(path, max_pages, None).map(|pages| {
pages
.into_iter()
.map(|img| {
let (w, h) = img.dimensions();
let nw = ((w as f32 * scale).round() as u32).max(1);
let nh = ((h as f32 * scale).round() as u32).max(1);
if nw == w && nh == h {
return img;
}
image::imageops::resize(&img, nw, nh, image::imageops::FilterType::Triangle).into()
})
.collect()
})
}
pub fn render_pdf_pages_with_limit(
path: &Path,
max_pages: usize,
max_long_side: Option<u32>,
) -> Option<Vec<DynamicImage>> {
with_pdfium(|pdfium| {
let document = pdfium.load_pdf_from_file(path, None)?;
let render_config = PdfRenderConfig::new();
let mut pages = Vec::new();
for page in document.pages().iter().take(max_pages) {
let mut img = page.render_with_config(&render_config)?.as_image()?;
if let Some(limit) = max_long_side {
let (w, h) = img.dimensions();
if w.max(h) > limit {
img = img.thumbnail(limit, limit);
}
}
pages.push(img);
}
Ok(pages)
})
}
pub(crate) fn extract_pages_text(path: &Path, max_pages: usize) -> Option<Vec<String>> {
with_pdfium(|pdfium| {
let document = pdfium.load_pdf_from_file(path, None)?;
let mut pages = Vec::new();
for page in document.pages().iter().take(max_pages) {
let text = match page.text() {
Ok(text) => text.all(),
Err(_) => String::new(),
};
pages.push(text);
}
Ok(pages)
})
}