imprimatur 0.1.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 (98) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/Cargo.lock +835 -0
  4. data/Cargo.toml +44 -0
  5. data/LICENSE.txt +29 -0
  6. data/README.md +314 -0
  7. data/ext/imprimatur_native/cli/Cargo.toml +30 -0
  8. data/ext/imprimatur_native/cli/src/app.rs +183 -0
  9. data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
  10. data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
  11. data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
  12. data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
  13. data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
  14. data/ext/imprimatur_native/cli/src/cli.rs +71 -0
  15. data/ext/imprimatur_native/cli/src/main.rs +15 -0
  16. data/ext/imprimatur_native/core/Cargo.toml +33 -0
  17. data/ext/imprimatur_native/core/LICENSE +29 -0
  18. data/ext/imprimatur_native/core/README.md +105 -0
  19. data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
  20. data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
  21. data/ext/imprimatur_native/core/src/doc.rs +210 -0
  22. data/ext/imprimatur_native/core/src/docx.rs +343 -0
  23. data/ext/imprimatur_native/core/src/error.rs +113 -0
  24. data/ext/imprimatur_native/core/src/lib.rs +38 -0
  25. data/ext/imprimatur_native/core/src/opc.rs +51 -0
  26. data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
  27. data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
  28. data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
  29. data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
  30. data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
  31. data/ext/imprimatur_native/core/src/pdf.rs +84 -0
  32. data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
  33. data/ext/imprimatur_native/core/src/read/content.rs +266 -0
  34. data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
  35. data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
  36. data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
  37. data/ext/imprimatur_native/core/src/read/image.rs +233 -0
  38. data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
  39. data/ext/imprimatur_native/core/src/read/json.rs +172 -0
  40. data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
  41. data/ext/imprimatur_native/core/src/read/package.rs +191 -0
  42. data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
  43. data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
  44. data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
  45. data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
  46. data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
  47. data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
  48. data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
  49. data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
  50. data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
  51. data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
  52. data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
  53. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
  54. data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
  55. data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
  56. data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
  57. data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
  58. data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
  59. data/ext/imprimatur_native/core/src/read/span.rs +22 -0
  60. data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
  61. data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
  62. data/ext/imprimatur_native/core/src/read.rs +145 -0
  63. data/ext/imprimatur_native/core/src/style.rs +82 -0
  64. data/ext/imprimatur_native/core/src/test_support.rs +163 -0
  65. data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
  66. data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
  67. data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
  68. data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
  69. data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
  70. data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
  71. data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
  72. data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
  73. data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
  74. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
  75. data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
  76. data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
  77. data/ext/imprimatur_native/core/src/xml.rs +95 -0
  78. data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
  79. data/ext/imprimatur_native/core/src/zip.rs +161 -0
  80. data/ext/imprimatur_native/extconf.rb +45 -0
  81. data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
  82. data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
  83. data/ext/imprimatur_native/ffi/src/document.rs +138 -0
  84. data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
  85. data/ext/imprimatur_native/ffi/src/read.rs +182 -0
  86. data/ext/imprimatur_native/ffi/src/style.rs +85 -0
  87. data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
  88. data/lib/imprimatur/document.rb +127 -0
  89. data/lib/imprimatur/error.rb +22 -0
  90. data/lib/imprimatur/formula.rb +13 -0
  91. data/lib/imprimatur/native.rb +17 -0
  92. data/lib/imprimatur/reader.rb +59 -0
  93. data/lib/imprimatur/style.rb +31 -0
  94. data/lib/imprimatur/validation.rb +35 -0
  95. data/lib/imprimatur/version.rb +5 -0
  96. data/lib/imprimatur/xlsx.rb +183 -0
  97. data/lib/imprimatur.rb +111 -0
  98. metadata +157 -0
