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,346 @@
1
+ //! Where each object lives (ISO 32000-1, 7.5.4 and 7.5.8): cross-reference
2
+ //! tables and streams, followed back through `/Prev` and `/XRefStm`. When
3
+ //! they are missing or wrong, as in damaged files, the table is rebuilt by
4
+ //! scanning the file for `N G obj`.
5
+
6
+ #[cfg(test)]
7
+ mod tests;
8
+
9
+ use std::collections::HashMap;
10
+
11
+ use super::filter;
12
+ use super::lexer::{Lexer, Token};
13
+ use super::object::{Dict, Name, Object, Parser, direct_length, find};
14
+ use crate::Result;
15
+ use crate::read::Budget;
16
+
17
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
18
+ pub enum Entry {
19
+ Free,
20
+ Offset { offset: usize, generation: u16 },
21
+ Compressed { stream: u32, index: u32 },
22
+ }
23
+
24
+ #[derive(Debug, Default)]
25
+ pub struct Xref {
26
+ pub entries: HashMap<u32, Entry>,
27
+ pub trailer: Dict,
28
+ }
29
+
30
+ impl Xref {
31
+ /// The table the file declares, or one rebuilt from its objects when
32
+ /// that has no `/Root`.
33
+ pub fn read(data: &[u8], budget: &Budget<'_>) -> Result<Self> {
34
+ let mut xref = Self::default();
35
+ let declared = start(data).map(|offset| xref.follow(data, offset, budget));
36
+ match declared {
37
+ Some(Ok(())) if xref.trailer.get(b"Root").is_some() && !xref.entries.is_empty() => {
38
+ Ok(xref)
39
+ }
40
+ Some(Err(error)) if is_limit(&error) => Err(error),
41
+ _ => Self::rebuild(data, budget),
42
+ }
43
+ }
44
+
45
+ /// Reads the section at `offset` and those before it. Newer sections
46
+ /// come first, so an object already known keeps its entry, except one
47
+ /// a hybrid file's table marks free for older readers.
48
+ fn follow(&mut self, data: &[u8], offset: usize, budget: &Budget<'_>) -> Result<()> {
49
+ let mut pending = vec![offset];
50
+ let mut seen = Vec::new();
51
+ while let Some(offset) = pending.pop() {
52
+ if seen.contains(&offset) || seen.len() > 1_000 {
53
+ continue;
54
+ }
55
+ seen.push(offset);
56
+ budget.check()?;
57
+ let mut lexer = Lexer::new(data, offset);
58
+ lexer.skip_space();
59
+ let trailer = if data.get(lexer.pos..).is_some_and(|rest| rest.starts_with(b"xref")) {
60
+ lexer.pos += 4;
61
+ self.table(&mut lexer, budget)?
62
+ } else {
63
+ self.stream(data, lexer.pos, budget)?
64
+ };
65
+ for key in [&b"Prev"[..], b"XRefStm"] {
66
+ if let Some(at) = trailer
67
+ .get(key)
68
+ .and_then(Object::as_integer)
69
+ .and_then(|at| usize::try_from(at).ok())
70
+ {
71
+ pending.push(at);
72
+ }
73
+ }
74
+ for (key, value) in trailer.0 {
75
+ if self.trailer.get(&key).is_none() {
76
+ self.trailer.0.push((key, value));
77
+ }
78
+ }
79
+ }
80
+ Ok(())
81
+ }
82
+
83
+ fn insert(&mut self, number: u32, entry: Entry) {
84
+ let slot = self.entries.entry(number).or_insert(entry);
85
+ if *slot == Entry::Free {
86
+ *slot = entry;
87
+ }
88
+ }
89
+
90
+ /// A classic table after its `xref` keyword, then its trailer.
91
+ fn table(&mut self, lexer: &mut Lexer<'_>, budget: &Budget<'_>) -> Result<Dict> {
92
+ loop {
93
+ let saved = lexer.pos;
94
+ match (lexer.next(), lexer.next()) {
95
+ (Some(Token::Integer(first)), Some(Token::Integer(count))) => {
96
+ let (Ok(first), Ok(count)) = (u32::try_from(first), u32::try_from(count))
97
+ else {
98
+ break;
99
+ };
100
+ for number in first..first.saturating_add(count) {
101
+ budget.tick()?;
102
+ let (
103
+ Some(Token::Integer(offset)),
104
+ Some(Token::Integer(generation)),
105
+ Some(Token::Keyword(kind)),
106
+ ) = (lexer.next(), lexer.next(), lexer.next())
107
+ else {
108
+ break;
109
+ };
110
+ let entry = match (kind, usize::try_from(offset)) {
111
+ (b"n", Ok(offset)) if offset > 0 => Entry::Offset {
112
+ offset,
113
+ generation: u16::try_from(generation).unwrap_or(0),
114
+ },
115
+ _ => Entry::Free,
116
+ };
117
+ self.insert(number, entry);
118
+ }
119
+ }
120
+ (Some(Token::Keyword(b"trailer")), Some(Token::DictStart)) => {
121
+ lexer.pos = saved;
122
+ lexer.next();
123
+ let mut parser = Parser::new(lexer.data(), lexer.pos, budget.max_depth());
124
+ return Ok(parser.object()?.as_dict().cloned().unwrap_or_default());
125
+ }
126
+ _ => break,
127
+ }
128
+ }
129
+ Ok(Dict::default())
130
+ }
131
+
132
+ /// A cross-reference stream at `offset`; its dictionary is the trailer.
133
+ fn stream(&mut self, data: &[u8], offset: usize, budget: &Budget<'_>) -> Result<Dict> {
134
+ let mut parser = Parser::new(data, offset, budget.max_depth());
135
+ let (_, _, object) = parser.indirect(&direct_length)?;
136
+ let Object::Stream(stream) = object else { return Ok(Dict::default()) };
137
+ let widths: Vec<usize> = stream
138
+ .dict
139
+ .get(b"W")
140
+ .and_then(Object::as_array)
141
+ .unwrap_or_default()
142
+ .iter()
143
+ .filter_map(|width| width.as_integer().and_then(|width| usize::try_from(width).ok()))
144
+ .collect();
145
+ if widths.len() < 3 || widths.iter().any(|&width| width > 8) {
146
+ return Ok(stream.dict);
147
+ }
148
+ let filters = filters_of(&stream.dict);
149
+ let decoded = filter::decode(&data[stream.data.clone()], &filters, budget)?;
150
+ let size = stream.dict.get(b"Size").and_then(Object::as_integer).unwrap_or(0);
151
+ let index: Vec<i64> = stream.dict.get(b"Index").and_then(Object::as_array).map_or_else(
152
+ || vec![0, size],
153
+ |index| index.iter().filter_map(Object::as_integer).collect(),
154
+ );
155
+ let row = widths.iter().sum::<usize>().max(1);
156
+ let mut rows = decoded.chunks_exact(row);
157
+ for pair in index.as_chunks::<2>().0 {
158
+ let (Ok(first), Ok(count)) = (u32::try_from(pair[0]), u32::try_from(pair[1])) else {
159
+ continue;
160
+ };
161
+ for number in first..first.saturating_add(count) {
162
+ let Some(row) = rows.next() else { break };
163
+ budget.tick()?;
164
+ let (kind, rest) = row.split_at(widths[0]);
165
+ let (second, third) = rest.split_at(widths[1]);
166
+ let kind = if widths[0] == 0 { 1 } else { be(kind) };
167
+ let entry = match kind {
168
+ 1 => Entry::Offset {
169
+ offset: usize::try_from(be(second)).unwrap_or(0),
170
+ generation: u16::try_from(be(third)).unwrap_or(0),
171
+ },
172
+ 2 => Entry::Compressed {
173
+ stream: u32::try_from(be(second)).unwrap_or(u32::MAX),
174
+ index: u32::try_from(be(third)).unwrap_or(0),
175
+ },
176
+ _ => Entry::Free,
177
+ };
178
+ self.insert(number, entry);
179
+ }
180
+ }
181
+ Ok(stream.dict)
182
+ }
183
+
184
+ /// Rebuilds the table from every `N G obj` in the file, later ones
185
+ /// winning as incremental updates do; the trailer from every `trailer`
186
+ /// dictionary, or from the catalog when there is none.
187
+ fn rebuild(data: &[u8], budget: &Budget<'_>) -> Result<Self> {
188
+ let mut xref = Self::default();
189
+ let mut at = 0;
190
+ while let Some(found) = find(&data[at..], b"obj") {
191
+ budget.tick()?;
192
+ let keyword = at + found;
193
+ at = keyword + 3;
194
+ if let Some((number, generation, offset)) = header_before(data, keyword) {
195
+ xref.entries.insert(number, Entry::Offset { offset, generation });
196
+ }
197
+ }
198
+ let mut at = 0;
199
+ while let Some(found) = find(&data[at..], b"trailer") {
200
+ at += found + 7;
201
+ let mut parser = Parser::new(data, at, budget.max_depth());
202
+ if let Ok(Object::Dict(trailer)) = parser.object() {
203
+ for (key, value) in trailer.0 {
204
+ xref.trailer.0.retain(|(existing, _)| *existing != key);
205
+ xref.trailer.0.push((key, value));
206
+ }
207
+ }
208
+ }
209
+ xref.register_object_streams(data, budget)?;
210
+ if xref.trailer.get(b"Root").is_none() {
211
+ for (number, offset) in xref.offsets() {
212
+ let mut parser = Parser::new(data, offset, budget.max_depth());
213
+ if let Ok((_, generation, Object::Dict(dict))) = parser.indirect(&|_| None)
214
+ && dict.has_name(b"Type", b"Catalog")
215
+ {
216
+ xref.trailer.0.push((Name::new(b"Root"), Object::Ref(number, generation)));
217
+ break;
218
+ }
219
+ }
220
+ }
221
+ Ok(xref)
222
+ }
223
+ }
224
+
225
+ impl Xref {
226
+ /// The objects stored in the file itself, with their offsets.
227
+ fn offsets(&self) -> Vec<(u32, usize)> {
228
+ self.entries
229
+ .iter()
230
+ .filter_map(|(&number, entry)| match *entry {
231
+ Entry::Offset { offset, .. } => Some((number, offset)),
232
+ _ => None,
233
+ })
234
+ .collect()
235
+ }
236
+
237
+ /// After a rebuild: the objects inside object streams, which the scan
238
+ /// cannot see, by the stream's own index of them.
239
+ fn register_object_streams(&mut self, data: &[u8], budget: &Budget<'_>) -> Result<()> {
240
+ let streams = self.offsets();
241
+ for (stream_number, offset) in streams {
242
+ budget.tick()?;
243
+ let mut parser = Parser::new(data, offset, budget.max_depth());
244
+ let Ok((_, _, Object::Stream(stream))) = parser.indirect(&direct_length) else {
245
+ continue;
246
+ };
247
+ if !stream.dict.has_name(b"Type", b"ObjStm") {
248
+ continue;
249
+ }
250
+ let Ok(decoded) =
251
+ filter::decode(&data[stream.data.clone()], &filters_of(&stream.dict), budget)
252
+ else {
253
+ continue;
254
+ };
255
+ let count = stream.dict.get(b"N").and_then(Object::as_integer).unwrap_or(0);
256
+ let mut lexer = Lexer::new(&decoded, 0);
257
+ for index in 0..u32::try_from(count).unwrap_or(0) {
258
+ let (Some(Token::Integer(number)), Some(Token::Integer(_))) =
259
+ (lexer.next(), lexer.next())
260
+ else {
261
+ break;
262
+ };
263
+ if let Ok(number) = u32::try_from(number) {
264
+ self.entries
265
+ .entry(number)
266
+ .or_insert(Entry::Compressed { stream: stream_number, index });
267
+ }
268
+ }
269
+ }
270
+ Ok(())
271
+ }
272
+ }
273
+
274
+ /// `N G` right before an `obj` keyword at `keyword`: the object's number,
275
+ /// generation and offset.
276
+ fn header_before(data: &[u8], keyword: usize) -> Option<(u32, u16, usize)> {
277
+ let before = &data[..keyword];
278
+ let generation_end = before.iter().rposition(|&byte| !super::lexer::is_whitespace(byte))? + 1;
279
+ if generation_end == keyword {
280
+ return None;
281
+ }
282
+ let generation_start = before[..generation_end]
283
+ .iter()
284
+ .rposition(|byte| !byte.is_ascii_digit())
285
+ .map_or(0, |at| at + 1);
286
+ let number_end =
287
+ before[..generation_start].iter().rposition(|&byte| !super::lexer::is_whitespace(byte))?
288
+ + 1;
289
+ let number_start =
290
+ before[..number_end].iter().rposition(|byte| !byte.is_ascii_digit()).map_or(0, |at| at + 1);
291
+ if generation_start == generation_end
292
+ || number_start == number_end
293
+ || number_end == generation_start
294
+ {
295
+ return None;
296
+ }
297
+ let digits =
298
+ |range: std::ops::Range<usize>| std::str::from_utf8(&data[range]).ok()?.parse::<u32>().ok();
299
+ Some((
300
+ digits(number_start..number_end)?,
301
+ u16::try_from(digits(generation_start..generation_end)?).ok()?,
302
+ number_start,
303
+ ))
304
+ }
305
+
306
+ /// The offset after the last `startxref`.
307
+ fn start(data: &[u8]) -> Option<usize> {
308
+ let tail = data.len().saturating_sub(4096);
309
+ let found = data[tail..].windows(9).rposition(|window| window == b"startxref")?;
310
+ let mut lexer = Lexer::new(data, tail + found + 9);
311
+ match lexer.next()? {
312
+ Token::Integer(offset) => {
313
+ usize::try_from(offset).ok().filter(|&offset| offset < data.len())
314
+ }
315
+ _ => None,
316
+ }
317
+ }
318
+
319
+ /// A stream's filters with their parameters, as written in its dictionary
320
+ /// (direct objects only, which is all cross-reference streams may use).
321
+ pub fn filters_of(dict: &Dict) -> Vec<(&[u8], Option<&Dict>)> {
322
+ let params = dict.get(b"DecodeParms");
323
+ match dict.get(b"Filter") {
324
+ Some(Object::Name(name)) => vec![(&**name, params.and_then(Object::as_dict))],
325
+ Some(Object::Array(names)) => names
326
+ .iter()
327
+ .enumerate()
328
+ .filter_map(|(index, name)| {
329
+ let params = params
330
+ .and_then(Object::as_array)
331
+ .and_then(|params| params.get(index))
332
+ .and_then(Object::as_dict);
333
+ Some((name.as_name()?, params))
334
+ })
335
+ .collect(),
336
+ _ => Vec::new(),
337
+ }
338
+ }
339
+
340
+ fn be(bytes: &[u8]) -> u64 {
341
+ bytes.iter().fold(0, |value, &byte| value << 8 | u64::from(byte))
342
+ }
343
+
344
+ pub const fn is_limit(error: &crate::Error) -> bool {
345
+ matches!(error, crate::Error::LimitExceeded(_) | crate::Error::Cancelled)
346
+ }
@@ -0,0 +1,53 @@
1
+ //! PDF: the text layer as Markdown. Every page's content runs through a
2
+ //! small interpreter that places each glyph; layout turns the glyphs into
3
+ //! lines, paragraphs and headings. Scanned pages have no text layer:
4
+ //! they are images, and need OCR.
5
+
6
+ mod cmap;
7
+ mod content;
8
+ mod crypt;
9
+ mod document;
10
+ mod filter;
11
+ mod font;
12
+ mod glyphs;
13
+ mod images;
14
+ mod layout;
15
+ mod lexer;
16
+ mod object;
17
+ #[cfg(test)]
18
+ mod tests;
19
+ mod xref;
20
+
21
+ use super::Budget;
22
+ use super::content::Page;
23
+ use crate::{Error, Result};
24
+ use content::{Fonts, PageText};
25
+ use document::Document;
26
+ pub use images::images;
27
+ use xref::is_limit;
28
+
29
+ /// `%PDF-` within the first kilobyte, where readers look for it.
30
+ pub fn is_pdf(document: &[u8]) -> bool {
31
+ document[..document.len().min(1024)].windows(5).any(|window| window == b"%PDF-")
32
+ }
33
+
34
+ pub fn content(data: &[u8], budget: &Budget<'_>) -> Result<Vec<Page>> {
35
+ let document = Document::open(data, budget)?;
36
+ let pages = document.pages()?;
37
+ let mut fonts = Fonts::new();
38
+ let mut lines = Vec::with_capacity(pages.len());
39
+ let mut text = PageText::default();
40
+ for page in &pages {
41
+ budget.check()?;
42
+ // A damaged page loses its text, not the document's.
43
+ match content::page_text(&document, page, &mut fonts, &mut text) {
44
+ Ok(()) => lines.push(layout::lines(&text)),
45
+ Err(error) if is_limit(&error) => return Err(error),
46
+ Err(_) => lines.push(layout::PageLines::default()),
47
+ }
48
+ }
49
+ if lines.iter().all(|page| page.text.trim().is_empty()) {
50
+ return Err(Error::NoTextLayer);
51
+ }
52
+ Ok(layout::pages(&lines))
53
+ }
@@ -0,0 +1,22 @@
1
+ //! Text kept in one buffer and referred to by spans: a page's glyphs and
2
+ //! lines, a font's encoding, a `CMap`'s targets. A `u32` span is half a
3
+ //! `Range<usize>`, and one buffer is one allocation instead of many.
4
+
5
+ use std::ops::Range;
6
+
7
+ /// The end of `text`, as a span bound.
8
+ pub fn end(text: &str) -> u32 {
9
+ u32::try_from(text.len()).unwrap_or(u32::MAX)
10
+ }
11
+
12
+ /// Appends what `push` writes to `text`; returns its span.
13
+ pub fn append(text: &mut String, push: impl FnOnce(&mut String)) -> Range<u32> {
14
+ let start = end(text);
15
+ push(text);
16
+ start..end(text)
17
+ }
18
+
19
+ /// The text of a span.
20
+ pub fn slice<'t>(text: &'t str, span: &Range<u32>) -> &'t str {
21
+ &text[span.start as usize..span.end as usize]
22
+ }
@@ -0,0 +1,155 @@
1
+ //! How XLSX number formats show numbers: which formats are dates, times
2
+ //! or percentages, and Excel's General format.
3
+
4
+ use std::fmt::Write as _;
5
+
6
+ use crate::Result;
7
+ use crate::read::Budget;
8
+ use crate::read::content::Value;
9
+ use crate::xlsx::DateTime;
10
+ use crate::xml::reader::{Event, XmlReader};
11
+
12
+ /// How a number format shows a number.
13
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14
+ pub enum Shown {
15
+ General,
16
+ Date,
17
+ Time,
18
+ DateTime,
19
+ Percent,
20
+ }
21
+
22
+ /// How each cell format (`cellXfs`, by index) shows numbers.
23
+ pub fn styles(text: &str, budget: &Budget<'_>) -> Result<Vec<Shown>> {
24
+ let mut xml = XmlReader::new(text, budget.max_depth());
25
+ let (mut custom, mut shown, mut in_cell_formats) = (Vec::new(), Vec::new(), false);
26
+ while let Some(event) = xml.next()? {
27
+ match event {
28
+ Event::Start(tag) => match tag.name() {
29
+ "numFmt" => {
30
+ if let (Some(id), Some(code)) = (tag.attr("numFmtId"), tag.attr("formatCode")) {
31
+ custom.push((id.parse::<u32>().unwrap_or(u32::MAX), classify(&code)));
32
+ }
33
+ }
34
+ "cellXfs" => in_cell_formats = true,
35
+ "xf" if in_cell_formats => {
36
+ let id =
37
+ tag.attr("numFmtId").and_then(|id| id.parse::<u32>().ok()).unwrap_or(0);
38
+ let custom =
39
+ custom.iter().find(|&&(custom, _)| custom == id).map(|&(_, shown)| shown);
40
+ shown.push(custom.unwrap_or_else(|| built_in(id)));
41
+ }
42
+ _ => {}
43
+ },
44
+ Event::End("cellXfs") => in_cell_formats = false,
45
+ _ => {}
46
+ }
47
+ }
48
+ Ok(shown)
49
+ }
50
+
51
+ /// Excel's built-in number formats (ECMA-376 Part 1, 18.8.30), with the
52
+ /// East Asian date formats.
53
+ const fn built_in(id: u32) -> Shown {
54
+ match id {
55
+ 9 | 10 => Shown::Percent,
56
+ 14..=17 | 27..=31 | 34..=36 | 50..=58 => Shown::Date,
57
+ 18..=21 | 32 | 33 | 45..=47 => Shown::Time,
58
+ 22 => Shown::DateTime,
59
+ _ => Shown::General,
60
+ }
61
+ }
62
+
63
+ /// What a custom format code shows, from its first section: date and time
64
+ /// tokens, outside quoted text, escapes and `[...]` (colors, locales;
65
+ /// `[h]` is elapsed time).
66
+ pub fn classify(code: &str) -> Shown {
67
+ let (mut date, mut time, mut month, mut percent) = (false, false, false, false);
68
+ let mut chars = code.chars();
69
+ while let Some(ch) = chars.next() {
70
+ match ch.to_ascii_lowercase() {
71
+ ';' => break,
72
+ '"' => {
73
+ chars.by_ref().find(|&ch| ch == '"');
74
+ }
75
+ '\\' | '_' | '*' => {
76
+ chars.next();
77
+ }
78
+ '[' => {
79
+ let inner: String = chars.by_ref().take_while(|&ch| ch != ']').collect();
80
+ time |= matches!(inner.to_ascii_lowercase().chars().next(), Some('h' | 'm' | 's'));
81
+ }
82
+ 'y' | 'd' => date = true,
83
+ 'h' | 's' => time = true,
84
+ 'm' => month = true,
85
+ '%' => percent = true,
86
+ _ => {}
87
+ }
88
+ }
89
+ match (date || (month && !time), time) {
90
+ (true, true) => Shown::DateTime,
91
+ (true, false) => Shown::Date,
92
+ (false, true) => Shown::Time,
93
+ (false, false) if percent => Shown::Percent,
94
+ (false, false) => Shown::General,
95
+ }
96
+ }
97
+
98
+ /// Writes `number` as `shown` says; returns the value, a date when it is
99
+ /// shown as one. (Writing to a String cannot fail: the results of `write!`
100
+ /// here are dropped on purpose.)
101
+ pub fn push_number(
102
+ out: &mut String,
103
+ number: f64,
104
+ shown: Shown,
105
+ date1904: bool,
106
+ scratch: &mut String,
107
+ ) -> Value {
108
+ let date = || DateTime::from_serial(number, date1904);
109
+ match shown {
110
+ Shown::Date if let Some(date) = date() => {
111
+ let date = date.date_only();
112
+ // Writing to a String cannot fail.
113
+ let _ = write!(out, "{date}");
114
+ return Value::Date(date);
115
+ }
116
+ Shown::DateTime if let Some(date) = date() => {
117
+ // Writing to a String cannot fail.
118
+ let _ = write!(out, "{date}");
119
+ return Value::Date(date);
120
+ }
121
+ Shown::Time => {
122
+ #[expect(
123
+ clippy::cast_possible_truncation,
124
+ clippy::cast_sign_loss,
125
+ reason = "0 to 86400 after rem_euclid"
126
+ )]
127
+ let seconds = (number.fract() * 86_400.0).round().rem_euclid(86_400.0) as u32;
128
+ // Writing to a String cannot fail.
129
+ let _ =
130
+ write!(out, "{:02}:{:02}:{:02}", seconds / 3600, seconds / 60 % 60, seconds % 60);
131
+ }
132
+ Shown::Percent => {
133
+ push_general(out, number * 100.0, scratch);
134
+ out.push('%');
135
+ }
136
+ _ => push_general(out, number, scratch),
137
+ }
138
+ Value::Number(number)
139
+ }
140
+
141
+ /// A number as Excel's General format shows it: 15 significant digits.
142
+ pub fn push_general(out: &mut String, number: f64, scratch: &mut String) {
143
+ #[expect(clippy::cast_possible_truncation, reason = "whole and under 1e15: exact in i64")]
144
+ if number.fract() == 0.0 && number.abs() < 1e15 {
145
+ // Writing to a String cannot fail.
146
+ let _ = write!(out, "{}", number as i64);
147
+ return;
148
+ }
149
+ scratch.clear();
150
+ // Writing to a String cannot fail.
151
+ let _ = write!(scratch, "{number:.14e}");
152
+ let rounded = scratch.parse::<f64>().unwrap_or(number);
153
+ // Writing to a String cannot fail.
154
+ let _ = write!(out, "{rounded}");
155
+ }