Skip to main content

tex_engine/pdflatex/
nodes.rs

1use crate::engine::utils::memory::MemoryManager;
2use crate::engine::{EngineExtension, EngineReferences, EngineTypes};
3use crate::tex::nodes::boxes::TeXBox;
4use crate::tex::nodes::CustomNodeTrait;
5use crate::tex::nodes::{display_do_indent, NodeTrait, NodeType};
6use crate::tex::numerics::TeXDimen;
7use crate::utils::errors::{TeXError, TeXResult};
8use std::fmt::Formatter;
9use std::path::{Path, PathBuf};
10
11const PDFIUM_VERSION: &str = "7350";
12
13#[derive(Clone, Debug)]
14pub enum PDFNode<ET: EngineTypes> {
15    Obj(PDFObj),
16    XForm(PDFXForm<ET>),
17    XImage(PDFXImage<ET>),
18    PDFLiteral(PDFLiteral),
19    PDFOutline(PDFOutline),
20    PDFCatalog(PDFCatalog),
21    PDFPageAttr(Box<[ET::Token]>),
22    PDFPagesAttr(Box<[ET::Token]>),
23    PDFDest(PDFDest<ET::Dim>),
24    Color(ColorStackAction),
25    PDFStartLink(PDFStartLink<ET>),
26    PDFAnnot(PDFAnnot<ET>),
27    PDFEndLink,
28    PDFSave,
29    PDFRestore,
30    PDFMatrix {
31        scale: f32,
32        rotate: f32,
33        skewx: f32,
34        skewy: f32,
35    },
36}
37
38#[derive(Debug, Clone)]
39pub enum ActionSpec {
40    User(String),
41    Goto(GotoAction),
42    Thread {
43        file: Option<String>,
44        target: NumOrName,
45    },
46}
47
48#[derive(Debug, Clone, Copy)]
49pub enum PDFBoxSpec {
50    MediaBox,
51    CropBox,
52    BleedBox,
53    TrimBox,
54    ArtBox,
55}
56
57#[derive(Debug, Clone)]
58pub enum GotoAction {
59    File {
60        filename: String,
61        struct_: Option<PDFStruct>,
62        page: Option<i64>,
63        target: String,
64        newwindow: Option<bool>,
65    },
66    Current {
67        struct_: Option<PDFStruct>,
68        page: Option<i64>,
69        target: NumOrName,
70    },
71}
72
73#[derive(Debug, Clone)]
74pub enum PDFDestType<D: TeXDimen> {
75    XYZ {
76        zoom: Option<i64>,
77    },
78    Fitr {
79        width: Option<D>,
80        height: Option<D>,
81        depth: Option<D>,
82    },
83    Fitbh,
84    Fitbv,
85    Fitb,
86    Fith,
87    Fitv,
88    Fit,
89}
90
91#[derive(Debug, Clone)]
92pub enum NumOrName {
93    Num(i64),
94    Name(String),
95}
96impl NumOrName {
97    pub fn as_name(self) -> String {
98        match self {
99            NumOrName::Name(s) => s,
100            NumOrName::Num(i) => format!("NUM_{}", i),
101        }
102    }
103}
104
105#[derive(Debug, Clone)]
106pub enum PDFStruct {
107    Num(i64),
108    Name(String),
109    Other(String),
110}
111
112pub fn num_or_name<ET: EngineTypes>(
113    engine: &mut EngineReferences<ET>,
114    token: &ET::Token,
115) -> TeXResult<Option<NumOrName>, ET> {
116    match engine.read_keywords(&[b"num", b"name"])? {
117        Some(b"num") => Ok(Some(NumOrName::Num(engine.read_int(false, token)?.into()))),
118        Some(b"name") => {
119            let mut str = String::new();
120            engine.read_maybe_braced_string(true, &mut str, token)?;
121            Ok(Some(NumOrName::Name(str)))
122        }
123        _ => Ok(None),
124    }
125}
126
127pub fn pdfdest_type<ET: EngineTypes>(
128    engine: &mut EngineReferences<ET>,
129    tk: &ET::Token,
130) -> TeXResult<PDFDestType<ET::Dim>, ET> {
131    match engine.read_keywords(&[
132        b"xyz", b"fitr", b"fitbh", b"fitbv", b"fitb", b"fith", b"fitv", b"fit",
133    ])? {
134        Some(b"xyz") => {
135            let zoom = if engine.read_keyword(b"zoom")? {
136                Some(engine.read_int(false, tk)?.into())
137            } else {
138                None
139            };
140            Ok(PDFDestType::XYZ { zoom })
141        }
142        Some(b"fitr") => {
143            let mut width = None;
144            let mut height = None;
145            let mut depth = None;
146            loop {
147                match engine.read_keywords(&[b"width", b"height", b"depth"])? {
148                    Some(b"width") => width = Some(engine.read_dim(false, tk)?),
149                    Some(b"height") => height = Some(engine.read_dim(false, tk)?),
150                    Some(b"depth") => depth = Some(engine.read_dim(false, tk)?),
151                    _ => break,
152                }
153            }
154            Ok(PDFDestType::Fitr {
155                width,
156                height,
157                depth,
158            })
159        }
160        Some(b"fitbh") => Ok(PDFDestType::Fitbh),
161        Some(b"fitbv") => Ok(PDFDestType::Fitbv),
162        Some(b"fitb") => Ok(PDFDestType::Fitb),
163        Some(b"fith") => Ok(PDFDestType::Fith),
164        Some(b"fitv") => Ok(PDFDestType::Fitv),
165        Some(b"fit") => Ok(PDFDestType::Fit),
166        _ => {
167            TeXError::missing_keyword(
168                engine.aux,
169                engine.state,
170                engine.mouth,
171                &[
172                    "xyz", "fitr", "fitbh", "fitbv", "fitb", "fith", "fitv", "fit",
173                ],
174            )?;
175            Ok(PDFDestType::XYZ { zoom: None })
176        }
177    }
178}
179
180pub fn action_spec<ET: EngineTypes>(
181    engine: &mut EngineReferences<ET>,
182    token: &ET::Token,
183) -> TeXResult<ActionSpec, ET> {
184    match engine.read_keywords(&[b"user", b"goto", b"thread"])? {
185        Some(b"user") => {
186            let mut ret = String::new();
187            engine.read_braced_string(true, true, token, &mut ret)?;
188            Ok(ActionSpec::User(ret))
189        }
190        Some(b"goto") => {
191            let file = if engine.read_keyword(b"file")? {
192                let mut file = String::new();
193                engine.read_braced_string(true, true, token, &mut file)?;
194                Some(file)
195            } else {
196                None
197            };
198            let struct_ = if engine.read_keyword(b"struct")? {
199                match num_or_name(engine, token)? {
200                    None => {
201                        let mut ret = String::new();
202                        engine.read_braced_string(true, true, token, &mut ret)?;
203                        Some(PDFStruct::Other(ret))
204                    }
205                    Some(NumOrName::Num(i)) => Some(PDFStruct::Num(i)),
206                    Some(NumOrName::Name(s)) => Some(PDFStruct::Name(s)),
207                }
208            } else {
209                None
210            };
211            match file {
212                None => match num_or_name(engine, token)? {
213                    Some(n) => Ok(ActionSpec::Goto(GotoAction::Current {
214                        struct_,
215                        page: None,
216                        target: n,
217                    })),
218                    _ => {
219                        let page = if engine.read_keyword(b"page")? {
220                            Some(engine.read_int(false, token)?.into())
221                        } else {
222                            None
223                        };
224                        let mut str = String::new();
225                        engine.read_braced_string(true, true, token, &mut str)?;
226                        Ok(ActionSpec::Goto(GotoAction::Current {
227                            struct_,
228                            page,
229                            target: NumOrName::Name(str),
230                        }))
231                    }
232                },
233                Some(filename) => {
234                    let (page, target) = if engine.read_keyword(b"name")? {
235                        let mut str = String::new();
236                        engine.read_braced_string(true, true, token, &mut str)?;
237                        (None, str)
238                    } else {
239                        let page = if engine.read_keyword(b"page")? {
240                            Some(engine.read_int(false, token)?.into())
241                        } else {
242                            None
243                        };
244                        let mut str = String::new();
245                        engine.read_braced_string(true, true, token, &mut str)?;
246                        (page, str)
247                    };
248                    let newwindow = match engine.read_keywords(&[b"newwindow", b"nonewwindow"])? {
249                        Some(b"newwindow") => Some(true),
250                        Some(b"nonewwindow") => Some(false),
251                        _ => None,
252                    };
253                    Ok(ActionSpec::Goto(GotoAction::File {
254                        filename,
255                        struct_,
256                        page,
257                        target,
258                        newwindow,
259                    }))
260                }
261            }
262        }
263        Some(b"thread") => {
264            let file = if engine.read_keyword(b"file")? {
265                let mut file = String::new();
266                engine.read_braced_string(true, true, token, &mut file)?;
267                Some(file)
268            } else {
269                None
270            };
271
272            match num_or_name(engine, token)? {
273                Some(n) => Ok(ActionSpec::Thread { file, target: n }),
274                None => {
275                    TeXError::missing_keyword(
276                        engine.aux,
277                        engine.state,
278                        engine.mouth,
279                        &["num", "name"],
280                    )?;
281                    Ok(ActionSpec::Thread {
282                        file,
283                        target: NumOrName::Num(0),
284                    })
285                }
286            }
287        }
288        None => {
289            TeXError::missing_keyword(
290                engine.aux,
291                engine.state,
292                engine.mouth,
293                &["user", "goto", "thread"],
294            )?;
295            Ok(ActionSpec::User(String::new()))
296        }
297        _ => unreachable!(),
298    }
299}
300
301// ----------------------------------------------------------------------------------------
302
303#[derive(Copy, Clone, Debug)]
304pub enum ColorStackAction {
305    Current(usize),
306    Push(usize, PDFColor),
307    Pop(usize),
308    Set(usize, PDFColor),
309}
310
311#[derive(Clone, Debug)]
312pub struct PDFStartLink<ET: EngineTypes> {
313    pub width: Option<ET::Dim>,
314    pub height: Option<ET::Dim>,
315    pub depth: Option<ET::Dim>,
316    pub attr: Option<String>,
317    pub action: ActionSpec,
318}
319
320#[derive(Clone, Debug)]
321pub struct PDFDest<D: TeXDimen> {
322    pub structnum: Option<i64>,
323    pub id: NumOrName,
324    pub dest: PDFDestType<D>,
325}
326
327#[derive(Clone, Debug)]
328pub struct PDFLiteral {
329    pub option: PDFLiteralOption,
330    pub literal: String,
331}
332
333impl<ET: EngineTypes> CustomNodeTrait<ET> for PDFNode<ET> where ET::CustomNode: From<PDFNode<ET>> {}
334
335impl<ET: EngineTypes> NodeTrait<ET> for PDFNode<ET>
336where
337    ET::CustomNode: From<PDFNode<ET>>,
338{
339    fn height(&self) -> ET::Dim {
340        match self {
341            PDFNode::XImage(img) => img.height(),
342            _ => ET::Dim::default(),
343        }
344    }
345    fn width(&self) -> ET::Dim {
346        match self {
347            PDFNode::XImage(img) => img.width(),
348            _ => ET::Dim::default(),
349        }
350    }
351    fn depth(&self) -> ET::Dim {
352        match self {
353            PDFNode::XImage(img) => img.depth(),
354            _ => ET::Dim::default(),
355        }
356    }
357    fn nodetype(&self) -> NodeType {
358        NodeType::WhatsIt
359    }
360    fn opaque(&self) -> bool {
361        matches!(self, PDFNode::Color(_))
362    }
363    fn display_fmt(&self, indent: usize, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        display_do_indent(indent, f)?;
365        match self {
366            PDFNode::PDFDest(PDFDest {
367                structnum,
368                id,
369                dest,
370            }) => write!(
371                f,
372                "<pdfdest structnum=\"{:?}\", id=\"{:?}\", dest=\"{:?}\">",
373                structnum, id, dest
374            ),
375            PDFNode::PDFCatalog(c) => write!(
376                f,
377                "<pdfcatalog literal=\"{}\", action=\"{:?}\">",
378                c.literal, c.action
379            ),
380            PDFNode::PDFOutline(o) => write!(
381                f,
382                "<pdfoutline attr=\"{}\", action=\"{:?}\", count=\"{:?}\", content=\"{}\">",
383                o.attr, o.action, o.count, o.content
384            ),
385            PDFNode::Obj(o) => write!(f, "<pdfobj literal=\"{}\">", o.0),
386            PDFNode::PDFAnnot(a) => write!(
387                f,
388                "<pdfannot width=\"{:?}\", height=\"{:?}\", depth=\"{:?}\", content=\"{}\">",
389                a.width, a.height, a.depth, a.content
390            ),
391            PDFNode::PDFMatrix {
392                scale,
393                rotate,
394                skewx,
395                skewy,
396            } => write!(
397                f,
398                "<pdfmatrix scale=\"{}\", rotate=\"{}\", skewx=\"{}\", skewy=\"{}\">",
399                scale, rotate, skewx, skewy
400            ),
401            PDFNode::XForm(x) => {
402                write!(
403                    f,
404                    "<pdfxform attr=\"{}\", resources=\"{}\">",
405                    x.attr, x.resources
406                )?;
407                if let Some(bx) = &x.bx {
408                    bx.display_fmt(indent + 2, f)?;
409                }
410                display_do_indent(indent, f)?;
411                write!(f, "</pdfxform>")
412            }
413            PDFNode::XImage(img) => write!(f, "<pdfximage {}>", img.filepath.display()),
414            PDFNode::PDFPageAttr(_) => write!(f, "<pdfpageattr/>"),
415            PDFNode::PDFPagesAttr(_) => write!(f, "<pdfpagesattr/>"),
416            PDFNode::PDFSave => write!(f, "<pdfsave>"),
417            PDFNode::PDFRestore => write!(f, "<pdfrestore>"),
418            PDFNode::PDFLiteral(PDFLiteral { option, literal }) => write!(
419                f,
420                "<pdfliteral option=\"{:?}\", literal=\"{:?}\">",
421                option, literal
422            ),
423            PDFNode::Color(c) => match c {
424                ColorStackAction::Current(i) => write!(f, "<pdfcolorstack current=\"{}\">", i),
425                ColorStackAction::Push(i, c) => {
426                    write!(f, "<pdfcolorstack push=\"{}\", color=\"{:?}\">", i, c)
427                }
428                ColorStackAction::Pop(i) => write!(f, "<pdfcolorstack pop=\"{}\">", i),
429                ColorStackAction::Set(i, c) => {
430                    write!(f, "<pdfcolorstack set=\"{}\", color=\"{:?}\">", i, c)
431                }
432            },
433            PDFNode::PDFStartLink(PDFStartLink {
434                width,
435                height,
436                depth,
437                attr,
438                action,
439            }) => {
440                write!(f, "<pdfstartlink")?;
441                if let Some(w) = width {
442                    write!(f, " width=\"{:?}\"", w)?;
443                }
444                if let Some(h) = height {
445                    write!(f, " height=\"{:?}\"", h)?;
446                }
447                if let Some(d) = depth {
448                    write!(f, " depth=\"{:?}\"", d)?;
449                }
450                if let Some(a) = attr {
451                    write!(f, " attr=\"{:?}\"", a)?;
452                }
453                write!(f, " action=\"{:?}\">", action)
454            }
455            PDFNode::PDFEndLink => write!(f, "<pdfendlink>"),
456        }
457    }
458}
459
460#[derive(Copy, Clone, Debug)]
461pub enum PDFLiteralOption {
462    None,
463    Direct,
464    Page,
465}
466
467#[cfg(all(target_os = "windows", feature = "pdfium"))]
468const PDFIUM_NAME: &str = "pdfium.dll";
469#[cfg(all(not(target_os = "windows"), feature = "pdfium"))]
470const PDFIUM_NAME: &str = "libpdfium.so";
471
472#[cfg(feature = "pdfium")]
473static PDFIUM_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
474
475#[cfg(feature = "pdfium")]
476fn download_pdfium(lib_dir: &std::path::Path) {
477    const BASE_URL: &str =
478        "https://github.com/bblanchon/pdfium-binaries/releases/download/chromium";
479
480    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
481    const PLATFORM: &str = "win-x64";
482    #[cfg(all(target_os = "windows", target_arch = "x86"))]
483    const PLATFORM: &str = "win-x86";
484    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
485    const PLATFORM: &str = "linux-x64";
486    #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
487    const PLATFORM: &str = "linux-arm64";
488    #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
489    const PLATFORM: &str = "mac-x64";
490    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
491    const PLATFORM: &str = "mac-arm64";
492
493    let _ = std::fs::create_dir_all(lib_dir);
494    let download_url = format!("{BASE_URL}/{PDFIUM_VERSION}/pdfium-{PLATFORM}.tgz");
495    let archive_path = lib_dir.join("pdfium.tgz");
496    let Ok(mut response) = reqwest::blocking::get(download_url) else {
497        log::warn!("Could not download pdfium");
498        return;
499    }; //.expect("Could not download pdfium");
500    let Ok(mut dest) = std::fs::File::create(&archive_path) else {
501        return;
502    };
503    let Ok(_) = std::io::copy(&mut response, &mut dest) else {
504        return;
505    };
506    let Ok(tar_gz) = std::fs::File::open(&archive_path) else {
507        return;
508    };
509    let tar = flate2::read::GzDecoder::new(tar_gz);
510    let mut archive = tar::Archive::new(tar);
511    let Ok(entries) = archive.entries() else {
512        return;
513    };
514
515    for entry in entries {
516        let Ok(mut entry) = entry else { continue };
517        let Ok(entry_path) = entry.path() else {
518            continue;
519        };
520        let Some(name) = entry_path.file_name() else {
521            continue;
522        };
523        if name.to_str() == Some(PDFIUM_NAME) {
524            let file_path = lib_dir.join(name);
525            let Ok(_) = entry.unpack(file_path) else {
526                continue;
527            };
528            break;
529        }
530    }
531    let _ = std::fs::remove_file(archive_path);
532}
533
534pub trait PDFExtension<ET: EngineTypes>: EngineExtension<ET> {
535    fn pdfmatches(&mut self) -> &mut Vec<String>;
536    fn elapsed(&mut self) -> &mut std::time::Instant;
537    fn colorstacks(&mut self) -> &mut Vec<Vec<PDFColor>>;
538    fn current_colorstack(&mut self) -> &mut usize;
539    fn pdfobjs(&mut self) -> &mut Vec<PDFObj>;
540    fn pdfannots(&mut self) -> &mut Vec<PDFAnnot<ET>>;
541    fn pdfxforms(&mut self) -> &mut Vec<PDFXForm<ET>>;
542    fn pdfximages(&mut self) -> &mut Vec<PDFXImage<ET>>;
543    #[cfg(feature = "pdfium")]
544    fn pdfium_direct(&mut self) -> &mut Option<Option<pdfium_render::prelude::Pdfium>>;
545
546    #[cfg(feature = "pdfium")]
547    fn pdfium(&mut self) -> Option<&pdfium_render::prelude::Pdfium> {
548        use pdfium_render::prelude::*;
549        match self.pdfium_direct() {
550            Some(p) => p.as_ref(),
551            r => {
552                let Ok(lock) = PDFIUM_LOCK.lock() else {
553                    log::warn!("Could not lock PDFium lock");
554                    return None;
555                };
556                let pdfium = Pdfium::bind_to_system_library().ok().or_else(|| {
557                    std::env::current_exe().ok().and_then(|d| {
558                        let lib_dir = d.parent()?.join("lib");
559                        let lib_path = lib_dir.join(PDFIUM_NAME);
560                        if !lib_path.exists() {
561                            download_pdfium(&lib_dir);
562                        }
563                        Pdfium::bind_to_library(&lib_path)
564                            .map_err(|e| {
565                                log::warn!(
566                                    "Could not bind to pdfium at {}: {e}",
567                                    lib_path.display()
568                                );
569                            })
570                            .ok()
571                    })
572                });
573                *r = Some(pdfium.map(Pdfium::new));
574                drop(lock);
575                r.as_ref().unwrap_or_else(|| unreachable!()).as_ref()
576            }
577        }
578    }
579}
580
581pub struct MinimalPDFExtension<ET: EngineTypes> {
582    matches: Vec<String>,
583    elapsed: std::time::Instant, //chrono::DateTime<chrono::Local>,
584    colorstacks: Vec<Vec<PDFColor>>,
585    current_colorstack: usize,
586    pdfobjs: Vec<PDFObj>,
587    pdfxforms: Vec<PDFXForm<ET>>,
588    pdfximages: Vec<PDFXImage<ET>>,
589    pdfannots: Vec<PDFAnnot<ET>>,
590    #[cfg(feature = "pdfium")]
591    pdfium: Option<Option<pdfium_render::prelude::Pdfium>>,
592}
593impl<ET: EngineTypes> EngineExtension<ET> for MinimalPDFExtension<ET> {
594    fn new(_memory: &mut MemoryManager<ET::Token>) -> Self {
595        Self {
596            matches: Vec::new(),
597            elapsed: std::time::Instant::now(),
598            colorstacks: vec![vec![PDFColor::black()]],
599            current_colorstack: 0,
600            pdfobjs: Vec::new(),
601            pdfannots: Vec::new(),
602            pdfxforms: Vec::new(),
603            pdfximages: Vec::new(),
604            #[cfg(feature = "pdfium")]
605            pdfium: None,
606        }
607    }
608}
609impl<ET: EngineTypes> PDFExtension<ET> for MinimalPDFExtension<ET> {
610    fn pdfmatches(&mut self) -> &mut Vec<String> {
611        &mut self.matches
612    }
613
614    fn elapsed(&mut self) -> &mut std::time::Instant {
615        &mut self.elapsed
616    }
617
618    fn colorstacks(&mut self) -> &mut Vec<Vec<PDFColor>> {
619        &mut self.colorstacks
620    }
621
622    fn current_colorstack(&mut self) -> &mut usize {
623        &mut self.current_colorstack
624    }
625
626    fn pdfobjs(&mut self) -> &mut Vec<PDFObj> {
627        &mut self.pdfobjs
628    }
629
630    fn pdfxforms(&mut self) -> &mut Vec<PDFXForm<ET>> {
631        &mut self.pdfxforms
632    }
633
634    fn pdfannots(&mut self) -> &mut Vec<PDFAnnot<ET>> {
635        &mut self.pdfannots
636    }
637
638    fn pdfximages(&mut self) -> &mut Vec<PDFXImage<ET>> {
639        &mut self.pdfximages
640    }
641
642    #[cfg(feature = "pdfium")]
643    fn pdfium_direct(&mut self) -> &mut Option<Option<pdfium_render::prelude::Pdfium>> {
644        &mut self.pdfium
645    }
646}
647
648#[derive(Debug, Clone)]
649pub struct PDFObj(pub String);
650
651#[derive(Debug, Clone)]
652pub struct PDFXForm<ET: EngineTypes> {
653    pub attr: String,
654    pub resources: String,
655    pub bx: Option<TeXBox<ET>>,
656}
657#[derive(Debug, Clone)]
658pub struct PDFAnnot<ET: EngineTypes> {
659    pub width: Option<ET::Dim>,
660    pub height: Option<ET::Dim>,
661    pub depth: Option<ET::Dim>,
662    pub content: String,
663}
664
665#[derive(Debug, Clone)]
666pub enum PDFImage {
667    None,
668    Img(image::DynamicImage),
669    #[cfg(feature = "pdfium")]
670    PDF(image::DynamicImage),
671}
672impl PDFImage {
673    pub fn width(&self) -> u32 {
674        match self {
675            PDFImage::None => 20,
676            PDFImage::Img(img) => img.width(),
677            #[cfg(feature = "pdfium")]
678            PDFImage::PDF(img) => img.width() / 5,
679        }
680    }
681    pub fn height(&self) -> u32 {
682        match self {
683            PDFImage::None => 20,
684            PDFImage::Img(img) => img.height(),
685            #[cfg(feature = "pdfium")]
686            PDFImage::PDF(img) => img.height() / 5,
687        }
688    }
689}
690
691#[derive(Debug, Clone)]
692pub struct PDFXImage<ET: EngineTypes> {
693    pub attr: String,
694    pub width: Option<ET::Dim>,
695    pub height: Option<ET::Dim>,
696    pub depth: Option<ET::Dim>,
697    pub colorspace: Option<i64>,
698    pub page: Option<i64>,
699    pub boxspec: Option<PDFBoxSpec>,
700    pub filepath: PathBuf,
701    pub img: PDFImage,
702}
703impl<ET: EngineTypes> PDFXImage<ET> {
704    pub fn height(&self) -> ET::Dim {
705        match (self.height, self.width) {
706            (None, Some(w)) => {
707                let scale = self.img.width() as f32 / (w.into() as f32);
708                ET::Dim::from_sp((self.img.height() as f32 / scale).round() as i32)
709            }
710            (Some(h), _) => h,
711            _ => ET::Dim::from_sp(65536 * (self.img.height() as i32)),
712        }
713    }
714
715    pub fn width(&self) -> ET::Dim {
716        match (self.height, self.width) {
717            (Some(h), None) => {
718                let scale = self.img.height() as f32 / (h.into() as f32);
719                ET::Dim::from_sp((self.img.width() as f32 / scale).round() as i32)
720            }
721            (_, Some(w)) => w,
722            _ => ET::Dim::from_sp(65536 * (self.img.width() as i32)),
723        }
724    }
725    pub fn depth(&self) -> ET::Dim {
726        self.depth.unwrap_or_default()
727    }
728}
729
730#[cfg(feature = "pdfium")]
731pub fn pdf_as_image<ET: EngineTypes, E: PDFExtension<ET>>(path: &Path, ext: &mut E) -> PDFImage {
732    use pdfium_render::prelude::PdfRenderConfig;
733    let Some(pdfium) = ext.pdfium() else {
734        log::warn!("PDFium not loaded");
735        return PDFImage::None;
736    };
737    let Ok(pdf) = pdfium.load_pdf_from_file(&path, None) else {
738        log::warn!("Failed to load PDF file {}", path.display());
739        return PDFImage::None;
740    };
741    let cfg = PdfRenderConfig::new().scale_page_by_factor(5.0);
742    let pages = pdf.pages();
743    let r = if let Ok(bmp) = pages.iter().next().unwrap().render_with_config(&cfg) {
744        let img = bmp.as_image();
745        PDFImage::PDF(img)
746    } else {
747        log::warn!("Failed to render PDF file {}", path.display());
748        PDFImage::None
749    };
750    r
751}
752
753#[cfg(not(feature = "pdfium"))]
754pub fn pdf_as_image<ET: EngineTypes, E: PDFExtension<ET>>(_path: &Path, _ext: &mut E) -> PDFImage {
755    PDFImage::None
756}
757
758#[derive(Debug, Clone)]
759pub struct PDFOutline {
760    pub attr: String,
761    pub action: ActionSpec,
762    pub count: Option<i64>,
763    pub content: String,
764}
765
766#[derive(Debug, Clone)]
767pub struct PDFCatalog {
768    pub literal: String,
769    pub action: Option<ActionSpec>,
770}
771
772#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
773#[allow(non_snake_case)]
774pub struct PDFColor {
775    R: u8,
776    G: u8,
777    B: u8,
778}
779impl PDFColor {
780    pub fn black() -> Self {
781        PDFColor { R: 0, G: 0, B: 0 }
782    }
783    pub fn parse<S: AsRef<str> + std::fmt::Display>(s: S) -> Self {
784        macro_rules! parse {
785            ($s:expr) => {
786                match $s.parse::<f32>() {
787                    Ok(f) => f,
788                    _ => return Self::black(),
789                }
790            };
791        }
792        let ls: Vec<_> = s.as_ref().split(' ').collect();
793        if matches!(ls.last(), Some(&"K")) && ls.len() > 4 {
794            let third = 1.0 - parse!(ls[3]);
795            let r = 255.0 * (1.0 - parse!(ls[0])) * third;
796            let g = 255.0 * (1.0 - parse!(ls[1])) * third;
797            let b = 255.0 * (1.0 - parse!(ls[2])) * third;
798            if r > 255.0 || g > 255.0 || b > 255.0 || r < 0.0 || g < 0.0 || b < 0.0 {
799                return Self::black();
800            }
801            PDFColor {
802                R: (r.round() as u8),
803                G: (g.round() as u8),
804                B: (b.round() as u8),
805            }
806        } else if matches!(ls.last(), Some(&"RG")) && ls.len() > 3 {
807            let r = 255.0 * parse!(ls[0]);
808            let g = 255.0 * parse!(ls[1]);
809            let b = 255.0 * parse!(ls[2]);
810            if r > 255.0 || g > 255.0 || b > 255.0 || r < 0.0 || g < 0.0 || b < 0.0 {
811                return Self::black();
812            }
813            PDFColor {
814                R: (r.round() as u8),
815                G: (g.round() as u8),
816                B: (b.round() as u8),
817            }
818        } else if matches!(ls.last(), Some(&"G")) && ls.len() > 1 {
819            let x = 255.0 * parse!(ls[0]);
820            if !(0.0..=255.0).contains(&x) {
821                return Self::black();
822            }
823            let x = (x.round()) as u8;
824            PDFColor { R: x, G: x, B: x }
825        } else {
826            Self::black()
827        }
828    }
829}
830impl std::fmt::Display for PDFColor {
831    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
832        write!(f, "#{:02x}{:02x}{:02x}", self.R, self.G, self.B)
833    }
834}