@@ -0,0 +1,439 @@
1
+ //! Runs a page's content streams (ISO 32000-1, 8 and 9.4) far enough to
2
+ //! know where each glyph lands: graphics and text state, text operators,
3
+ //! and form `XObjects`. Images are counted, or collected to be extracted;
4
+ //! paths and colours are skipped.
5
+
6
+ mod geometry;
7
+ mod operands;
8
+ #[cfg(test)]
9
+ mod tests;
10
+
11
+ use std::collections::HashMap;
12
+ use std::ops::Range;
13
+ use std::rc::Rc;
14
+
15
+ use super::document::{Document, Page};
16
+ use super::font::Font;
17
+ use super::glyphs::push_clean;
18
+ use super::lexer::{Lexer, Token, hex_into, literal_into, name_bytes};
19
+ use super::object::{Dict, Object};
20
+ use crate::Result;
21
+ use crate::read::span;
22
+ use geometry::{Frame, IDENTITY, Matrix, multiply, quarter_turns};
23
+ use operands::{Operand, inline_image, operand};
24
+
25
+ /// A glyph where it shows: its origin and advance along the baseline, in
26
+ /// the page's displayed frame (rotation applied), and its text.
27
+ #[derive(Debug, Clone)]
28
+ pub struct Glyph {
29
+ pub x: f64,
30
+ pub y: f64,
31
+ pub width: f64,
32
+ pub size: f64,
33
+ /// Quarter turns of the baseline from left-to-right: 0 is upright.
34
+ pub turns: u8,
35
+ pub bold: bool,
36
+ pub text: Range<u32>,
37
+ }
38
+
39
+ /// An image a page draws: an image `XObject` (its object number, when it
40
+ /// has one) or an inline image, and the transformation it is drawn with.
41
+ pub struct Drawn {
42
+ pub image: Drawing,
43
+ pub ctm: Matrix,
44
+ }
45
+
46
+ pub enum Drawing {
47
+ Object { number: Option<u32>, object: Rc<Object> },
48
+ Inline { dict: Dict, data: Vec<u8> },
49
+ }
50
+
51
+ /// A page's glyphs, their text in one buffer, and how many images it draws.
52
+ #[derive(Debug, Default)]
53
+ pub struct PageText {
54
+ pub text: String,
55
+ pub glyphs: Vec<Glyph>,
56
+ pub images: usize,
57
+ /// The displayed page: width and height.
58
+ 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
+ };
105
+ }
106
+
107
+ /// Fonts by the object number of their dictionary, shared across pages.
108
+ pub type Fonts = HashMap<u32, Rc<Font>>;
109
+
110
+ struct Run<'d, 'p> {
111
+ document: &'d Document<'p>,
112
+ fonts: &'d mut Fonts,
113
+ page: &'d mut PageText,
114
+ rotation: Matrix,
115
+ bounds: [f64; 4],
116
+ forms: Vec<u32>,
117
+ scratch: String,
118
+ /// A string operand's decoded bytes.
119
+ bytes: Vec<u8>,
120
+ /// Where the images go, when reading them rather than text.
121
+ drawn: Option<&'d mut Vec<Drawn>>,
122
+ }
123
+
124
+ /// The page's glyphs, into `out` (cleared first: its buffers are reused
125
+ /// from page to page).
126
+ pub fn page_text(
127
+ document: &Document<'_>,
128
+ page: &Page,
129
+ fonts: &mut Fonts,
130
+ out: &mut PageText,
131
+ ) -> Result<()> {
132
+ run(document, page, fonts, out, None).map(|_| ())
133
+ }
134
+
135
+ /// The images the page draws, into `out`; and how the page is shown.
136
+ pub fn page_images(document: &Document<'_>, page: &Page, out: &mut Vec<Drawn>) -> Result<Frame> {
137
+ let mut text = PageText::default();
138
+ run(document, page, &mut Fonts::new(), &mut text, Some(out))
139
+ }
140
+
141
+ fn run(
142
+ document: &Document<'_>,
143
+ page: &Page,
144
+ fonts: &mut Fonts,
145
+ out: &mut PageText,
146
+ drawn: Option<&mut Vec<Drawn>>,
147
+ ) -> Result<Frame> {
148
+ let frame = Frame::of(page);
149
+ out.size = frame.size;
150
+ out.text.clear();
151
+ out.glyphs.clear();
152
+ out.images = 0;
153
+ let content = match page.contents.as_slice() {
154
+ [single] => document.stream_data(single)?.unwrap_or_default(),
155
+ parts => {
156
+ let mut content = Vec::new();
157
+ for part in parts {
158
+ if let Some(data) = document.stream_data(part)? {
159
+ content.extend_from_slice(&data);
160
+ content.push(b'\n');
161
+ }
162
+ }
163
+ content
164
+ }
165
+ };
166
+ let empty = Dict::default();
167
+ let resources = page.resources.as_deref().and_then(Object::as_dict).unwrap_or(&empty);
168
+ let mut run = Run {
169
+ document,
170
+ fonts,
171
+ page: out,
172
+ rotation: frame.rotation,
173
+ bounds: frame.bounds,
174
+ forms: Vec::new(),
175
+ scratch: String::new(),
176
+ bytes: Vec::new(),
177
+ drawn,
178
+ };
179
+ run.stream(&content, resources, State::START, 0)?;
180
+ Ok(frame)
181
+ }
182
+
183
+ impl Run<'_, '_> {
184
+ fn stream(
185
+ &mut self,
186
+ content: &[u8],
187
+ resources: &Dict,
188
+ mut state: State,
189
+ depth: usize,
190
+ ) -> Result<()> {
191
+ let budget = self.document.budget();
192
+ budget.nest(depth)?;
193
+ let mut stack: Vec<State> = Vec::new();
194
+ let mut cursor = Cursor::START;
195
+ let (mut operands, mut items): (Vec<Operand<'_>>, Vec<Operand<'_>>) =
196
+ (Vec::new(), Vec::new());
197
+ let mut lexer = Lexer::new(content, 0);
198
+ while let Some(token) = lexer.next() {
199
+ budget.tick()?;
200
+ let Token::Keyword(operator) = token else {
201
+ let operand = operand(token, &mut lexer, &mut items);
202
+ if operands.len() < 64 {
203
+ operands.push(operand);
204
+ }
205
+ continue;
206
+ };
207
+ let number = |index: usize| match operands.get(index) {
208
+ Some(&Operand::Number(number)) => number,
209
+ _ => 0.0,
210
+ };
211
+ let name = |index: usize| match operands.get(index) {
212
+ Some(&Operand::Name(raw)) => Some(name_bytes(raw)),
213
+ _ => None,
214
+ };
215
+ match operator {
216
+ b"q" => {
217
+ if stack.len() < 256 {
218
+ stack.push(state.clone());
219
+ }
220
+ }
221
+ b"Q" => {
222
+ if let Some(saved) = stack.pop() {
223
+ state = saved;
224
+ }
225
+ }
226
+ b"cm" if operands.len() == 6 => {
227
+ state.ctm = multiply(&std::array::from_fn(number), &state.ctm);
228
+ }
229
+ b"BT" => cursor = Cursor::START,
230
+ b"Tf" => {
231
+ state.size = number(1);
232
+ state.font = match name(0) {
233
+ // Reading images, fonts are no use.
234
+ Some(name) if self.drawn.is_none() => self.font(resources, &name)?,
235
+ _ => None,
236
+ };
237
+ }
238
+ b"Tc" => state.char_spacing = number(0),
239
+ b"Tw" => state.word_spacing = number(0),
240
+ b"Tz" => state.scale = number(0) / 100.0,
241
+ b"TL" => state.leading = number(0),
242
+ b"Ts" => state.rise = number(0),
243
+ b"Td" | b"TD" => {
244
+ if operator == b"TD" {
245
+ state.leading = -number(1);
246
+ }
247
+ cursor.next_line(number(0), number(1));
248
+ }
249
+ b"Tm" if operands.len() == 6 => cursor = Cursor::at(std::array::from_fn(number)),
250
+ b"T*" => cursor.next_line(0.0, -state.leading),
251
+ b"Tj" | b"'" | b"\"" | b"TJ" => {
252
+ if operator == b"\"" {
253
+ state.word_spacing = number(0);
254
+ state.char_spacing = number(1);
255
+ }
256
+ if operator == b"'" || operator == b"\"" {
257
+ cursor.next_line(0.0, -state.leading);
258
+ }
259
+ self.show_operand(operands.last(), &items, &state, &mut cursor.matrix);
260
+ }
261
+ b"Do" => {
262
+ if let Some(name) = name(0) {
263
+ self.draw(resources, &name, &state, depth)?;
264
+ }
265
+ }
266
+ b"BI" => {
267
+ self.page.images += 1;
268
+ let image = inline_image(&mut lexer, budget.max_depth());
269
+ if let (Some(drawn), Some((dict, data))) = (self.drawn.as_deref_mut(), image) {
270
+ let image = Drawing::Inline { dict, data: data.to_vec() };
271
+ drawn.push(Drawn { image, ctm: state.ctm });
272
+ }
273
+ }
274
+ _ => {}
275
+ }
276
+ operands.clear();
277
+ items.clear();
278
+ }
279
+ Ok(())
280
+ }
281
+
282
+ /// The operand of `Tj` and its kin: a string, or `TJ`'s array of
283
+ /// strings and adjustments in thousandths of the font size.
284
+ fn show_operand(
285
+ &mut self,
286
+ operand: Option<&Operand<'_>>,
287
+ items: &[Operand<'_>],
288
+ state: &State,
289
+ matrix: &mut Matrix,
290
+ ) {
291
+ let shown = match operand {
292
+ Some(&Operand::Array(start, end)) => items.get(start..end).unwrap_or_default(),
293
+ Some(single) => std::slice::from_ref(single),
294
+ None => &[],
295
+ };
296
+ let mut bytes = std::mem::take(&mut self.bytes);
297
+ for item in shown {
298
+ match *item {
299
+ Operand::String { raw, hex } => {
300
+ bytes.clear();
301
+ if hex {
302
+ hex_into(raw, &mut bytes);
303
+ } else {
304
+ literal_into(raw, &mut bytes);
305
+ }
306
+ self.show(&bytes, state, matrix);
307
+ }
308
+ Operand::Number(adjustment) => {
309
+ let shift = -adjustment / 1000.0 * state.size * state.scale;
310
+ matrix[4] = shift.mul_add(matrix[0], matrix[4]);
311
+ matrix[5] = shift.mul_add(matrix[1], matrix[5]);
312
+ }
313
+ _ => {}
314
+ }
315
+ }
316
+ self.bytes = bytes;
317
+ }
318
+
319
+ /// A font resource, loaded once per document.
320
+ fn font(&mut self, resources: &Dict, name: &[u8]) -> Result<Option<Rc<Font>>> {
321
+ let document = self.document;
322
+ let Some(fonts) = document.entry(resources, b"Font")? else { return Ok(None) };
323
+ let Some(entry) = fonts.as_dict().and_then(|fonts| fonts.get(name)) else {
324
+ return Ok(None);
325
+ };
326
+ if let Some((number, _)) = entry.as_reference()
327
+ && let Some(font) = self.fonts.get(&number)
328
+ {
329
+ return Ok(Some(Rc::clone(font)));
330
+ }
331
+ let resolved = document.resolve(entry)?;
332
+ let Some(dict) = resolved.as_dict() else { return Ok(None) };
333
+ let font = Rc::new(Font::load(document, dict)?);
334
+ if let Some((number, _)) = entry.as_reference() {
335
+ self.fonts.insert(number, Rc::clone(&font));
336
+ }
337
+ Ok(Some(font))
338
+ }
339
+
340
+ /// 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 };
343
+ let device = multiply(matrix, &state.ctm);
344
+ let shown = multiply(&device, &self.rotation);
345
+ let along = shown[0].hypot(shown[1]);
346
+ let size = state.size * shown[2].hypot(shown[3]);
347
+ let turns = quarter_turns(shown[0], shown[1]);
348
+ let rise = (state.rise * shown[2], state.rise * shown[3]);
349
+ let mut advance = 0.0;
350
+ let mut scratch = std::mem::take(&mut self.scratch);
351
+ let bounds = self.bounds;
352
+ font.glyphs(bytes, &mut scratch, |text, width, space| {
353
+ let x = f64::mul_add(advance, shown[0], shown[4]) + rise.0;
354
+ let y = f64::mul_add(advance, shown[1], shown[5]) + rise.1;
355
+ let user = (
356
+ f64::mul_add(advance, device[0], device[4]),
357
+ f64::mul_add(advance, device[1], device[5]),
358
+ );
359
+ let visible = user.0 >= bounds[0] - 1.0
360
+ && user.0 <= bounds[2] + 1.0
361
+ && user.1 >= bounds[1] - 1.0
362
+ && user.1 <= bounds[3] + 1.0;
363
+ if visible && !text.is_empty() {
364
+ let span = span::append(&mut self.page.text, |page| push_clean(page, text));
365
+ if !span.is_empty() {
366
+ let width = width * state.size * state.scale * along;
367
+ self.page.glyphs.push(Glyph {
368
+ x,
369
+ y,
370
+ width,
371
+ size,
372
+ turns,
373
+ bold: font.bold,
374
+ text: span,
375
+ });
376
+ }
377
+ }
378
+ advance = f64::mul_add(
379
+ f64::mul_add(width, state.size, state.char_spacing)
380
+ + if space { state.word_spacing } else { 0.0 },
381
+ state.scale,
382
+ advance,
383
+ );
384
+ });
385
+ self.scratch = scratch;
386
+ matrix[4] = f64::mul_add(advance, matrix[0], matrix[4]);
387
+ matrix[5] = f64::mul_add(advance, matrix[1], matrix[5]);
388
+ }
389
+
390
+ /// `Do`: a form `XObject`'s content, in its own matrix and resources;
391
+ /// images are counted.
392
+ fn draw(&mut self, resources: &Dict, name: &[u8], state: &State, depth: usize) -> Result<()> {
393
+ let document = self.document;
394
+ let Some(objects) = document.entry(resources, b"XObject")? else { return Ok(()) };
395
+ let Some(entry) = objects.as_dict().and_then(|objects| objects.get(name)).cloned() else {
396
+ return Ok(());
397
+ };
398
+ let number = entry.as_reference().map(|(number, _)| number);
399
+ if number.is_some_and(|number| self.forms.contains(&number)) {
400
+ return Ok(());
401
+ }
402
+ let resolved = document.resolve(&entry)?;
403
+ let Object::Stream(stream) = &*resolved else { return Ok(()) };
404
+ if stream.dict.has_name(b"Subtype", b"Image") {
405
+ self.page.images += 1;
406
+ if let Some(drawn) = self.drawn.as_deref_mut() {
407
+ let object = Rc::clone(match &resolved {
408
+ super::document::Resolved::Shared(object) => object,
409
+ super::document::Resolved::Borrowed(_) => return Ok(()),
410
+ });
411
+ drawn.push(Drawn { image: Drawing::Object { number, object }, ctm: state.ctm });
412
+ }
413
+ return Ok(());
414
+ }
415
+ if !stream.dict.has_name(b"Subtype", b"Form") {
416
+ return Ok(());
417
+ }
418
+ let content = document.decode(stream, number)?;
419
+ let own = document.entry(&stream.dict, b"Resources")?;
420
+ let form_resources = own.as_deref().and_then(Object::as_dict).unwrap_or(resources);
421
+ let matrix =
422
+ match document.entry(&stream.dict, b"Matrix")?.as_deref().and_then(Object::as_array) {
423
+ Some(values) if values.len() == 6 => {
424
+ std::array::from_fn(|index| values[index].as_number().unwrap_or(0.0))
425
+ }
426
+ _ => IDENTITY,
427
+ };
428
+ let mut inner = state.clone();
429
+ inner.ctm = multiply(&matrix, &state.ctm);
430
+ if let Some(number) = number {
431
+ self.forms.push(number);
432
+ }
433
+ let result = self.stream(&content, form_resources, inner, depth + 1);
434
+ if number.is_some() {
435
+ self.forms.pop();
436
+ }
437
+ result
438
+ }
439
+ }
@@ -0,0 +1,256 @@
1
+ //! The standard security handler (ISO 32000-1, 7.6.3; ISO 32000-2, 7.6.4).
2
+ //! Most encrypted PDFs, certificates among them, only restrict printing or
3
+ //! copying: their user password is empty, and opening them takes no secret.
4
+ //! That is the only password tried; anything else is
5
+ //! [`Error::PasswordRequired`].
6
+
7
+ #[cfg(test)]
8
+ mod tests;
9
+
10
+ use aes::cipher::{Array, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
11
+ use aes::{Aes128, Aes256};
12
+ use md5::{Digest, Md5};
13
+ use sha2::{Sha256, Sha384, Sha512};
14
+
15
+ use super::object::{Dict, Object};
16
+ use crate::{Error, Result};
17
+
18
+ /// The 32 bytes that pad a password (Algorithm 2, step a).
19
+ const PAD: [u8; 32] = [
20
+ 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
21
+ 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
22
+ ];
23
+
24
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
25
+ enum Method {
26
+ Identity,
27
+ Rc4,
28
+ Aes128,
29
+ Aes256,
30
+ }
31
+
32
+ #[derive(Debug)]
33
+ pub struct Crypt {
34
+ key: Vec<u8>,
35
+ strings: Method,
36
+ streams: Method,
37
+ }
38
+
39
+ impl Crypt {
40
+ /// Opens the handler with the empty user password.
41
+ ///
42
+ /// # Errors
43
+ /// [`Error::PasswordRequired`] when the user password is not empty, and
44
+ /// [`Error::UnsupportedDocument`] for other security handlers.
45
+ pub fn new(encrypt: &Dict, id: &[u8]) -> Result<Self> {
46
+ if !encrypt.has_name(b"Filter", b"Standard") {
47
+ return Err(Error::UnsupportedDocument(
48
+ "a PDF security handler other than Standard".to_owned(),
49
+ ));
50
+ }
51
+ let number = |key: &[u8]| encrypt.get(key).and_then(Object::as_integer);
52
+ let bytes = |key: &[u8]| encrypt.get(key).and_then(Object::as_bytes).unwrap_or_default();
53
+ let (version, revision) = (number(b"V").unwrap_or(0), number(b"R").unwrap_or(2));
54
+ let (owner, user) = (bytes(b"O"), bytes(b"U"));
55
+ if version >= 5 {
56
+ let key = aes256_key(revision, user, bytes(b"UE"))?;
57
+ let method = |name: &[u8]| filter_method(encrypt, name, Method::Aes256);
58
+ return Ok(Self { key, strings: method(b"StrF"), streams: method(b"StmF") });
59
+ }
60
+ let (strings, streams, length) = if version == 4 {
61
+ let method = |name: &[u8]| filter_method(encrypt, name, Method::Rc4);
62
+ (method(b"StrF"), method(b"StmF"), 16)
63
+ } else {
64
+ let bits = if version == 1 { 40 } else { number(b"Length").unwrap_or(40) };
65
+ (Method::Rc4, Method::Rc4, usize::try_from(bits / 8).unwrap_or(5).clamp(5, 16))
66
+ };
67
+ #[expect(clippy::cast_possible_truncation, reason = "P is a 32-bit signed field")]
68
+ let permissions = number(b"P").unwrap_or(0) as i32;
69
+ let metadata = encrypt.get(b"EncryptMetadata") != Some(&Object::Bool(false));
70
+ let mut hash = Md5::new();
71
+ hash.update(PAD);
72
+ hash.update(&owner[..owner.len().min(32)]);
73
+ hash.update(permissions.to_le_bytes());
74
+ hash.update(id);
75
+ if revision >= 4 && !metadata {
76
+ hash.update([0xFF; 4]);
77
+ }
78
+ let mut key = hash.finalize().to_vec();
79
+ if revision >= 3 {
80
+ for _ in 0..50 {
81
+ key = Md5::digest(&key[..length]).to_vec();
82
+ }
83
+ }
84
+ key.truncate(length);
85
+ let valid = if revision == 2 {
86
+ rc4(&key, &PAD) == user
87
+ } else {
88
+ let mut hash = Md5::new();
89
+ hash.update(PAD);
90
+ hash.update(id);
91
+ let mut check = rc4(&key, &hash.finalize());
92
+ for round in 1..=19_u8 {
93
+ let round_key: Vec<u8> = key.iter().map(|byte| byte ^ round).collect();
94
+ check = rc4(&round_key, &check);
95
+ }
96
+ user.get(..16) == Some(&check[..])
97
+ };
98
+ if !valid {
99
+ return Err(Error::PasswordRequired);
100
+ }
101
+ Ok(Self { key, strings, streams })
102
+ }
103
+
104
+ /// Decrypts a string or stream of object `number`, generation
105
+ /// `generation`.
106
+ pub fn decrypt(&self, stream: bool, (number, generation): (u32, u16), data: &[u8]) -> Vec<u8> {
107
+ let method = if stream { self.streams } else { self.strings };
108
+ match method {
109
+ Method::Identity => data.to_vec(),
110
+ Method::Aes256 => aes_cbc::<Aes256>(&self.key, data),
111
+ Method::Rc4 | Method::Aes128 => {
112
+ let mut hash = Md5::new();
113
+ hash.update(&self.key);
114
+ hash.update(&number.to_le_bytes()[..3]);
115
+ hash.update(generation.to_le_bytes());
116
+ if method == Method::Aes128 {
117
+ hash.update(b"sAlT");
118
+ }
119
+ let digest = hash.finalize();
120
+ let key = &digest[..(self.key.len() + 5).min(16)];
121
+ if method == Method::Rc4 { rc4(key, data) } else { aes_cbc::<Aes128>(key, data) }
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ /// A crypt filter's method (`/StmF`, `/StrF` naming an entry of `/CF`).
128
+ fn filter_method(encrypt: &Dict, key: &[u8], default: Method) -> Method {
129
+ let Some(name) = encrypt.get(key).and_then(Object::as_name) else {
130
+ return Method::Identity;
131
+ };
132
+ if name == b"Identity" {
133
+ return Method::Identity;
134
+ }
135
+ let filters = encrypt.get(b"CF").and_then(Object::as_dict);
136
+ let method = filters
137
+ .and_then(|filters| filters.get(name))
138
+ .and_then(Object::as_dict)
139
+ .and_then(|filter| filter.get(b"CFM"))
140
+ .and_then(Object::as_name);
141
+ match method {
142
+ Some(b"None") => Method::Identity,
143
+ Some(b"V2") => Method::Rc4,
144
+ Some(b"AESV2") => Method::Aes128,
145
+ Some(b"AESV3") => Method::Aes256,
146
+ _ => default,
147
+ }
148
+ }
149
+
150
+ /// The file key of revisions 5 and 6 (AES-256): check the empty password
151
+ /// against `U`, then unwrap `UE` with the key it derives.
152
+ fn aes256_key(revision: i64, user: &[u8], wrapped: &[u8]) -> Result<Vec<u8>> {
153
+ let (Some(check), Some(validation_salt), Some(key_salt)) =
154
+ (user.get(..32), user.get(32..40), user.get(40..48))
155
+ else {
156
+ return Err(Error::PasswordRequired);
157
+ };
158
+ let hash = |salt: &[u8]| {
159
+ if revision >= 6 { hardened_hash(salt) } else { Sha256::digest(salt).to_vec() }
160
+ };
161
+ if hash(validation_salt).get(..32) != Some(check) || wrapped.len() < 32 {
162
+ return Err(Error::PasswordRequired);
163
+ }
164
+ let cipher =
165
+ Aes256::new_from_slice(&hash(key_salt)[..32]).map_err(|_| Error::PasswordRequired)?;
166
+ Ok(cbc(&cipher, [0; 16], &wrapped[..32]))
167
+ }
168
+
169
+ /// CBC decryption of whole blocks; a torn last block is dropped.
170
+ fn cbc<C>(cipher: &C, iv: [u8; 16], data: &[u8]) -> Vec<u8>
171
+ where
172
+ C: BlockCipherDecrypt + aes::cipher::BlockSizeUser<BlockSize = aes::cipher::consts::U16>,
173
+ {
174
+ let mut out = Vec::with_capacity(data.len());
175
+ let mut previous = iv;
176
+ for chunk in data.as_chunks::<16>().0 {
177
+ let mut block = Array::from(*chunk);
178
+ cipher.decrypt_block(&mut block);
179
+ out.extend(block.iter().zip(previous).map(|(byte, iv)| byte ^ iv));
180
+ previous = *chunk;
181
+ }
182
+ out
183
+ }
184
+
185
+ /// Algorithm 2.B (ISO 32000-2, 7.6.4.3.4) for the empty password and no
186
+ /// user data: rounds of AES-128-CBC and SHA-2 until the last byte allows.
187
+ fn hardened_hash(salt: &[u8]) -> Vec<u8> {
188
+ let mut key = Sha256::digest(salt).to_vec();
189
+ let mut round = 0_u32;
190
+ loop {
191
+ let repeated: Vec<u8> = key.iter().copied().cycle().take(key.len() * 64).collect();
192
+ let Ok(cipher) = Aes128::new_from_slice(&key[..16]) else { return key };
193
+ let mut encrypted = Vec::with_capacity(repeated.len());
194
+ let mut previous: [u8; 16] = key[16..32].try_into().unwrap_or_default();
195
+ for chunk in repeated.as_chunks::<16>().0 {
196
+ let mut block =
197
+ Array::from(std::array::from_fn::<u8, 16, _>(|at| chunk[at] ^ previous[at]));
198
+ cipher.encrypt_block(&mut block);
199
+ previous.copy_from_slice(&block);
200
+ encrypted.extend_from_slice(&block);
201
+ }
202
+ let remainder = encrypted[..16].iter().map(|&byte| u32::from(byte)).sum::<u32>() % 3;
203
+ key = match remainder {
204
+ 0 => Sha256::digest(&encrypted).to_vec(),
205
+ 1 => Sha384::digest(&encrypted).to_vec(),
206
+ _ => Sha512::digest(&encrypted).to_vec(),
207
+ };
208
+ round += 1;
209
+ if round >= 64 && encrypted.last().is_some_and(|&last| u32::from(last) + 32 <= round) {
210
+ key.truncate(32);
211
+ return key;
212
+ }
213
+ }
214
+ }
215
+
216
+ /// AES-CBC with the IV in the first block and PKCS #7 padding, as PDF
217
+ /// writes it. A torn last block is dropped rather than failing the read.
218
+ fn aes_cbc<C>(key: &[u8], data: &[u8]) -> Vec<u8>
219
+ where
220
+ C: BlockCipherDecrypt
221
+ + KeyInit
222
+ + aes::cipher::BlockSizeUser<BlockSize = aes::cipher::consts::U16>,
223
+ {
224
+ let (Some((iv, data)), Ok(cipher)) = (data.split_first_chunk::<16>(), C::new_from_slice(key))
225
+ else {
226
+ return Vec::new();
227
+ };
228
+ let mut out = cbc(&cipher, *iv, data);
229
+ if let Some(&padding) = out.last()
230
+ && (1..=16).contains(&padding)
231
+ && out.len() >= usize::from(padding)
232
+ && out[out.len() - usize::from(padding)..].iter().all(|&byte| byte == padding)
233
+ {
234
+ out.truncate(out.len() - usize::from(padding));
235
+ }
236
+ out
237
+ }
238
+
239
+ fn rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
240
+ #[expect(clippy::cast_possible_truncation, reason = "indices of a 256-entry table")]
241
+ let mut state: [u8; 256] = std::array::from_fn(|index| index as u8);
242
+ let mut j = 0_u8;
243
+ for i in 0..256 {
244
+ j = j.wrapping_add(state[i]).wrapping_add(key[i % key.len().max(1)]);
245
+ state.swap(i, usize::from(j));
246
+ }
247
+ let (mut i, mut j) = (0_u8, 0_u8);
248
+ data.iter()
249
+ .map(|&byte| {
250
+ i = i.wrapping_add(1);
251
+ j = j.wrapping_add(state[usize::from(i)]);
252
+ state.swap(usize::from(i), usize::from(j));
253
+ byte ^ state[usize::from(state[usize::from(i)].wrapping_add(state[usize::from(j)]))]
254
+ })
255
+ .collect()
256
+ }