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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/Cargo.lock +123 -3
- data/Cargo.toml +1 -1
- data/README.md +98 -20
- data/ext/imprimatur_native/cli/Cargo.toml +1 -1
- data/ext/imprimatur_native/core/Cargo.toml +6 -1
- data/ext/imprimatur_native/core/README.md +12 -3
- data/ext/imprimatur_native/core/src/archive.rs +3 -16
- data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
- data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
- data/ext/imprimatur_native/core/src/doc.rs +86 -2
- data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
- data/ext/imprimatur_native/core/src/docx.rs +91 -28
- data/ext/imprimatur_native/core/src/error.rs +12 -0
- data/ext/imprimatur_native/core/src/font.rs +229 -0
- data/ext/imprimatur_native/core/src/lib.rs +3 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +38 -15
- data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
- data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
- data/ext/imprimatur_native/core/src/pdf.rs +31 -3
- data/ext/imprimatur_native/core/src/read/content.rs +75 -2
- data/ext/imprimatur_native/core/src/read/json.rs +94 -7
- data/ext/imprimatur_native/core/src/read/numbers.rs +65 -42
- data/ext/imprimatur_native/core/src/read/ods.rs +1 -2
- data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
- data/ext/imprimatur_native/core/src/read/xls.rs +1 -1
- data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -19
- data/ext/imprimatur_native/core/src/read.rs +4 -1
- data/ext/imprimatur_native/core/src/script.rs +116 -0
- data/ext/imprimatur_native/core/src/style.rs +21 -0
- data/ext/imprimatur_native/core/src/test_support.rs +3 -2
- data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
- data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
- data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
- data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
- data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
- data/ext/imprimatur_native/ffi/src/archive.rs +1 -3
- data/ext/imprimatur_native/ffi/src/document.rs +48 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +38 -1
- data/ext/imprimatur_native/ffi/src/read.rs +8 -11
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/document.rb +30 -6
- data/lib/imprimatur/reader.rb +2 -2
- data/lib/imprimatur/spreadsheet.rb +151 -0
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/xlsx.rb +25 -4
- data/lib/imprimatur.rb +15 -3
- 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
|
|
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
|
-
|
|
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> {
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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(
|
|
159
|
-
|
|
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.
|
|
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,
|
|
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};
|