imprimatur 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/Cargo.lock +472 -5
  4. data/Cargo.toml +16 -1
  5. data/README.md +59 -7
  6. data/ext/imprimatur_native/cli/Cargo.toml +1 -1
  7. data/ext/imprimatur_native/cli/src/app.rs +3 -1
  8. data/ext/imprimatur_native/core/Cargo.toml +29 -13
  9. data/ext/imprimatur_native/core/README.md +13 -1
  10. data/ext/imprimatur_native/core/src/bin/imprimatur-render-worker.rs +10 -0
  11. data/ext/imprimatur_native/core/src/bytes.rs +16 -0
  12. data/ext/imprimatur_native/core/src/doc/picture.rs +3 -7
  13. data/ext/imprimatur_native/core/src/doc/size.rs +14 -0
  14. data/ext/imprimatur_native/core/src/doc/symbol.rs +3 -5
  15. data/ext/imprimatur_native/core/src/doc.rs +24 -0
  16. data/ext/imprimatur_native/core/src/docx.rs +6 -5
  17. data/ext/imprimatur_native/core/src/error.rs +6 -0
  18. data/ext/imprimatur_native/core/src/font.rs +12 -0
  19. data/ext/imprimatur_native/core/src/lib.rs +6 -0
  20. data/ext/imprimatur_native/core/src/pdf/layout.rs +13 -5
  21. data/ext/imprimatur_native/core/src/pdf/objects.rs +1 -1
  22. data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +1 -0
  23. data/ext/imprimatur_native/core/src/read/budget.rs +10 -5
  24. data/ext/imprimatur_native/core/src/read/content.rs +16 -0
  25. data/ext/imprimatur_native/core/src/read/image/dimensions.rs +2 -2
  26. data/ext/imprimatur_native/core/src/read/ole.rs +1 -8
  27. data/ext/imprimatur_native/core/src/read/pdf/bounded.rs +190 -0
  28. data/ext/imprimatur_native/core/src/read/pdf/content/state.rs +50 -0
  29. data/ext/imprimatur_native/core/src/read/pdf/content.rs +43 -54
  30. data/ext/imprimatur_native/core/src/read/pdf/document/pages.rs +140 -0
  31. data/ext/imprimatur_native/core/src/read/pdf/document.rs +20 -110
  32. data/ext/imprimatur_native/core/src/read/pdf/filter.rs +25 -13
  33. data/ext/imprimatur_native/core/src/read/pdf/font.rs +4 -3
  34. data/ext/imprimatur_native/core/src/read/pdf/isolation/protocol.rs +155 -0
  35. data/ext/imprimatur_native/core/src/read/pdf/isolation/system.rs +71 -0
  36. data/ext/imprimatur_native/core/src/read/pdf/isolation/worker.rs +43 -0
  37. data/ext/imprimatur_native/core/src/read/pdf/isolation.rs +166 -0
  38. data/ext/imprimatur_native/core/src/read/pdf/render.rs +206 -0
  39. data/ext/imprimatur_native/core/src/read/pdf.rs +16 -8
  40. data/ext/imprimatur_native/core/src/read/xls/strings.rs +2 -1
  41. data/ext/imprimatur_native/core/src/read/xls.rs +1 -8
  42. data/ext/imprimatur_native/core/src/read.rs +6 -5
  43. data/ext/imprimatur_native/core/src/test_support/font.rs +37 -0
  44. data/ext/imprimatur_native/core/src/xlsx/validation.rs +28 -18
  45. data/ext/imprimatur_native/core/src/zip/reader.rs +1 -8
  46. data/ext/imprimatur_native/extconf.rb +18 -0
  47. data/ext/imprimatur_native/ffi/Cargo.toml +2 -2
  48. data/ext/imprimatur_native/ffi/src/document.rs +6 -0
  49. data/ext/imprimatur_native/ffi/src/lib.rs +7 -0
  50. data/ext/imprimatur_native/ffi/src/pdf.rs +73 -0
  51. data/ext/imprimatur_native/ffi/src/read.rs +18 -12
  52. data/lib/imprimatur/document.rb +4 -2
  53. data/lib/imprimatur/native.rb +7 -0
  54. data/lib/imprimatur/pdf_reader.rb +27 -0
  55. data/lib/imprimatur/reader.rb +48 -11
  56. data/lib/imprimatur/spreadsheet.rb +1 -1
  57. data/lib/imprimatur/version.rb +1 -1
  58. data/lib/imprimatur.rb +28 -3
  59. metadata +19 -4
