1use std::ffi::{c_char, c_double, c_int, c_uchar, c_void, CStr, CString};
40use std::path::{Path, PathBuf};
41use std::sync::OnceLock;
42
43use image::RgbImage;
44use libloading::{Library, Symbol};
45
46const ABI_VERSION: c_int = 2;
49
50type AbiVersionFn = unsafe extern "C" fn() -> c_int;
51type VersionFn = unsafe extern "C" fn() -> *const c_char;
52type InitFn = unsafe extern "C" fn(*const c_char, *mut c_char, c_int) -> c_int;
53type OpenFn =
54 unsafe extern "C" fn(*const c_uchar, usize, *const c_char, *mut c_char, c_int) -> *mut c_void;
55type PageCountFn = unsafe extern "C" fn(*mut c_void) -> c_int;
56type RenderFn = unsafe extern "C" fn(
57 *mut c_void,
58 c_int,
59 c_double,
60 c_double,
61 *mut *mut c_uchar,
62 *mut c_int,
63 *mut c_int,
64 *mut c_char,
65 c_int,
66) -> c_int;
67type ReleasePageFn = unsafe extern "C" fn(*mut c_void, c_int);
68type FreeFn = unsafe extern "C" fn(*mut c_uchar);
69type CloseFn = unsafe extern "C" fn(*mut c_void);
70
71pub struct Plugin {
74 open: Symbol<'static, OpenFn>,
76 page_count: Symbol<'static, PageCountFn>,
77 render: Symbol<'static, RenderFn>,
78 release_page: Symbol<'static, ReleasePageFn>,
79 free: Symbol<'static, FreeFn>,
80 close: Symbol<'static, CloseFn>,
81 pub docling_parse_version: String,
83 pub path: PathBuf,
85 _lib: &'static Library,
86}
87
88const ERR_LEN: usize = 1024;
89
90fn err_string(buf: &[c_char]) -> String {
91 let bytes: Vec<u8> = buf
93 .iter()
94 .take_while(|&&c| c != 0)
95 .map(|&c| c as u8)
96 .collect();
97 String::from_utf8_lossy(&bytes).into_owned()
98}
99
100#[derive(Clone, Copy, PartialEq, Eq, Debug)]
102pub enum Choice {
103 Auto,
105 DoclingParse,
107 Pdfium,
109}
110
111pub fn choice() -> Choice {
114 match docling_core::env::nonempty("DOCLING_RS_RENDERER") {
115 None => Choice::Auto,
116 Some(v) => match v.trim().to_ascii_lowercase().as_str() {
117 "auto" | "" => Choice::Auto,
118 "docling-parse" | "docling_parse" | "dparse" => Choice::DoclingParse,
119 "pdfium" => Choice::Pdfium,
120 other => {
121 eprintln!(
122 "docling-pdf: unknown DOCLING_RS_RENDERER={other:?} (auto | docling-parse | pdfium); using auto"
123 );
124 Choice::Auto
125 }
126 },
127 }
128}
129
130pub fn requested() -> bool {
132 choice() == Choice::DoclingParse
133}
134
135fn platform_lib_name() -> &'static str {
136 if cfg!(target_os = "macos") {
137 "libdparse_render.dylib"
138 } else if cfg!(target_os = "windows") {
139 "dparse_render.dll"
140 } else {
141 "libdparse_render.so"
142 }
143}
144
145fn lib_path() -> PathBuf {
148 let candidate = docling_core::env::nonempty("DOCLING_PARSE_RENDER_LIB")
149 .map(PathBuf::from)
150 .unwrap_or_else(|| PathBuf::from(crate::resolve_asset(".docling-parse/lib")));
151 if candidate.is_dir() {
152 candidate.join(platform_lib_name())
153 } else {
154 candidate
155 }
156}
157
158fn resources_dir(lib: &Path) -> Option<PathBuf> {
161 if let Some(dir) = docling_core::env::nonempty("DOCLING_PARSE_RESOURCES") {
162 return Some(PathBuf::from(dir));
163 }
164 let sibling = lib.parent()?.parent()?.join("pdf_resources");
165 sibling.is_dir().then_some(sibling)
166}
167
168fn load() -> Result<Plugin, String> {
169 let path = lib_path();
170 let lib = unsafe { Library::new(&path) }
173 .map_err(|e| format!("cannot load {}: {e}", path.display()))?;
174 let lib: &'static Library = Box::leak(Box::new(lib));
175 unsafe {
177 let abi: Symbol<AbiVersionFn> = lib
178 .get(b"dpr_abi_version\0")
179 .map_err(|e| format!("{}: {e}", path.display()))?;
180 let got = abi();
181 if got != ABI_VERSION {
182 return Err(format!(
183 "{}: shim ABI {got}, this build expects {ABI_VERSION} — rebuild it with \
184 scripts/install/build_docling_parse_render.sh",
185 path.display()
186 ));
187 }
188 let version: Symbol<VersionFn> = lib
189 .get(b"dpr_docling_parse_version\0")
190 .map_err(|e| format!("{}: {e}", path.display()))?;
191 let docling_parse_version = CStr::from_ptr(version()).to_string_lossy().into_owned();
192 let init: Symbol<InitFn> = lib
193 .get(b"dpr_init\0")
194 .map_err(|e| format!("{}: {e}", path.display()))?;
195 let resources = resources_dir(&path)
196 .map(|p| CString::new(p.to_string_lossy().into_owned()).unwrap_or_default());
197 let mut err = [0 as c_char; ERR_LEN];
198 let rc = init(
199 resources
200 .as_ref()
201 .map(|c| c.as_ptr())
202 .unwrap_or(std::ptr::null()),
203 err.as_mut_ptr(),
204 ERR_LEN as c_int,
205 );
206 if rc != 0 {
207 return Err(format!("dpr_init: {}", err_string(&err)));
208 }
209 Ok(Plugin {
210 open: lib.get(b"dpr_open\0").map_err(|e| e.to_string())?,
211 page_count: lib.get(b"dpr_page_count\0").map_err(|e| e.to_string())?,
212 render: lib.get(b"dpr_render\0").map_err(|e| e.to_string())?,
213 release_page: lib.get(b"dpr_release_page\0").map_err(|e| e.to_string())?,
214 free: lib.get(b"dpr_free\0").map_err(|e| e.to_string())?,
215 close: lib.get(b"dpr_close\0").map_err(|e| e.to_string())?,
216 docling_parse_version,
217 path,
218 _lib: lib,
219 })
220 }
221}
222
223pub fn plugin() -> Option<&'static Plugin> {
228 static PLUGIN: OnceLock<Option<Plugin>> = OnceLock::new();
229 PLUGIN
230 .get_or_init(|| {
231 let choice = choice();
232 if choice == Choice::Pdfium {
233 return None;
234 }
235 match load() {
236 Ok(p) => {
237 docling_core::debug_log!(
238 "docling-pdf: rendering page images with docling-parse {} ({})",
239 p.docling_parse_version,
240 p.path.display()
241 );
242 Some(p)
243 }
244 Err(e) if choice == Choice::DoclingParse => {
245 eprintln!(
246 "docling-pdf: DOCLING_RS_RENDERER=docling-parse but the renderer plugin \
247 is unavailable ({e}); rendering with pdfium"
248 );
249 None
250 }
251 Err(e) => {
252 docling_core::debug_log!(
253 "docling-pdf: docling-parse renderer plugin not loaded ({e}); rendering with pdfium"
254 );
255 None
256 }
257 }
258 })
259 .as_ref()
260}
261
262pub fn active_name() -> &'static str {
265 if plugin().is_some() {
266 "docling-parse"
267 } else {
268 "pdfium"
269 }
270}
271
272pub struct Doc {
274 plugin: &'static Plugin,
275 handle: *mut c_void,
276}
277
278unsafe impl Send for Doc {}
281
282impl Doc {
283 pub fn open_if_enabled(bytes: &[u8], password: Option<&str>) -> Option<Doc> {
287 let plugin = plugin()?;
288 match Doc::open(plugin, bytes, password) {
289 Ok(doc) => Some(doc),
290 Err(e) => {
291 eprintln!("docling-pdf: docling-parse could not open the document ({e}); rendering with pdfium");
292 None
293 }
294 }
295 }
296
297 fn open(plugin: &'static Plugin, bytes: &[u8], password: Option<&str>) -> Result<Doc, String> {
298 let password = password
299 .map(|p| CString::new(p).map_err(|e| e.to_string()))
300 .transpose()?;
301 let mut err = [0 as c_char; ERR_LEN];
302 let handle = unsafe {
305 (plugin.open)(
306 bytes.as_ptr(),
307 bytes.len(),
308 password
309 .as_ref()
310 .map(|c| c.as_ptr())
311 .unwrap_or(std::ptr::null()),
312 err.as_mut_ptr(),
313 ERR_LEN as c_int,
314 )
315 };
316 if handle.is_null() {
317 return Err(err_string(&err));
318 }
319 Ok(Doc { plugin, handle })
320 }
321
322 pub fn page_count(&self) -> usize {
324 unsafe { (self.plugin.page_count)(self.handle) }.max(0) as usize
326 }
327
328 pub fn render(&self, page: usize, scale: f64, bitmap_hint: f64) -> Result<RgbImage, String> {
338 let mut rgba: *mut c_uchar = std::ptr::null_mut();
339 let (mut w, mut h) = (0 as c_int, 0 as c_int);
340 let mut err = [0 as c_char; ERR_LEN];
341 let rc = unsafe {
344 (self.plugin.render)(
345 self.handle,
346 page as c_int,
347 scale,
348 bitmap_hint,
349 &mut rgba,
350 &mut w,
351 &mut h,
352 err.as_mut_ptr(),
353 ERR_LEN as c_int,
354 )
355 };
356 if rc != 0 || rgba.is_null() || w <= 0 || h <= 0 {
357 return Err(err_string(&err));
358 }
359 let (w, h) = (w as u32, h as u32);
360 let n = (w as usize) * (h as usize);
361 let src = unsafe { std::slice::from_raw_parts(rgba, n * 4) };
363 let mut rgb = Vec::with_capacity(n * 3);
364 for px in src.chunks_exact(4) {
365 rgb.extend_from_slice(&px[..3]);
366 }
367 unsafe { (self.plugin.free)(rgba) };
369 RgbImage::from_raw(w, h, rgb)
370 .ok_or_else(|| "docling-parse canvas size mismatch".to_string())
371 }
372
373 pub fn release_page(&self, page: usize) {
375 unsafe { (self.plugin.release_page)(self.handle, page as c_int) }
377 }
378}
379
380impl Drop for Doc {
381 fn drop(&mut self) {
382 unsafe { (self.plugin.close)(self.handle) }
384 }
385}