1use lopdf::{Dictionary, Document, Object, ObjectId};
8
9use crate::inspect::{
10 deref_obj, load_pdf, object_number, page_content_stream_ids, resolve_inherited, resolve_rect,
11 ContentBudget,
12};
13
14#[derive(Debug, thiserror::Error)]
16pub enum RasterError {
17 #[error("PDF is malformed, or the page does not exist")]
18 Malformed,
19 #[error("encrypted PDF cannot be rasterized")]
20 Encrypted,
21 #[error("dpi {0} is outside 1..=1200")]
22 InvalidDpi(f32),
23 #[error("page would be {width}x{height} pixels, over the limit of {max_pixels}")]
24 TooLarge {
25 width: u64,
26 height: u64,
27 max_pixels: u64,
28 },
29 #[error("unsupported image: {0}")]
30 UnsupportedImage(String),
31 #[error("image is too large to decode")]
32 ImageTooLarge,
33 #[error("image could not be decoded: {0}")]
34 Decode(String),
35 #[error("page content exceeds the walk limits")]
36 TooMuchWork,
37 #[error("PNG encoding failed: {0}")]
38 Png(String),
39}
40
41pub(crate) type Matrix6 = [f64; 6];
43
44pub(crate) const IDENTITY: Matrix6 = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
45
46pub(crate) fn mul(a: &Matrix6, b: &Matrix6) -> Matrix6 {
48 [
49 a[0] * b[0] + a[1] * b[2],
50 a[0] * b[1] + a[1] * b[3],
51 a[2] * b[0] + a[3] * b[2],
52 a[2] * b[1] + a[3] * b[3],
53 a[4] * b[0] + a[5] * b[2] + b[4],
54 a[4] * b[1] + a[5] * b[3] + b[5],
55 ]
56}
57
58const MAX_FORM_DEPTH: usize = 8;
59const MAX_XOBJECT_INVOCATIONS: usize = 4096;
60const MAX_Q_DEPTH: usize = 1024;
61
62pub(crate) enum Event<'a> {
64 Text { visible: bool },
67 Vector,
69 InlineImage,
71 #[cfg_attr(not(feature = "raster"), allow(dead_code))]
75 Image {
76 id: ObjectId,
77 ctm: Matrix6,
78 fill: [u8; 3],
79 resources: Option<&'a Dictionary>,
80 },
81}
82
83#[derive(Clone)]
84struct GState {
85 ctm: Matrix6,
86 fill: [u8; 3],
87 tr: i64,
88}
89
90struct Walker<'a, 'f> {
91 doc: &'a Document,
92 budget: ContentBudget,
93 invocations: usize,
94 sink: &'f mut dyn FnMut(Event<'a>) -> Result<(), RasterError>,
95}
96
97pub(crate) fn walk_page<'a>(
99 doc: &'a Document,
100 page_id: ObjectId,
101 base: Matrix6,
102 sink: &mut dyn FnMut(Event<'a>) -> Result<(), RasterError>,
103) -> Result<(), RasterError> {
104 let page_dict = doc
105 .get_object(page_id)
106 .ok()
107 .and_then(|o| o.as_dict().ok())
108 .ok_or(RasterError::Malformed)?;
109 if page_dict.get(b"Contents").is_err() {
110 return Ok(());
111 }
112 let resources = resolve_inherited(doc, page_dict, b"Resources")
113 .and_then(|o| deref_obj(doc, o))
114 .and_then(|o| o.as_dict().ok());
115 let ids = page_content_stream_ids(doc, page_id).map_err(|_| RasterError::Malformed)?;
116 let mut walker = Walker {
117 doc,
118 budget: ContentBudget::new(),
119 invocations: 0,
120 sink,
121 };
122 let mut gs = GState {
124 ctm: base,
125 fill: [0, 0, 0],
126 tr: 0,
127 };
128 let mut stack: Vec<GState> = Vec::new();
129 for id in ids {
130 let content = walker
131 .budget
132 .content_to_read(doc, id)
133 .map_err(|_| RasterError::Malformed)?;
134 walker.run(&content.operations, resources, &mut gs, &mut stack, 0)?;
135 }
136 Ok(())
137}
138
139impl<'a, 'f> Walker<'a, 'f> {
140 fn run(
141 &mut self,
142 ops: &[lopdf::content::Operation],
143 resources: Option<&'a Dictionary>,
144 gs: &mut GState,
145 stack: &mut Vec<GState>,
146 depth: usize,
147 ) -> Result<(), RasterError> {
148 let nums = |operands: &[Object]| -> Vec<f64> {
149 operands.iter().filter_map(object_number).collect()
150 };
151 for op in ops {
152 match op.operator.as_str() {
153 "q" => {
154 if stack.len() >= MAX_Q_DEPTH {
155 return Err(RasterError::TooMuchWork);
156 }
157 stack.push(gs.clone());
158 }
159 "Q" => {
160 if let Some(prev) = stack.pop() {
161 *gs = prev;
162 }
163 }
164 "cm" => {
165 let n = nums(&op.operands);
166 if n.len() == 6 && n.iter().all(|v| v.is_finite()) {
167 gs.ctm = mul(&[n[0], n[1], n[2], n[3], n[4], n[5]], &gs.ctm);
168 }
169 }
170 "g" => {
171 let n = nums(&op.operands);
172 if n.len() == 1 {
173 let v = unit_to_u8(n[0]);
174 gs.fill = [v, v, v];
175 }
176 }
177 "rg" => {
178 let n = nums(&op.operands);
179 if n.len() == 3 {
180 gs.fill = [unit_to_u8(n[0]), unit_to_u8(n[1]), unit_to_u8(n[2])];
181 }
182 }
183 "k" => {
184 let n = nums(&op.operands);
185 if n.len() == 4 {
186 gs.fill = cmyk_to_rgb(n[0], n[1], n[2], n[3]);
187 }
188 }
189 "Tr" => {
190 if let Some(v) = op.operands.first().and_then(|o| o.as_i64().ok()) {
191 gs.tr = v;
192 }
193 }
194 "Tj" | "'" => {
195 if op.operands.first().is_some_and(non_empty_string) {
196 self.text(gs)?;
197 }
198 }
199 "\"" => {
200 if op.operands.get(2).is_some_and(non_empty_string) {
201 self.text(gs)?;
202 }
203 }
204 "TJ" => {
205 let any = op
206 .operands
207 .first()
208 .and_then(|o| o.as_array().ok())
209 .is_some_and(|a| a.iter().any(non_empty_string));
210 if any {
211 self.text(gs)?;
212 }
213 }
214 "S" | "s" | "f" | "F" | "f*" | "B" | "B*" | "b" | "b*" | "sh" => {
215 (self.sink)(Event::Vector)?;
216 }
217 "BI" => (self.sink)(Event::InlineImage)?,
218 "Do" => {
219 self.invocations += 1;
220 if self.invocations > MAX_XOBJECT_INVOCATIONS {
221 return Err(RasterError::TooMuchWork);
222 }
223 self.do_xobject(op, resources, gs, depth)?;
224 }
225 _ => {}
226 }
227 }
228 Ok(())
229 }
230
231 fn text(&mut self, gs: &GState) -> Result<(), RasterError> {
232 let visible = gs.tr != 3 && gs.tr != 7;
233 (self.sink)(Event::Text { visible })
234 }
235
236 fn do_xobject(
237 &mut self,
238 op: &lopdf::content::Operation,
239 resources: Option<&'a Dictionary>,
240 gs: &GState,
241 depth: usize,
242 ) -> Result<(), RasterError> {
243 let doc = self.doc;
244 let Some(name) = op.operands.first().and_then(|o| o.as_name().ok()) else {
245 return Ok(());
246 };
247 let Some(entry) = resources
248 .and_then(|r| r.get(b"XObject").ok())
249 .and_then(|o| deref_obj(doc, o))
250 .and_then(|o| o.as_dict().ok())
251 .and_then(|d| d.get(name).ok())
252 else {
253 return Ok(());
254 };
255 let Some(id) = (match entry {
256 Object::Reference(id) => Some(*id),
257 _ => None,
258 }) else {
259 return Ok(());
260 };
261 let Some(stream) = doc.get_object(id).ok().and_then(|o| o.as_stream().ok()) else {
262 return Ok(());
263 };
264 let subtype = stream
265 .dict
266 .get(b"Subtype")
267 .ok()
268 .and_then(|o| o.as_name().ok())
269 .unwrap_or(&b""[..]);
270 match subtype {
271 b"Image" => (self.sink)(Event::Image {
272 id,
273 ctm: gs.ctm,
274 fill: gs.fill,
275 resources,
276 }),
277 b"Form" => {
278 if depth >= MAX_FORM_DEPTH {
279 return Err(RasterError::TooMuchWork);
280 }
281 let matrix = stream
282 .dict
283 .get(b"Matrix")
284 .ok()
285 .and_then(|o| deref_obj(doc, o))
286 .and_then(|o| o.as_array().ok())
287 .map(|a| a.iter().filter_map(object_number).collect::<Vec<_>>())
288 .filter(|n| n.len() == 6 && n.iter().all(|v| v.is_finite()))
289 .map(|n| [n[0], n[1], n[2], n[3], n[4], n[5]])
290 .unwrap_or(IDENTITY);
291 let form_resources = stream
292 .dict
293 .get(b"Resources")
294 .ok()
295 .and_then(|o| deref_obj(doc, o))
296 .and_then(|o| o.as_dict().ok())
297 .or(resources);
298 let content = self
299 .budget
300 .content_to_read(doc, id)
301 .map_err(|_| RasterError::Malformed)?;
302 let mut inner = gs.clone();
303 inner.ctm = mul(&matrix, &gs.ctm);
304 let mut inner_stack = Vec::new();
305 self.run(
306 &content.operations,
307 form_resources,
308 &mut inner,
309 &mut inner_stack,
310 depth + 1,
311 )
312 }
313 _ => Ok(()),
314 }
315 }
316}
317
318fn non_empty_string(o: &Object) -> bool {
319 matches!(o, Object::String(b, _) if !b.is_empty())
320}
321
322pub(crate) fn unit_to_u8(v: f64) -> u8 {
323 (v.clamp(0.0, 1.0) * 255.0).round() as u8
324}
325
326pub(crate) fn cmyk_to_rgb(c: f64, m: f64, y: f64, k: f64) -> [u8; 3] {
327 let k = k.clamp(0.0, 1.0);
328 let f = |v: f64| unit_to_u8((1.0 - v.clamp(0.0, 1.0)) * (1.0 - k));
329 [f(c), f(m), f(y)]
330}
331
332#[derive(Clone, Debug, PartialEq)]
334pub struct PageProfile {
335 pub page: u32,
337 pub has_text: bool,
340 pub has_visible_text: bool,
342 pub image_count: u32,
344 pub inline_image_count: u32,
345 pub has_vector_graphics: bool,
347 pub largest_image_coverage: f64,
349}
350
351impl PageProfile {
352 pub const SCAN_COVERAGE: f64 = 0.5;
354
355 pub fn is_scanned(&self) -> bool {
358 !self.has_text && self.largest_image_coverage >= Self::SCAN_COVERAGE
359 }
360}
361
362pub(crate) fn page_box(doc: &Document, page_id: ObjectId) -> [f64; 4] {
364 let dict = doc.get_object(page_id).ok().and_then(|o| o.as_dict().ok());
365 let Some(dict) = dict else {
366 return [0.0, 0.0, 612.0, 792.0];
367 };
368 let m = resolve_rect(doc, dict, b"MediaBox").unwrap_or([0.0, 0.0, 612.0, 792.0]);
369 let r = resolve_rect(doc, dict, b"CropBox").unwrap_or(m);
370 [
371 r[0].min(r[2]),
372 r[1].min(r[3]),
373 r[0].max(r[2]),
374 r[1].max(r[3]),
375 ]
376}
377
378pub(crate) fn page_id_of(doc: &Document, page: u32) -> Result<ObjectId, RasterError> {
379 if doc.is_encrypted() {
380 return Err(RasterError::Encrypted);
381 }
382 doc.get_pages()
383 .get(&page)
384 .copied()
385 .ok_or(RasterError::Malformed)
386}
387
388pub fn classify_page_doc(doc: &Document, page: u32) -> Result<PageProfile, RasterError> {
390 let page_id = page_id_of(doc, page)?;
391 let b = page_box(doc, page_id);
392 let area = ((b[2] - b[0]) * (b[3] - b[1])).abs().max(1e-9);
393 let mut profile = PageProfile {
394 page,
395 has_text: false,
396 has_visible_text: false,
397 image_count: 0,
398 inline_image_count: 0,
399 has_vector_graphics: false,
400 largest_image_coverage: 0.0,
401 };
402 walk_page(doc, page_id, IDENTITY, &mut |event| {
403 match event {
404 Event::Text { visible } => {
405 profile.has_text = true;
406 profile.has_visible_text |= visible;
407 }
408 Event::Vector => profile.has_vector_graphics = true,
409 Event::InlineImage => profile.inline_image_count += 1,
410 Event::Image { ctm, .. } => {
411 profile.image_count += 1;
412 let det = (ctm[0] * ctm[3] - ctm[1] * ctm[2]).abs();
413 if det.is_finite() {
414 profile.largest_image_coverage =
415 profile.largest_image_coverage.max((det / area).min(1.0));
416 }
417 }
418 }
419 Ok(())
420 })?;
421 Ok(profile)
422}
423
424pub fn classify_page(bytes: &[u8], page: u32) -> Result<PageProfile, RasterError> {
426 let doc = load_pdf(bytes).map_err(|_| RasterError::Malformed)?;
427 classify_page_doc(&doc, page)
428}
429
430pub fn scanned_pages(bytes: &[u8]) -> Result<Vec<u32>, RasterError> {
433 let doc = load_pdf(bytes).map_err(|_| RasterError::Malformed)?;
434 if doc.is_encrypted() {
435 return Err(RasterError::Encrypted);
436 }
437 let numbers: Vec<u32> = doc.get_pages().keys().copied().collect();
438 Ok(numbers
439 .into_iter()
440 .filter(|n| {
441 classify_page_doc(&doc, *n)
442 .map(|p| p.is_scanned())
443 .unwrap_or(false)
444 })
445 .collect())
446}
447
448#[cfg(test)]
449pub(crate) mod tests {
450 use super::*;
451 use crate::test_fixtures::{one_page_pdf, PageFixture};
452 use lopdf::{dictionary, Stream};
453
454 pub(crate) fn scan_doc(
456 content: &[u8],
457 image: Stream,
458 extras: Vec<(&'static str, Object)>,
459 ) -> Document {
460 let mut doc = Document::with_version("1.7");
461 let pages_id = doc.new_object_id();
462 let image_id = doc.add_object(image);
463 let content_id = doc.add_object(Stream::new(dictionary! {}, content.to_vec()));
464 let mut page = dictionary! {
465 "Type" => "Page",
466 "Parent" => pages_id,
467 "Contents" => content_id,
468 "MediaBox" => vec![0.into(), 0.into(), 72.into(), 72.into()],
469 "Resources" => dictionary! { "XObject" => dictionary! { "Im0" => image_id } },
470 };
471 for (k, v) in extras {
472 page.set(k, v);
473 }
474 let page_id = doc.add_object(page);
475 doc.objects.insert(
476 pages_id,
477 Object::Dictionary(dictionary! {
478 "Type" => "Pages",
479 "Kids" => vec![page_id.into()],
480 "Count" => 1,
481 }),
482 );
483 let catalog = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id });
484 doc.trailer.set("Root", catalog);
485 doc
486 }
487
488 pub(crate) fn gray_2x2() -> Stream {
489 Stream::new(
490 dictionary! {
491 "Type" => "XObject",
492 "Subtype" => "Image",
493 "Width" => 2,
494 "Height" => 2,
495 "ColorSpace" => "DeviceGray",
496 "BitsPerComponent" => 8,
497 },
498 vec![0, 255, 255, 0],
499 )
500 }
501
502 #[test]
503 fn an_image_only_page_is_scanned() {
504 let doc = scan_doc(b"q 72 0 0 72 0 0 cm /Im0 Do Q", gray_2x2(), vec![]);
505 let p = classify_page_doc(&doc, 1).unwrap();
506 assert_eq!(p.image_count, 1);
507 assert!(!p.has_text);
508 assert!(p.is_scanned());
509 }
510
511 #[test]
512 fn a_small_image_does_not_make_a_scan() {
513 let doc = scan_doc(b"q 10 0 0 10 0 0 cm /Im0 Do Q", gray_2x2(), vec![]);
514 assert!(!classify_page_doc(&doc, 1).unwrap().is_scanned());
515 }
516
517 #[test]
518 fn a_text_page_is_not_scanned() {
519 let doc = one_page_pdf(PageFixture::for_content(
520 b"BT /F1 12 Tf 72 700 Td (Hello) Tj ET",
521 ));
522 let p = classify_page_doc(&doc, 1).unwrap();
523 assert!(p.has_text && p.has_visible_text);
524 assert!(!p.is_scanned());
525 }
526
527 #[test]
528 fn invisible_text_over_an_image_counts_as_a_text_layer() {
529 let doc = scan_doc(
530 b"q 72 0 0 72 0 0 cm /Im0 Do Q BT 3 Tr 0 0 Td (hi) Tj ET",
531 gray_2x2(),
532 vec![],
533 );
534 let p = classify_page_doc(&doc, 1).unwrap();
535 assert!(p.has_text && !p.has_visible_text);
536 assert!(!p.is_scanned());
537 }
538
539 #[test]
540 fn a_blank_page_is_not_scanned() {
541 let doc = one_page_pdf(PageFixture::for_content(b""));
542 let p = classify_page_doc(&doc, 1).unwrap();
543 assert_eq!(p.image_count, 0);
544 assert!(!p.is_scanned());
545 }
546
547 #[test]
548 fn scanned_pages_reads_bytes() {
549 let mut doc = scan_doc(b"q 72 0 0 72 0 0 cm /Im0 Do Q", gray_2x2(), vec![]);
550 let mut bytes = Vec::new();
551 doc.save_to(&mut bytes).unwrap();
552 assert_eq!(scanned_pages(&bytes).unwrap(), vec![1]);
553 assert!(classify_page(&bytes, 2).is_err());
554 }
555}