use std::ffi::{c_char, c_double, c_int, c_uchar, c_void, CStr, CString};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use image::RgbImage;
use libloading::{Library, Symbol};
const ABI_VERSION: c_int = 2;
type AbiVersionFn = unsafe extern "C" fn() -> c_int;
type VersionFn = unsafe extern "C" fn() -> *const c_char;
type InitFn = unsafe extern "C" fn(*const c_char, *mut c_char, c_int) -> c_int;
type OpenFn =
unsafe extern "C" fn(*const c_uchar, usize, *const c_char, *mut c_char, c_int) -> *mut c_void;
type PageCountFn = unsafe extern "C" fn(*mut c_void) -> c_int;
type RenderFn = unsafe extern "C" fn(
*mut c_void,
c_int,
c_double,
c_double,
*mut *mut c_uchar,
*mut c_int,
*mut c_int,
*mut c_char,
c_int,
) -> c_int;
type ReleasePageFn = unsafe extern "C" fn(*mut c_void, c_int);
type FreeFn = unsafe extern "C" fn(*mut c_uchar);
type CloseFn = unsafe extern "C" fn(*mut c_void);
pub struct Plugin {
open: Symbol<'static, OpenFn>,
page_count: Symbol<'static, PageCountFn>,
render: Symbol<'static, RenderFn>,
release_page: Symbol<'static, ReleasePageFn>,
free: Symbol<'static, FreeFn>,
close: Symbol<'static, CloseFn>,
pub docling_parse_version: String,
pub path: PathBuf,
_lib: &'static Library,
}
const ERR_LEN: usize = 1024;
fn err_string(buf: &[c_char]) -> String {
let bytes: Vec<u8> = buf
.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect();
String::from_utf8_lossy(&bytes).into_owned()
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Choice {
Auto,
DoclingParse,
Rust,
Pdfium,
}
pub fn choice() -> Choice {
match docling_core::env::nonempty("DOCLING_RS_RENDERER") {
None => Choice::Auto,
Some(v) => match v.trim().to_ascii_lowercase().as_str() {
"auto" | "" => Choice::Auto,
"docling-parse" | "docling_parse" | "dparse" => Choice::DoclingParse,
"rust" => Choice::Rust,
#[cfg(feature = "pdfium")]
"pdfium" => Choice::Pdfium,
#[cfg(not(feature = "pdfium"))]
"pdfium" => {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
eprintln!(
"docling-pdf: DOCLING_RS_RENDERER=pdfium but pdfium support is not compiled in \
(docling-pdf feature `pdfium`); rendering with the Rust renderer"
);
});
Choice::Auto
}
other => {
eprintln!(
"docling-pdf: unknown DOCLING_RS_RENDERER={other:?} (auto | docling-parse | rust | pdfium); using auto"
);
Choice::Auto
}
},
}
}
pub fn requested() -> bool {
choice() == Choice::DoclingParse
}
fn platform_lib_name() -> &'static str {
if cfg!(target_os = "macos") {
"libdparse_render.dylib"
} else if cfg!(target_os = "windows") {
"dparse_render.dll"
} else {
"libdparse_render.so"
}
}
fn lib_path() -> PathBuf {
let candidate = docling_core::env::nonempty("DOCLING_PARSE_RENDER_LIB")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(crate::resolve_asset(".docling-parse/lib")));
if candidate.is_dir() {
candidate.join(platform_lib_name())
} else {
candidate
}
}
fn resources_dir(lib: &Path) -> Option<PathBuf> {
if let Some(dir) = docling_core::env::nonempty("DOCLING_PARSE_RESOURCES") {
return Some(PathBuf::from(dir));
}
let sibling = lib.parent()?.parent()?.join("pdf_resources");
sibling.is_dir().then_some(sibling)
}
fn load() -> Result<Plugin, String> {
let path = lib_path();
let lib = unsafe { Library::new(&path) }
.map_err(|e| format!("cannot load {}: {e}", path.display()))?;
let lib: &'static Library = Box::leak(Box::new(lib));
unsafe {
let abi: Symbol<AbiVersionFn> = lib
.get(b"dpr_abi_version\0")
.map_err(|e| format!("{}: {e}", path.display()))?;
let got = abi();
if got != ABI_VERSION {
return Err(format!(
"{}: shim ABI {got}, this build expects {ABI_VERSION} — rebuild it with \
scripts/install/build_docling_parse_render.sh",
path.display()
));
}
let version: Symbol<VersionFn> = lib
.get(b"dpr_docling_parse_version\0")
.map_err(|e| format!("{}: {e}", path.display()))?;
let docling_parse_version = CStr::from_ptr(version()).to_string_lossy().into_owned();
let init: Symbol<InitFn> = lib
.get(b"dpr_init\0")
.map_err(|e| format!("{}: {e}", path.display()))?;
let resources = resources_dir(&path)
.map(|p| CString::new(p.to_string_lossy().into_owned()).unwrap_or_default());
let mut err = [0 as c_char; ERR_LEN];
let rc = init(
resources
.as_ref()
.map(|c| c.as_ptr())
.unwrap_or(std::ptr::null()),
err.as_mut_ptr(),
ERR_LEN as c_int,
);
if rc != 0 {
return Err(format!("dpr_init: {}", err_string(&err)));
}
Ok(Plugin {
open: lib.get(b"dpr_open\0").map_err(|e| e.to_string())?,
page_count: lib.get(b"dpr_page_count\0").map_err(|e| e.to_string())?,
render: lib.get(b"dpr_render\0").map_err(|e| e.to_string())?,
release_page: lib.get(b"dpr_release_page\0").map_err(|e| e.to_string())?,
free: lib.get(b"dpr_free\0").map_err(|e| e.to_string())?,
close: lib.get(b"dpr_close\0").map_err(|e| e.to_string())?,
docling_parse_version,
path,
_lib: lib,
})
}
}
pub fn plugin() -> Option<&'static Plugin> {
static PLUGIN: OnceLock<Option<Plugin>> = OnceLock::new();
PLUGIN
.get_or_init(|| {
let choice = choice();
if choice != Choice::DoclingParse {
return None;
}
match load() {
Ok(p) => {
docling_core::debug_log!(
"docling-pdf: rendering page images with docling-parse {} ({})",
p.docling_parse_version,
p.path.display()
);
Some(p)
}
Err(e) if choice == Choice::DoclingParse => {
eprintln!(
"docling-pdf: DOCLING_RS_RENDERER=docling-parse but the renderer plugin \
is unavailable ({e}); rendering with the Rust renderer"
);
None
}
Err(e) => {
docling_core::debug_log!(
"docling-pdf: docling-parse renderer plugin not loaded ({e}); rendering with the Rust renderer"
);
None
}
}
})
.as_ref()
}
pub fn active_name() -> &'static str {
if plugin().is_some() {
"docling-parse"
} else if choice() == Choice::Pdfium {
"pdfium"
} else {
"rust"
}
}
pub struct Doc {
plugin: &'static Plugin,
handle: *mut c_void,
}
unsafe impl Send for Doc {}
impl Doc {
pub fn open_if_enabled(bytes: &[u8], password: Option<&str>) -> Option<Doc> {
let plugin = plugin()?;
match Doc::open(plugin, bytes, password) {
Ok(doc) => Some(doc),
Err(e) => {
eprintln!("docling-pdf: docling-parse could not open the document ({e}); rendering with the Rust renderer");
None
}
}
}
fn open(plugin: &'static Plugin, bytes: &[u8], password: Option<&str>) -> Result<Doc, String> {
let password = password
.map(|p| CString::new(p).map_err(|e| e.to_string()))
.transpose()?;
let mut err = [0 as c_char; ERR_LEN];
let handle = unsafe {
(plugin.open)(
bytes.as_ptr(),
bytes.len(),
password
.as_ref()
.map(|c| c.as_ptr())
.unwrap_or(std::ptr::null()),
err.as_mut_ptr(),
ERR_LEN as c_int,
)
};
if handle.is_null() {
return Err(err_string(&err));
}
Ok(Doc { plugin, handle })
}
pub fn page_count(&self) -> usize {
unsafe { (self.plugin.page_count)(self.handle) }.max(0) as usize
}
pub fn render(&self, page: usize, scale: f64, bitmap_hint: f64) -> Result<RgbImage, String> {
let mut rgba: *mut c_uchar = std::ptr::null_mut();
let (mut w, mut h) = (0 as c_int, 0 as c_int);
let mut err = [0 as c_char; ERR_LEN];
let rc = unsafe {
(self.plugin.render)(
self.handle,
page as c_int,
scale,
bitmap_hint,
&mut rgba,
&mut w,
&mut h,
err.as_mut_ptr(),
ERR_LEN as c_int,
)
};
if rc != 0 || rgba.is_null() || w <= 0 || h <= 0 {
return Err(err_string(&err));
}
let (w, h) = (w as u32, h as u32);
let n = (w as usize) * (h as usize);
let src = unsafe { std::slice::from_raw_parts(rgba, n * 4) };
let mut rgb = Vec::with_capacity(n * 3);
for px in src.chunks_exact(4) {
rgb.extend_from_slice(&px[..3]);
}
unsafe { (self.plugin.free)(rgba) };
RgbImage::from_raw(w, h, rgb)
.ok_or_else(|| "docling-parse canvas size mismatch".to_string())
}
pub fn release_page(&self, page: usize) {
unsafe { (self.plugin.release_page)(self.handle, page as c_int) }
}
}
impl Drop for Doc {
fn drop(&mut self) {
unsafe { (self.plugin.close)(self.handle) }
}
}