cadmus_core/document/
djvu.rs

1use super::djvulibre_sys::*;
2
3use super::{chapter, chapter_relative};
4use super::{BoundedText, Document, Location, TextLocation, TocEntry};
5use crate::framebuffer::Pixmap;
6use crate::geom::{Boundary, CycleDir, Rectangle};
7use crate::metadata::TextAlign;
8use std::ffi::{CStr, CString};
9use std::os::unix::ffi::OsStrExt;
10use std::path::Path;
11use std::ptr;
12use std::rc::Rc;
13use tracing::error;
14
15impl Into<DjvuRect> for Rectangle {
16    fn into(self) -> DjvuRect {
17        DjvuRect {
18            x: self.min.y as libc::c_int,
19            y: self.min.x as libc::c_int,
20            w: self.width() as libc::c_uint,
21            h: self.height() as libc::c_uint,
22        }
23    }
24}
25
26struct DjvuContext(*mut ExoContext);
27
28pub struct DjvuOpener(Rc<DjvuContext>);
29
30pub struct DjvuDocument {
31    ctx: Rc<DjvuContext>,
32    doc: *mut ExoDocument,
33}
34
35pub struct DjvuPage<'a> {
36    page: *mut ExoPage,
37    doc: &'a DjvuDocument,
38}
39
40impl DjvuContext {
41    fn handle_message(&self) {
42        unsafe {
43            let msg = ddjvu_message_wait(self.0);
44            if (*msg).tag == DDJVU_ERROR {
45                let msg = (*msg).u.error;
46                let message = CStr::from_ptr(msg.message).to_string_lossy();
47                let filename = msg.filename;
48                let lineno = msg.lineno;
49                if filename.is_null() {
50                    error!("Error: {}.", message);
51                } else {
52                    let filename = CStr::from_ptr(filename).to_string_lossy();
53                    error!("Error: {}: '{}:{}'.", message, filename, lineno);
54                }
55            }
56            ddjvu_message_pop(self.0);
57        }
58    }
59}
60
61impl DjvuOpener {
62    pub fn new() -> Option<DjvuOpener> {
63        unsafe {
64            let name = CString::new("Cadmus").unwrap();
65            let ctx = ddjvu_context_create(name.as_ptr());
66            if ctx.is_null() {
67                None
68            } else {
69                ddjvu_cache_set_size(ctx, CACHE_SIZE);
70                Some(DjvuOpener(Rc::new(DjvuContext(ctx))))
71            }
72        }
73    }
74
75    pub fn open<P: AsRef<Path>>(&self, path: P) -> Option<DjvuDocument> {
76        unsafe {
77            let c_path = CString::new(path.as_ref().as_os_str().as_bytes()).unwrap();
78            let doc = ddjvu_document_create_by_filename_utf8((self.0).0, c_path.as_ptr(), 1);
79            if doc.is_null() {
80                return None;
81            }
82            let job = ddjvu_document_job(doc);
83            while ddjvu_job_status(job) < DDJVU_JOB_OK {
84                self.0.handle_message();
85            }
86            if ddjvu_job_status(job) >= DDJVU_JOB_FAILED {
87                None
88            } else {
89                Some(DjvuDocument {
90                    ctx: self.0.clone(),
91                    doc,
92                })
93            }
94        }
95    }
96}
97
98unsafe impl Send for DjvuDocument {}
99unsafe impl Sync for DjvuDocument {}
100
101impl Document for DjvuDocument {
102    fn dims(&self, index: usize) -> Option<(f32, f32)> {
103        self.page(index).map(|page| {
104            let dims = page.dims();
105            (dims.0 as f32, dims.1 as f32)
106        })
107    }
108
109    fn pages_count(&self) -> usize {
110        unsafe { ddjvu_document_get_pagenum(self.doc) as usize }
111    }
112
113    fn pixmap(&mut self, loc: Location, scale: f32, samples: usize) -> Option<(Pixmap, usize)> {
114        let index = self.resolve_location(loc)? as usize;
115        self.page(index)
116            .and_then(|page| page.pixmap(scale, samples))
117            .map(|pixmap| (pixmap, index))
118    }
119
120    fn toc(&mut self) -> Option<Vec<TocEntry>> {
121        unsafe {
122            let mut exp = ddjvu_document_get_outline(self.doc);
123            while exp == MINIEXP_DUMMY {
124                self.ctx.handle_message();
125                exp = ddjvu_document_get_outline(self.doc);
126            }
127            if exp == MINIEXP_NIL {
128                None
129            } else {
130                let mut index = 0;
131                let toc = Self::walk_toc(exp, &mut index);
132                ddjvu_miniexp_release(self.doc, exp);
133                Some(toc)
134            }
135        }
136    }
137
138    fn chapter<'a>(&mut self, offset: usize, toc: &'a [TocEntry]) -> Option<(&'a TocEntry, f32)> {
139        chapter(offset, self.pages_count(), toc)
140    }
141
142    fn chapter_relative<'a>(
143        &mut self,
144        offset: usize,
145        dir: CycleDir,
146        toc: &'a [TocEntry],
147    ) -> Option<&'a TocEntry> {
148        chapter_relative(offset, dir, toc)
149    }
150
151    fn words(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)> {
152        self.text(loc, b"word")
153    }
154
155    fn lines(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)> {
156        self.text(loc, b"line")
157    }
158
159    fn links(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)> {
160        unsafe {
161            let index = self.resolve_location(loc)?;
162            let mut exp = ddjvu_document_get_pageanno(self.doc, index as libc::c_int);
163            while exp == MINIEXP_DUMMY {
164                self.ctx.handle_message();
165                exp = ddjvu_document_get_pageanno(self.doc, index as libc::c_int);
166            }
167            if exp == MINIEXP_NIL {
168                None
169            } else {
170                let links = ddjvu_anno_get_hyperlinks(exp);
171                if links.is_null() {
172                    ddjvu_miniexp_release(self.doc, exp);
173                    return None;
174                }
175                let height = self.page(index).map(|p| p.height()).unwrap() as i32;
176                let c_rect = CString::new("rect").unwrap();
177                let s_rect = miniexp_symbol(c_rect.as_ptr()) as *mut MiniExp;
178                let mut link = links;
179                let mut result = Vec::new();
180                let mut offset = 0;
181                while !(*link).is_null() {
182                    let uri = miniexp_nth(1, *link);
183                    let area = miniexp_nth(3, *link);
184                    if miniexp_stringp(uri) == 1 && miniexp_nth(0, area) == s_rect {
185                        let text = CStr::from_ptr(miniexp_to_str(uri))
186                            .to_string_lossy()
187                            .into_owned();
188                        let rect = {
189                            let x_min = miniexp_nth(1, area) as i32 >> 2;
190                            let y_max = height - (miniexp_nth(2, area) as i32 >> 2);
191                            let r_width = miniexp_nth(3, area) as i32 >> 2;
192                            let r_height = miniexp_nth(4, area) as i32 >> 2;
193                            bndr![
194                                x_min as f32,
195                                (y_max - r_height) as f32,
196                                (x_min + r_width) as f32,
197                                y_max as f32
198                            ]
199                        };
200                        result.push(BoundedText {
201                            text,
202                            rect,
203                            location: TextLocation::Static(index, offset),
204                        });
205                    }
206                    offset += 1;
207                    link = link.offset(1);
208                }
209                libc::free(links as *mut libc::c_void);
210                ddjvu_miniexp_release(self.doc, exp);
211                Some((result, index))
212            }
213        }
214    }
215
216    fn images(&mut self, _loc: Location) -> Option<(Vec<Boundary>, usize)> {
217        None
218    }
219
220    fn metadata(&self, key: &str) -> Option<String> {
221        unsafe {
222            let mut exp = ddjvu_document_get_anno(self.doc, 1);
223            while exp == MINIEXP_DUMMY {
224                self.ctx.handle_message();
225                exp = ddjvu_document_get_anno(self.doc, 1);
226            }
227            if exp == MINIEXP_NIL {
228                None
229            } else {
230                let key = CString::new(key).unwrap();
231                let key = miniexp_symbol(key.as_ptr());
232                let val = ddjvu_anno_get_metadata(exp, key);
233                if val.is_null() {
234                    None
235                } else {
236                    ddjvu_miniexp_release(self.doc, exp);
237                    Some(CStr::from_ptr(val).to_string_lossy().into_owned())
238                }
239            }
240        }
241    }
242
243    fn title(&self) -> Option<String> {
244        self.metadata("title")
245    }
246
247    fn author(&self) -> Option<String> {
248        self.metadata("author")
249    }
250
251    fn is_reflowable(&self) -> bool {
252        false
253    }
254
255    fn layout(&mut self, _width: u32, _height: u32, _font_size: f32, _dpi: u16) {}
256
257    fn set_text_align(&mut self, _text_align: TextAlign) {}
258
259    fn set_font_family(&mut self, _family_name: &str, _search_path: &str) {}
260
261    fn set_margin_width(&mut self, _width: i32) {}
262
263    fn set_line_height(&mut self, _line_height: f32) {}
264
265    fn set_hyphen_penalty(&mut self, _hyphen_penalty: i32) {}
266
267    fn set_stretch_tolerance(&mut self, _stretch_tolerance: f32) {}
268
269    fn set_ignore_document_css(&mut self, _ignore: bool) {}
270}
271
272impl DjvuDocument {
273    pub fn page(&self, index: usize) -> Option<DjvuPage<'_>> {
274        unsafe {
275            let page = ddjvu_page_create_by_pageno(self.doc, index as libc::c_int);
276            if page.is_null() {
277                return None;
278            }
279            let job = ddjvu_page_job(page);
280            while ddjvu_job_status(job) < DDJVU_JOB_OK {
281                self.ctx.handle_message();
282            }
283            if ddjvu_job_status(job) >= DDJVU_JOB_FAILED {
284                None
285            } else {
286                Some(DjvuPage { page, doc: self })
287            }
288        }
289    }
290
291    fn text(&mut self, loc: Location, kind: &[u8]) -> Option<(Vec<BoundedText>, usize)> {
292        unsafe {
293            let index = self.resolve_location(loc)?;
294            let page = self.page(index)?;
295            let height = page.height() as i32;
296            let grain = CString::new(kind).unwrap();
297            let mut exp =
298                ddjvu_document_get_pagetext(self.doc, index as libc::c_int, grain.as_ptr());
299            while exp == MINIEXP_DUMMY {
300                self.ctx.handle_message();
301                exp = ddjvu_document_get_pagetext(self.doc, index as libc::c_int, grain.as_ptr());
302            }
303            if exp == MINIEXP_NIL {
304                None
305            } else {
306                let mut data = Vec::new();
307                let mut offset = 0;
308                Self::walk_text(exp, height, kind, index, &mut offset, &mut data);
309                ddjvu_miniexp_release(self.doc, exp);
310                Some((data, index))
311            }
312        }
313    }
314
315    fn walk_text(
316        exp: *mut MiniExp,
317        height: i32,
318        kind: &[u8],
319        index: usize,
320        offset: &mut usize,
321        data: &mut Vec<BoundedText>,
322    ) {
323        unsafe {
324            let len = miniexp_length(exp);
325            let rect = {
326                let x_min = miniexp_nth(1, exp) as i32 >> 2;
327                let y_max = height - (miniexp_nth(2, exp) as i32 >> 2);
328                let x_max = miniexp_nth(3, exp) as i32 >> 2;
329                let y_min = height - (miniexp_nth(4, exp) as i32 >> 2);
330                bndr![x_min as f32, y_min as f32, x_max as f32, y_max as f32]
331            };
332            let grain = {
333                let raw = miniexp_to_name(miniexp_nth(0, exp));
334                CStr::from_ptr(raw).to_bytes()
335            };
336            let has_text = miniexp_stringp(miniexp_nth(5, exp)) == 1;
337            if grain == kind && has_text {
338                let raw = miniexp_to_str(miniexp_nth(5, exp));
339                let c_str = CStr::from_ptr(raw);
340                let text = c_str.to_string_lossy().into_owned();
341                *offset += 1;
342                data.push(BoundedText {
343                    rect,
344                    text,
345                    location: TextLocation::Static(index, *offset),
346                });
347            } else if !has_text {
348                for i in 5..len {
349                    Self::walk_text(miniexp_nth(i, exp), height, kind, index, offset, data);
350                }
351            }
352        }
353    }
354
355    fn walk_toc(exp: *mut MiniExp, index: &mut usize) -> Vec<TocEntry> {
356        unsafe {
357            let mut vec = Vec::new();
358            let len = miniexp_length(exp);
359            for i in 0..len {
360                let itm = miniexp_nth(i, exp);
361                // Skip `itm` if it isn't a list.
362                if (itm as libc::size_t) & 3 != 0 {
363                    continue;
364                }
365                let raw = miniexp_to_str(miniexp_nth(0, itm));
366                let title = CStr::from_ptr(raw).to_string_lossy().into_owned();
367                let raw = miniexp_to_str(miniexp_nth(1, itm));
368                let bytes = CStr::from_ptr(raw).to_bytes();
369                // TODO: handle the case #page_name: we need to call ddjvu_document_get_fileinfo
370                // for every file and try to find a matching page_name
371                let digits = bytes
372                    .iter()
373                    .map(|v| *v as u8 as char)
374                    .filter(|c| c.is_digit(10))
375                    .collect::<String>();
376                let location =
377                    Location::Exact(digits.parse::<usize>().unwrap_or(1).saturating_sub(1));
378                let current_index = *index;
379                *index += 1;
380                let children = if miniexp_length(itm) > 2 {
381                    Self::walk_toc(itm, index)
382                } else {
383                    Vec::new()
384                };
385                vec.push(TocEntry {
386                    title,
387                    location,
388                    index: current_index,
389                    children,
390                });
391            }
392            vec
393        }
394    }
395
396    pub fn year(&self) -> Option<String> {
397        self.metadata("year")
398    }
399
400    pub fn publisher(&self) -> Option<String> {
401        self.metadata("publisher")
402    }
403
404    pub fn series(&self) -> Option<String> {
405        self.metadata("series")
406    }
407}
408
409impl<'a> DjvuPage<'a> {
410    pub fn pixmap(&self, scale: f32, samples: usize) -> Option<Pixmap> {
411        unsafe {
412            let (width, height) = self.dims();
413            let rect = DjvuRect {
414                x: 0,
415                y: 0,
416                w: (scale * width as f32) as libc::c_uint,
417                h: (scale * height as f32) as libc::c_uint,
418            };
419
420            let style = if samples == 1 {
421                DDJVU_FORMAT_GREY8
422            } else {
423                DDJVU_FORMAT_RGB24
424            };
425            let fmt = ddjvu_format_create(style, 0, ptr::null());
426
427            if fmt.is_null() {
428                return None;
429            }
430
431            ddjvu_format_set_row_order(fmt, 1);
432            ddjvu_format_set_y_direction(fmt, 1);
433
434            let len = samples * (rect.w * rect.h) as usize;
435            let mut data = Vec::new();
436            if data.try_reserve_exact(len).is_err() {
437                ddjvu_format_release(fmt);
438                return None;
439            }
440            data.resize(len, 0xff);
441
442            let row_size = (samples * rect.w as usize) as libc::c_ulong;
443            ddjvu_page_render(
444                self.page,
445                DDJVU_RENDER_COLOR,
446                &rect,
447                &rect,
448                fmt,
449                row_size,
450                data.as_mut_ptr(),
451            );
452
453            let job = ddjvu_page_job(self.page);
454
455            while ddjvu_job_status(job) < DDJVU_JOB_OK {
456                self.doc.ctx.handle_message();
457            }
458
459            ddjvu_format_release(fmt);
460
461            if ddjvu_job_status(job) >= DDJVU_JOB_FAILED {
462                return None;
463            }
464
465            Some(Pixmap {
466                width: rect.w as u32,
467                height: rect.h as u32,
468                samples,
469                data,
470            })
471        }
472    }
473
474    pub fn dims(&self) -> (u32, u32) {
475        (self.width(), self.height())
476    }
477
478    pub fn width(&self) -> u32 {
479        unsafe { ddjvu_page_get_width(self.page) as u32 }
480    }
481
482    pub fn height(&self) -> u32 {
483        unsafe { ddjvu_page_get_height(self.page) as u32 }
484    }
485
486    pub fn dpi(&self) -> u16 {
487        unsafe { ddjvu_page_get_resolution(self.page) as u16 }
488    }
489}
490
491impl<'a> Drop for DjvuPage<'a> {
492    fn drop(&mut self) {
493        unsafe {
494            ddjvu_job_release(ddjvu_page_job(self.page));
495        }
496    }
497}
498
499impl Drop for DjvuDocument {
500    fn drop(&mut self) {
501        unsafe {
502            ddjvu_job_release(ddjvu_document_job(self.doc));
503        }
504    }
505}
506
507impl Drop for DjvuContext {
508    fn drop(&mut self) {
509        unsafe {
510            ddjvu_context_release(self.0);
511        }
512    }
513}