imprimatur 0.2.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/Cargo.lock +123 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +98 -20
  6. data/ext/imprimatur_native/cli/Cargo.toml +1 -1
  7. data/ext/imprimatur_native/core/Cargo.toml +6 -1
  8. data/ext/imprimatur_native/core/README.md +12 -3
  9. data/ext/imprimatur_native/core/src/archive.rs +3 -16
  10. data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
  11. data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
  12. data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
  13. data/ext/imprimatur_native/core/src/doc.rs +86 -2
  14. data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
  15. data/ext/imprimatur_native/core/src/docx.rs +91 -28
  16. data/ext/imprimatur_native/core/src/error.rs +12 -0
  17. data/ext/imprimatur_native/core/src/font.rs +229 -0
  18. data/ext/imprimatur_native/core/src/lib.rs +3 -0
  19. data/ext/imprimatur_native/core/src/pdf/layout.rs +38 -15
  20. data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
  21. data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
  22. data/ext/imprimatur_native/core/src/pdf.rs +31 -3
  23. data/ext/imprimatur_native/core/src/read/content.rs +75 -2
  24. data/ext/imprimatur_native/core/src/read/json.rs +94 -7
  25. data/ext/imprimatur_native/core/src/read/numbers.rs +65 -42
  26. data/ext/imprimatur_native/core/src/read/ods.rs +1 -2
  27. data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
  28. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
  29. data/ext/imprimatur_native/core/src/read/xls.rs +1 -1
  30. data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
  31. data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -19
  32. data/ext/imprimatur_native/core/src/read.rs +4 -1
  33. data/ext/imprimatur_native/core/src/script.rs +116 -0
  34. data/ext/imprimatur_native/core/src/style.rs +21 -0
  35. data/ext/imprimatur_native/core/src/test_support.rs +3 -2
  36. data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
  37. data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
  38. data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
  39. data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
  40. data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
  41. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
  42. data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
  43. data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
  44. data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
  45. data/ext/imprimatur_native/ffi/src/archive.rs +1 -3
  46. data/ext/imprimatur_native/ffi/src/document.rs +48 -1
  47. data/ext/imprimatur_native/ffi/src/lib.rs +38 -1
  48. data/ext/imprimatur_native/ffi/src/read.rs +8 -11
  49. data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
  50. data/lib/imprimatur/document.rb +30 -6
  51. data/lib/imprimatur/reader.rb +2 -2
  52. data/lib/imprimatur/spreadsheet.rb +151 -0
  53. data/lib/imprimatur/version.rb +1 -1
  54. data/lib/imprimatur/xlsx.rb +25 -4
  55. data/lib/imprimatur.rb +15 -3
  56. metadata +11 -1
@@ -2,15 +2,18 @@
2
2
  //! glyphs, paragraphs by the gaps between lines, headings by size, and
3
3
  //! the running headers and footers left out.
4
4
 
5
+ mod bidi;
5
6
  #[cfg(test)]
6
7
  mod tests;
7
8
 
8
9
  use std::collections::HashMap;
9
10
  use std::ops::Range;
10
11
 
12
+ use self::bidi::{Scratch, is_mark};
11
13
  use super::content::{Glyph, PageText};
12
14
  use crate::read::content::{Block, Bounds, Page, Placed, Text};
13
15
  use crate::read::span;
16
+ use crate::script::Counts;
14
17
 
15
18
  /// Share of the page height, at the top and the bottom, where running
16
19
  /// headers and footers live.
@@ -61,7 +64,9 @@ pub fn lines(page: &PageText) -> PageLines {
61
64
  height,
62
65
  images: u32::try_from(page.images).unwrap_or(u32::MAX),
63
66
  };
67
+ let right_to_left = right_to_left(page);
64
68
  let mut pieces: Vec<&str> = Vec::new();
69
+ let mut scratch = Scratch::default();
65
70
  let mut glyphs: Vec<(f64, f64, &Glyph)> = Vec::with_capacity(page.glyphs.len());
