imprimatur 0.2.0 → 0.4.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 +31 -0
- data/Cargo.lock +153 -3
- data/Cargo.toml +1 -1
- data/README.md +139 -21
- data/ext/imprimatur_native/cli/Cargo.toml +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +20 -4
- data/ext/imprimatur_native/core/README.md +36 -4
- data/ext/imprimatur_native/core/src/archive.rs +3 -16
- data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
- data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
- data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
- data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
- data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
- data/ext/imprimatur_native/core/src/barcode.rs +314 -0
- 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/symbol.rs +136 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
- data/ext/imprimatur_native/core/src/doc.rs +96 -2
- data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
- data/ext/imprimatur_native/core/src/docx.rs +103 -28
- data/ext/imprimatur_native/core/src/error.rs +18 -0
- data/ext/imprimatur_native/core/src/font.rs +231 -0
- data/ext/imprimatur_native/core/src/lib.rs +6 -1
- data/ext/imprimatur_native/core/src/pdf/layout.rs +72 -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/truetype/descriptor.rs +129 -0
- data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
- data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
- data/ext/imprimatur_native/core/src/pdf.rs +40 -4
- data/ext/imprimatur_native/core/src/read/content.rs +75 -2
- data/ext/imprimatur_native/core/src/read/image.rs +17 -0
- 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 +5 -1
- data/ext/imprimatur_native/core/src/script.rs +116 -0
- data/ext/imprimatur_native/core/src/style.rs +32 -0
- 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/barcode.rs +147 -0
- data/ext/imprimatur_native/ffi/src/document.rs +76 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +40 -1
- data/ext/imprimatur_native/ffi/src/read.rs +8 -11
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/barcode.rb +78 -0
- data/lib/imprimatur/document.rb +46 -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 +19 -3
- metadata +23 -2
- data/ext/imprimatur_native/core/src/test_support.rs +0 -250
|
@@ -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,13 @@ 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
|
};
|
|
41
|
+
#[cfg(test)]
|
|
42
|
+
pub(crate) use image::png;
|
|
40
43
|
pub use image::{Image, ImageFormat, ImageLocation};
|
|
44
|
+
pub(crate) use image::{PngColor, png_with_transparency};
|
|
41
45
|
|
|
42
46
|
use crate::zip::reader::ZipReader;
|
|
43
47
|
use crate::{Error, Result};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//! Scripts and the direction they are written in, for reading text back
|
|
2
|
+
//! in order and for writing it with the right direction.
|
|
3
|
+
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
mod tests;
|
|
6
|
+
|
|
7
|
+
use crate::style::TextDirection;
|
|
8
|
+
|
|
9
|
+
/// What a character is, for the direction of the text around it (its
|
|
10
|
+
/// UAX #9 bidirectional class, simplified).
|
|
11
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
12
|
+
pub enum Class {
|
|
13
|
+
/// A letter of Hebrew or another right-to-left script but Arabic's, or
|
|
14
|
+
/// the right-to-left mark.
|
|
15
|
+
Right,
|
|
16
|
+
/// A letter of the Arabic script (Arabic, Persian, Urdu..., Syriac,
|
|
17
|
+
/// Thaana): right to left, and the digits after it are Arabic numbers.
|
|
18
|
+
Arabic,
|
|
19
|
+
/// Any other letter, or the left-to-right mark.
|
|
20
|
+
Left,
|
|
21
|
+
/// A number: its digits read left to right in any line, but in a
|
|
22
|
+
/// right-to-left run the spaces between two numbers read right to left.
|
|
23
|
+
Number,
|
|
24
|
+
/// Arabic-Indic digits (not the Persian ones), and digits after Arabic
|
|
25
|
+
/// letters: a number that only `,` `.` `:` `/` join, and no sign goes
|
|
26
|
+
/// with (UAX #9, W2).
|
|
27
|
+
ArabicNumber,
|
|
28
|
+
/// Spaces, punctuation and symbols: they take the direction around them.
|
|
29
|
+
Neutral,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Class {
|
|
33
|
+
pub const fn right_to_left(self) -> bool {
|
|
34
|
+
matches!(self, Self::Right | Self::Arabic)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub const fn number(self) -> bool {
|
|
38
|
+
matches!(self, Self::Number | Self::ArabicNumber)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub const fn strong(self) -> bool {
|
|
42
|
+
matches!(self, Self::Right | Self::Arabic | Self::Left)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// The letters and digits of some text, by direction.
|
|
47
|
+
#[derive(Debug, Default, Clone, Copy)]
|
|
48
|
+
pub struct Counts {
|
|
49
|
+
pub right: usize,
|
|
50
|
+
pub left: usize,
|
|
51
|
+
pub numbers: usize,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl Counts {
|
|
55
|
+
pub fn add(&mut self, text: &str) {
|
|
56
|
+
for ch in text.chars() {
|
|
57
|
+
match class(ch) {
|
|
58
|
+
Class::Right | Class::Arabic => self.right += 1,
|
|
59
|
+
Class::Left if ch.is_alphabetic() => self.left += 1,
|
|
60
|
+
Class::Number | Class::ArabicNumber => self.numbers += 1,
|
|
61
|
+
Class::Left | Class::Neutral => {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Whether more of the letters read right to left than left to right.
|
|
67
|
+
pub const fn right_to_left(self) -> bool {
|
|
68
|
+
self.right > self.left
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The direction most of the letters read in; `None` without a letter.
|
|
72
|
+
pub const fn direction(self) -> Option<TextDirection> {
|
|
73
|
+
if self.right > self.left {
|
|
74
|
+
Some(TextDirection::RightToLeft)
|
|
75
|
+
} else if self.left > 0 {
|
|
76
|
+
Some(TextDirection::LeftToRight)
|
|
77
|
+
} else {
|
|
78
|
+
None
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// The direction of a character.
|
|
84
|
+
///
|
|
85
|
+
/// Right to left: Hebrew (and Yiddish, Ladino), Arabic (and Persian, Urdu,
|
|
86
|
+
/// Pashto, Sindhi, Kurdish, Uyghur), Syriac, Thaana (Dhivehi), N'Ko,
|
|
87
|
+
/// Samaritan, Mandaic, their presentation forms; Adlam (Fulani), Hanifi
|
|
88
|
+
/// Rohingya, Mende Kikakui, Yezidi, Garay and the historic scripts
|
|
89
|
+
/// (Phoenician, Aramaic, Sogdian...); and the right-to-left mark. Of
|
|
90
|
+
/// those, the Arabic script (with Syriac, Thaana, Hanifi Rohingya and the
|
|
91
|
+
/// Siyaq numbers) is set apart. Digits are numbers, the Persian ones
|
|
92
|
+
/// (Extended Arabic-Indic) too; Arabic-Indic ones are Arabic numbers. The
|
|
93
|
+
/// Arabic comma, percent sign and separators are neutral, like their Latin
|
|
94
|
+
/// counterparts.
|
|
95
|
+
pub fn class(ch: char) -> Class {
|
|
96
|
+
match ch {
|
|
97
|
+
'0'..='9' | '\u{06F0}'..='\u{06F9}' => Class::Number,
|
|
98
|
+
'\u{0660}'..='\u{0669}' => Class::ArabicNumber,
|
|
99
|
+
'\u{060C}' | '\u{066A}'..='\u{066C}' => Class::Neutral,
|
|
100
|
+
'\u{0600}'..='\u{07BF}'
|
|
101
|
+
| '\u{0860}'..='\u{08FF}'
|
|
102
|
+
| '\u{FB50}'..='\u{FDFF}'
|
|
103
|
+
| '\u{FE70}'..='\u{FEFC}'
|
|
104
|
+
| '\u{10D00}'..='\u{10D3F}'
|
|
105
|
+
| '\u{10EC0}'..='\u{10EFF}'
|
|
106
|
+
| '\u{1EC70}'..='\u{1EEFF}' => Class::Arabic,
|
|
107
|
+
'\u{0590}'..='\u{08FF}'
|
|
108
|
+
| '\u{200F}'
|
|
109
|
+
| '\u{FB1D}'..='\u{FB4F}'
|
|
110
|
+
| '\u{10800}'..='\u{10FFF}'
|
|
111
|
+
| '\u{1E800}'..='\u{1EFFF}' => Class::Right,
|
|
112
|
+
'\u{200E}' => Class::Left,
|
|
113
|
+
ch if ch.is_alphanumeric() => Class::Left,
|
|
114
|
+
_ => Class::Neutral,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#[cfg(test)]
|
|
4
4
|
mod tests;
|
|
5
5
|
|
|
6
|
+
use crate::script::{Class, class};
|
|
6
7
|
use crate::{Error, Result};
|
|
7
8
|
|
|
8
9
|
/// An opaque RGB color.
|
|
@@ -40,6 +41,17 @@ impl Color {
|
|
|
40
41
|
pub(crate) fn argb(self) -> String {
|
|
41
42
|
format!("FF{:06X}", self.0)
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
/// `#RRGGBB`, as SVG takes it.
|
|
46
|
+
pub(crate) fn hex(self) -> String {
|
|
47
|
+
format!("#{:06X}", self.0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Red, green and blue bytes.
|
|
51
|
+
pub(crate) const fn bytes(self) -> [u8; 3] {
|
|
52
|
+
let [_, r, g, b] = self.0.to_be_bytes();
|
|
53
|
+
[r, g, b]
|
|
54
|
+
}
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
/// Horizontal alignment within the cell.
|
|
@@ -72,6 +84,26 @@ pub enum TextDirection {
|
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
impl TextDirection {
|
|
87
|
+
/// The direction text takes on its own, the way Word and browsers settle
|
|
88
|
+
/// a paragraph left to automatic: that of its first letter (UAX #9, P2).
|
|
89
|
+
/// `None` when it has no letter: digits, punctuation, nothing.
|
|
90
|
+
///
|
|
91
|
+
/// ```
|
|
92
|
+
/// use imprimatur::style::TextDirection;
|
|
93
|
+
///
|
|
94
|
+
/// assert_eq!(TextDirection::of("2024 شهادة"), Some(TextDirection::RightToLeft));
|
|
95
|
+
/// assert_eq!(TextDirection::of("ISO 9001 אישור"), Some(TextDirection::LeftToRight));
|
|
96
|
+
/// assert_eq!(TextDirection::of("1,250.50"), None);
|
|
97
|
+
/// ```
|
|
98
|
+
#[must_use]
|
|
99
|
+
pub fn of(text: &str) -> Option<Self> {
|
|
100
|
+
text.chars().find_map(|ch| match class(ch) {
|
|
101
|
+
Class::Right | Class::Arabic => Some(Self::RightToLeft),
|
|
102
|
+
Class::Left => Some(Self::LeftToRight),
|
|
103
|
+
Class::Number | Class::ArabicNumber | Class::Neutral => None,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
75
107
|
/// `readingOrder`: 1 left to right, 2 right to left (0 is context).
|
|
76
108
|
pub(crate) const fn reading_order(self) -> &'static str {
|
|
77
109
|
match self {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//! Column widths from the cells. There is no font to measure text with, so
|
|
2
|
+
//! a column is as wide as its widest cell's character count says, scaled by
|
|
3
|
+
//! that cell's font.
|
|
4
|
+
|
|
5
|
+
#[cfg(test)]
|
|
6
|
+
mod tests;
|
|
7
|
+
|
|
8
|
+
use std::collections::BTreeMap;
|
|
9
|
+
|
|
10
|
+
use super::cell::Cell;
|
|
11
|
+
use super::format::Font;
|
|
12
|
+
use super::worksheet::Worksheet;
|
|
13
|
+
|
|
14
|
+
/// Excel's widest column, in characters.
|
|
15
|
+
const MAX_WIDTH: f64 = 255.0;
|
|
16
|
+
|
|
17
|
+
/// The width each column without one of its own needs for its widest cell:
|
|
18
|
+
/// `(characters + 3) × points / 10`, a bold cell counting one and a half
|
|
19
|
+
/// times, at most 255. Formulas are left out: their results are unknown.
|
|
20
|
+
pub fn widths(sheet: &Worksheet, base: &Font) -> BTreeMap<u16, f64> {
|
|
21
|
+
let mut widths = BTreeMap::new();
|
|
22
|
+
for (_, cells) in sheet.rows() {
|
|
23
|
+
for (&column, entry) in cells {
|
|
24
|
+
if sheet.column_widths().contains_key(&column) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
let Some(characters) = characters(&entry.cell) else {
|
|
28
|
+
continue;
|
|
29
|
+
};
|
|
30
|
+
let font = usize::try_from(entry.style)
|
|
31
|
+
.ok()
|
|
32
|
+
.and_then(|style| style.checked_sub(1))
|
|
33
|
+
.and_then(|index| sheet.formats().get(index))
|
|
34
|
+
.map_or(base, |format| &format.font);
|
|
35
|
+
let bold = if font.bold { 1.5 } else { 1.0 };
|
|
36
|
+
#[expect(
|
|
37
|
+
clippy::cast_precision_loss,
|
|
38
|
+
reason = "a cell holds at most 32 767 characters"
|
|
39
|
+
)]
|
|
40
|
+
let width = (characters as f64 + 3.0) * f64::from(font.half_points(base)) / 20.0 * bold;
|
|
41
|
+
let width = (width.min(MAX_WIDTH) * 100.0).round() / 100.0;
|
|
42
|
+
let widest = widths.entry(column).or_insert(0.0_f64);
|
|
43
|
+
*widest = widest.max(width);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
widths
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// How many characters a cell shows unformatted: a text's longest line,
|
|
50
|
+
/// a number's digits, `TRUE`, a date as `yyyy-mm-dd` and a time
|
|
51
|
+
/// `yyyy-mm-dd hh:mm:ss`.
|
|
52
|
+
fn characters(cell: &Cell) -> Option<usize> {
|
|
53
|
+
match cell {
|
|
54
|
+
Cell::Text(text) => text.split('\n').map(|line| line.chars().count()).max(),
|
|
55
|
+
Cell::Number(number) => Some(number.to_string().len()),
|
|
56
|
+
Cell::Bool(value) => Some(if *value { 4 } else { 5 }),
|
|
57
|
+
Cell::DateTime(date) => Some(if date.has_time() { 19 } else { 10 }),
|
|
58
|
+
Cell::Formula(_) => None,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -90,6 +90,12 @@ impl DateTime {
|
|
|
90
90
|
Self { hour: 0, minute: 0, second: 0, nanosecond: 0, ..self }
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/// Year, month and day.
|
|
94
|
+
#[must_use]
|
|
95
|
+
pub const fn year_month_day(&self) -> (u16, u8, u8) {
|
|
96
|
+
(self.year, self.month, self.day)
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
/// Hours, minutes and seconds.
|
|
94
100
|
#[must_use]
|
|
95
101
|
pub const fn time_of_day(&self) -> (u8, u8, u8) {
|
|
@@ -147,7 +153,7 @@ const fn days_in_month(year: u16, month: u8) -> u8 {
|
|
|
147
153
|
|
|
148
154
|
/// The calendar date `days` after 1970-01-01: the inverse of
|
|
149
155
|
/// [`days_from_civil`] (Howard Hinnant's `civil_from_days`).
|
|
150
|
-
const fn civil_from_days(days: i64) -> (i64, i64, i64) {
|
|
156
|
+
pub const fn civil_from_days(days: i64) -> (i64, i64, i64) {
|
|
151
157
|
let days = days + 719_468;
|
|
152
158
|
let era = days.div_euclid(146_097);
|
|
153
159
|
let day_of_era = days - era * 146_097;
|