use image::DynamicImage;
#[cfg(pdfium_embedded)]
use std::path::Path;
use std::path::PathBuf;
use std::sync::mpsc::{self, Sender};
use std::sync::OnceLock;
struct Job {
path: String,
page: usize,
width: u32,
reply: Sender<Result<DynamicImage, String>>,
}
static SERVICE: OnceLock<Option<Sender<Job>>> = OnceLock::new();
fn service() -> Option<&'static Sender<Job>> {
SERVICE.get_or_init(init_service).as_ref()
}
pub fn available() -> bool {
service().is_some()
}
pub fn render(path: &str, page: usize, width: u32) -> Result<DynamicImage, String> {
let svc = service().ok_or("pdfium unavailable")?;
let (tx, rx) = mpsc::channel();
svc.send(Job {
path: path.to_string(),
page,
width,
reply: tx,
})
.map_err(|_| "pdfium service gone".to_string())?;
rx.recv()
.map_err(|_| "pdfium service dropped".to_string())?
}
fn init_service() -> Option<Sender<Job>> {
let lib = resolve_library_path()?;
let (tx, rx) = mpsc::channel::<Job>();
let (ready_tx, ready_rx) = mpsc::channel::<bool>();
std::thread::Builder::new()
.name("pdfium".into())
.spawn(move || service_loop(lib, rx, ready_tx))
.ok()?;
match ready_rx.recv() {
Ok(true) => Some(tx),
_ => None,
}
}
fn service_loop(lib: PathBuf, rx: mpsc::Receiver<Job>, ready: Sender<bool>) {
use pdfium_render::prelude::*;
let bindings = match Pdfium::bind_to_library(&lib) {
Ok(b) => b,
Err(_) => {
let _ = ready.send(false);
return;
}
};
let pdfium = Pdfium::new(bindings);
let _ = ready.send(true);
let mut cached_path: Option<String> = None;
let mut cached_doc: Option<PdfDocument> = None;
while let Ok(job) = rx.recv() {
if cached_path.as_deref() != Some(job.path.as_str()) {
cached_doc = None;
cached_path = None;
match pdfium.load_pdf_from_file(&job.path, None) {
Ok(doc) => {
cached_doc = Some(doc);
cached_path = Some(job.path.clone());
}
Err(e) => {
let _ = job.reply.send(Err(format!("pdfium load: {e:?}")));
continue;
}
}
}
let res = match &cached_doc {
Some(doc) => render_one(doc, job.page, job.width),
None => Err("pdfium: no document".to_string()),
};
let _ = job.reply.send(res);
}
}
fn render_one(
doc: &pdfium_render::prelude::PdfDocument,
page: usize,
width: u32,
) -> Result<DynamicImage, String> {
use pdfium_render::prelude::*;
let pages = doc.pages();
if page >= pages.len() as usize {
return Err("pdfium: page out of range".to_string());
}
let page = pages.get(page as u16).map_err(|e| format!("{e:?}"))?;
let pw = page.width().value;
if pw <= 0.0 {
return Err("pdfium: non-positive page width".to_string());
}
let factor = (width as f32 / pw).clamp(0.05, 20.0);
let cfg = PdfRenderConfig::new().scale_page_by_factor(factor);
let bitmap = page
.render_with_config(&cfg)
.map_err(|e| format!("{e:?}"))?;
Ok(bitmap.as_image())
}
fn lib_file_name() -> &'static str {
#[cfg(target_os = "macos")]
{
"libpdfium.dylib"
}
#[cfg(target_os = "windows")]
{
"pdfium.dll"
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
"libpdfium.so"
}
}
fn resolve_library_path() -> Option<PathBuf> {
if let Ok(p) = std::env::var("SUCHER_PDFIUM_LIB") {
let pb = PathBuf::from(p);
if pb.is_file() {
return Some(pb);
}
}
let file = lib_file_name();
let mut dirs: Vec<PathBuf> = Vec::new();
if let Ok(exe) = std::env::current_exe() {
if let Some(d) = exe.parent() {
dirs.push(d.to_path_buf());
}
}
#[cfg(target_os = "macos")]
{
dirs.push(PathBuf::from("/opt/homebrew/lib"));
dirs.push(PathBuf::from("/usr/local/lib"));
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
dirs.push(PathBuf::from("/usr/local/lib"));
dirs.push(PathBuf::from("/usr/lib"));
}
for d in dirs {
let cand = d.join(file);
if cand.is_file() {
return Some(cand);
}
}
#[cfg(pdfium_embedded)]
if let Some(p) = materialize_embedded() {
return Some(p);
}
None
}
#[cfg(pdfium_embedded)]
const EMBEDDED_LIB: &[u8] = include_bytes!(env!("SUCHER_PDFIUM_EMBEDDED"));
#[cfg(pdfium_embedded)]
const EMBEDDED_LIBFILE: &str = env!("SUCHER_PDFIUM_LIBFILE");
#[cfg(pdfium_embedded)]
fn materialize_embedded() -> Option<PathBuf> {
let stamp = fnv1a_hex(EMBEDDED_LIB);
let ext = Path::new(EMBEDDED_LIBFILE)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let name = if ext.is_empty() {
format!("libpdfium-{stamp}")
} else {
format!("libpdfium-{stamp}.{ext}")
};
let dir = dirs::cache_dir()?.join("sucher");
std::fs::create_dir_all(&dir).ok()?;
let path = dir.join(&name);
if path.is_file()
&& std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0) == EMBEDDED_LIB.len() as u64
{
return Some(path); }
let tmp = dir.join(format!(".{stamp}.{}.tmp", std::process::id()));
std::fs::write(&tmp, EMBEDDED_LIB).ok()?;
let _ = std::fs::rename(&tmp, &path);
let _ = std::fs::remove_file(&tmp);
path.is_file().then_some(path)
}
#[cfg(pdfium_embedded)]
fn fnv1a_hex(bytes: &[u8]) -> String {
let mut h: u64 = 0xcbf29ce484222325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
format!("{:08x}", h as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lib_file_name_matches_platform() {
let name = lib_file_name();
#[cfg(target_os = "macos")]
assert_eq!(name, "libpdfium.dylib");
#[cfg(target_os = "windows")]
assert_eq!(name, "pdfium.dll");
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
assert_eq!(name, "libpdfium.so");
}
#[test]
fn env_override_is_honoured_when_the_file_exists() {
let dir = std::env::temp_dir().join(format!("sucher-pdfium-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("libpdfium.dylib");
std::fs::write(&f, b"not a real library").unwrap();
unsafe { std::env::set_var("SUCHER_PDFIUM_LIB", &f) };
assert_eq!(resolve_library_path().as_deref(), Some(f.as_path()));
unsafe { std::env::set_var("SUCHER_PDFIUM_LIB", dir.join("nope.dylib")) };
assert_ne!(
resolve_library_path().as_deref(),
Some(dir.join("nope.dylib").as_path())
);
unsafe { std::env::remove_var("SUCHER_PDFIUM_LIB") };
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore]
fn renders_a_real_pdf() {
assert!(
available(),
"pdfium unavailable — build with embedding or set SUCHER_PDFIUM_LIB"
);
let img = render("samples/sample.pdf", 0, 800).expect("render page 0");
assert_eq!(img.width(), 800, "should raster to the requested width");
assert!(img.height() > 0);
let p2 = render("samples/sample.pdf", 1, 800).expect("render page 1");
assert_eq!(p2.width(), 800);
}
}