66
71
  for turns in 0..4_u8 {
67
72
  glyphs.clear();
@@ -89,7 +94,7 @@ pub fn lines(page: &PageText) -> PageLines {
89
94
  let row = &mut glyphs[start..end];
90
95
  row.sort_by(|a, b| a.0.total_cmp(&b.0));
91
96
  let added = out.lines.len();
92
- line(page, row, turns, &mut pieces, &mut out);
97
+ line(page, row, turns, right_to_left, &mut pieces, &mut scratch, &mut out);
93
98
  if let Some(line) = out.lines.get_mut(added)
94
99
  && turns == 0
95
100
  && (line.y > height * (1.0 - EDGE) || line.y < height * EDGE)
@@ -103,14 +108,31 @@ pub fn lines(page: &PageText) -> PageLines {
103
108
  out
104
109
  }
105
110
 
111
+ /// Whether a page reads right to left: more of its letters, those in the
112
+ /// top and bottom margins of upright text aside, are right to left.
113
+ fn right_to_left(page: &PageText) -> bool {
114
+ let height = page.size.1.max(1.0);
115
+ let mut counts = Counts::default();
116
+ for glyph in &page.glyphs {
117
+ if glyph.turns == 0 && (glyph.y > height * (1.0 - EDGE) || glyph.y < height * EDGE) {
118
+ continue;
119
+ }
120
+ counts.add(&page.text[glyph.text.start as usize..glyph.text.end as usize]);
121
+ }
122
+ counts.right_to_left()
123
+ }
124
+
106
125
  /// One line's text, appended to `out.text`: spaces where glyphs leave a
107
126
  /// word gap, a glyph drawn twice on itself (fake bold) kept once, and
108
- /// right-to-left lines in reading order. A line with no text is not added.
127
+ /// right-to-left text in reading order, in the page's direction. A line
128
+ /// with no text is not added.
109
129
  fn line<'p>(
110
130
  page: &'p PageText,
111
131
  row: &[(f64, f64, &Glyph)],
112
132
  turns: u8,
133
+ right_to_left: bool,
113
134
  pieces: &mut Vec<&'p str>,
135
+ scratch: &mut Scratch,
114
136
  out: &mut PageLines,
115
137
  ) {
116
138
  // Glyph texts and the spaces between them, left to right as drawn.
@@ -139,9 +161,7 @@ fn line<'p>(
139
161
  count += 1;
140
162
  previous = Some((x, glyph, glyph_text));
141
163
  }
142
- if right_to_left(pieces) {
143
- reorder(pieces);
144
- }
164
+ bidi::reading_order(pieces, right_to_left, scratch);
145
165
  // Spaces collapsed and trimmed on the way in.
146
166
  let start = out.text.len();
147
167
  for ch in pieces.iter().flat_map(|piece| piece.chars()) {
@@ -172,120 +192,6 @@ fn margin_key(text: &str) -> u64 {
172
192
  hash.max(1)
173
193
  }
174
194
 
175
- /// The strong direction of a character: `Some(true)` for the scripts
176
- /// written right to left, `Some(false)` for other letters and for digits
177
- /// (which keep their order within right-to-left text), `None` for spaces
178
- /// and punctuation.
179
- ///
180
- /// Right to left: Hebrew (and Yiddish, Ladino), Arabic (and Persian, Urdu,
181
- /// Pashto, Sindhi, Kurdish, Uyghur), Syriac, Thaana (Dhivehi), N'Ko,
182
- /// Samaritan, Mandaic, their presentation forms; Adlam (Fulani), Hanifi
183
- /// Rohingya, Mende Kikakui, Yezidi, Garay and the historic scripts
184
- /// (Phoenician, Aramaic, Sogdian...); and the right-to-left mark.
185
- fn direction(ch: char) -> Option<bool> {
186
- match ch {
187
- '\u{0590}'..='\u{08FF}'
188
- | '\u{200F}'
189
- | '\u{FB1D}'..='\u{FDFF}'
190
- | '\u{FE70}'..='\u{FEFC}'
191
- | '\u{10800}'..='\u{10FFF}'
192
- | '\u{1E800}'..='\u{1EFFF}' => Some(true),
193
- '\u{200E}' => Some(false),
194
- ch if ch.is_alphanumeric() => Some(false),
195
- _ => None,
196
- }
197
- }
198
-
199
- /// Combining marks: diacritics, and the vowel points and signs of the
200
- /// right-to-left scripts. Drawn as glyphs of their own, they never start a
201
- /// word.
202
- const fn is_mark(ch: char) -> bool {
203
- matches!(
204
- ch,
205
- '\u{0300}'..='\u{036F}'
206
- // Hebrew points and cantillation.
207
- | '\u{0591}'..='\u{05C7}'
208
- | '\u{FB1E}'
209
- // Arabic harakat, Quranic annotation, extended Arabic marks.
210
- | '\u{0610}'..='\u{061A}'
211
- | '\u{064B}'..='\u{065F}'
212
- | '\u{0670}'
213
- | '\u{06D6}'..='\u{06ED}'
214
- | '\u{0898}'..='\u{089F}'
215
- | '\u{08CA}'..='\u{08E1}'
216
- | '\u{08E3}'..='\u{08FF}'
217
- // Syriac, Thaana, N'Ko, Samaritan, Mandaic.
218
- | '\u{0711}'
219
- | '\u{0730}'..='\u{074A}'
220
- | '\u{07A6}'..='\u{07B0}'
221
- | '\u{07EB}'..='\u{07F3}'
222
- | '\u{07FD}'
223
- | '\u{0816}'..='\u{0819}'
224
- | '\u{081B}'..='\u{0823}'
225
- | '\u{0825}'..='\u{0827}'
226
- | '\u{0829}'..='\u{082D}'
227
- | '\u{0859}'..='\u{085B}'
228
- | '\u{1AB0}'..='\u{1AFF}'
229
- | '\u{20D0}'..='\u{20FF}'
230
- | '\u{FE20}'..='\u{FE2F}'
231
- // Hanifi Rohingya, Adlam.
232
- | '\u{10D24}'..='\u{10D27}'
233
- | '\u{1E944}'..='\u{1E94A}'
234
- )
235
- }
236
-
237
- fn piece_direction(piece: &str) -> Option<bool> {
238
- piece.chars().find_map(direction)
239
- }
240
-
241
- /// Whether more of the line's letters read right to left than left to right.
242
- fn right_to_left(pieces: &[&str]) -> bool {
243
- let (mut right, mut left) = (0_usize, 0_usize);
244
- for ch in pieces.iter().flat_map(|piece| piece.chars()) {
245
- match direction(ch) {
246
- Some(true) => right += 1,
247
- Some(false) if ch.is_alphabetic() => left += 1,
248
- _ => {}
249
- }
250
- }
251
- right > left
252
- }
253
-
254
- /// Puts a right-to-left line, drawn left to right, in reading order: runs
255
- /// of left-to-right text (Latin words, numbers) keep their order inside,
256
- /// everything else reads from the right. Spaces and punctuation between two
257
- /// left-to-right pieces belong to their run.
258
- fn reorder(pieces: &mut [&str]) {
259
- let directions: Vec<Option<bool>> = pieces.iter().map(|piece| piece_direction(piece)).collect();
260
- let left_to_right: Vec<bool> = (0..pieces.len())
261
- .map(|at| {
262
- directions[at].map_or_else(
263
- || {
264
- let before = directions[..at].iter().rev().find_map(|direction| *direction);
265
- let after = directions[at + 1..].iter().find_map(|direction| *direction);
266
- before == Some(false) && after == Some(false)
267
- },
268
- |rtl| !rtl,
269
- )
270
- })
271
- .collect();
272
- pieces.reverse();
273
- let mut at = 0;
274
- let total = pieces.len();
275
- while at < total {
276
- // Reversed, so a left-to-right run is now backwards: turn it back.
277
- if left_to_right[total - 1 - at] {
278
- let start = at;
279
- while at < total && left_to_right[total - 1 - at] {
280
- at += 1;
281
- }
282
- pieces[start..at].reverse();
283
- } else {
284
- at += 1;
285
- }
286
- }
287
- }
288
-
289
195
  /// The document's pages, from their lines: blocks of the lines that are
290
196
  /// not running headers or footers.
291
197
  pub fn pages(pages: &[PageLines]) -> Vec<Page> {
@@ -52,7 +52,7 @@ pub fn content(ole: &Ole<'_>, budget: &Budget<'_>) -> Result<Vec<Sheet>> {
52
52
  name: listed.name.clone(),
53
53
  hidden: listed.hidden,
54
54
  cells,
55
- comments: Vec::new(),
55
+ ..Sheet::default()
56
56
  });
57
57
  }
58
58
  Ok(sheets)
@@ -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
+ }
@@ -3,19 +3,21 @@
3
3
  //! digits), formulas without a cached result as `=FORMULA`, then the sheet's