@@ -5,6 +5,7 @@
5
5
  #[cfg(test)]
6
6
  mod tests;
7
7
 
8
+ use crate::bytes::{u16_le as u16_at, u32_le as u32_at};
8
9
  use crate::read::Budget;
9
10
  use crate::{Error, Result};
10
11
 
@@ -38,14 +39,6 @@ fn words(bytes: &[u8]) -> impl Iterator<Item = u32> + '_ {
38
39
  bytes.as_chunks::<4>().0.iter().map(|word| u32::from_le_bytes(*word))
39
40
  }
40
41
 
41
- fn u16_at(bytes: &[u8], at: usize) -> Option<u16> {
42
- Some(u16::from_le_bytes(bytes.get(at..at.checked_add(2)?)?.try_into().ok()?))
43
- }
44
-
45
- fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
46
- Some(u32::from_le_bytes(bytes.get(at..at.checked_add(4)?)?.try_into().ok()?))
47
- }
48
-
49
42
  impl<'a> Ole<'a> {
50
43
  /// Reads the header, the allocation tables and the directory.
51
44
  ///
@@ -0,0 +1,190 @@
1
+ //! PDF inspection and bounded reads without losing individual page failures.
2
+
3
+ #[cfg(test)]
4
+ mod tests;
5
+
6
+ use super::content::{Fonts, PageText, page_text};
7
+ use super::document::Document;
8
+ use super::{is_pdf, layout, xref};
9
+ use crate::read::{Budget, Limits};
10
+ use crate::{Error, Result};
11
+
12
+ /// Metadata read without interpreting page content.
13
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14
+ pub struct Info {
15
+ pub page_count: u32,
16
+ /// Opened with the empty password: permissions, not a locked document.
17
+ pub encrypted: bool,
18
+ }
19
+
20
+ /// A contiguous, one-based page selection and its output limits.
21
+ #[derive(Debug, Clone)]
22
+ pub struct ReadOptions {
23
+ pub first_page: u32,
24
+ pub last_page: u32,
25
+ pub max_pages: usize,
26
+ pub max_page_bytes: usize,
27
+ pub max_output_bytes: usize,
28
+ }
29
+
30
+ impl Default for ReadOptions {
31
+ fn default() -> Self {
32
+ Self {
33
+ first_page: 1,
34
+ last_page: u32::MAX,
35
+ max_pages: 500,
36
+ max_page_bytes: 256 * 1024,
37
+ max_output_bytes: 8 * 1024 * 1024,
38
+ }
39
+ }
40
+ }
41
+
42
+ impl ReadOptions {
43
+ const fn validate(&self) -> Result<()> {
44
+ if self.first_page == 0 || self.last_page < self.first_page {
45
+ return Err(Error::InvalidPdfOption("pages must be an ascending one-based range"));
46
+ }
47
+ if self.max_page_bytes == 0 || self.max_output_bytes == 0 {
48
+ return Err(Error::InvalidPdfOption("text limits must be positive"));
49
+ }
50
+ Ok(())
51
+ }
52
+ }
53
+
54
+ /// What is known about a page; failure is never reported as empty.
55
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
56
+ pub enum Status {
57
+ Text,
58
+ Empty,
59
+ Failed,
60
+ Skipped,
61
+ }
62
+
63
+ impl Status {
64
+ #[must_use]
65
+ pub const fn as_str(self) -> &'static str {
66
+ match self {
67
+ Self::Text => "text",
68
+ Self::Empty => "empty",
69
+ Self::Failed => "failed",
70
+ Self::Skipped => "skipped",
71
+ }
72
+ }
73
+ }
74
+
75
+ #[derive(Debug)]
76
+ pub struct PageResult {
77
+ pub number: u32,
78
+ pub status: Status,
79
+ pub text: String,
80
+ /// Unknown if the page could not be interpreted.
81
+ pub images: Option<u32>,
82
+ pub truncated: bool,
83
+ pub reason: Option<String>,
84
+ }
85
+
86
+ #[derive(Debug)]
87
+ pub struct Pages {
88
+ pub info: Info,
89
+ pub pages: Vec<PageResult>,
90
+ }
91
+
92
+ /// Counts actual leaves, not the page tree's advertised `/Count`.
93
+ ///
94
+ /// # Errors
95
+ /// A malformed, locked, unsupported or over-budget document.
96
+ pub fn info(data: &[u8], limits: &Limits) -> Result<Info> {
97
+ let budget = Budget::new(limits, data.len())?;
98
+ let document = open(data, &budget)?;
99
+ let selected = document.select_pages(1, u32::MAX, 0)?;
100
+ Ok(Info { page_count: selected.count, encrypted: document.encrypted() })
101
+ }
102
+
103
+ /// Reads only selected page streams. Page failures are retained alongside
104
+ /// successes; cancellation and exhausted document budgets remain errors.
105
+ ///
106
+ /// # Errors
107
+ /// As [`info`], and invalid options.
108
+ pub fn pages(data: &[u8], options: &ReadOptions, limits: &Limits) -> Result<Pages> {
109
+ options.validate()?;
110
+ let budget = Budget::new(limits, data.len())?;
111
+ let document = open(data, &budget)?;
112
+ let selected =
113
+ document.select_pages(options.first_page, options.last_page, options.max_pages)?;
114
+ let mut fonts = Fonts::new();
115
+ let mut text = PageText { strict: true, ..PageText::default() };
116
+ let mut remaining = options.max_output_bytes;
117
+ let mut results = Vec::with_capacity(selected.pages.len());
118
+ for (number, page) in selected.pages {
119
+ budget.check()?;
120
+ if remaining == 0 {
121
+ results.push(unread(
122
+ number,
123
+ Status::Skipped,
124
+ "the text output limit was reached".into(),
125
+ ));
126
+ continue;
127
+ }
128
+ text.max_bytes = Some(options.max_page_bytes.min(remaining));
129
+ let read = page.and_then(|page| page_text(&document, &page, &mut fonts, &mut text));
130
+ match read {
131
+ Ok(()) => {
132
+ let lines = layout::lines(&text);
133
+ let mut output = String::new();
134
+ for line in &lines.lines {
135
+ if !output.is_empty() {
136
+ output.push('\n');
137
+ }
138
+ output.push_str(lines.text(line));
139
+ }
140
+ let limit = options.max_page_bytes.min(remaining);
141
+ let truncated = text.truncated || output.len() > limit;
142
+ truncate(&mut output, limit);
143
+ remaining -= output.len();
144
+ results.push(PageResult {
145
+ number,
146
+ status: if output.trim().is_empty() { Status::Empty } else { Status::Text },
147
+ text: output,
148
+ images: Some(u32::try_from(text.images).unwrap_or(u32::MAX)),
149
+ truncated,
150
+ reason: truncated.then(|| "the page text limit was reached".into()),
151
+ });
152
+ }
153
+ Err(error) if xref::is_limit(&error) || matches!(error, Error::PasswordRequired) => {
154
+ return Err(error);
155
+ }
156
+ Err(error) => results.push(unread(number, Status::Failed, error.to_string())),
157
+ }
158
+ }
159
+ Ok(Pages {
160
+ info: Info { page_count: selected.count, encrypted: document.encrypted() },
161
+ pages: results,
162
+ })
163
+ }
164
+
165
+ pub(super) fn open<'a>(data: &'a [u8], budget: &'a Budget<'a>) -> Result<Document<'a>> {
166
+ if !is_pdf(data) {
167
+ return Err(Error::UnsupportedDocument("not a PDF file".into()));
168
+ }
169
+ budget.check()?;
170
+ Document::open(data, budget).map(Document::strict)
171
+ }
172
+
173
+ const fn unread(number: u32, status: Status, reason: String) -> PageResult {
174
+ PageResult {
175
+ number,
176
+ status,
177
+ reason: Some(reason),
178
+ text: String::new(),
179
+ images: None,
180
+ truncated: false,
181
+ }
182
+ }
183
+
184
+ fn truncate(text: &mut String, maximum: usize) {
185
+ let mut end = text.len().min(maximum);
186
+ while !text.is_char_boundary(end) {
187
+ end -= 1;
188
+ }
189
+ text.truncate(end);
190
+ }
@@ -0,0 +1,50 @@
1
+ //! Graphics and text state for a content stream.
2
+
3
+ use std::rc::Rc;
4
+
5
+ use super::Font;
6
+ use super::geometry::{IDENTITY, Matrix, multiply};
7
+
8
+ #[derive(Clone, Copy)]
9
+ pub(super) struct Cursor {
10
+ pub matrix: Matrix,
11
+ line: Matrix,
12
+ }
13
+
14
+ impl Cursor {
15
+ pub const START: Self = Self { matrix: IDENTITY, line: IDENTITY };
16
+
17
+ pub const fn at(matrix: Matrix) -> Self {
18
+ Self { matrix, line: matrix }
19
+ }
20
+
21
+ pub fn next_line(&mut self, x: f64, y: f64) {
22
+ self.line = multiply(&[1.0, 0.0, 0.0, 1.0, x, y], &self.line);
23
+ self.matrix = self.line;
24
+ }
25
+ }
26
+
27
+ #[derive(Clone)]
28
+ pub(super) struct State {
29
+ pub ctm: Matrix,
30
+ pub font: Option<Rc<Font>>,
31
+ pub size: f64,
32
+ pub char_spacing: f64,
33
+ pub word_spacing: f64,
34
+ pub scale: f64,
35
+ pub leading: f64,
36
+ pub rise: f64,
37
+ }
38
+
39
+ impl State {
40
+ pub const START: Self = Self {
41
+ ctm: IDENTITY,
42
+ font: None,
43
+ size: 0.0,
44
+ char_spacing: 0.0,
45
+ word_spacing: 0.0,
46
+ scale: 1.0,
47
+ leading: 0.0,
48
+ rise: 0.0,
49
+ };
50
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  mod geometry;
7
7
  mod operands;
8
+ mod state;
8
9
  #[cfg(test)]
9
10
  mod tests;
10
11
 
@@ -17,10 +18,11 @@ use super::font::Font;
17
18
  use super::glyphs::push_clean;
18
19
  use super::lexer::{Lexer, Token, hex_into, literal_into, name_bytes};
19
20
  use super::object::{Dict, Object};
20
- use crate::Result;
21
21
  use crate::read::span;
22
+ use crate::{Error, Result};
22
23
  use geometry::{Frame, IDENTITY, Matrix, multiply, quarter_turns};
23
24
  use operands::{Operand, inline_image, operand};
25
+ use state::{Cursor, State};
24
26
 
25
27
  /// A glyph where it shows: its origin and advance along the baseline, in
26
28
  /// the page's displayed frame (rotation applied), and its text.
@@ -56,52 +58,9 @@ pub struct PageText {
56
58
  pub images: usize,
57
59
  /// The displayed page: width and height.
58
60
  pub size: (f64, f64),
59
- }
60
-
61
- /// The text matrix and the text line matrix (ISO 32000-1, 9.4.2).
62
- #[derive(Clone, Copy)]
63
- struct Cursor {
64
- matrix: Matrix,
65
- line: Matrix,
66
- }
67
-
68
- impl Cursor {
69
- const START: Self = Self { matrix: IDENTITY, line: IDENTITY };
70
-
71
- const fn at(matrix: Matrix) -> Self {
72
- Self { matrix, line: matrix }
73
- }
74
-
75
- /// `Td`: the start of a new line, offset from the current line's start.
76
- fn next_line(&mut self, x: f64, y: f64) {
77
- self.line = multiply(&[1.0, 0.0, 0.0, 1.0, x, y], &self.line);
78
- self.matrix = self.line;
79
- }
80
- }
81
-
82
- #[derive(Clone)]
83
- struct State {
84
- ctm: Matrix,
85
- font: Option<Rc<Font>>,
86
- size: f64,
87
- char_spacing: f64,
88
- word_spacing: f64,
89
- scale: f64,
90
- leading: f64,
91
- rise: f64,
92
- }
93
-
94
- impl State {
95
- const START: Self = Self {
96
- ctm: IDENTITY,
97
- font: None,
98
- size: 0.0,
99
- char_spacing: 0.0,
100
- word_spacing: 0.0,
101
- scale: 1.0,
102
- leading: 0.0,
103
- rise: 0.0,
104
- };
61
+ pub max_bytes: Option<usize>,
62
+ pub truncated: bool,
63
+ pub strict: bool,
105
64
  }
106
65
 
107
66
  /// Fonts by the object number of their dictionary, shared across pages.
@@ -150,14 +109,23 @@ fn run(
150
109
  out.text.clear();
151
110
  out.glyphs.clear();
152
111
  out.images = 0;
112
+ out.truncated = false;
153
113
  let content = match page.contents.as_slice() {
154
- [single] => document.stream_data(single)?.unwrap_or_default(),
114
+ [single] => match document.stream_data(single)? {
115
+ Some(data) => data,
116
+ None if out.strict => {
117
+ return Err(Error::malformed("PDF", "missing page content stream"));
118
+ }
119
+ None => Vec::new(),
120
+ },
155
121
  parts => {
156
122
  let mut content = Vec::new();
157
123
  for part in parts {
158
124
  if let Some(data) = document.stream_data(part)? {
159
125
  content.extend_from_slice(&data);
160
126
  content.push(b'\n');
127
+ } else if out.strict {
128
+ return Err(Error::malformed("PDF", "missing page content stream"));
161
129
  }
162
130
  }
163
131
  content
@@ -256,7 +224,7 @@ impl Run<'_, '_> {
256
224
  if operator == b"'" || operator == b"\"" {
257
225
  cursor.next_line(0.0, -state.leading);
258
226
  }
259
- self.show_operand(operands.last(), &items, &state, &mut cursor.matrix);
227
+ self.show_operand(operands.last(), &items, &state, &mut cursor.matrix)?;
260
228
  }
261
229
  b"Do" => {
262
230
  if let Some(name) = name(0) {
@@ -287,7 +255,7 @@ impl Run<'_, '_> {
287
255
  items: &[Operand<'_>],
288
256
  state: &State,
289
257
  matrix: &mut Matrix,
290
- ) {
258
+ ) -> Result<()> {
291
259
  let shown = match operand {
292
260
  Some(&Operand::Array(start, end)) => items.get(start..end).unwrap_or_default(),
293
261
  Some(single) => std::slice::from_ref(single),
@@ -303,7 +271,7 @@ impl Run<'_, '_> {
303
271
  } else {
304
272
  literal_into(raw, &mut bytes);
305
273
  }
306
- self.show(&bytes, state, matrix);
274
+ self.show(&bytes, state, matrix)?;
307
275
  }
308
276
  Operand::Number(adjustment) => {
309
277
  let shift = -adjustment / 1000.0 * state.size * state.scale;
@@ -314,6 +282,7 @@ impl Run<'_, '_> {
314
282
  }
315
283
  }
316
284
  self.bytes = bytes;
285
+ Ok(())
317
286
  }
318
287
 
319
288
  /// A font resource, loaded once per document.
@@ -338,8 +307,14 @@ impl Run<'_, '_> {
338
307
  }
339
308
 
340
309
  /// Emits a string's glyphs and advances the text matrix past them.
341
- fn show(&mut self, bytes: &[u8], state: &State, matrix: &mut Matrix) {
342
- let Some(font) = state.font.clone() else { return };
310
+ fn show(&mut self, bytes: &[u8], state: &State, matrix: &mut Matrix) -> Result<()> {
311
+ let Some(font) = state.font.clone() else {
312
+ return if self.page.strict && !bytes.is_empty() {
313
+ Err(Error::malformed("PDF", "missing font for page text"))
314
+ } else {
315
+ Ok(())
316
+ };
317
+ };
343
318
  let device = multiply(matrix, &state.ctm);
344
319
  let shown = multiply(&device, &self.rotation);
345
320
  let along = shown[0].hypot(shown[1]);
@@ -350,6 +325,7 @@ impl Run<'_, '_> {
350
325
  let mut scratch = std::mem::take(&mut self.scratch);
351
326
  let bounds = self.bounds;
352
327
  font.glyphs(bytes, &mut scratch, |text, width, space| {
328
+ self.document.budget().tick()?;
353
329
  let x = f64::mul_add(advance, shown[0], shown[4]) + rise.0;
354
330
  let y = f64::mul_add(advance, shown[1], shown[5]) + rise.1;
355
331
  let user = (
@@ -361,6 +337,17 @@ impl Run<'_, '_> {
361
337
  && user.1 >= bounds[1] - 1.0
362
338
  && user.1 <= bounds[3] + 1.0;
363
339
  if visible && !text.is_empty() {
340
+ if self.page.truncated {
341
+ return Ok(());
342
+ }
343
+ if self
344
+ .page
345
+ .max_bytes
346
+ .is_some_and(|limit| self.page.text.len().saturating_add(text.len()) > limit)
347
+ {
348
+ self.page.truncated = true;
349
+ return Ok(());
350
+ }
364
351
  let span = span::append(&mut self.page.text, |page| push_clean(page, text));
365
352
  if !span.is_empty() {
366
353
  let width = width * state.size * state.scale * along;
@@ -381,10 +368,12 @@ impl Run<'_, '_> {
381
368
  state.scale,
382
369
  advance,
383
370
  );
384
- });
371
+ Ok(())
372
+ })?;
385
373
  self.scratch = scratch;
386
374
  matrix[4] = f64::mul_add(advance, matrix[0], matrix[4]);
387
375
  matrix[5] = f64::mul_add(advance, matrix[1], matrix[5]);
376
+ Ok(())
388
377
  }
389
378
 
390
379
  /// `Do`: a form `XObject`'s content, in its own matrix and resources;
@@ -0,0 +1,140 @@
1
+ //! Count the real page tree; only materialize the requested page descriptors.
2
+
3
+ use std::collections::HashSet;
4
+
5
+ use super::{Dict, Document, Error, Object, Page, Rc, Result, malformed};
6
+
7
+ pub struct Selection {
8
+ pub count: u32,
9
+ pub pages: Vec<(u32, Result<Page>)>,
10
+ first: u32,
11
+ last: u32,
12
+ maximum: usize,
13
+ }
14
+
15
+ impl Selection {
16
+ fn add(&mut self, page: impl FnOnce() -> Result<Page>) -> Result<()> {
17
+ self.count = self.count.checked_add(1).ok_or(Error::LimitExceeded("page count"))?;
18
+ if (self.first..=self.last).contains(&self.count) && self.pages.len() < self.maximum {
19
+ self.pages.push((self.count, page()));
20
+ }
21
+ Ok(())
22
+ }
23
+ }
24
+
25
+ impl Document<'_> {
26
+ pub fn pages(&self) -> Result<Vec<Page>> {
27
+ self.select_pages(1, u32::MAX, usize::MAX)?
28
+ .pages
29
+ .into_iter()
30
+ .map(|(_, page)| page)
31
+ .collect()
32
+ }
33
+
34
+ pub fn select_pages(&self, first: u32, last: u32, maximum: usize) -> Result<Selection> {
35
+ let mut selected = Selection { count: 0, pages: Vec::new(), first, last, maximum };
36
+ if let Some(root) = self.trailer().get(b"Root") {
37
+ let root = self.resolve(root)?;
38
+ if let Some(tree) = root.as_dict().and_then(|root| root.get(b"Pages")) {
39
+ self.walk(tree, &Inherited::default(), 0, &mut HashSet::new(), &mut selected)?;
40
+ }
41
+ }
42
+ if selected.count == 0 {
43
+ let mut numbers: Vec<u32> = self.xref.entries.keys().copied().collect();
44
+ numbers.sort_unstable();
45
+ for number in numbers {
46
+ self.budget.check()?;
47
+ let object = self.get(number)?;
48
+ if let Some(dict) = object.as_dict()
49
+ && dict.has_name(b"Type", b"Page")
50
+ {
51
+ selected.add(|| self.page(dict, &Inherited::default()))?;
52
+ }
53
+ }
54
+ }
55
+ if selected.count == 0 {
56
+ return Err(malformed("the document has no pages"));
57
+ }
58
+ Ok(selected)
59
+ }
60
+
61
+ fn walk(
62
+ &self,
63
+ node: &Object,
64
+ inherited: &Inherited,
65
+ depth: usize,
66
+ visited: &mut HashSet<u32>,
67
+ selected: &mut Selection,
68
+ ) -> Result<()> {
69
+ self.budget.check()?;
70
+ self.budget.nest(depth)?;
71
+ if let Some((number, _)) = node.as_reference()
72
+ && !visited.insert(number)
73
+ {
74
+ return Ok(());
75
+ }
76
+ let node = self.resolve(node)?;
77
+ let Some(dict) = node.as_dict() else { return Ok(()) };
78
+ if dict.has_name(b"Type", b"Page") {
79
+ return selected.add(|| self.page(dict, inherited));
80
+ }
81
+ let inherited = self.inherit(dict, inherited)?;
82
+ if let Some(Object::Array(kids)) = self.entry(dict, b"Kids")?.as_deref() {
83
+ for kid in kids {
84
+ self.walk(kid, &inherited, depth + 1, visited, selected)?;
85
+ }
86
+ }
87
+ Ok(())
88
+ }
89
+
90
+ fn inherit(&self, dict: &Dict, inherited: &Inherited) -> Result<Inherited> {
91
+ let resources = match dict.get(b"Resources") {
92
+ Some(object) => Some(self.resolve(object)?.into_shared()),
93
+ None => inherited.resources.clone(),
94
+ };
95
+ let media_box = match self.entry(dict, b"MediaBox")?.as_deref().and_then(Object::as_array) {
96
+ Some(corners) if corners.len() == 4 => {
97
+ let mut media_box = [0.0; 4];
98
+ for (slot, corner) in media_box.iter_mut().zip(corners) {
99
+ *slot = self.resolve(corner)?.as_number().unwrap_or(0.0);
100
+ }
101
+ media_box
102
+ }
103
+ _ => inherited.media_box,
104
+ };
105
+ let rotate =
106
+ self.entry(dict, b"Rotate")?.and_then(|it| it.as_integer()).unwrap_or(inherited.rotate);
107
+ Ok(Inherited { resources, media_box, rotate })
108
+ }
109
+
110
+ fn page(&self, dict: &Dict, inherited: &Inherited) -> Result<Page> {
111
+ let inherited = self.inherit(dict, inherited)?;
112
+ let contents = match dict.get(b"Contents") {
113
+ Some(reference @ Object::Ref(..)) => match &*self.resolve(reference)? {
114
+ Object::Array(parts) => parts.clone(),
115
+ _ => vec![reference.clone()],
116
+ },
117
+ Some(Object::Array(parts)) => parts.clone(),
118
+ Some(other) => vec![other.clone()],
119
+ None => Vec::new(),
120
+ };
121
+ Ok(Page {
122
+ resources: inherited.resources,
123
+ contents,
124
+ media_box: inherited.media_box,
125
+ rotate: inherited.rotate,
126
+ })
127
+ }
128
+ }
129
+
130
+ struct Inherited {
131
+ resources: Option<Rc<Object>>,
132
+ media_box: [f64; 4],
133
+ rotate: i64,
134
+ }
135
+
136
+ impl Default for Inherited {
137
+ fn default() -> Self {
138
+ Self { resources: None, media_box: [0.0, 0.0, 612.0, 792.0], rotate: 0 }
139
+ }
140
+ }