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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/Cargo.lock +124 -3
- data/Cargo.toml +1 -1
- data/README.md +123 -23
- data/ext/imprimatur_native/cli/Cargo.toml +2 -2
- data/ext/imprimatur_native/cli/src/cli/grep.rs +1 -1
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +2 -2
- data/ext/imprimatur_native/cli/src/cli.rs +5 -5
- data/ext/imprimatur_native/cli/src/main.rs +1 -1
- data/ext/imprimatur_native/core/Cargo.toml +12 -2
- data/ext/imprimatur_native/core/README.md +15 -1
- data/ext/imprimatur_native/core/src/archive.rs +279 -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/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 +13 -1
- data/ext/imprimatur_native/core/src/font.rs +229 -0
- data/ext/imprimatur_native/core/src/lib.rs +5 -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 +91 -3
- data/ext/imprimatur_native/core/src/read/json.rs +96 -7
- data/ext/imprimatur_native/core/src/read/markdown.rs +1 -1
- data/ext/imprimatur_native/core/src/read/{xlsx/numbers.rs → numbers.rs} +69 -45
- data/ext/imprimatur_native/core/src/read/ods.rs +356 -0
- data/ext/imprimatur_native/core/src/read/ole.rs +244 -0
- data/ext/imprimatur_native/core/src/read/package.rs +0 -5
- 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/strings.rs +156 -0
- data/ext/imprimatur_native/core/src/read/xls.rs +342 -0
- data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -20
- data/ext/imprimatur_native/core/src/read.rs +68 -21
- 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 +90 -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 +119 -0
- data/ext/imprimatur_native/ffi/src/blocking.rs +63 -0
- data/ext/imprimatur_native/ffi/src/document.rs +48 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +52 -1
- data/ext/imprimatur_native/ffi/src/read.rs +9 -14
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/document.rb +30 -6
- data/lib/imprimatur/error.rb +4 -3
- data/lib/imprimatur/reader.rb +3 -3
- data/lib/imprimatur/spreadsheet.rb +151 -0
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/xlsx.rb +25 -4
- data/lib/imprimatur/zip.rb +127 -0
- data/lib/imprimatur.rb +18 -5
- metadata +23 -4
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
//! digits), formulas without a cached result as `=FORMULA`, then the sheet's
|
|
4
4
|
//! comments.
|
|
5
5
|
|
|
6
|
-
mod
|
|
6
|
+
mod formats;
|
|
7
7
|
#[cfg(test)]
|
|
8
8
|
mod tests;
|
|
9
9
|
|
|
10
10
|
use std::sync::Arc;
|
|
11
11
|
|
|
12
|
+
use self::formats::{Formats, formats};
|
|
12
13
|
use super::Budget;
|
|
13
|
-
use super::content::{Grid, Note, Sheet, Value};
|
|
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};
|
|
19
|
-
use numbers::{Shown, push_number, styles};
|
|
20
|
+
use crate::xml::reader::{Event, Tag, XmlReader};
|
|
20
21
|
|
|
21
22
|
/// A cell's `t` attribute.
|
|
22
23
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
@@ -52,13 +53,24 @@ impl Strings {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
/// Where a cell's text events go.
|
|
56
|
+
/// Where a cell's text events go, or a validation's formulas'.
|
|
56
57
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
57
58
|
enum Into {
|
|
58
59
|
Nowhere,
|
|
59
60
|
Value,
|
|
60
61
|
Formula,
|
|
61
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)>,
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
pub fn content(package: &Package<'_>, budget: &Budget<'_>) -> Result<Vec<Sheet>> {
|
|
@@ -68,21 +80,31 @@ pub fn content(package: &Package<'_>, budget: &Budget<'_>) -> Result<Vec<Sheet>>
|
|
|
68
80
|
let (listed, date1904) = workbook(&text, &relationships, budget)?;
|
|
69
81
|
let strings = package
|
|
70
82
|
.parse_related(&relationships, "sharedStrings", |text| shared_strings(text, budget))?;
|
|
71
|
-
let
|
|
72
|
-
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 };
|
|
73
85
|
let mut sheets = Vec::with_capacity(listed.len());
|
|
74
86
|
let mut scratch = String::new();
|
|
75
87
|
for sheet in listed {
|
|
76
88
|
budget.check()?;
|
|
77
89
|
let Some(text) = package.text(&sheet.part)? else { continue };
|
|
78
|
-
let cells = cells(&text, &book, &mut scratch, budget)?;
|
|
90
|
+
let (cells, parts) = cells(&text, &book, &mut scratch, budget)?;
|
|
79
91
|
let comments = match package.related(&sheet.part, "comments")? {
|
|
80
92
|
Some(part) => package
|
|
81
93
|
.text(&part)?
|
|
82
94
|
.map_or_else(|| Ok(Vec::new()), |text| comments(&text, budget))?,
|
|
83
95
|
None => Vec::new(),
|
|
84
96
|
};
|
|
85
|
-
|
|
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
|
+
});
|
|
86
108
|
}
|
|
87
109
|
Ok(sheets)
|
|
88
110
|
}
|
|
@@ -156,8 +178,13 @@ struct Head {
|
|
|
156
178
|
style: usize,
|
|
157
179
|
}
|
|
158
180
|
|
|
159
|
-
fn cells(
|
|
160
|
-
|
|
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);
|
|
161
188
|
let mut xml = XmlReader::new(text, budget.max_depth());
|
|
162
189
|
let (mut row, mut next_row, mut next_column) = (0_u32, 0_u32, 0_u32);
|
|
163
190
|
let (mut value, mut formula, mut inline) = (String::new(), String::new(), String::new());
|
|
@@ -167,10 +194,7 @@ fn cells(text: &str, book: &Book<'_>, scratch: &mut String, budget: &Budget<'_>)
|
|
|
167
194
|
match event {
|
|
168
195
|
Event::Start(tag) => match tag.name() {
|
|
169
196
|
"row" => {
|
|
170
|
-
row = tag
|
|
171
|
-
.attr("r")
|
|
172
|
-
.and_then(|r| r.parse::<u32>().ok())
|
|
173
|
-
.map_or(next_row, |r| r.saturating_sub(1));
|
|
197
|
+
row = tag.parse::<u32>("r").map_or(next_row, |r| r.saturating_sub(1));
|
|
174
198
|
next_column = 0;
|
|
175
199
|
}
|
|
176
200
|
"c" => {
|
|
@@ -187,7 +211,7 @@ fn cells(text: &str, book: &Book<'_>, scratch: &mut String, budget: &Budget<'_>)
|
|
|
187
211
|
Some("d") => Kind::Iso,
|
|
188
212
|
_ => Kind::Number,
|
|
189
213
|
};
|
|
190
|
-
let style = tag.
|
|
214
|
+
let style = tag.parse("s").unwrap_or(0);
|
|
191
215
|
head = Some(Head { row: at_row, column, kind, style });
|
|
192
216
|
value.clear();
|
|
193
217
|
formula.clear();
|
|
@@ -199,17 +223,36 @@ fn cells(text: &str, book: &Book<'_>, scratch: &mut String, budget: &Budget<'_>)
|
|
|
199
223
|
into = Into::Inline;
|
|
200
224
|
}
|
|
201
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()),
|
|
202
230
|
_ => {}
|
|
203
231
|
},
|
|
204
232
|
Event::Text(text) => match into {
|
|
205
|
-
Into::Value => value.push_str(&text),
|
|
233
|
+
Into::Value | Into::First | Into::Second => value.push_str(&text),
|
|
206
234
|
Into::Formula => formula.push_str(&text),
|
|
207
235
|
Into::Inline => inline.push_str(&unescape_xstring(&text)),
|
|
208
236
|
Into::Nowhere => {}
|
|
209
237
|
},
|
|
210
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()),
|
|
211
251
|
Event::End("c") => {
|
|
212
252
|
if let Some(head) = head.take() {
|
|
253
|
+
if head.style != 0 {
|
|
254
|
+
parts.styled.push((head.row, head.column, head.style));
|
|
255
|
+
}
|
|
213
256
|
write_cell(&mut grid, &head, book, [&value, &formula, &inline], scratch);
|
|
214
257
|
next_column = head.column + 1;
|
|
215
258
|
}
|
|
@@ -218,7 +261,7 @@ fn cells(text: &str, book: &Book<'_>, scratch: &mut String, budget: &Budget<'_>)
|
|
|
218
261
|
Event::End(_) => {}
|
|
219
262
|
}
|
|
220
263
|
}
|
|
221
|
-
Ok(grid.finish())
|
|
264
|
+
Ok((grid.finish(), parts))
|
|
222
265
|
}
|
|
223
266
|
|
|
224
267
|
fn write_cell(
|
|
@@ -273,6 +316,48 @@ fn write_cell(
|
|
|
273
316
|
grid.end_cell(head.row, head.column, kind);
|
|
274
317
|
}
|
|
275
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
|
+
|
|
276
361
|
/// The sheet's comments.
|
|
277
362
|
fn comments(text: &str, budget: &Budget<'_>) -> Result<Vec<Note>> {
|
|
278
363
|
let mut xml = XmlReader::new(text, budget.max_depth());
|
|
@@ -287,8 +372,7 @@ fn comments(text: &str, budget: &Budget<'_>) -> Result<Vec<Note>> {
|
|
|
287
372
|
"author" => in_author = true,
|
|
288
373
|
"comment" => {
|
|
289
374
|
position = parse_cell_reference(&tag.attr("ref").unwrap_or_default()).ok();
|
|
290
|
-
author_id =
|
|
291
|
-
tag.attr("authorId").and_then(|id| id.parse().ok()).unwrap_or(usize::MAX);
|
|
375
|
+
author_id = tag.parse("authorId").unwrap_or(usize::MAX);
|
|
292
376
|
body.clear();
|
|
293
377
|
}
|
|
294
378
|
"t" => in_text = true,
|
|
@@ -21,19 +21,26 @@ pub(crate) mod inflate;
|
|
|
21
21
|
#[cfg(feature = "json")]
|
|
22
22
|
pub mod json;
|
|
23
23
|
mod markdown;
|
|
24
|
+
mod numbers;
|
|
25
|
+
mod ods;
|
|
26
|
+
mod ole;
|
|
24
27
|
mod package;
|
|
25
28
|
mod pdf;
|
|
26
29
|
mod span;
|
|
27
30
|
#[cfg(test)]
|
|
28
31
|
mod tests;
|
|
32
|
+
mod xls;
|
|
29
33
|
mod xlsx;
|
|
30
34
|
|
|
31
35
|
pub(crate) use budget::Budget;
|
|
32
36
|
pub use budget::Limits;
|
|
33
37
|
pub use content::{
|
|
34
|
-
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,
|
|
35
40
|
};
|
|
36
41
|
pub use image::{Image, ImageFormat, ImageLocation};
|
|
42
|
+
#[cfg(test)]
|
|
43
|
+
pub(crate) use image::{PngColor, png};
|
|
37
44
|
|
|
38
45
|
use crate::zip::reader::ZipReader;
|
|
39
46
|
use crate::{Error, Result};
|
|
@@ -46,41 +53,62 @@ pub enum Format {
|
|
|
46
53
|
Pdf,
|
|
47
54
|
Xlsx,
|
|
48
55
|
Docx,
|
|
56
|
+
Ods,
|
|
57
|
+
Xls,
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
impl Format {
|
|
52
|
-
/// `pdf`, `xlsx` or `
|
|
61
|
+
/// `pdf`, `xlsx`, `docx`, `ods` or `xls`.
|
|
53
62
|
#[must_use]
|
|
54
63
|
pub const fn as_str(self) -> &'static str {
|
|
55
64
|
match self {
|
|
56
65
|
Self::Pdf => "pdf",
|
|
57
66
|
Self::Xlsx => "xlsx",
|
|
58
67
|
Self::Docx => "docx",
|
|
68
|
+
Self::Ods => "ods",
|
|
69
|
+
Self::Xls => "xls",
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
/// Reads a document, its format told by its content
|
|
64
|
-
///
|
|
65
|
-
///
|
|
74
|
+
/// Reads a document, its format told by its content.
|
|
75
|
+
///
|
|
76
|
+
/// That is `%PDF-` in the first kilobyte; a compound file holding a legacy
|
|
77
|
+
/// Excel workbook (`.xls`); a
|
|
78
|
+
/// zip that is an ODF spreadsheet (`.ods`), or whose main part is
|
|
79
|
+
/// a workbook or a Word document (macro-enabled ones and templates
|
|
80
|
+
/// included).
|
|
66
81
|
///
|
|
67
82
|
/// # Errors
|
|
68
|
-
/// [`Error::UnsupportedDocument`] for other formats (legacy `.
|
|
69
|
-
/// `.
|
|
70
|
-
/// of scanned pages, [`Error::PasswordRequired`] for a
|
|
71
|
-
/// [`Error::LimitExceeded`] and [`Error::Cancelled`] when the
|
|
72
|
-
/// stop the read, and [`Error::Malformed`] for a corrupt document.
|
|
83
|
+
/// [`Error::UnsupportedDocument`] for other formats (legacy `.doc`, binary
|
|
84
|
+
/// `.xlsb`, presentations, ODF text), [`Error::NoTextLayer`] for a
|
|
85
|
+
/// PDF of scanned pages, [`Error::PasswordRequired`] for a document that
|
|
86
|
+
/// needs one, [`Error::LimitExceeded`] and [`Error::Cancelled`] when the
|
|
87
|
+
/// [`Limits`] stop the read, and [`Error::Malformed`] for a corrupt document.
|
|
73
88
|
pub fn content(document: &[u8], limits: &Limits) -> Result<Content> {
|
|
74
89
|
let budget = Budget::new(limits, document.len())?;
|
|
75
90
|
if pdf::is_pdf(document) {
|
|
76
91
|
return pdf::content(document, &budget).map(Content::Pdf);
|
|
77
92
|
}
|
|
93
|
+
if document.starts_with(&ole::SIGNATURE) {
|
|
94
|
+
let ole = ole::Ole::open(document, &budget)?;
|
|
95
|
+
return xls::content(&ole, &budget).map(Content::Xls);
|
|
96
|
+
}
|
|
78
97
|
let zip = zip(document)?;
|
|
98
|
+
match ods::spreadsheet(&zip, &budget)? {
|
|
99
|
+
Some(true) => return ods::content(&zip, &budget).map(Content::Ods),
|
|
100
|
+
Some(false) => {
|
|
101
|
+
return Err(Error::UnsupportedDocument(
|
|
102
|
+
"an OpenDocument file that is not a spreadsheet".to_owned(),
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
None => {}
|
|
106
|
+
}
|
|
79
107
|
let package = Package::open(&zip, &budget)?;
|
|
80
108
|
match package.format()? {
|
|
81
109
|
Format::Xlsx => xlsx::content(&package, &budget).map(Content::Xlsx),
|
|
82
110
|
Format::Docx => docx::content(&package, &budget).map(Content::Docx),
|
|
83
|
-
|
|
111
|
+
other => Err(Error::UnsupportedDocument(format!("a {} inside a zip", other.as_str()))),
|
|
84
112
|
}
|
|
85
113
|
}
|
|
86
114
|
|
|
@@ -96,12 +124,13 @@ pub fn markdown(document: &[u8], limits: &Limits) -> Result<String> {
|
|
|
96
124
|
/// Calls `each` with every image in a document, as a file.
|
|
97
125
|
///
|
|
98
126
|
/// A PDF's images come once each, from the first page that draws them; a
|
|
99
|
-
/// Word or
|
|
100
|
-
/// stores raw become PNG. Stops at the first error
|
|
127
|
+
/// Word, Excel or ODF package gives its media. A JPEG stays a
|
|
128
|
+
/// JPEG; pixels a PDF stores raw become PNG. Stops at the first error
|
|
129
|
+
/// `each` returns.
|
|
101
130
|
///
|
|
102
131
|
/// # Errors
|
|
103
|
-
/// As [`content`] (but a PDF without text is no error here
|
|
104
|
-
/// `each` returns.
|
|
132
|
+
/// As [`content`] (but a PDF without text is no error here, and a legacy
|
|
133
|
+
/// `.xls` is one), and what `each` returns.
|
|
105
134
|
pub fn images(
|
|
106
135
|
document: &[u8],
|
|
107
136
|
limits: &Limits,
|
|
@@ -111,19 +140,35 @@ pub fn images(
|
|
|
111
140
|
if pdf::is_pdf(document) {
|
|
112
141
|
return pdf::images(document, &budget, &mut each);
|
|
113
142
|
}
|
|
143
|
+
if document.starts_with(&ole::SIGNATURE) {
|
|
144
|
+
return Err(Error::UnsupportedDocument("the images of a legacy Office file".to_owned()));
|
|
145
|
+
}
|
|
114
146
|
let zip = zip(document)?;
|
|
147
|
+
if ods::spreadsheet(&zip, &budget)?.is_some() {
|
|
148
|
+
return media(&zip, "Pictures/", &[], &budget, &mut each);
|
|
149
|
+
}
|
|
115
150
|
let package = Package::open(&zip, &budget)?;
|
|
116
|
-
let
|
|
117
|
-
let sheets = match format {
|
|
151
|
+
let sheets = match package.format()? {
|
|
118
152
|
Format::Xlsx => xlsx::image_sheets(&package, &budget)?,
|
|
119
153
|
_ => Vec::new(),
|
|
120
154
|
};
|
|
121
155
|
let folder = package.main().rsplit_once('/').map_or("", |(folder, _)| folder);
|
|
122
|
-
|
|
123
|
-
|
|
156
|
+
media(&zip, &format!("{folder}/media/"), &sheets, &budget, &mut each)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// The package's parts under `folder`, as images; `sheets` says which
|
|
160
|
+
/// sheet shows each, by part.
|
|
161
|
+
fn media(
|
|
162
|
+
zip: &ZipReader<'_>,
|
|
163
|
+
folder: &str,
|
|
164
|
+
sheets: &[(String, String)],
|
|
165
|
+
budget: &Budget<'_>,
|
|
166
|
+
each: &mut impl FnMut(Image) -> Result<()>,
|
|
167
|
+
) -> Result<()> {
|
|
168
|
+
let names: Vec<&str> = zip.names().filter(|name| name.starts_with(folder)).collect();
|
|
124
169
|
for name in names {
|
|
125
170
|
budget.check()?;
|
|
126
|
-
let Some(data) =
|
|
171
|
+
let Some(data) = zip.read(name, budget)? else { continue };
|
|
127
172
|
let format = ImageFormat::sniff(&data, name);
|
|
128
173
|
let (width, height) = format.dimensions(&data).unzip();
|
|
129
174
|
let sheet = sheets
|
|
@@ -139,7 +184,9 @@ pub fn images(
|
|
|
139
184
|
/// A document that is not a PDF, as the zip an Office document is.
|
|
140
185
|
fn zip(document: &[u8]) -> Result<ZipReader<'_>> {
|
|
141
186
|
if !document.starts_with(b"PK") {
|
|
142
|
-
return Err(Error::UnsupportedDocument(
|
|
187
|
+
return Err(Error::UnsupportedDocument(
|
|
188
|
+
"not a PDF, XLSX, XLS, ODS or DOCX file".to_owned(),
|
|
189
|
+
));
|
|
143
190
|
}
|
|
144
191
|
ZipReader::new(document)
|
|
145
192
|
}
|
|
@@ -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.
|
|
@@ -72,6 +73,26 @@ pub enum TextDirection {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
impl TextDirection {
|
|
76
|
+
/// The direction text takes on its own, the way Word and browsers settle
|
|
77
|
+
/// a paragraph left to automatic: that of its first letter (UAX #9, P2).
|
|
78
|
+
/// `None` when it has no letter: digits, punctuation, nothing.
|
|
79
|
+
///
|
|
80
|
+
/// ```
|
|
81
|
+
/// use imprimatur::style::TextDirection;
|
|
82
|
+
///
|
|
83
|
+
/// assert_eq!(TextDirection::of("2024 شهادة"), Some(TextDirection::RightToLeft));
|
|
84
|
+
/// assert_eq!(TextDirection::of("ISO 9001 אישור"), Some(TextDirection::LeftToRight));
|
|
85
|
+
/// assert_eq!(TextDirection::of("1,250.50"), None);
|
|
86
|
+
/// ```
|
|
87
|
+
#[must_use]
|
|
88
|
+
pub fn of(text: &str) -> Option<Self> {
|
|
89
|
+
text.chars().find_map(|ch| match class(ch) {
|
|
90
|
+
Class::Right | Class::Arabic => Some(Self::RightToLeft),
|
|
91
|
+
Class::Left => Some(Self::LeftToRight),
|
|
92
|
+
Class::Number | Class::ArabicNumber | Class::Neutral => None,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
/// `readingOrder`: 1 left to right, 2 right to left (0 is context).
|
|
76
97
|
pub(crate) const fn reading_order(self) -> &'static str {
|
|
77
98
|
match self {
|
|
@@ -16,11 +16,12 @@ pub fn read_zip(bytes: Vec<u8>) -> Vec<(String, Vec<u8>)> {
|
|
|
16
16
|
.collect()
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/// Every entry of a zip archive as text, by name, read as [`read_zip`] does
|
|
19
|
+
/// Every entry of a zip archive as text, by name, read as [`read_zip`] does;
|
|
20
|
+
/// media come out garbled, for their names.
|
|
20
21
|
pub fn text_parts(bytes: Vec<u8>) -> Vec<(String, String)> {
|
|
21
22
|
read_zip(bytes)
|
|
22
23
|
.into_iter()
|
|
23
|
-
.map(|(name, data)| (name, String::
|
|
24
|
+
.map(|(name, data)| (name, String::from_utf8_lossy(&data).into_owned()))
|
|
24
25
|
.collect()
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -161,3 +162,90 @@ pub fn tiff_of(data: &[u8]) -> (std::collections::BTreeMap<u16, u32>, &[u8]) {
|
|
|
161
162
|
let (offset, length) = (tags[&273] as usize, tags[&279] as usize);
|
|
162
163
|
(tags, &data[offset..offset + length])
|
|
163
164
|
}
|
|
165
|
+
|
|
166
|
+
/// A compound file (MS-CFB, version 3) holding one stream named `name` in
|
|
167
|
+
/// its root storage; a stream under 4096 bytes goes to the mini stream, as
|
|
168
|
+
/// writers put it.
|
|
169
|
+
pub fn ole_of(name: &str, stream: &[u8]) -> Vec<u8> {
|
|
170
|
+
const SECTOR: usize = 512;
|
|
171
|
+
const END: u32 = 0xFFFF_FFFE;
|
|
172
|
+
let small = stream.len() < 4096;
|
|
173
|
+
let pad = |data: &[u8], size: usize| {
|
|
174
|
+
let mut data = data.to_vec();
|
|
175
|
+
data.resize(data.len().div_ceil(size).max(1) * size, 0);
|
|
176
|
+
data
|
|
177
|
+
};
|
|
178
|
+
// Sectors: FAT, directory, mini FAT (when small), then the data: the
|
|
179
|
+
// stream itself, or the mini stream holding it.
|
|
180
|
+
let data = if small { pad(stream, 64) } else { stream.to_vec() };
|
|
181
|
+
let data_sectors = data.len().div_ceil(SECTOR).max(1);
|
|
182
|
+
let first_data = if small { 3 } else { 2 };
|
|
183
|
+
let mut fat = vec![0xFFFF_FFFD_u32, END];
|
|
184
|
+
if small {
|
|
185
|
+
fat.push(END);
|
|
186
|
+
}
|
|
187
|
+
fat.extend((0..data_sectors).map(|index| {
|
|
188
|
+
if index + 1 == data_sectors { END } else { u32::try_from(first_data + index + 1).unwrap() }
|
|
189
|
+
}));
|
|
190
|
+
fat.resize(SECTOR / 4, 0xFFFF_FFFF);
|
|
191
|
+
let mut header = vec![0_u8; SECTOR];
|
|
192
|
+
header[..8].copy_from_slice(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]);
|
|
193
|
+
let put = |bytes: &mut Vec<u8>, at: usize, value: u32| {
|
|
194
|
+
bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
|
|
195
|
+
};
|
|
196
|
+
header[0x18..0x1A].copy_from_slice(&0x3E_u16.to_le_bytes());
|
|
197
|
+
header[0x1A..0x1C].copy_from_slice(&3_u16.to_le_bytes());
|
|
198
|
+
header[0x1C..0x1E].copy_from_slice(&0xFFFE_u16.to_le_bytes());
|
|
199
|
+
header[0x1E..0x20].copy_from_slice(&9_u16.to_le_bytes());
|
|
200
|
+
header[0x20..0x22].copy_from_slice(&6_u16.to_le_bytes());
|
|
201
|
+
put(&mut header, 0x2C, 1);
|
|
202
|
+
put(&mut header, 0x30, 1);
|
|
203
|
+
put(&mut header, 0x38, 4096);
|
|
204
|
+
put(&mut header, 0x3C, if small { 2 } else { END });
|
|
205
|
+
put(&mut header, 0x40, u32::from(small));
|
|
206
|
+
put(&mut header, 0x44, END);
|
|
207
|
+
put(&mut header, 0x4C, 0);
|
|
208
|
+
for index in 1..109 {
|
|
209
|
+
put(&mut header, 0x4C + index * 4, 0xFFFF_FFFF);
|
|
210
|
+
}
|
|
211
|
+
let entry = |name: &str, kind: u8, child: u32, start: u32, size: usize| {
|
|
212
|
+
let mut bytes = vec![0_u8; 128];
|
|
213
|
+
let units: Vec<u16> = name.encode_utf16().collect();
|
|
214
|
+
for (index, unit) in units.iter().enumerate() {
|
|
215
|
+
bytes[index * 2..index * 2 + 2].copy_from_slice(&unit.to_le_bytes());
|
|
216
|
+
}
|
|
217
|
+
bytes[0x40..0x42]
|
|
218
|
+
.copy_from_slice(&u16::try_from(units.len() * 2 + 2).unwrap().to_le_bytes());
|
|
219
|
+
bytes[0x42] = kind;
|
|
220
|
+
for at in [0x44, 0x48, 0x4C] {
|
|
221
|
+
put(&mut bytes, at, 0xFFFF_FFFF);
|
|
222
|
+
}
|
|
223
|
+
put(&mut bytes, 0x4C, child);
|
|
224
|
+
put(&mut bytes, 0x74, start);
|
|
225
|
+
put(&mut bytes, 0x78, u32::try_from(size).unwrap());
|
|
226
|
+
bytes
|
|
227
|
+
};
|
|
228
|
+
let first = u32::try_from(first_data).unwrap();
|
|
229
|
+
let mut directory = if small {
|
|
230
|
+
let mut directory = entry("Root Entry", 5, 1, first, data.len());
|
|
231
|
+
directory.extend(entry(name, 2, 0xFFFF_FFFF, 0, stream.len()));
|
|
232
|
+
directory
|
|
233
|
+
} else {
|
|
234
|
+
let mut directory = entry("Root Entry", 5, 1, END, 0);
|
|
235
|
+
directory.extend(entry(name, 2, 0xFFFF_FFFF, first, stream.len()));
|
|
236
|
+
directory
|
|
237
|
+
};
|
|
238
|
+
directory.resize(SECTOR, 0);
|
|
239
|
+
let mut file = header;
|
|
240
|
+
file.extend(fat.iter().flat_map(|word| word.to_le_bytes()));
|
|
241
|
+
file.extend(directory);
|
|
242
|
+
if small {
|
|
243
|
+
let mut mini_fat: Vec<u32> =
|
|
244
|
+
(1..data.len() / 64).map(|next| u32::try_from(next).unwrap()).collect();
|
|
245
|
+
mini_fat.push(END);
|
|
246
|
+
mini_fat.resize(SECTOR / 4, 0xFFFF_FFFF);
|
|
247
|
+
file.extend(mini_fat.iter().flat_map(|word| word.to_le_bytes()));
|
|
248
|
+
}
|
|
249
|
+
file.extend(pad(&data, SECTOR));
|
|
250
|
+
file
|
|
251
|
+
}
|