4
4
  //! comments.
5
5
 
6
+ mod formats;
6
7
  #[cfg(test)]
7
8
  mod tests;
8
9
 
9
10
  use std::sync::Arc;
10
11
 
12
+ use self::formats::{Formats, formats};
11
13
  use super::Budget;
12
- use super::content::{Grid, Note, Sheet, Value};
13
- use super::numbers::{Shown, push_number, styles};
14
+ use super::content::{Columns, Grid, Note, Sheet, Styled, Validation, Value};
15
+ use super::numbers::{Shown, push_number};
14
16
  use super::package::Package;
15
17
  use crate::Result;
16
18
  use crate::xlsx::parse_cell_reference;
17
19
  use crate::xlsx::text::unescape as unescape_xstring;
18
- use crate::xml::reader::{Event, XmlReader};
20
+ use crate::xml::reader::{Event, Tag, XmlReader};
19
21
 
20
22
  /// A cell's `t` attribute.
21
23
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -51,13 +53,24 @@ impl Strings {
51
53
  }
52
54
  }
53
55
 
54
- /// Where a cell's text events go.
56
+ /// Where a cell's text events go, or a validation's formulas'.
55
57
  #[derive(Clone, Copy, PartialEq, Eq)]
56
58
  enum Into {
57
59
  Nowhere,
58
60
  Value,
59
61
  Formula,
60
62
  Inline,
63
+ First,
64
+ Second,
65
+ }
66
+
67
+ /// What a sheet holds besides its cells' values.
68
+ #[derive(Default)]
69
+ struct Parts {
70
+ columns: Vec<Columns>,
71
+ validations: Vec<Validation>,
72
+ /// Cells with a format of their own: row, column, format.
73
+ styled: Vec<(u32, u32, usize)>,
61
74
  }
