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#[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 }; 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, 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}