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,191 @@
1
+ //! The Open Packaging Conventions side of reading: content types, and the
2
+ //! relationships that lead from one part to the next.
3
+
4
+ use std::borrow::Cow;
5
+
6
+ use super::{Budget, Format};
7
+ use crate::xml::reader::{Event, XmlReader, into_text};
8
+ use crate::zip::reader::ZipReader;
9
+ use crate::{Error, Result};
10
+
11
+ const OFFICE_DOCUMENT: &str = "officeDocument";
12
+
13
+ /// A relationship from a part: its id, its type's last segment
14
+ /// (`worksheet`, `styles`, `hyperlink`...) and its target, resolved to a
15
+ /// part name unless it is external.
16
+ #[derive(Debug, Clone, PartialEq, Eq)]
17
+ pub struct Relationship {
18
+ pub id: String,
19
+ pub kind: String,
20
+ pub target: String,
21
+ pub external: bool,
22
+ }
23
+
24
+ pub struct Package<'a> {
25
+ zip: &'a ZipReader<'a>,
26
+ budget: &'a Budget<'a>,
27
+ main: String,
28
+ content_type: String,
29
+ }
30
+
31
+ impl<'a> Package<'a> {
32
+ /// Finds the main part through the package relationships.
33
+ pub fn open(zip: &'a ZipReader<'a>, budget: &'a Budget<'a>) -> Result<Self> {
34
+ let mut package = Self { zip, budget, main: String::new(), content_type: String::new() };
35
+ let main = package
36
+ .relationships("")?
37
+ .into_iter()
38
+ .find(|relationship| relationship.kind == OFFICE_DOCUMENT && !relationship.external)
39
+ .ok_or_else(|| {
40
+ Error::UnsupportedDocument("a zip that is not an Office document".to_owned())
41
+ })?;
42
+ package.content_type = package.content_type_of(&main.target)?;
43
+ package.main = main.target;
44
+ Ok(package)
45
+ }
46
+
47
+ pub fn main(&self) -> &str {
48
+ &self.main
49
+ }
50
+
51
+ /// What the main part's content type says the document is.
52
+ pub fn format(&self) -> Result<Format> {
53
+ let kind = &self.content_type;
54
+ let unsupported = || Error::UnsupportedDocument(format!("content type {kind}"));
55
+ if kind.contains("binary") {
56
+ return Err(unsupported());
57
+ }
58
+ if kind.contains("spreadsheetml") || kind.contains("ms-excel") {
59
+ Ok(Format::Xlsx)
60
+ } else if kind.contains("wordprocessingml") || kind.contains("ms-word") {
61
+ Ok(Format::Docx)
62
+ } else {
63
+ Err(unsupported())
64
+ }
65
+ }
66
+
67
+ /// The main part's text: the workbook, or the document.
68
+ pub fn main_text(&self) -> Result<String> {
69
+ self.text(&self.main)?.ok_or_else(|| {
70
+ let format = if self.format().ok() == Some(Format::Xlsx) { "XLSX" } else { "DOCX" };
71
+ Error::malformed(format, format!("the part {} is missing", self.main))
72
+ })
73
+ }
74
+
75
+ /// Parses the part `source` relates to as `kind`, or gives the default
76
+ /// when there is none: styles, shared strings, numbering.
77
+ pub fn parse_related<T: Default>(
78
+ &self,
79
+ relationships: &[Relationship],
80
+ kind: &str,
81
+ parse: impl FnOnce(&str) -> Result<T>,
82
+ ) -> Result<T> {
83
+ let Some(part) = relationships.iter().find(|r| r.kind == kind && !r.external) else {
84
+ return Ok(T::default());
85
+ };
86
+ self.text(&part.target)?.map_or_else(|| Ok(T::default()), |text| parse(&text))
87
+ }
88
+
89
+ /// The parts' names.
90
+ pub fn parts(&self) -> impl Iterator<Item = &'a str> + '_ {
91
+ self.zip.names()
92
+ }
93
+
94
+ /// A part's bytes, `None` when the package has no such part.
95
+ pub fn part(&self, name: &str) -> Result<Option<Vec<u8>>> {
96
+ self.zip.read(name, self.budget)
97
+ }
98
+
99
+ /// The relationships of `source` (`""` for the package's own), in order.
100
+ pub fn relationships(&self, source: &str) -> Result<Vec<Relationship>> {
101
+ let (folder, file) = source.rsplit_once('/').unwrap_or(("", source));
102
+ let rels = if folder.is_empty() {
103
+ format!("_rels/{file}.rels")
104
+ } else {
105
+ format!("{folder}/_rels/{file}.rels")
106
+ };
107
+ let Some(text) = self.text(&rels)? else {
108
+ return Ok(Vec::new());
109
+ };
110
+ let mut xml = XmlReader::new(&text, self.budget.max_depth());
111
+ let mut found = Vec::new();
112
+ while let Some(event) = xml.next()? {
113
+ let Event::Start(tag) = event else { continue };
114
+ if tag.name() != "Relationship" {
115
+ continue;
116
+ }
117
+ let (Some(id), Some(kind), Some(target)) =
118
+ (tag.attr("Id"), tag.attr("Type"), tag.attr("Target"))
119
+ else {
120
+ continue;
121
+ };
122
+ let external = tag.attr("TargetMode").is_some_and(|mode| mode == "External");
123
+ let kind = kind.rsplit('/').next().unwrap_or_default().to_owned();
124
+ let target = if external { target.into_owned() } else { resolve(folder, &target) };
125
+ found.push(Relationship { id: id.into_owned(), kind, target, external });
126
+ }
127
+ Ok(found)
128
+ }
129
+
130
+ /// The target of `source`'s first relationship of `kind`.
131
+ pub fn related(&self, source: &str, kind: &str) -> Result<Option<String>> {
132
+ Ok(self
133
+ .relationships(source)?
134
+ .into_iter()
135
+ .find(|relationship| relationship.kind == kind && !relationship.external)
136
+ .map(|relationship| relationship.target))
137
+ }
138
+
139
+ /// A part as text, `None` when missing.
140
+ pub fn text(&self, name: &str) -> Result<Option<String>> {
141
+ self.part(name)?.map(into_text).transpose()
142
+ }
143
+
144
+ fn content_type_of(&self, part: &str) -> Result<String> {
145
+ let text = self
146
+ .text("[Content_Types].xml")?
147
+ .ok_or_else(|| Error::UnsupportedDocument("a zip without content types".to_owned()))?;
148
+ let mut xml = XmlReader::new(&text, self.budget.max_depth());
149
+ let extension = part.rsplit_once('.').map_or("", |(_, extension)| extension);
150
+ let mut default = None;
151
+ while let Some(event) = xml.next()? {
152
+ let Event::Start(tag) = event else { continue };
153
+ match tag.name() {
154
+ "Override"
155
+ if tag.attr("PartName").is_some_and(|name| {
156
+ name.trim_start_matches('/').eq_ignore_ascii_case(part)
157
+ }) =>
158
+ {
159
+ return Ok(tag.attr("ContentType").unwrap_or_default().into_owned());
160
+ }
161
+ "Default"
162
+ if tag
163
+ .attr("Extension")
164
+ .is_some_and(|ext| ext.eq_ignore_ascii_case(extension)) =>
165
+ {
166
+ default = tag.attr("ContentType").map(Cow::into_owned);
167
+ }
168
+ _ => {}
169
+ }
170
+ }
171
+ Ok(default.unwrap_or_default())
172
+ }
173
+ }
174
+
175
+ /// A relationship target as a part name: relative to the source's folder,
176
+ /// or absolute with a leading `/`.
177
+ fn resolve(folder: &str, target: &str) -> String {
178
+ let (base, target) =
179
+ target.strip_prefix('/').map_or((folder, target), |absolute| ("", absolute));
180
+ let mut segments: Vec<&str> = base.split('/').filter(|segment| !segment.is_empty()).collect();
181
+ for segment in target.split('/') {
182
+ match segment {
183
+ "" | "." => {}
184
+ ".." => {
185
+ segments.pop();
186
+ }
187
+ segment => segments.push(segment),
188
+ }
189
+ }
190
+ segments.join("/")
191
+ }
@@ -0,0 +1,216 @@
1
+ //! `CMaps` (ISO 32000-1, 9.7.5 and 9.10.3): how many bytes a character code
2
+ //! takes, which CID it is, and the text it stands for.
3
+
4
+ #[cfg(test)]
5
+ mod tests;
6
+
7
+ use std::ops::Range;
8
+
9
+ use super::glyphs::named;
10
+ use super::lexer::{Lexer, Token, hex_bytes, name_bytes};
11
+ use crate::read::span;
12
+
13
+ /// Where a code range's text comes from.
14
+ #[derive(Debug, Clone)]
15
+ enum Target {
16
+ /// One text for one code, or the first of a range whose last
17
+ /// character counts up with the code.
18
+ Text(Range<u32>),
19
+ /// One text per code of the range.
20
+ List(Vec<Range<u32>>),
21
+ }
22
+
23
+ #[derive(Debug, Clone, Default)]
24
+ pub struct CMap {
25
+ /// Code space ranges: low, high and length in bytes.
26
+ spaces: Vec<(u32, u32, u8)>,
27
+ /// Code ranges to text, sorted by their low end.
28
+ unicode: Vec<(u32, u32, Target)>,
29
+ /// Code ranges to CIDs: low, high, first CID.
30
+ cids: Vec<(u32, u32, u32)>,
31
+ text: String,
32
+ }
33
+
34
+ impl CMap {
35
+ pub fn parse(data: &[u8]) -> Self {
36
+ let mut cmap = Self::default();
37
+ let mut lexer = Lexer::new(data, 0);
38
+ let mut operands: Vec<Token<'_>> = Vec::new();
39
+ while let Some(token) = lexer.next() {
40
+ let Token::Keyword(keyword) = token else {
41
+ if operands.len() < 1024 {
42
+ operands.push(token);
43
+ }
44
+ if token == Token::ArrayStart {
45
+ let items = array(&mut lexer);
46
+ operands.pop();
47
+ operands.push(Token::Hex(b""));
48
+ cmap.push_list(&operands, items);
49
+ }
50
+ continue;
51
+ };
52
+ match keyword {
53
+ b"endcodespacerange" => {
54
+ for pair in operands.as_chunks::<2>().0 {
55
+ if let (Some((low, length)), Some((high, _))) =
56
+ (code(&pair[0]), code(&pair[1]))
57
+ {
58
+ cmap.spaces.push((low, high, length));
59
+ }
60
+ }
61
+ }
62
+ b"endbfchar" => {
63
+ for pair in operands.as_chunks::<2>().0 {
64
+ if let Some((code, _)) = code(&pair[0]) {
65
+ let target = cmap.target(&pair[1]);
66
+ cmap.unicode.push((code, code, target));
67
+ }
68
+ }
69
+ }
70
+ b"endbfrange" => {
71
+ for triple in operands.as_chunks::<3>().0 {
72
+ if let (Some((low, _)), Some((high, _)), Token::Hex(raw)) =
73
+ (code(&triple[0]), code(&triple[1]), triple[2])
74
+ && !raw.is_empty()
75
+ {
76
+ let target = cmap.target(&triple[2]);
77
+ cmap.unicode.push((low, high, target));
78
+ }
79
+ }
80
+ }
81
+ b"endcidrange" => {
82
+ for triple in operands.as_chunks::<3>().0 {
83
+ if let (Some((low, _)), Some((high, _)), Token::Integer(cid)) =
84
+ (code(&triple[0]), code(&triple[1]), triple[2])
85
+ {
86
+ cmap.cids.push((low, high, u32::try_from(cid).unwrap_or(0)));
87
+ }
88
+ }
89
+ }
90
+ b"endcidchar" => {
91
+ for pair in operands.as_chunks::<2>().0 {
92
+ if let (Some((code, _)), Token::Integer(cid)) = (code(&pair[0]), pair[1]) {
93
+ cmap.cids.push((code, code, u32::try_from(cid).unwrap_or(0)));
94
+ }
95
+ }
96
+ }
97
+ _ => {}
98
+ }
99
+ operands.clear();
100
+ }
101
+ cmap.unicode.sort_by_key(|&(low, _, _)| low);
102
+ cmap.cids.sort_by_key(|&(low, _, _)| low);
103
+ cmap
104
+ }
105
+
106
+ /// A `bfrange` whose destination is an array, one text per code.
107
+ fn push_list(&mut self, operands: &[Token<'_>], items: Vec<&[u8]>) {
108
+ let [.., low, high, _] = operands else { return };
109
+ let (Some((low, _)), Some((high, _))) = (code(low), code(high)) else { return };
110
+ let list = items.into_iter().map(|raw| self.store(&utf16(&hex_bytes(raw)))).collect();
111
+ self.unicode.push((low, high, Target::List(list)));
112
+ }
113
+
114
+ fn target(&mut self, token: &Token<'_>) -> Target {
115
+ let text = match *token {
116
+ Token::Hex(raw) => utf16(&hex_bytes(raw)),
117
+ Token::Name(raw) => std::str::from_utf8(&name_bytes(raw))
118
+ .ok()
119
+ .and_then(named)
120
+ .map(std::borrow::Cow::into_owned)
121
+ .unwrap_or_default(),
122
+ _ => String::new(),
123
+ };
124
+ Target::Text(self.store(&text))
125
+ }
126
+
127
+ fn store(&mut self, text: &str) -> Range<u32> {
128
+ span::append(&mut self.text, |buffer| buffer.push_str(text))
129
+ }
130
+
131
+ pub const fn has_spaces(&self) -> bool {
132
+ !self.spaces.is_empty()
133
+ }
134
+
135
+ /// The length of the code starting `bytes`: the code space range it
136
+ /// falls in, or the shortest one when it falls in none.
137
+ pub fn code_length(&self, bytes: &[u8]) -> usize {
138
+ for &(low, high, length) in &self.spaces {
139
+ let length = usize::from(length);
140
+ if let Some(prefix) = bytes.get(..length)
141
+ && (low..=high).contains(&be(prefix))
142
+ {
143
+ return length;
144
+ }
145
+ }
146
+ self.spaces.iter().map(|&(_, _, length)| usize::from(length)).min().unwrap_or(1)
147
+ }
148
+
149
+ /// The text of `code`, pushed onto `out`; false when unmapped.
150
+ pub fn push_text(&self, code: u32, out: &mut String) -> bool {
151
+ let at = self.unicode.partition_point(|&(low, _, _)| low <= code);
152
+ for (low, high, target) in self.unicode[..at].iter().rev().take(8) {
153
+ if code > *high {
154
+ continue;
155
+ }
156
+ let offset = code - low;
157
+ match target {
158
+ Target::Text(span) => {
159
+ let text = span::slice(&self.text, span);
160
+ let mut chars = text.chars();
161
+ let Some(last) = chars.next_back() else { return false };
162
+ out.push_str(chars.as_str());
163
+ out.push(char::from_u32(u32::from(last) + offset).unwrap_or(last));
164
+ }
165
+ Target::List(list) => {
166
+ let Some(span) = list.get(offset as usize) else { return false };
167
+ out.push_str(span::slice(&self.text, span));
168
+ }
169
+ }
170
+ return true;
171
+ }
172
+ false
173
+ }
174
+
175
+ /// The CID of `code`, if the map says.
176
+ pub fn cid(&self, code: u32) -> Option<u32> {
177
+ let at = self.cids.partition_point(|&(low, _, _)| low <= code);
178
+ self.cids[..at]
179
+ .iter()
180
+ .rev()
181
+ .take(8)
182
+ .find(|&&(_, high, _)| code <= high)
183
+ .map(|&(low, _, cid)| cid + (code - low))
184
+ }
185
+ }
186
+
187
+ /// The hex strings of an array after its `[`.
188
+ fn array<'a>(lexer: &mut Lexer<'a>) -> Vec<&'a [u8]> {
189
+ let mut items = Vec::new();
190
+ while let Some(token) = lexer.next() {
191
+ match token {
192
+ Token::ArrayEnd => break,
193
+ Token::Hex(raw) => items.push(raw),
194
+ _ => {}
195
+ }
196
+ }
197
+ items
198
+ }
199
+
200
+ /// A hex string token as a code and its length in bytes (up to 4).
201
+ fn code(token: &Token<'_>) -> Option<(u32, u8)> {
202
+ let Token::Hex(raw) = *token else { return None };
203
+ let bytes = hex_bytes(raw);
204
+ let length = u8::try_from(bytes.len()).ok().filter(|&length| (1..=4).contains(&length))?;
205
+ Some((be(&bytes), length))
206
+ }
207
+
208
+ pub fn be(bytes: &[u8]) -> u32 {
209
+ bytes.iter().fold(0, |value, &byte| value << 8 | u32::from(byte))
210
+ }
211
+
212
+ /// UTF-16BE, as `ToUnicode` maps write text; lone surrogates are dropped.
213
+ pub fn utf16(bytes: &[u8]) -> String {
214
+ let units = bytes.as_chunks::<2>().0.iter().map(|pair| u16::from_be_bytes([pair[0], pair[1]]));
215
+ char::decode_utf16(units).filter_map(Result::ok).collect()
216
+ }
@@ -0,0 +1,79 @@
1
+ //! Where things land on a page: transforms, and the frame a page is shown
2
+ //! in once its rotation is applied.
3
+
4
+ use super::super::document::Page;
5
+ use crate::read::content::Bounds;
6
+
7
+ /// A 2D affine transform `[a b c d e f]`, as PDF writes them.
8
+ pub type Matrix = [f64; 6];
9
+ pub const IDENTITY: Matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
10
+
11
+ pub fn multiply(m: &Matrix, n: &Matrix) -> Matrix {
12
+ [
13
+ m[1].mul_add(n[2], m[0] * n[0]),
14
+ m[1].mul_add(n[3], m[0] * n[1]),
15
+ m[3].mul_add(n[2], m[2] * n[0]),
16
+ m[3].mul_add(n[3], m[2] * n[1]),
17
+ m[5].mul_add(n[2], m[4] * n[0]) + n[4],
18
+ m[5].mul_add(n[3], m[4] * n[1]) + n[5],
19
+ ]
20
+ }
21
+
22
+ /// How a page is shown: its rotation into the displayed frame, its size
23
+ /// there, and its media box.
24
+ #[derive(Debug, Clone, Copy)]
25
+ pub struct Frame {
26
+ pub rotation: Matrix,
27
+ pub size: (f64, f64),
28
+ pub bounds: [f64; 4],
29
+ }
30
+
31
+ impl Frame {
32
+ pub fn of(page: &Page) -> Self {
33
+ let [x0, y0, x1, y1] = page.media_box;
34
+ let (left, bottom, right, top) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1));
35
+ // Displayed frame: rotated clockwise by /Rotate, y upward from the
36
+ // bottom-left corner.
37
+ let rotation = match page.rotate.rem_euclid(360) {
38
+ 90 => [0.0, -1.0, 1.0, 0.0, -bottom, right],
39
+ 180 => [-1.0, 0.0, 0.0, -1.0, right, top],
40
+ 270 => [0.0, 1.0, -1.0, 0.0, top, -left],
41
+ _ => [1.0, 0.0, 0.0, 1.0, -left, -bottom],
42
+ };
43
+ let (width, height) = (right - left, top - bottom);
44
+ let size =
45
+ if page.rotate.rem_euclid(180) == 90 { (height, width) } else { (width, height) };
46
+ Self { rotation, size, bounds: [left, bottom, right, top] }
47
+ }
48
+
49
+ /// Where the unit square, drawn with `ctm`, lands: in points from the
50
+ /// top-left corner of the page as shown.
51
+ pub fn place(&self, ctm: &Matrix) -> Bounds {
52
+ let shown = multiply(ctm, &self.rotation);
53
+ let corners = [(0.0_f64, 0.0_f64), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)].map(|(x, y)| {
54
+ (
55
+ x.mul_add(shown[0], y.mul_add(shown[2], shown[4])),
56
+ x.mul_add(shown[1], y.mul_add(shown[3], shown[5])),
57
+ )
58
+ });
59
+ let (mut left, mut right, mut low, mut high) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
60
+ for (x, y) in corners {
61
+ (left, right, low, high) = (left.min(x), right.max(x), low.min(y), high.max(y));
62
+ }
63
+ let height = self.size.1;
64
+ Bounds { left, top: height - high, right, bottom: height - low }
65
+ }
66
+ }
67
+
68
+ /// How many quarter turns, counterclockwise, a baseline along `(dx, dy)`
69
+ /// makes from left-to-right.
70
+ pub fn quarter_turns(dx: f64, dy: f64) -> u8 {
71
+ let angle = dy.atan2(dx).to_degrees().rem_euclid(360.0);
72
+ #[expect(
73
+ clippy::cast_possible_truncation,
74
+ clippy::cast_sign_loss,
75
+ reason = "0 to 4 after rounding"
76
+ )]
77
+ let turns = ((angle / 90.0).round() as u8) % 4;
78
+ turns
79
+ }
@@ -0,0 +1,156 @@
1
+ //! Content stream operands, borrowed from the stream: reading a page
2
+ //! allocates nothing per operator. `TJ`'s arrays keep their items in one
3
+ //! buffer shared by the whole stream.
4
+
5
+ use crate::read::pdf::lexer::{Lexer, Token, is_whitespace, name_bytes};
6
+ use crate::read::pdf::object::{Dict, Name, Object, Parser};
7
+
8
+ #[derive(Debug, Clone, Copy, PartialEq)]
9
+ pub enum Operand<'c> {
10
+ Number(f64),
11
+ /// A name as written, `#xx` escapes and all.
12
+ Name(&'c [u8]),
13
+ /// A string as written: a literal's bytes between its parentheses, or
14
+ /// a hexadecimal string's digits.
15
+ String {
16
+ raw: &'c [u8],
17
+ hex: bool,
18
+ },
19
+ /// An array of numbers and strings: where its items start and end in
20
+ /// the buffer.
21
+ Array(usize, usize),
22
+ /// Anything no operator here reads: dictionaries, nested arrays.
23
+ Other,
24
+ }
25
+
26
+ /// The operand a token starts; arrays fill `items`, dictionaries are skipped.
27
+ pub fn operand<'c>(
28
+ token: Token<'c>,
29
+ lexer: &mut Lexer<'c>,
30
+ items: &mut Vec<Operand<'c>>,
31
+ ) -> Operand<'c> {
32
+ match token {
33
+ Token::ArrayStart => {
34
+ let start = items.len();
35
+ while let Some(token) = lexer.next() {
36
+ match token {
37
+ Token::ArrayEnd => break,
38
+ Token::ArrayStart | Token::DictStart => skip(lexer),
39
+ token => match scalar(token) {
40
+ Operand::Other => {}
41
+ item => items.push(item),
42
+ },
43
+ }
44
+ }
45
+ Operand::Array(start, items.len())
46
+ }
47
+ Token::DictStart => {
48
+ skip(lexer);
49
+ Operand::Other
50
+ }
51
+ token => scalar(token),
52
+ }
53
+ }
54
+
55
+ #[expect(clippy::cast_precision_loss, reason = "content stream numbers are small")]
56
+ const fn scalar(token: Token<'_>) -> Operand<'_> {
57
+ match token {
58
+ Token::Integer(number) => Operand::Number(number as f64),
59
+ Token::Real(number) => Operand::Number(number),
60
+ Token::Name(raw) => Operand::Name(raw),
61
+ Token::String(raw) => Operand::String { raw, hex: false },
62
+ Token::Hex(raw) => Operand::String { raw, hex: true },
63
+ _ => Operand::Other,
64
+ }
65
+ }
66
+
67
+ /// Skips a nested array or dictionary, its opening token already read.
68
+ fn skip(lexer: &mut Lexer<'_>) {
69
+ let mut depth = 1_usize;
70
+ while depth > 0 {
71
+ match lexer.next() {
72
+ None => return,
73
+ Some(Token::ArrayStart | Token::DictStart) => depth += 1,
74
+ Some(Token::ArrayEnd | Token::DictEnd) => depth -= 1,
75
+ Some(_) => {}
76
+ }
77
+ }
78
+ }
79
+
80
+ /// An inline image after its `BI`: its dictionary, abbreviations spelled
81
+ /// out, and its data up to `EI`. The lexer moves past it either way.
82
+ pub fn inline_image<'c>(lexer: &mut Lexer<'c>, max_depth: usize) -> Option<(Dict, &'c [u8])> {
83
+ let data = lexer.data();
84
+ let mut parser = Parser::new(data, lexer.pos, max_depth);
85
+ let mut entries = Vec::new();
86
+ let found = loop {
87
+ match parser.lexer.next() {
88
+ None => break false,
89
+ Some(Token::Keyword(b"ID")) => break true,
90
+ Some(Token::Name(key)) => {
91
+ let Some(token) = parser.lexer.next() else { break false };
92
+ if token == Token::Keyword(b"ID") {
93
+ break true;
94
+ }
95
+ let Ok(value) = parser.value(token, 0) else { break false };
96
+ entries.push((Name::new(full_key(&name_bytes(key))), full_value(value)));
97
+ }
98
+ Some(_) => {}
99
+ }
100
+ };
101
+ // One whitespace byte separates `ID` from the data.
102
+ let start = (parser.lexer.pos + 1).min(data.len());
103
+ let mut at = start;
104
+ while at + 2 <= data.len() {
105
+ if &data[at..at + 2] == b"EI"
106
+ && data.get(at.wrapping_sub(1)).is_some_and(|&byte| is_whitespace(byte))
107
+ && data.get(at + 2).is_none_or(|&byte| is_whitespace(byte))
108
+ {
109
+ lexer.pos = at + 2;
110
+ let end = if at > start { at - 1 } else { at };
111
+ return found.then(|| (Dict(entries), &data[start..end]));
112
+ }
113
+ at += 1;
114
+ }
115
+ lexer.pos = data.len();
116
+ None
117
+ }
118
+
119
+ /// Inline images abbreviate their keys (ISO 32000-1, Table 93).
120
+ const fn full_key(key: &[u8]) -> &[u8] {
121
+ match key {
122
+ b"BPC" => b"BitsPerComponent",
123
+ b"CS" => b"ColorSpace",
124
+ b"D" => b"Decode",
125
+ b"DP" => b"DecodeParms",
126
+ b"F" => b"Filter",
127
+ b"H" => b"Height",
128
+ b"IM" => b"ImageMask",
129
+ b"I" => b"Interpolate",
130
+ b"W" => b"Width",
131
+ b"L" => b"Length",
132
+ key => key,
133
+ }
134
+ }
135
+
136
+ /// And their names (Table 94): colour spaces and filters.
137
+ fn full_value(value: Object) -> Object {
138
+ match value {
139
+ Object::Name(name) => Object::Name(match &*name {
140
+ b"G" => Name::new(b"DeviceGray"),
141
+ b"RGB" => Name::new(b"DeviceRGB"),
142
+ b"CMYK" => Name::new(b"DeviceCMYK"),
143
+ b"I" => Name::new(b"Indexed"),
144
+ b"AHx" => Name::new(b"ASCIIHexDecode"),
145
+ b"A85" => Name::new(b"ASCII85Decode"),
146
+ b"LZW" => Name::new(b"LZWDecode"),
147
+ b"Fl" => Name::new(b"FlateDecode"),
148
+ b"RL" => Name::new(b"RunLengthDecode"),
149
+ b"CCF" => Name::new(b"CCITTFaxDecode"),
150
+ b"DCT" => Name::new(b"DCTDecode"),
151
+ _ => name,
152
+ }),
153
+ Object::Array(items) => Object::Array(items.into_iter().map(full_value).collect()),
154
+ value => value,
155
+ }
156
+ }