62
75
 
63
76
  pub fn content(package: &Package<'_>, budget: &Budget<'_>) -> Result<Vec<Sheet>> {
@@ -67,21 +80,31 @@ pub fn content(package: &Package<'_>, budget: &Budget<'_>) -> Result<Vec<Sheet>>
67
80
  let (listed, date1904) = workbook(&text, &relationships, budget)?;
68
81
  let strings = package
69
82
  .parse_related(&relationships, "sharedStrings", |text| shared_strings(text, budget))?;
70
- let shown = package.parse_related(&relationships, "styles", |text| styles(text, budget))?;
71
- let book = Book { strings: &strings, shown: &shown, date1904 };
83
+ let formats = package.parse_related(&relationships, "styles", |text| formats(text, budget))?;
84
+ let book = Book { strings: &strings, shown: &formats.shown, date1904 };
72
85
  let mut sheets = Vec::with_capacity(listed.len());
73
86
  let mut scratch = String::new();
74
87
  for sheet in listed {
75
88
  budget.check()?;
76
89
  let Some(text) = package.text(&sheet.part)? else { continue };
77
- let cells = cells(&text, &book, &mut scratch, budget)?;
90
+ let (cells, parts) = cells(&text, &book, &mut scratch, budget)?;
78
91
  let comments = match package.related(&sheet.part, "comments")? {
79
92
  Some(part) => package
80
93
  .text(&part)?
81
94
  .map_or_else(|| Ok(Vec::new()), |text| comments(&text, budget))?,
82
95
  None => Vec::new(),
83
96
  };
84
- sheets.push(Sheet { name: sheet.name, hidden: sheet.hidden, cells, comments });
97
+ let styles = styled(&parts.styled, &formats);
98
+ let (columns, validations) = (parts.columns, parts.validations);
99
+ sheets.push(Sheet {
100
+ name: sheet.name,
101
+ hidden: sheet.hidden,
102
+ cells,
103
+ comments,
104
+ columns,
105
+ validations,
106
+ styles,
107
+ });
85
108
  }
86
109
  Ok(sheets)
87
110
  }
