imprimatur 0.1.0 → 0.3.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/Cargo.lock +124 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +123 -23
  6. data/ext/imprimatur_native/cli/Cargo.toml +2 -2
  7. data/ext/imprimatur_native/cli/src/cli/grep.rs +1 -1
  8. data/ext/imprimatur_native/cli/src/cli/markdown.rs +2 -2
  9. data/ext/imprimatur_native/cli/src/cli.rs +5 -5
  10. data/ext/imprimatur_native/cli/src/main.rs +1 -1
  11. data/ext/imprimatur_native/core/Cargo.toml +12 -2
  12. data/ext/imprimatur_native/core/README.md +15 -1
  13. data/ext/imprimatur_native/core/src/archive.rs +279 -0
  14. data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
  15. data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
  16. data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
  17. data/ext/imprimatur_native/core/src/doc.rs +86 -2
  18. data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
  19. data/ext/imprimatur_native/core/src/docx.rs +91 -28
  20. data/ext/imprimatur_native/core/src/error.rs +13 -1
  21. data/ext/imprimatur_native/core/src/font.rs +229 -0
  22. data/ext/imprimatur_native/core/src/lib.rs +5 -0
  23. data/ext/imprimatur_native/core/src/pdf/layout.rs +38 -15
  24. data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
  25. data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
  26. data/ext/imprimatur_native/core/src/pdf.rs +31 -3
  27. data/ext/imprimatur_native/core/src/read/content.rs +91 -3
  28. data/ext/imprimatur_native/core/src/read/json.rs +96 -7
  29. data/ext/imprimatur_native/core/src/read/markdown.rs +1 -1
  30. data/ext/imprimatur_native/core/src/read/{xlsx/numbers.rs → numbers.rs} +69 -45
  31. data/ext/imprimatur_native/core/src/read/ods.rs +356 -0
  32. data/ext/imprimatur_native/core/src/read/ole.rs +244 -0
  33. data/ext/imprimatur_native/core/src/read/package.rs +0 -5
  34. data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
  35. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
  36. data/ext/imprimatur_native/core/src/read/xls/strings.rs +156 -0
  37. data/ext/imprimatur_native/core/src/read/xls.rs +342 -0
  38. data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
  39. data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -20
  40. data/ext/imprimatur_native/core/src/read.rs +68 -21
  41. data/ext/imprimatur_native/core/src/script.rs +116 -0
  42. data/ext/imprimatur_native/core/src/style.rs +21 -0
  43. data/ext/imprimatur_native/core/src/test_support.rs +90 -2
  44. data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
  45. data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
  46. data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
  47. data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
  48. data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
  49. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
  50. data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
  51. data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
  52. data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
  53. data/ext/imprimatur_native/ffi/src/archive.rs +119 -0
  54. data/ext/imprimatur_native/ffi/src/blocking.rs +63 -0
  55. data/ext/imprimatur_native/ffi/src/document.rs +48 -1
  56. data/ext/imprimatur_native/ffi/src/lib.rs +52 -1
  57. data/ext/imprimatur_native/ffi/src/read.rs +9 -14
  58. data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
  59. data/lib/imprimatur/document.rb +30 -6
  60. data/lib/imprimatur/error.rb +4 -3
  61. data/lib/imprimatur/reader.rb +3 -3
  62. data/lib/imprimatur/spreadsheet.rb +151 -0
  63. data/lib/imprimatur/version.rb +1 -1
  64. data/lib/imprimatur/xlsx.rb +25 -4
  65. data/lib/imprimatur/zip.rb +127 -0
  66. data/lib/imprimatur.rb +18 -5
  67. metadata +23 -4
