cadmus_core/document/
mod.rs

1pub mod djvu;
2pub mod epub;
3pub mod html;
4pub mod pdf;
5
6mod djvulibre_sys;
7mod mupdf_sys;
8
9use self::djvu::DjvuOpener;
10use self::epub::EpubDocument;
11use self::html::HtmlDocument;
12use self::pdf::PdfOpener;
13use crate::device::CURRENT_DEVICE;
14use crate::framebuffer::Pixmap;
15use crate::geom::{Boundary, CycleDir};
16use crate::metadata::{Annotation, TextAlign};
17use crate::settings::INTERNAL_CARD_ROOT;
18use anyhow::{format_err, Error};
19use fxhash::FxHashMap;
20use nix::sys::statvfs;
21#[cfg(target_os = "linux")]
22use nix::sys::sysinfo;
23use regex::Regex;
24use serde::{Deserialize, Serialize};
25use std::collections::BTreeSet;
26use std::env;
27use std::ffi::OsStr;
28use std::fs::{self, File};
29use std::os::unix::fs::FileExt;
30use std::path::Path;
31use std::process::Command;
32use tracing::error;
33use unicode_normalization::char::is_combining_mark;
34use unicode_normalization::UnicodeNormalization;
35
36pub const BYTES_PER_PAGE: f64 = 2048.0;
37
38#[derive(Debug, Clone)]
39pub enum Location {
40    Exact(usize),
41    Previous(usize),
42    Next(usize),
43    LocalUri(usize, String),
44    Uri(String),
45}
46
47#[derive(Debug, Clone)]
48pub struct BoundedText {
49    pub text: String,
50    pub rect: Boundary,
51    pub location: TextLocation,
52}
53
54#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
55#[serde(untagged)]
56pub enum TextLocation {
57    Static(usize, usize),
58    Dynamic(usize),
59}
60
61impl TextLocation {
62    pub fn location(self) -> usize {
63        match self {
64            TextLocation::Static(page, _) => page,
65            TextLocation::Dynamic(offset) => offset,
66        }
67    }
68
69    #[inline]
70    pub fn min_max(self, other: Self) -> (Self, Self) {
71        if self < other {
72            (self, other)
73        } else {
74            (other, self)
75        }
76    }
77}
78
79#[derive(Debug, Clone)]
80pub struct TocEntry {
81    pub title: String,
82    pub location: Location,
83    pub index: usize,
84    pub children: Vec<TocEntry>,
85}
86
87#[derive(Debug, Clone)]
88pub struct Neighbors {
89    pub previous_page: Option<usize>,
90    pub next_page: Option<usize>,
91}
92
93pub trait Document: Send + Sync {
94    fn dims(&self, index: usize) -> Option<(f32, f32)>;
95    fn pages_count(&self) -> usize;
96
97    fn toc(&mut self) -> Option<Vec<TocEntry>>;
98    fn chapter<'a>(&mut self, offset: usize, toc: &'a [TocEntry]) -> Option<(&'a TocEntry, f32)>;
99    fn chapter_relative<'a>(
100        &mut self,
101        offset: usize,
102        dir: CycleDir,
103        toc: &'a [TocEntry],
104    ) -> Option<&'a TocEntry>;
105    fn words(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)>;
106    fn lines(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)>;
107    fn links(&mut self, loc: Location) -> Option<(Vec<BoundedText>, usize)>;
108    fn images(&mut self, loc: Location) -> Option<(Vec<Boundary>, usize)>;
109
110    fn pixmap(&mut self, loc: Location, scale: f32, samples: usize) -> Option<(Pixmap, usize)>;
111    fn layout(&mut self, width: u32, height: u32, font_size: f32, dpi: u16);
112    fn set_font_family(&mut self, family_name: &str, search_path: &str);
113    fn set_margin_width(&mut self, width: i32);
114    fn set_text_align(&mut self, text_align: TextAlign);
115    fn set_line_height(&mut self, line_height: f32);
116    fn set_hyphen_penalty(&mut self, hyphen_penalty: i32);
117    fn set_stretch_tolerance(&mut self, stretch_tolerance: f32);
118    fn set_ignore_document_css(&mut self, ignore: bool);
119
120    fn title(&self) -> Option<String>;
121    fn author(&self) -> Option<String>;
122    fn metadata(&self, key: &str) -> Option<String>;
123
124    fn is_reflowable(&self) -> bool;
125
126    fn has_synthetic_page_numbers(&self) -> bool {
127        false
128    }
129
130    fn save(&self, _path: &str) -> Result<(), Error> {
131        Err(format_err!("this document can't be saved"))
132    }
133
134    fn preview_pixmap(&mut self, width: f32, height: f32, samples: usize) -> Option<Pixmap> {
135        self.dims(0)
136            .and_then(|dims| {
137                let scale = (width / dims.0).min(height / dims.1);
138                self.pixmap(Location::Exact(0), scale, samples)
139            })
140            .map(|(pixmap, _)| pixmap)
141    }
142
143    fn resolve_location(&mut self, loc: Location) -> Option<usize> {
144        if self.pages_count() == 0 {
145            return None;
146        }
147
148        match loc {
149            Location::Exact(index) => {
150                if index >= self.pages_count() {
151                    None
152                } else {
153                    Some(index)
154                }
155            }
156            Location::Previous(index) => {
157                if index > 0 {
158                    Some(index - 1)
159                } else {
160                    None
161                }
162            }
163            Location::Next(index) => {
164                if index < self.pages_count() - 1 {
165                    Some(index + 1)
166                } else {
167                    None
168                }
169            }
170            _ => None,
171        }
172    }
173}
174
175pub fn file_kind<P: AsRef<Path>>(path: P) -> Option<String> {
176    path.as_ref()
177        .extension()
178        .and_then(OsStr::to_str)
179        .map(str::to_lowercase)
180        .or_else(|| guess_kind(path.as_ref()).ok().map(String::from))
181}
182
183pub fn guess_kind<P: AsRef<Path>>(path: P) -> Result<&'static str, Error> {
184    let file = File::open(path.as_ref())?;
185    let mut magic = [0; 4];
186    file.read_exact_at(&mut magic, 0)?;
187
188    if &magic == b"PK\x03\x04" {
189        let mut mime_type = [0; 28];
190        file.read_exact_at(&mut mime_type, 30)?;
191        if &mime_type == b"mimetypeapplication/epub+zip" {
192            return Ok("epub");
193        }
194    } else if &magic == b"%PDF" {
195        return Ok("pdf");
196    } else if &magic == b"AT&T" {
197        return Ok("djvu");
198    }
199
200    Err(format_err!("Unknown file type"))
201}
202
203pub trait HumanSize {
204    fn human_size(&self) -> String;
205}
206
207const SIZE_BASE: f32 = 1024.0;
208
209impl HumanSize for u64 {
210    fn human_size(&self) -> String {
211        let value = *self as f32;
212        let level = (value.max(1.0).log(SIZE_BASE).floor() as usize).min(3);
213        let factor = value / (SIZE_BASE).powi(level as i32);
214        let precision = level.saturating_sub(1 + factor.log(10.0).floor() as usize);
215        format!(
216            "{0:.1$} {2}",
217            factor,
218            precision,
219            ['B', 'K', 'M', 'G'][level]
220        )
221    }
222}
223
224pub fn asciify(name: &str) -> String {
225    name.nfkd()
226        .filter(|&c| !is_combining_mark(c))
227        .collect::<String>()
228        .replace('œ', "oe")
229        .replace('Œ', "Oe")
230        .replace('æ', "ae")
231        .replace('Æ', "Ae")
232        .replace('—', "-")
233        .replace('–', "-")
234        .replace('’', "'")
235}
236
237pub fn open<P: AsRef<Path>>(path: P) -> Option<Box<dyn Document>> {
238    file_kind(path.as_ref()).and_then(|k| match k.as_ref() {
239        "epub" => EpubDocument::new(&path)
240            .map_err(|e| error!("{}: {:#}.", path.as_ref().display(), e))
241            .map(|d| Box::new(d) as Box<dyn Document>)
242            .ok(),
243        "html" | "htm" => HtmlDocument::new(&path)
244            .map_err(|e| error!("{}: {:#}.", path.as_ref().display(), e))
245            .map(|d| Box::new(d) as Box<dyn Document>)
246            .ok(),
247        "djvu" | "djv" => {
248            DjvuOpener::new().and_then(|o| o.open(path).map(|d| Box::new(d) as Box<dyn Document>))
249        }
250        _ => PdfOpener::new().and_then(|mut o| {
251            if matches!(k.as_ref(), "mobi" | "fb2" | "xps" | "txt") {
252                o.load_user_stylesheet();
253            }
254            o.open(path).map(|d| Box::new(d) as Box<dyn Document>)
255        }),
256    })
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
260#[serde(untagged)]
261pub enum SimpleTocEntry {
262    Leaf(String, TocLocation),
263    Container(String, TocLocation, Vec<SimpleTocEntry>),
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(untagged)]
268pub enum TocLocation {
269    Exact(usize),
270    Uri(String),
271}
272
273impl From<TocLocation> for Location {
274    fn from(loc: TocLocation) -> Location {
275        match loc {
276            TocLocation::Exact(n) => Location::Exact(n),
277            TocLocation::Uri(uri) => Location::Uri(uri),
278        }
279    }
280}
281
282pub fn toc_as_html(toc: &[TocEntry], chap_index: usize) -> String {
283    let mut buf = "<html>\n\t<head>\n\t\t<title>Table of Contents</title>\n\t\t\
284                   <link rel=\"stylesheet\" type=\"text/css\" href=\"css/toc.css\"/>\n\t\
285                   </head>\n\t<body>\n"
286        .to_string();
287    toc_as_html_aux(toc, chap_index, 0, &mut buf);
288    buf.push_str("\t</body>\n</html>");
289    buf
290}
291
292pub fn toc_as_html_aux(toc: &[TocEntry], chap_index: usize, depth: usize, buf: &mut String) {
293    buf.push_str(&"\t".repeat(depth + 2));
294    buf.push_str("<ul>\n");
295    for entry in toc {
296        buf.push_str(&"\t".repeat(depth + 3));
297        match entry.location {
298            Location::Exact(n) => buf.push_str(&format!("<li><a href=\"@{}\">", n)),
299            Location::Uri(ref uri) => buf.push_str(&format!("<li><a href=\"@{}\">", uri)),
300            _ => buf.push_str("<li><a href=\"#\">"),
301        }
302        let title = entry.title.replace('<', "&lt;").replace('>', "&gt;");
303        if entry.index == chap_index {
304            buf.push_str(&format!("<strong>{}</strong>", title));
305        } else {
306            buf.push_str(&title);
307        }
308        buf.push_str("</a></li>\n");
309        if !entry.children.is_empty() {
310            toc_as_html_aux(&entry.children, chap_index, depth + 1, buf);
311        }
312    }
313    buf.push_str(&"\t".repeat(depth + 2));
314    buf.push_str("</ul>\n");
315}
316
317pub fn annotations_as_html(
318    annotations: &[Annotation],
319    active_range: Option<(TextLocation, TextLocation)>,
320) -> String {
321    let mut buf = "<html>\n\t<head>\n\t\t<title>Annotations</title>\n\t\t\
322                   <link rel=\"stylesheet\" type=\"text/css\" href=\"css/annotations.css\"/>\n\t\
323                   </head>\n\t<body>\n"
324        .to_string();
325    buf.push_str("\t\t<ul>\n");
326    for annot in annotations {
327        let mut note = annot.note.replace('<', "&lt;").replace('>', "&gt;");
328        let mut text = annot.text.replace('<', "&lt;").replace('>', "&gt;");
329        let start = annot.selection[0];
330        if active_range.map_or(false, |(first, last)| start >= first && start <= last) {
331            if !note.is_empty() {
332                note = format!("<b>{}</b>", note);
333            }
334            text = format!("<b>{}</b>", text);
335        }
336        if note.is_empty() {
337            buf.push_str(&format!(
338                "\t\t<li><a href=\"@{}\">{}</a></li>\n",
339                start.location(),
340                text
341            ));
342        } else {
343            buf.push_str(&format!(
344                "\t\t<li><a href=\"@{}\"><i>{}</i> — {}</a></li>\n",
345                start.location(),
346                note,
347                text
348            ));
349        }
350    }
351    buf.push_str("\t\t</ul>\n");
352    buf.push_str("\t</body>\n</html>");
353    buf
354}
355
356pub fn bookmarks_as_html(bookmarks: &BTreeSet<usize>, index: usize, synthetic: bool) -> String {
357    let mut buf = "<html>\n\t<head>\n\t\t<title>Bookmarks</title>\n\t\t\
358                   <link rel=\"stylesheet\" type=\"text/css\" href=\"css/bookmarks.css\"/>\n\t\
359                   </head>\n\t<body>\n"
360        .to_string();
361    buf.push_str("\t\t<ul>\n");
362    for bkm in bookmarks {
363        let mut text = if synthetic {
364            format!("{:.1}", *bkm as f64 / BYTES_PER_PAGE)
365        } else {
366            format!("{}", bkm + 1)
367        };
368        if *bkm == index {
369            text = format!("<b>{}</b>", text);
370        }
371        buf.push_str(&format!("\t\t<li><a href=\"@{}\">{}</a></li>\n", bkm, text));
372    }
373    buf.push_str("\t\t</ul>\n");
374    buf.push_str("\t</body>\n</html>");
375    buf
376}
377
378#[inline]
379fn chapter(index: usize, pages_count: usize, toc: &[TocEntry]) -> Option<(&TocEntry, f32)> {
380    let mut chap = None;
381    let mut chap_index = 0;
382    let mut end_index = pages_count;
383    chapter_aux(toc, index, &mut chap, &mut chap_index, &mut end_index);
384    chap.zip(Some(
385        (index - chap_index) as f32 / (end_index - chap_index) as f32,
386    ))
387}
388
389fn chapter_aux<'a>(
390    toc: &'a [TocEntry],
391    index: usize,
392    chap: &mut Option<&'a TocEntry>,
393    chap_index: &mut usize,
394    end_index: &mut usize,
395) {
396    for entry in toc {
397        if let Location::Exact(entry_index) = entry.location {
398            if entry_index <= index && (chap.is_none() || entry_index > *chap_index) {
399                *chap = Some(entry);
400                *chap_index = entry_index;
401            }
402            if entry_index > index && entry_index < *end_index {
403                *end_index = entry_index;
404            }
405        }
406        chapter_aux(&entry.children, index, chap, chap_index, end_index);
407    }
408}
409
410#[inline]
411fn chapter_relative(index: usize, dir: CycleDir, toc: &[TocEntry]) -> Option<&TocEntry> {
412    let chap = chapter(index, usize::MAX, toc).map(|(c, _)| c);
413
414    match dir {
415        CycleDir::Previous => previous_chapter(chap, index, toc),
416        CycleDir::Next => next_chapter(chap, index, toc),
417    }
418}
419
420fn previous_chapter<'a>(
421    chap: Option<&TocEntry>,
422    index: usize,
423    toc: &'a [TocEntry],
424) -> Option<&'a TocEntry> {
425    for entry in toc.iter().rev() {
426        let result = previous_chapter(chap, index, &entry.children);
427        if result.is_some() {
428            return result;
429        }
430
431        if let Some(chap) = chap {
432            if entry.index < chap.index {
433                if let Location::Exact(entry_index) = entry.location {
434                    if entry_index != index {
435                        return Some(entry);
436                    }
437                }
438            }
439        } else {
440            if let Location::Exact(entry_index) = entry.location {
441                if entry_index < index {
442                    return Some(entry);
443                }
444            }
445        }
446    }
447    None
448}
449
450fn next_chapter<'a>(
451    chap: Option<&TocEntry>,
452    index: usize,
453    toc: &'a [TocEntry],
454) -> Option<&'a TocEntry> {
455    for entry in toc {
456        if let Some(chap) = chap {
457            if entry.index > chap.index {
458                if let Location::Exact(entry_index) = entry.location {
459                    if entry_index != index {
460                        return Some(entry);
461                    }
462                }
463            }
464        } else {
465            if let Location::Exact(entry_index) = entry.location {
466                if entry_index > index {
467                    return Some(entry);
468                }
469            }
470        }
471
472        let result = next_chapter(chap, index, &entry.children);
473        if result.is_some() {
474            return result;
475        }
476    }
477    None
478}
479
480pub fn chapter_from_uri<'a>(target_uri: &str, toc: &'a [TocEntry]) -> Option<&'a TocEntry> {
481    for entry in toc {
482        if let Location::Uri(ref uri) = entry.location {
483            if uri.starts_with(target_uri) {
484                return Some(entry);
485            }
486        }
487        let result = chapter_from_uri(target_uri, &entry.children);
488        if result.is_some() {
489            return result;
490        }
491    }
492    None
493}
494
495const CPUINFO_KEYS: [&str; 3] = ["Processor", "Features", "Hardware"];
496const HWINFO_KEYS: [&str; 19] = [
497    "CPU",
498    "PCB",
499    "DisplayPanel",
500    "DisplayCtrl",
501    "DisplayBusWidth",
502    "DisplayResolution",
503    "FrontLight",
504    "FrontLight_LEDrv",
505    "FL_PWM",
506    "TouchCtrl",
507    "TouchType",
508    "Battery",
509    "IFlash",
510    "RamSize",
511    "RamType",
512    "LightSensor",
513    "HallSensor",
514    "RSensor",
515    "Wifi",
516];
517
518pub fn sys_info_as_html() -> String {
519    let mut buf = "<html>\n\t<head>\n\t\t<title>System Info</title>\n\t\t\
520                   <link rel=\"stylesheet\" type=\"text/css\" \
521                   href=\"css/sysinfo.css\"/>\n\t</head>\n\t<body>\n"
522        .to_string();
523
524    buf.push_str("\t\t<table>\n");
525
526    buf.push_str("\t\t\t<tr>\n");
527    buf.push_str("\t\t\t\t<td class=\"key\">Model name</td>\n");
528    buf.push_str(&format!(
529        "\t\t\t\t<td class=\"value\">{}</td>\n",
530        CURRENT_DEVICE.model
531    ));
532    buf.push_str("\t\t\t</tr>\n");
533
534    buf.push_str("\t\t\t<tr>\n");
535    buf.push_str("\t\t\t\t<td class=\"key\">Hardware</td>\n");
536    buf.push_str(&format!(
537        "\t\t\t\t<td class=\"value\">Mark {}</td>\n",
538        CURRENT_DEVICE.mark()
539    ));
540    buf.push_str("\t\t\t</tr>\n");
541    buf.push_str("\t\t\t<tr class=\"sep\"></tr>\n");
542
543    for (name, var) in [
544        ("Code name", "PRODUCT"),
545        ("Model number", "MODEL_NUMBER"),
546        ("Firmware version", "FIRMWARE_VERSION"),
547    ]
548    .iter()
549    {
550        if let Ok(value) = env::var(var) {
551            buf.push_str("\t\t\t<tr>\n");
552            buf.push_str(&format!("\t\t\t\t<td class=\"key\">{}</td>\n", name));
553            buf.push_str(&format!("\t\t\t\t<td class=\"value\">{}</td>\n", value));
554            buf.push_str("\t\t\t</tr>\n");
555        }
556    }
557
558    buf.push_str("\t\t\t<tr class=\"sep\"></tr>\n");
559
560    let output = Command::new("scripts/ip.sh")
561        .output()
562        .map_err(|e| error!("Can't execute command: {:#}.", e))
563        .ok();
564
565    if let Some(stdout) = output
566        .filter(|output| output.status.success())
567        .and_then(|output| String::from_utf8(output.stdout).ok())
568        .filter(|stdout| !stdout.is_empty())
569    {
570        buf.push_str("\t\t\t<tr>\n");
571        buf.push_str("\t\t\t\t<td>IP Address</td>\n");
572        buf.push_str(&format!("\t\t\t\t<td>{}</td>\n", stdout));
573        buf.push_str("\t\t\t</tr>\n");
574    }
575
576    if let Ok(info) = statvfs::statvfs(INTERNAL_CARD_ROOT) {
577        let fbs = info.fragment_size() as u64;
578        let free = info.blocks_free() as u64 * fbs;
579        let total = info.blocks() as u64 * fbs;
580        buf.push_str("\t\t\t<tr>\n");
581        buf.push_str("\t\t\t\t<td>Storage (Free / Total)</td>\n");
582        buf.push_str(&format!(
583            "\t\t\t\t<td>{} / {}</td>\n",
584            free.human_size(),
585            total.human_size()
586        ));
587        buf.push_str("\t\t\t</tr>\n");
588    }
589
590    #[cfg(target_os = "linux")]
591    if let Ok(info) = sysinfo::sysinfo() {
592        buf.push_str("\t\t\t<tr>\n");
593        buf.push_str("\t\t\t\t<td>Memory (Free / Total)</td>\n");
594        buf.push_str(&format!(
595            "\t\t\t\t<td>{} / {}</td>\n",
596            info.ram_unused().human_size(),
597            info.ram_total().human_size()
598        ));
599        buf.push_str("\t\t\t</tr>\n");
600        let load = info.load_average();
601        buf.push_str("\t\t\t<tr>\n");
602        buf.push_str("\t\t\t\t<td>Load Average</td>\n");
603        buf.push_str(&format!(
604            "\t\t\t\t<td>{:.1}% {:.1}% {:.1}%</td>\n",
605            load.0 * 100.0,
606            load.1 * 100.0,
607            load.2 * 100.0
608        ));
609        buf.push_str("\t\t\t</tr>\n");
610    }
611
612    buf.push_str("\t\t\t<tr class=\"sep\"></tr>\n");
613
614    if let Ok(info) = fs::read_to_string("/proc/cpuinfo") {
615        for line in info.lines() {
616            if let Some(index) = line.find(':') {
617                let key = line[0..index].trim();
618                let value = line[index + 1..].trim();
619                if CPUINFO_KEYS.contains(&key) {
620                    buf.push_str("\t\t\t<tr>\n");
621                    buf.push_str(&format!("\t\t\t\t<td class=\"key\">{}</td>\n", key));
622                    buf.push_str(&format!("\t\t\t\t<td class=\"value\">{}</td>\n", value));
623                    buf.push_str("\t\t\t</tr>\n");
624                }
625            }
626        }
627    }
628
629    buf.push_str("\t\t\t<tr class=\"sep\"></tr>\n");
630
631    let output = Command::new("/bin/ntx_hwconfig")
632        .args(&["-s", "/dev/mmcblk0"])
633        .output()
634        .map_err(|e| error!("Can't execute command: {:#}.", e))
635        .ok();
636
637    let mut map = FxHashMap::default();
638
639    if let Some(stdout) = output.and_then(|output| String::from_utf8(output.stdout).ok()) {
640        let re = Regex::new(r#"\[\d+\]\s+(?P<key>[^=]+)='(?P<value>[^']+)'"#).unwrap();
641        for caps in re.captures_iter(&stdout) {
642            map.insert(caps["key"].to_string(), caps["value"].to_string());
643        }
644    }
645
646    if !map.is_empty() {
647        for key in HWINFO_KEYS.iter() {
648            if let Some(value) = map.get(*key) {
649                buf.push_str("\t\t\t<tr>\n");
650                buf.push_str(&format!("\t\t\t\t<td>{}</td>\n", key));
651                buf.push_str(&format!("\t\t\t\t<td>{}</td>\n", value));
652                buf.push_str("\t\t\t</tr>\n");
653            }
654        }
655    }
656
657    buf.push_str("\t\t</table>\n\t</body>\n</html>");
658    buf
659}