@@ -155,8 +178,13 @@ struct Head {
155
178
  style: usize,
156
179
  }
157
180
 
158
- fn cells(text: &str, book: &Book<'_>, scratch: &mut String, budget: &Budget<'_>) -> Result<Grid> {
159
- let mut grid = Grid::default();
181
+ fn cells(
182
+ text: &str,
183
+ book: &Book<'_>,
184
+ scratch: &mut String,
185
+ budget: &Budget<'_>,
186
+ ) -> Result<(Grid, Parts)> {
187
+ let (mut grid, mut parts, mut rule) = (Grid::default(), Parts::default(), None);
160
188
  let mut xml = XmlReader::new(text, budget.max_depth());
161
189
  let (mut row, mut next_row, mut next_column) = (0_u32, 0_u32, 0_u32);
162
190
  let (mut value, mut formula, mut inline) = (String::new(), String::new(), String::new());
@@ -166,10 +194,7 @@ fn cells(text: &str, book: &Book<'_>, scratch: &mut String, budget: &Budget<'_>)
166
194
  match event {
167
195
  Event::Start(tag) => match tag.name() {
168
196
  "row" => {
169
- row = tag
170
- .attr("r")
171
- .and_then(|r| r.parse::<u32>().ok())
172
- .map_or(next_row, |r| r.saturating_sub(1));
197
+ row = tag.parse::<u32>("r").map_or(next_row, |r| r.saturating_sub(1));
173
198
  next_column = 0;
174
199
  }
175
200
  "c" => {
@@ -186,7 +211,7 @@ fn cells(text: &str, book: &Book<'_>, scratch: &mut String, budget: &Budget<'_>)
186
211
  Some("d") => Kind::Iso,
187
212
  _ => Kind::Number,
188
213
  };
189
- let style = tag.attr("s").and_then(|s| s.parse().ok()).unwrap_or(0);
214
+ let style = tag.parse("s").unwrap_or(0);
190
215
  head = Some(Head { row: at_row, column, kind, style });
191
216
  value.clear();
192
217
  formula.clear();
@@ -198,17 +223,36 @@ fn cells(text: &str, book: &Book<'_>, scratch: &mut String, budget: &Budget<'_>)
198
223
  into = Into::Inline;
199
224
  }
200
225
  "rPh" => xml.skip()?,
226
+ "col" => parts.columns.extend(columns(tag)),
227
+ "dataValidation" => rule = Some(validation(tag)),
228
+ "formula1" if rule.is_some() => (into, value) = (Into::First, String::new()),
229
+ "formula2" if rule.is_some() => (into, value) = (Into::Second, String::new()),
201
230
  _ => {}
202
231
  },
203
232
  Event::Text(text) => match into {
204
- Into::Value => value.push_str(&text),
233
+ Into::Value | Into::First | Into::Second => value.push_str(&text),
205
234
  Into::Formula => formula.push_str(&text),
206
235
  Into::Inline => inline.push_str(&unescape_xstring(&text)),
207
236
  Into::Nowhere => {}
208
237
  },
209
238
  Event::End("v" | "f" | "t") => into = Into::Nowhere,
239
+ Event::End(end @ ("formula1" | "formula2")) => {
240
+ if let Some(rule) = rule.as_mut() {
241
+ let formula = Some(std::mem::take(&mut value));
242
+ if end == "formula1" {
243
+ rule.formula1 = formula;
244
+ } else {
245
+ rule.formula2 = formula;
246
+ }
247
+ }
248
+ into = Into::Nowhere;
249
+ }
250
+ Event::End("dataValidation") => parts.validations.extend(rule.take()),
210
251
  Event::End("c") => {
211
252
  if let Some(head) = head.take() {
253
+ if head.style != 0 {
254
+ parts.styled.push((head.row, head.column, head.style));
255
+ }
212
256
  write_cell(&mut grid, &head, book, [&value, &formula, &inline], scratch);
213
257
  next_column = head.column + 1;
214
258
  }
@@ -217,7 +261,7 @@ fn cells(text: &str, book: &Book<'_>, scratch: &mut String, budget: &Budget<'_>)
217
261
  Event::End(_) => {}
218
262
  }
219
263
  }
220
- Ok(grid.finish())
264
+ Ok((grid.finish(), parts))
221
265
  }
