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 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 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}