@@ -0,0 +1,156 @@
1
+ //! Workbook strings: BIFF8's Latin-1 or UTF-16, BIFF5's Windows-1252
2
+ //! bytes, and BIFF8's shared string table, whose strings may be split
3
+ //! across `CONTINUE` records.
4
+
5
+ use crate::Result;
6
+ use crate::pdf::metrics::WIN_ANSI_HIGH;
7
+ use crate::read::Budget;
8
+
9
+ /// How a workbook's strings are stored.
10
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11
+ pub enum Encoding {
12
+ /// BIFF8: each string says whether it is Latin-1 or UTF-16.
13
+ Unicode,
14
+ /// BIFF5: bytes in the workbook's code page.
15
+ Windows1252,
16
+ }
17
+
18
+ /// A string with a 1-byte or 2-byte character count at `at`; `None` past
19
+ /// the end.
20
+ pub fn string(bytes: &[u8], at: usize, wide_count: bool, encoding: Encoding) -> Option<String> {
21
+ let (count, at) = if wide_count {
22
+ (usize::from(u16::from_le_bytes(bytes.get(at..at + 2)?.try_into().ok()?)), at + 2)
23
+ } else {
24
+ (usize::from(*bytes.get(at)?), at + 1)
25
+ };
26
+ if encoding == Encoding::Windows1252 {
27
+ let chars = bytes.get(at..at + count).or_else(|| bytes.get(at..))?;
28
+ return Some(chars.iter().map(|&byte| windows_1252(byte)).collect());
29
+ }
30
+ let high = bytes.get(at)? & 1 != 0;
31
+ let mut out = String::with_capacity(count);
32
+ let mut units = Vec::new();
33
+ let width = if high { 2 } else { 1 };
34
+ let chars = bytes.get(at + 1..at + 1 + count * width).or_else(|| bytes.get(at + 1..))?;
35
+ decode(chars, high, &mut units, &mut out);
36
+ flush(&mut units, &mut out);
37
+ Some(out)
38
+ }
39
+
40
+ /// A Windows-1252 byte: Latin-1, but for the typographic characters at
41
+ /// 0x80 to 0x9F.
42
+ fn windows_1252(byte: u8) -> char {
43
+ match byte {
44
+ 0x80..=0x9F => WIN_ANSI_HIGH[usize::from(byte - 0x80)].unwrap_or_else(|| char::from(byte)),
45
+ _ => char::from(byte),
46
+ }
47
+ }
48
+
49
+ /// Appends characters: Latin-1 bytes at once, UTF-16 units to `units`
50
+ /// (a surrogate pair may span two pieces), decoded by [`flush`].
51
+ fn decode(chars: &[u8], high: bool, units: &mut Vec<u16>, out: &mut String) {
52
+ if high {
53
+ units.extend(chars.as_chunks::<2>().0.iter().map(|unit| u16::from_le_bytes(*unit)));
54
+ } else {
55
+ flush(units, out);
56
+ out.extend(chars.iter().map(|&byte| char::from(byte)));
57
+ }
58
+ }
59
+
60
+ fn flush(units: &mut Vec<u16>, out: &mut String) {
61
+ out.extend(
62
+ char::decode_utf16(units.drain(..)).map(|ch| ch.unwrap_or(char::REPLACEMENT_CHARACTER)),
63
+ );
64
+ }
65
+
66
+ /// The shared string table (`SST`): its record's body, then each
67
+ /// `CONTINUE` body after it.
68
+ pub fn shared(parts: &[&[u8]], budget: &Budget<'_>) -> Result<Vec<String>> {
69
+ let mut reader = Cursor { parts, part: 0, at: 8 };
70
+ let Some(unique) = parts.first().and_then(|first| first.get(4..8)) else {
71
+ return Ok(Vec::new());
72
+ };
73
+ let unique = u32::from_le_bytes(unique.try_into().unwrap_or([0; 4])) as usize;
74
+ let mut strings = Vec::with_capacity(unique.min(1 << 20));
75
+ let mut units = Vec::new();
76
+ while strings.len() < unique {
77
+ budget.tick()?;
78
+ let Some(string) = reader.string(&mut units) else { break };
79
+ strings.push(string);
80
+ }
81
+ Ok(strings)
82
+ }
83
+
84
+ /// A cursor over record bodies read as one stream.
85
+ struct Cursor<'p, 'a> {
86
+ parts: &'p [&'a [u8]],
87
+ part: usize,
88
+ at: usize,
89
+ }
90
+
91
+ impl Cursor<'_, '_> {
92
+ fn byte(&mut self) -> Option<u8> {
93
+ while self.at >= self.parts.get(self.part)?.len() {
94
+ self.part += 1;
95
+ self.at = 0;
96
+ }
97
+ let byte = self.parts[self.part][self.at];
98
+ self.at += 1;
99
+ Some(byte)
100
+ }
101
+
102
+ fn u16(&mut self) -> Option<u16> {
103
+ Some(u16::from_le_bytes([self.byte()?, self.byte()?]))
104
+ }
105
+
106
+ fn u32(&mut self) -> Option<u32> {
107
+ Some(u32::from_le_bytes([self.byte()?, self.byte()?, self.byte()?, self.byte()?]))
108
+ }
109
+
110
+ fn skip(&mut self, mut count: usize) -> Option<()> {
111
+ while count > 0 {
112
+ let left = self.parts.get(self.part)?.len().saturating_sub(self.at);
113
+ if left == 0 {
114
+ self.part += 1;
115
+ self.at = 0;
116
+ continue;
117
+ }
118
+ let step = left.min(count);
119
+ self.at += step;
120
+ count -= step;
121
+ }
122
+ Some(())
123
+ }
124
+
125
+ /// An `XLUnicodeRichExtendedString`: where its characters cross into the
126
+ /// next record, that record starts with their width again.
127
+ fn string(&mut self, units: &mut Vec<u16>) -> Option<String> {
128
+ let count = usize::from(self.u16()?);
129
+ let flags = self.byte()?;
130
+ let runs = if flags & 0x08 != 0 { usize::from(self.u16()?) } else { 0 };
131
+ let extended = if flags & 0x04 != 0 { self.u32()? as usize } else { 0 };
132
+ let mut high = flags & 1 != 0;
133
+ let mut out = String::with_capacity(count);
134
+ let mut left = count;
135
+ while left > 0 {
136
+ let part = self.parts.get(self.part)?;
137
+ if self.at >= part.len() {
138
+ self.part += 1;
139
+ self.at = 1;
140
+ high = self.parts.get(self.part)?.first()? & 1 != 0;
141
+ continue;
142
+ }
143
+ let width = if high { 2 } else { 1 };
144
+ let take = ((part.len() - self.at) / width).min(left);
145
+ if take == 0 {
146
+ return None;
147
+ }
148
+ decode(&part[self.at..self.at + take * width], high, units, &mut out);
149
+ self.at += take * width;
150
+ left -= take;
151
+ }
152
+ flush(units, &mut out);
153
+ self.skip(runs * 4 + extended)?;
154
+ Some(out)
155
+ }
156
+ }
@@ -0,0 +1,342 @@
1
+ //! Legacy Excel workbooks (`.xls`): BIFF8 (Excel 97 to 2003) and BIFF5
2
+ //! (Excel 5.0 and 95), the `Workbook` or `Book` stream of a compound file,
3
+ //! a sequence of records. Cell values come from the cells' records (shared
4
+ //! or inline strings, numbers, RK numbers, booleans, errors, formula
5
+ //! results) and show through their number formats as in XLSX. Comments,
6
+ //! charts and macro sheets are left out.
7
+
8
+ mod strings;
9
+ #[cfg(test)]
10
+ mod tests;
11
+
12
+ use super::Budget;
13
+ use super::content::{Grid, Sheet, Value};
14
+ use super::numbers::{Shown, built_in, classify, push_number};
15
+ use super::ole::Ole;
16
+ use crate::{Error, Result};
17
+ use strings::Encoding;
18
+
19
+ const BOF: u16 = 0x0809;
20
+ const EOF: u16 = 0x000A;
21
+ const CONTINUE: u16 = 0x003C;
22
+ /// BIFF8, the version of Excel 97 and later; BIFF5, of Excel 5.0 and 95.
23
+ const BIFF8: u16 = 0x0600;
24
+ const BIFF5: u16 = 0x0500;
25
+
26
+ fn malformed(reason: &str) -> Error {
27
+ Error::malformed("XLS", reason)
28
+ }
29
+
30
+ /// The workbook's sheets, in order, its chart and macro sheets left out.
31
+ ///
32
+ /// # Errors
33
+ /// [`Error::UnsupportedDocument`] for a compound file that is no workbook,
34
+ /// one older than Excel 5.0 or in a code page other than Western European,
35
+ /// [`Error::PasswordRequired`] for an encrypted one, [`Error::Malformed`]
36
+ /// for a damaged one.
37
+ pub fn content(ole: &Ole<'_>, budget: &Budget<'_>) -> Result<Vec<Sheet>> {
38
+ let stream = match ole.stream("Workbook", budget)? {
39
+ Some(stream) => stream,
40
+ None => ole.stream("Book", budget)?.ok_or_else(|| {
41
+ Error::UnsupportedDocument(
42
+ "an Office file that is not a workbook (a legacy .doc?)".to_owned(),
43
+ )
44
+ })?,
45
+ };
46
+ let book = globals(&stream, budget)?;
47
+ let mut scratch = String::new();
48
+ let mut sheets = Vec::with_capacity(book.sheets.len());
49
+ for listed in &book.sheets {
50
+ let cells = cells(&stream, listed.offset, &book, &mut scratch, budget)?;
51
+ sheets.push(Sheet {
52
+ name: listed.name.clone(),
53
+ hidden: listed.hidden,
54
+ cells,
55
+ ..Sheet::default()
56
+ });
57
+ }
58
+ Ok(sheets)
59
+ }
60
+
61
+ /// One record: its type and its body, `CONTINUE` records left apart.
62
+ fn records(data: &[u8], mut at: usize) -> impl Iterator<Item = (u16, &[u8])> {
63
+ std::iter::from_fn(move || {
64
+ let header = data.get(at..at + 4)?;
65
+ let kind = u16::from_le_bytes([header[0], header[1]]);
66
+ let length = usize::from(u16::from_le_bytes([header[2], header[3]]));
67
+ let body = data.get(at + 4..at + 4 + length)?;
68
+ at += 4 + length;
69
+ Some((kind, body))
70
+ })
71
+ }
72
+
73
+ fn u16_at(body: &[u8], at: usize) -> Option<u16> {
74
+ Some(u16::from_le_bytes(body.get(at..at + 2)?.try_into().ok()?))
75
+ }
76
+
77
+ fn u32_at(body: &[u8], at: usize) -> Option<u32> {
78
+ Some(u32::from_le_bytes(body.get(at..at + 4)?.try_into().ok()?))
79
+ }
80
+
81
+ /// A worksheet the workbook lists.
82
+ struct Listed {
83
+ name: String,
84
+ hidden: bool,
85
+ offset: usize,
86
+ }
87
+
88
+ /// What the workbook's global records say: its sheets, its shared strings,
89
+ /// how each cell format shows numbers, and its date system.
90
+ struct Book {
91
+ sheets: Vec<Listed>,
92
+ strings: Vec<String>,
93
+ shown: Vec<Shown>,
94
+ date1904: bool,
95
+ encoding: Encoding,
96
+ }
97
+
98
+ impl Book {
99
+ /// How the cell format `xf` shows numbers.
100
+ fn shown(&self, xf: Option<u16>) -> Shown {
101
+ xf.and_then(|xf| self.shown.get(usize::from(xf))).copied().unwrap_or(Shown::General)
102
+ }
103
+
104
+ /// A string in the workbook's encoding.
105
+ fn string(&self, bytes: &[u8], at: usize, wide_count: bool) -> String {
106
+ strings::string(bytes, at, wide_count, self.encoding).unwrap_or_default()
107
+ }
108
+ }
109
+
110
+ fn globals(stream: &[u8], budget: &Budget<'_>) -> Result<Book> {
111
+ let mut all = records(stream, 0).peekable();
112
+ let encoding = match all.next() {
113
+ Some((BOF, body)) if u16_at(body, 0) == Some(BIFF8) => Encoding::Unicode,
114
+ Some((BOF, body)) if u16_at(body, 0) == Some(BIFF5) => Encoding::Windows1252,
115
+ Some((BOF, _)) => {
116
+ return Err(Error::UnsupportedDocument(
117
+ "a workbook older than Excel 5.0 (.xls); save it as .xlsx".into(),
118
+ ));
119
+ }
120
+ _ => return Err(malformed("the workbook stream does not start with BOF")),
121
+ };
122
+ let mut book = Book {
123
+ sheets: Vec::new(),
124
+ strings: Vec::new(),
125
+ shown: Vec::new(),
126
+ date1904: false,
127
+ encoding,
128
+ };
129
+ let mut formats: Vec<(u16, Shown)> = Vec::new();
130
+ let mut format_ids = Vec::new();
131
+ while let Some((kind, body)) = all.next() {
132
+ budget.tick()?;
133
+ match kind {
134
+ EOF => break,
135
+ // FILEPASS: the rest is encrypted.
136
+ 0x002F => return Err(Error::PasswordRequired),
137
+ 0x0022 => book.date1904 = u16_at(body, 0) == Some(1),
138
+ // CODEPAGE: BIFF5's strings are bytes of it; Western European
139
+ // (or ASCII) only, rather than misread text.
140
+ 0x0042 if encoding == Encoding::Windows1252 => {
141
+ let page = u16_at(body, 0).unwrap_or(1252);
142
+ if !matches!(page, 1252 | 367 | 0x8001) {
143
+ return Err(Error::UnsupportedDocument(format!(
144
+ "an Excel 5.0/95 workbook in code page {page} (.xls); save it as .xlsx"
145
+ )));
146
+ }
147
+ }
148
+ 0x041E => {
149
+ if let Some(id) = u16_at(body, 0) {
150
+ let code = book.string(body, 2, encoding == Encoding::Unicode);
151
+ formats.push((id, classify(&code)));
152
+ }
153
+ }
154
+ 0x00E0 => format_ids.push(u16_at(body, 2).unwrap_or(0)),
155
+ 0x0085 => {
156
+ let (Some(offset), Some(&state), Some(&kind)) =
157
+ (u32_at(body, 0), body.get(4), body.get(5))
158
+ else {
159
+ continue;
160
+ };
161
+ // Worksheets only: macro sheets, charts and modules hold no table.
162
+ if kind == 0 {
163
+ let name = book.string(body, 6, false);
164
+ book.sheets.push(Listed {
165
+ name,
166
+ hidden: state & 3 != 0,
167
+ offset: offset as usize,
168
+ });
169
+ }
170
+ }
171
+ 0x00FC => {
172
+ let mut parts = vec![body];
173
+ while let Some(&(CONTINUE, more)) = all.peek() {
174
+ parts.push(more);
175
+ all.next();
176
+ }
177
+ book.strings = strings::shared(&parts, budget)?;
178
+ }
179
+ _ => {}
180
+ }
181
+ }
182
+ book.shown = format_ids
183
+ .iter()
184
+ .map(|&id| {
185
+ formats
186
+ .iter()
187
+ .find(|&&(custom, _)| custom == id)
188
+ .map_or_else(|| built_in(u32::from(id)), |&(_, shown)| shown)
189
+ })
190
+ .collect();
191
+ Ok(book)
192
+ }
193
+
194
+ /// An `RK` number: a 30-bit integer or the high bits of a double, either
195
+ /// perhaps divided by 100.
196
+ fn rk(value: u32) -> f64 {
197
+ #[expect(clippy::cast_possible_wrap, reason = "the bits are a signed integer")]
198
+ let number = if value & 2 != 0 {
199
+ f64::from((value as i32) >> 2)
200
+ } else {
201
+ f64::from_bits(u64::from(value & 0xFFFF_FFFC) << 32)
202
+ };
203
+ if value & 1 != 0 { number / 100.0 } else { number }
204
+ }
205
+
206
+ /// An error code as Excel shows it.
207
+ const fn error_text(code: u8) -> &'static str {
208
+ match code {
209
+ 0x00 => "#NULL!",
210
+ 0x07 => "#DIV/0!",
211
+ 0x0F => "#VALUE!",
212
+ 0x17 => "#REF!",
213
+ 0x1D => "#NAME?",
214
+ 0x24 => "#NUM!",
215
+ _ => "#N/A",
216
+ }
217
+ }
218
+
219
+ /// A worksheet's cells, from its BOF at `offset` to its EOF.
220
+ fn cells(
221
+ stream: &[u8],
222
+ offset: usize,
223
+ book: &Book,
224
+ scratch: &mut String,
225
+ budget: &Budget<'_>,
226
+ ) -> Result<Grid> {
227
+ let mut grid = Grid::default();
228
+ let mut all = records(stream, offset);
229
+ if !matches!(all.next(), Some((BOF, _))) {
230
+ return Err(malformed("a sheet that does not start with BOF"));
231
+ }
232
+ // A formula's string result follows it in a STRING record.
233
+ let mut pending: Option<(u32, u32)> = None;
234
+ for (kind, body) in all {
235
+ budget.tick()?;
236
+ let (Some(row), Some(column)) = (u16_at(body, 0), u16_at(body, 2)) else {
237
+ if kind == EOF {
238
+ break;
239
+ }
240
+ continue;
241
+ };
242
+ let (row, column) = (u32::from(row), u32::from(column));
243
+ // NUMBER, RK and numeric FORMULA results, written after the match.
244
+ let number = match kind {
245
+ EOF => break,
246
+ // LABELSST
247
+ 0x00FD => {
248
+ let text = u32_at(body, 6).and_then(|index| book.strings.get(index as usize));
249
+ text_cell(&mut grid, (row, column), text.map_or("", String::as_str));
250
+ None
251
+ }
252
+ // LABEL, RSTRING
253
+ 0x0204 | 0x00D6 => {
254
+ text_cell(&mut grid, (row, column), &book.string(body, 6, true));
255
+ None
256
+ }
257
+ 0x0203 => {
258
+ body.get(6..14).map(|bits| f64::from_le_bytes(bits.try_into().unwrap_or([0; 8])))
259
+ }
260
+ 0x027E => u32_at(body, 6).map(rk),
261
+ // MULRK: (format, RK) pairs from the first column on.
262
+ 0x00BD => {
263
+ let pairs = body.get(4..body.len().saturating_sub(2)).unwrap_or_default();
264
+ for (index, pair) in (0..).zip(pairs.as_chunks::<6>().0) {
265
+ let value = rk(u32::from_le_bytes([pair[2], pair[3], pair[4], pair[5]]));
266
+ let shown = book.shown(Some(u16::from_le_bytes([pair[0], pair[1]])));
267
+ number_cell(
268
+ &mut grid,
269
+ (row, column + index),
270
+ value,
271
+ shown,
272
+ book.date1904,
273
+ scratch,
274
+ );
275
+ }
276
+ None
277
+ }
278
+ // BOOLERR
279
+ 0x0205 => {
280
+ let (Some(&value), Some(&error)) = (body.get(6), body.get(7)) else { continue };
281
+ flag_cell(&mut grid, (row, column), value, error != 0);
282
+ None
283
+ }
284
+ // FORMULA: its cached result.
285
+ 0x0006 => {
286
+ let Some(result) = body.get(6..14) else { continue };
287
+ if result[6..8] == [0xFF, 0xFF] {
288
+ match result[0] {
289
+ 0 => pending = Some((row, column)),
290
+ 1 => flag_cell(&mut grid, (row, column), result[2], false),
291
+ 2 => flag_cell(&mut grid, (row, column), result[2], true),
292
+ _ => {}
293
+ }
294
+ None
295
+ } else {
296
+ Some(f64::from_le_bytes(result.try_into().unwrap_or([0; 8])))
297
+ }
298
+ }
299
+ // STRING: the string result of the formula before it.
300
+ 0x0207 => {
301
+ if let Some(at) = pending.take() {
302
+ text_cell(&mut grid, at, &book.string(body, 0, true));
303
+ }
304
+ None
305
+ }
306
+ _ => None,
307
+ };
308
+ if let Some(number) = number {
309
+ let shown = book.shown(u16_at(body, 4));
310
+ number_cell(&mut grid, (row, column), number, shown, book.date1904, scratch);
311
+ }
312
+ }
313
+ Ok(grid.finish())
314
+ }
315
+
316
+ fn number_cell(
317
+ grid: &mut Grid,
318
+ (row, column): (u32, u32),
319
+ number: f64,
320
+ shown: Shown,
321
+ date1904: bool,
322
+ scratch: &mut String,
323
+ ) {
324
+ let value = push_number(grid.buffer(), number, shown, date1904, scratch);
325
+ grid.end_cell(row, column, value);
326
+ }
327
+
328
+ fn text_cell(grid: &mut Grid, (row, column): (u32, u32), text: &str) {
329
+ grid.buffer().push_str(text);
330
+ grid.end_cell(row, column, Value::Text);
331
+ }
332
+
333
+ /// A boolean, or an error code.
334
+ fn flag_cell(grid: &mut Grid, (row, column): (u32, u32), value: u8, error: bool) {
335
+ let (text, value) = match (error, value) {
336
+ (true, code) => (error_text(code), Value::Error),
337
+ (false, 0) => ("FALSE", Value::Bool(false)),
338
+ (false, _) => ("TRUE", Value::Bool(true)),
339
+ };
340
+ grid.buffer().push_str(text);
341
+ grid.end_cell(row, column, value);
342
+ }
@@ -0,0 +1,155 @@
1
+ //! A workbook's cell formats (`styles.xml`): for each, how it shows numbers
2
+ //! and how it looks.
3
+
4
+ use std::borrow::Cow;
5
+ use std::sync::Arc;
6
+
7
+ use crate::Result;
8
+ use crate::read::Budget;
9
+ use crate::read::content::Look;
10
+ use crate::read::numbers::{Shown, built_in, classify};
11
+ use crate::xml::reader::{Event, XmlReader};
12
+
13
+ /// The cell formats (`cellXfs`), by index.
14
+ #[derive(Default)]
15
+ pub struct Formats {
16
+ pub shown: Vec<Shown>,
17
+ pub looks: Vec<Arc<Look>>,
18
+ }
19
+
20
+ /// Where in `styles.xml` the reader is.
21
+ #[derive(Clone, Copy, PartialEq, Eq)]
22
+ enum Section {
23
+ Other,
24
+ Fonts,
25
+ Fills,
26
+ Borders,
27
+ CellFormats,
28
+ }
29
+
30
+ /// A font, fill and border by their index, before the formats pick them.
31
+ #[derive(Default)]
32
+ struct Parts {
33
+ codes: Vec<(u32, String)>,
34
+ fonts: Vec<Look>,
35
+ fills: Vec<Option<String>>,
36
+ borders: Vec<Option<String>>,
37
+ }
38
+
39
+ pub fn formats(text: &str, budget: &Budget<'_>) -> Result<Formats> {
40
+ let mut xml = XmlReader::new(text, budget.max_depth());
41
+ let (mut parts, mut section, mut side) = (Parts::default(), Section::Other, "");
42
+ let mut formats = Formats { shown: Vec::new(), looks: Vec::new() };
43
+ while let Some(event) = xml.next()? {
44
+ match event {
45
+ Event::Start(tag) => match (section, tag.name()) {
46
+ (_, "numFmt") => {
47
+ if let (Some(id), Some(code)) = (tag.attr("numFmtId"), tag.attr("formatCode")) {
48
+ parts.codes.push((id.parse().unwrap_or(u32::MAX), code.into_owned()));
49
+ }
50
+ }
51
+ (_, "fonts") => section = Section::Fonts,
52
+ (_, "fills") => section = Section::Fills,
53
+ (_, "borders") => section = Section::Borders,
54
+ (_, "cellXfs") => section = Section::CellFormats,
55
+ (Section::Fonts, "font") => parts.fonts.push(Look::default()),
56
+ (Section::Fonts, name) => font_part(&mut parts.fonts, name, tag),
57
+ (Section::Fills, "fill") => parts.fills.push(None),
58
+ (Section::Fills, "fgColor") => {
59
+ if let Some(fill) = parts.fills.last_mut() {
60
+ *fill = tag.attr("rgb").map(|rgb| rgb.to_uppercase());
61
+ }
62
+ }
63
+ (Section::Borders, "border") => parts.borders.push(None),
64
+ (Section::Borders, name) if side.is_empty() => {
65
+ side = name;
66
+ if name == "left"
67
+ && let Some(border) = parts.borders.last_mut()
68
+ {
69
+ *border = tag.attr("style").map(Cow::into_owned);
70
+ }
71
+ }
72
+ (Section::CellFormats, "xf") => {
73
+ let id = |name| tag.parse::<usize>(name);
74
+ let number = tag.parse("numFmtId").unwrap_or(0);
75
+ let custom = parts.codes.iter().find(|(id, _)| *id == number);
76
+ let shown = custom.map_or_else(|| built_in(number), |(_, code)| classify(code));
77
+ let font = parts.fonts.get(id("fontId").unwrap_or(0));
78
+ let mut look = font.cloned().unwrap_or_default();
79
+ look.fill = parts.fills.get(id("fillId").unwrap_or(0)).cloned().flatten();
80
+ look.border = parts.borders.get(id("borderId").unwrap_or(0)).cloned().flatten();
81
+ look.number_format = custom.map_or_else(
82
+ || built_in_code(number).map_or_else(|| number.to_string(), str::to_owned),
83
+ |(_, code)| code.clone(),
84
+ );
85
+ formats.shown.push(shown);
86
+ formats.looks.push(Arc::new(look));
87
+ }
88
+ (Section::CellFormats, "alignment") => {
89
+ if let Some(look) = formats.looks.last_mut().map(Arc::make_mut) {
90
+ look.align = tag.attr("horizontal").map(Cow::into_owned);
91
+ look.valign = tag.attr("vertical").map(Cow::into_owned);
92
+ look.wrap = tag.on("wrapText");
93
+ }
94
+ }
95
+ _ => {}
96
+ },
97
+ Event::End(name) => match name {
98
+ "fonts" | "fills" | "borders" | "cellXfs" => section = Section::Other,
99
+ _ if name == side => side = "",
100
+ _ => {}
101
+ },
102
+ Event::Text(_) => {}
103
+ }
104
+ }
105
+ Ok(formats)
106
+ }
107
+
108
+ /// A property of the font being read.
109
+ fn font_part(fonts: &mut [Look], name: &str, tag: crate::xml::reader::Tag<'_>) {
110
+ let Some(font) = fonts.last_mut() else { return };
111
+ match name {
112
+ "b" => font.bold = tag.flag("val"),
113
+ "i" => font.italic = tag.flag("val"),
114
+ "u" => font.underline = tag.attr("val").is_none_or(|style| style != "none"),
115
+ "sz" => font.size = tag.parse("val").unwrap_or(0.0),
116
+ "name" => font.font = tag.attr("val").map(Cow::into_owned).unwrap_or_default(),
117
+ "color" => font.color = tag.attr("rgb").map(|rgb| rgb.to_uppercase()),
118
+ _ => {}
119
+ }
120
+ }
121
+
122
+ /// The codes of Excel's built-in number formats (ECMA-376 Part 1, 18.8.30).
123
+ const fn built_in_code(id: u32) -> Option<&'static str> {
124
+ Some(match id {
125
+ 0 => "General",
126
+ 1 => "0",
127
+ 2 => "0.00",
128
+ 3 => "#,##0",
129
+ 4 => "#,##0.00",
130
+ 9 => "0%",
131
+ 10 => "0.00%",
132
+ 11 => "0.00E+00",
133
+ 12 => "# ?/?",
134
+ 13 => "# ??/??",
135
+ 14 => "mm-dd-yy",
136
+ 15 => "d-mmm-yy",
137
+ 16 => "d-mmm",
138
+ 17 => "mmm-yy",
139
+ 18 => "h:mm AM/PM",
140
+ 19 => "h:mm:ss AM/PM",
141
+ 20 => "h:mm",
142
+ 21 => "h:mm:ss",
143
+ 22 => "m/d/yy h:mm",
144
+ 37 => "#,##0 ;(#,##0)",
145
+ 38 => "#,##0 ;[Red](#,##0)",
146
+ 39 => "#,##0.00;(#,##0.00)",
147
+ 40 => "#,##0.00;[Red](#,##0.00)",
148
+ 45 => "mm:ss",
149
+ 46 => "[h]:mm:ss",
150
+ 47 => "mmss.0",
151
+ 48 => "##0.0E+0",
152
+ 49 => "@",
153
+ _ => return None,
154
+ })
155
+ }