222
266
 
223
267
  fn write_cell(
@@ -272,6 +316,48 @@ fn write_cell(
272
316
  grid.end_cell(head.row, head.column, kind);
273
317
  }
274
318
 
319
+ /// A `<col>`: a run of columns, from 0.
320
+ fn columns(tag: Tag<'_>) -> Option<Columns> {
321
+ let number = |name| tag.parse::<u32>(name);
322
+ let (first, last) = (number("min")?.checked_sub(1)?, number("max")?.checked_sub(1)?);
323
+ let width = tag.parse("width");
324
+ let hidden = tag.on("hidden");
325
+ Some(Columns { first, last, width, hidden })
326
+ }
327
+
328
+ /// A `<dataValidation>`, its formulas still to come.
329
+ fn validation(tag: Tag<'_>) -> Validation {
330
+ let text = |name| tag.attr(name).map(std::borrow::Cow::into_owned);
331
+ Validation {
332
+ range: text("sqref").unwrap_or_default(),
333
+ kind: text("type").unwrap_or_else(|| "none".to_owned()),
334
+ operator: text("operator"),
335
+ allow_blank: tag.on("allowBlank"),
336
+ // Inverted in the file format: showDropDown="1" hides the arrow.
337
+ dropdown: !tag.on("showDropDown"),
338
+ alert: tag
339
+ .on("showErrorMessage")
340
+ .then(|| text("errorStyle").unwrap_or_else(|| "stop".to_owned())),
341
+ error_title: text("errorTitle"),
342
+ error: text("error"),
343
+ prompt_title: text("promptTitle"),
344
+ prompt: text("prompt"),
345
+ ..Validation::default()
346
+ }
347
+ }
348
+
349
+ /// The formatted cells with their look; formats the workbook lacks are
350
+ /// left out.
351
+ fn styled(cells: &[(u32, u32, usize)], formats: &Formats) -> Vec<Styled> {
352
+ cells
353
+ .iter()
354
+ .filter_map(|&(row, column, format)| {
355
+ let look = Arc::clone(formats.looks.get(format)?);
356
+ Some(Styled { row, column, look })
357
+ })
358
+ .collect()
359
+ }
360
+
275
361
  /// The sheet's comments.
276
362
  fn comments(text: &str, budget: &Budget<'_>) -> Result<Vec<Note>> {
277
363
  let mut xml = XmlReader::new(text, budget.max_depth());
@@ -286,8 +372,7 @@ fn comments(text: &str, budget: &Budget<'_>) -> Result<Vec<Note>> {
286
372
  "author" => in_author = true,
287
373
  "comment" => {
288
374
  position = parse_cell_reference(&tag.attr("ref").unwrap_or_default()).ok();
289
- author_id =
290
- tag.attr("authorId").and_then(|id| id.parse().ok()).unwrap_or(usize::MAX);
375
+ author_id = tag.parse("authorId").unwrap_or(usize::MAX);
291
376
  body.clear();
292
377
  }
293
378
  "t" => in_text = true,
@@ -35,9 +35,12 @@ mod xlsx;
35
35
  pub(crate) use budget::Budget;
36
36
  pub use budget::Limits;
37
37
  pub use content::{
38
- Block, Bounds, Cell, Content, Grid, Mark, Note, Page, Placed, Sheet, Text, Value,
38
+ Block, Bounds, Cell, Columns, Content, Grid, Look, Mark, Note, Page, Placed, Sheet, Styled,
39
+ Text, Validation, Value,
39
40
  };
40
41
  pub use image::{Image, ImageFormat, ImageLocation};
42
+ #[cfg(test)]
43
+ pub(crate) use image::{PngColor, png};
41
44
 
42
45
  use crate::zip::reader::ZipReader;
43
46
  use crate::{Error, Result};