pdfboss_core/
thumbnail.rs1use crate::document::Page;
5use crate::object::{Object, Stream};
6use crate::source::AsyncObjectSource;
7
8#[derive(Debug, Clone, PartialEq)]
12pub struct Thumbnail {
13 pub width: u32,
15 pub height: u32,
17 pub bits_per_component: Option<u32>,
19 pub color_space: Option<Object>,
22 pub decode: Option<Vec<f64>>,
24 pub stream: Stream,
26}
27
28pub async fn thumbnail_with<S: AsyncObjectSource>(src: &S, page: &Page) -> Option<Thumbnail> {
34 let Object::Stream(stream) = src.resolve(page.dict().get("Thumb")?).await.ok()? else {
35 return None;
36 };
37 let dict = &stream.dict;
38 let count = |key: &str| u32::try_from(dict.get_int(key)?).ok();
39 Some(Thumbnail {
40 width: count("Width")?,
41 height: count("Height")?,
42 bits_per_component: count("BitsPerComponent"),
43 color_space: dict.get("ColorSpace").cloned(),
44 decode: dict
45 .get_array("Decode")
46 .map(|values| values.iter().filter_map(Object::as_f64).collect()),
47 stream,
48 })
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use crate::document::Document;
55 use crate::object::Name;
56 use pdfboss_testkit::PdfBuilder;
57
58 fn doc_with(page_extra: &str) -> Document {
61 let mut b = PdfBuilder::new();
62 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
63 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
64 b.object(
65 3,
66 &format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] {page_extra} >>"),
67 );
68 b.stream(
69 7,
70 "/Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace /DeviceRGB \
71 /BitsPerComponent 8 /Decode [0 1 0 1 0 1] /Interpolate true",
72 &[255, 0, 0, 0, 0, 255],
73 );
74 b.stream(
75 8,
76 "/Width 1 /Height 1 /ColorSpace [/Indexed /DeviceRGB 0 <FF0000>] /BitsPerComponent 8",
77 &[0],
78 );
79 Document::load(b.build(1)).unwrap()
80 }
81
82 #[test]
86 fn reads_the_pages_thumbnail_and_its_significant_entries() {
87 let doc = doc_with("/Thumb 7 0 R");
88 let page = doc.page(0).unwrap();
89 let thumbnail = doc.thumbnail(&page).unwrap();
90 assert_eq!((thumbnail.width, thumbnail.height), (2, 1));
91 assert_eq!(thumbnail.bits_per_component, Some(8));
92 assert_eq!(
93 thumbnail.color_space,
94 Some(Object::Name(Name("DeviceRGB".to_string())))
95 );
96 assert_eq!(thumbnail.decode, Some(vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0]));
97 assert_eq!(thumbnail.stream.data, vec![255, 0, 0, 0, 0, 255]);
98 assert!(thumbnail.stream.dict.get("Interpolate").is_some());
99 }
100
101 #[test]
106 fn indexed_thumbnails_keep_their_space_and_non_streams_are_none() {
107 let doc = doc_with("/Thumb 8 0 R");
108 let thumbnail = doc.thumbnail(&doc.page(0).unwrap()).unwrap();
109 assert_eq!((thumbnail.width, thumbnail.height), (1, 1));
110 assert!(matches!(thumbnail.color_space, Some(Object::Array(_))));
111 assert_eq!(thumbnail.decode, None);
112 for page_extra in [
113 "",
114 "/Thumb 5",
115 "/Thumb << /Width 1 /Height 1 >>",
116 "/Thumb 3 0 R",
117 ] {
118 let doc = doc_with(page_extra);
119 assert_eq!(doc.thumbnail(&doc.page(0).unwrap()), None, "{page_extra}");
120 }
121 }
122}