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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/Cargo.lock +472 -5
- data/Cargo.toml +16 -1
- data/README.md +59 -7
- data/ext/imprimatur_native/cli/Cargo.toml +1 -1
- data/ext/imprimatur_native/cli/src/app.rs +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +29 -13
- data/ext/imprimatur_native/core/README.md +13 -1
- data/ext/imprimatur_native/core/src/bin/imprimatur-render-worker.rs +10 -0
- data/ext/imprimatur_native/core/src/bytes.rs +16 -0
- data/ext/imprimatur_native/core/src/doc/picture.rs +3 -7
- data/ext/imprimatur_native/core/src/doc/size.rs +14 -0
- data/ext/imprimatur_native/core/src/doc/symbol.rs +3 -5
- data/ext/imprimatur_native/core/src/doc.rs +24 -0
- data/ext/imprimatur_native/core/src/docx.rs +6 -5
- data/ext/imprimatur_native/core/src/error.rs +6 -0
- data/ext/imprimatur_native/core/src/font.rs +12 -0
- data/ext/imprimatur_native/core/src/lib.rs +6 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +13 -5
- data/ext/imprimatur_native/core/src/pdf/objects.rs +1 -1
- data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +1 -0
- data/ext/imprimatur_native/core/src/read/budget.rs +10 -5
- data/ext/imprimatur_native/core/src/read/content.rs +16 -0
- data/ext/imprimatur_native/core/src/read/image/dimensions.rs +2 -2
- data/ext/imprimatur_native/core/src/read/ole.rs +1 -8
- data/ext/imprimatur_native/core/src/read/pdf/bounded.rs +190 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/state.rs +50 -0
- data/ext/imprimatur_native/core/src/read/pdf/content.rs +43 -54
- data/ext/imprimatur_native/core/src/read/pdf/document/pages.rs +140 -0
- data/ext/imprimatur_native/core/src/read/pdf/document.rs +20 -110
- data/ext/imprimatur_native/core/src/read/pdf/filter.rs +25 -13
- data/ext/imprimatur_native/core/src/read/pdf/font.rs +4 -3
- data/ext/imprimatur_native/core/src/read/pdf/isolation/protocol.rs +155 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/system.rs +71 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/worker.rs +43 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation.rs +166 -0
- data/ext/imprimatur_native/core/src/read/pdf/render.rs +206 -0
- data/ext/imprimatur_native/core/src/read/pdf.rs +16 -8
- data/ext/imprimatur_native/core/src/read/xls/strings.rs +2 -1
- data/ext/imprimatur_native/core/src/read/xls.rs +1 -8
- data/ext/imprimatur_native/core/src/read.rs +6 -5
- data/ext/imprimatur_native/core/src/test_support/font.rs +37 -0
- data/ext/imprimatur_native/core/src/xlsx/validation.rs +28 -18
- data/ext/imprimatur_native/core/src/zip/reader.rs +1 -8
- data/ext/imprimatur_native/extconf.rb +18 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +2 -2
- data/ext/imprimatur_native/ffi/src/document.rs +6 -0
- data/ext/imprimatur_native/ffi/src/lib.rs +7 -0
- data/ext/imprimatur_native/ffi/src/pdf.rs +73 -0
- data/ext/imprimatur_native/ffi/src/read.rs +18 -12
- data/lib/imprimatur/document.rb +4 -2
- data/lib/imprimatur/native.rb +7 -0
- data/lib/imprimatur/pdf_reader.rb +27 -0
- data/lib/imprimatur/reader.rb +48 -11
- data/lib/imprimatur/spreadsheet.rb +1 -1
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur.rb +28 -3
- 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
|
-
|
|
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)
|
|
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 {
|
|
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
|
+
}
|