1use std::ffi::{c_char, c_double, c_int, c_uchar, c_void, CStr, CString};
35use std::path::{Path, PathBuf};
36use std::sync::OnceLock;
37
38use image::RgbImage;
39use libloading::{Library, Symbol};
40
41const ABI_VERSION: c_int = 1;
43
44type AbiVersionFn = unsafe extern "C" fn() -> c_int;
45type VersionFn = unsafe extern "C" fn() -> *const c_char;
46type InitFn = unsafe extern "C" fn(*const c_char, *mut c_char, c_int) -> c_int;
47type OpenFn =
48 unsafe extern "C" fn(*const c_uchar, usize, *const c_char, *mut c_char, c_int) -> *mut c_void;
49type PageCountFn = unsafe extern "C" fn(*mut c_void) -> c_int;
50type RenderFn = unsafe extern "C" fn(
51 *mut c_void,
52 c_int,
53 c_double,
54 *mut *mut c_uchar,
55 *mut c_int,
56 *mut c_int,
57 *mut c_char,
58 c_int,
59) -> c_int;
60type ReleasePageFn = unsafe extern "C" fn(*mut c_void, c_int);
61type FreeFn = unsafe extern "C" fn(*mut c_uchar);
62type CloseFn = unsafe extern "C" fn(*mut c_void);
63
64pub struct Plugin {
67 open: Symbol<'static, OpenFn>,
69 page_count: Symbol<'static, PageCountFn>,
70 render: Symbol<'static, RenderFn>,
71 release_page: Symbol<'static, ReleasePageFn>,
72 free: Symbol<'static, FreeFn>,
73 close: Symbol<'static, CloseFn>,
74 pub docling_parse_version: String,
76 _lib: &'static Library,
77}
78
79const ERR_LEN: usize = 1024;
80
81fn err_string(buf: &[c_char]) -> String {
82 let bytes: Vec<u8> = buf
84 .iter()
85 .take_while(|&&c| c != 0)
86 .map(|&c| c as u8)
87 .collect();
88 String::from_utf8_lossy(&bytes).into_owned()
89}
90
91pub fn requested() -> bool {
93 docling_core::env::nonempty("DOCLING_RS_RENDERER")
94 .map(|v| {
95 let v = v.trim().to_ascii_lowercase();
96 v == "docling-parse" || v == "docling_parse" || v == "dparse"
97 })
98 .unwrap_or(false)
99}
100
101fn platform_lib_name() -> &'static str {
102 if cfg!(target_os = "macos") {
103 "libdparse_render.dylib"
104 } else if cfg!(target_os = "windows") {
105 "dparse_render.dll"
106 } else {
107 "libdparse_render.so"
108 }
109}
110
111fn lib_path() -> PathBuf {
114 let candidate = docling_core::env::nonempty("DOCLING_PARSE_RENDER_LIB")
115 .map(PathBuf::from)
116 .unwrap_or_else(|| PathBuf::from(crate::resolve_asset(".docling-parse/lib")));
117 if candidate.is_dir() {
118 candidate.join(platform_lib_name())
119 } else {
120 candidate
121 }
122}
123
124fn resources_dir(lib: &Path) -> Option<PathBuf> {
127 if let Some(dir) = docling_core::env::nonempty("DOCLING_PARSE_RESOURCES") {
128 return Some(PathBuf::from(dir));
129 }
130 let sibling = lib.parent()?.parent()?.join("pdf_resources");
131 sibling.is_dir().then_some(sibling)
132}
133
134fn load() -> Result<Plugin, String> {
135 let path = lib_path();
136 let lib = unsafe { Library::new(&path) }
139 .map_err(|e| format!("cannot load {}: {e}", path.display()))?;
140 let lib: &'static Library = Box::leak(Box::new(lib));
141 unsafe {
143 let abi: Symbol<AbiVersionFn> = lib
144 .get(b"dpr_abi_version\0")
145 .map_err(|e| format!("{}: {e}", path.display()))?;
146 let got = abi();
147 if got != ABI_VERSION {
148 return Err(format!(
149 "{}: shim ABI {got}, this build expects {ABI_VERSION} — rebuild it with \
150 scripts/install/build_docling_parse_render.sh",
151 path.display()
152 ));
153 }
154 let version: Symbol<VersionFn> = lib
155 .get(b"dpr_docling_parse_version\0")
156 .map_err(|e| format!("{}: {e}", path.display()))?;
157 let docling_parse_version = CStr::from_ptr(version()).to_string_lossy().into_owned();
158 let init: Symbol<InitFn> = lib
159 .get(b"dpr_init\0")
160 .map_err(|e| format!("{}: {e}", path.display()))?;
161 let resources = resources_dir(&path)
162 .map(|p| CString::new(p.to_string_lossy().into_owned()).unwrap_or_default());
163 let mut err = [0 as c_char; ERR_LEN];
164 let rc = init(
165 resources
166 .as_ref()
167 .map(|c| c.as_ptr())
168 .unwrap_or(std::ptr::null()),
169 err.as_mut_ptr(),
170 ERR_LEN as c_int,
171 );
172 if rc != 0 {
173 return Err(format!("dpr_init: {}", err_string(&err)));
174 }
175 Ok(Plugin {
176 open: lib.get(b"dpr_open\0").map_err(|e| e.to_string())?,
177 page_count: lib.get(b"dpr_page_count\0").map_err(|e| e.to_string())?,
178 render: lib.get(b"dpr_render\0").map_err(|e| e.to_string())?,
179 release_page: lib.get(b"dpr_release_page\0").map_err(|e| e.to_string())?,
180 free: lib.get(b"dpr_free\0").map_err(|e| e.to_string())?,
181 close: lib.get(b"dpr_close\0").map_err(|e| e.to_string())?,
182 docling_parse_version,
183 _lib: lib,
184 })
185 }
186}
187
188pub fn plugin() -> Option<&'static Plugin> {
192 static PLUGIN: OnceLock<Option<Plugin>> = OnceLock::new();
193 PLUGIN
194 .get_or_init(|| {
195 if !requested() {
196 return None;
197 }
198 match load() {
199 Ok(p) => {
200 docling_core::debug_log!(
201 "docling-pdf: rendering page images with docling-parse {} (DOCLING_RS_RENDERER)",
202 p.docling_parse_version
203 );
204 Some(p)
205 }
206 Err(e) => {
207 eprintln!(
208 "docling-pdf: DOCLING_RS_RENDERER=docling-parse but the renderer plugin \
209 is unavailable ({e}); rendering with pdfium"
210 );
211 None
212 }
213 }
214 })
215 .as_ref()
216}
217
218pub struct Doc {
220 plugin: &'static Plugin,
221 handle: *mut c_void,
222}
223
224unsafe impl Send for Doc {}
227
228impl Doc {
229 pub fn open_if_enabled(bytes: &[u8], password: Option<&str>) -> Option<Doc> {
233 let plugin = plugin()?;
234 match Doc::open(plugin, bytes, password) {
235 Ok(doc) => Some(doc),
236 Err(e) => {
237 eprintln!("docling-pdf: docling-parse could not open the document ({e}); rendering with pdfium");
238 None
239 }
240 }
241 }
242
243 fn open(plugin: &'static Plugin, bytes: &[u8], password: Option<&str>) -> Result<Doc, String> {
244 let password = password
245 .map(|p| CString::new(p).map_err(|e| e.to_string()))
246 .transpose()?;
247 let mut err = [0 as c_char; ERR_LEN];
248 let handle = unsafe {
251 (plugin.open)(
252 bytes.as_ptr(),
253 bytes.len(),
254 password
255 .as_ref()
256 .map(|c| c.as_ptr())
257 .unwrap_or(std::ptr::null()),
258 err.as_mut_ptr(),
259 ERR_LEN as c_int,
260 )
261 };
262 if handle.is_null() {
263 return Err(err_string(&err));
264 }
265 Ok(Doc { plugin, handle })
266 }
267
268 pub fn page_count(&self) -> usize {
270 unsafe { (self.plugin.page_count)(self.handle) }.max(0) as usize
272 }
273
274 pub fn render(&self, page: usize, scale: f64) -> Result<RgbImage, String> {
279 let mut rgba: *mut c_uchar = std::ptr::null_mut();
280 let (mut w, mut h) = (0 as c_int, 0 as c_int);
281 let mut err = [0 as c_char; ERR_LEN];
282 let rc = unsafe {
285 (self.plugin.render)(
286 self.handle,
287 page as c_int,
288 scale,
289 &mut rgba,
290 &mut w,
291 &mut h,
292 err.as_mut_ptr(),
293 ERR_LEN as c_int,
294 )
295 };
296 if rc != 0 || rgba.is_null() || w <= 0 || h <= 0 {
297 return Err(err_string(&err));
298 }
299 let (w, h) = (w as u32, h as u32);
300 let n = (w as usize) * (h as usize);
301 let src = unsafe { std::slice::from_raw_parts(rgba, n * 4) };
303 let mut rgb = Vec::with_capacity(n * 3);
304 for px in src.chunks_exact(4) {
305 rgb.extend_from_slice(&px[..3]);
306 }
307 unsafe { (self.plugin.free)(rgba) };
309 RgbImage::from_raw(w, h, rgb)
310 .ok_or_else(|| "docling-parse canvas size mismatch".to_string())
311 }
312
313 pub fn release_page(&self, page: usize) {
315 unsafe { (self.plugin.release_page)(self.handle, page as c_int) }
317 }
318}
319
320impl Drop for Doc {
321 fn drop(&mut self) {
322 unsafe { (self.plugin.close)(self.handle) }
324 }
325}