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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/Cargo.lock +153 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +139 -21
  6. data/ext/imprimatur_native/cli/Cargo.toml +3 -1
  7. data/ext/imprimatur_native/core/Cargo.toml +20 -4
  8. data/ext/imprimatur_native/core/README.md +36 -4
  9. data/ext/imprimatur_native/core/src/archive.rs +3 -16
  10. data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
  11. data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
  12. data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
  13. data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
  14. data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
  15. data/ext/imprimatur_native/core/src/barcode.rs +314 -0
  16. data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
  17. data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
  18. data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
  19. data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
  20. data/ext/imprimatur_native/core/src/doc.rs +96 -2
  21. data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
  22. data/ext/imprimatur_native/core/src/docx.rs +103 -28
  23. data/ext/imprimatur_native/core/src/error.rs +18 -0
  24. data/ext/imprimatur_native/core/src/font.rs +231 -0
  25. data/ext/imprimatur_native/core/src/lib.rs +6 -1
  26. data/ext/imprimatur_native/core/src/pdf/layout.rs +72 -15
  27. data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
  28. data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
  29. data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +129 -0
  30. data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
  31. data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
  32. data/ext/imprimatur_native/core/src/pdf.rs +40 -4
  33. data/ext/imprimatur_native/core/src/read/content.rs +75 -2
  34. data/ext/imprimatur_native/core/src/read/image.rs +17 -0
  35. data/ext/imprimatur_native/core/src/read/json.rs +94 -7
  36. data/ext/imprimatur_native/core/src/read/numbers.rs +65 -42
  37. data/ext/imprimatur_native/core/src/read/ods.rs +1 -2
  38. data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
  39. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
  40. data/ext/imprimatur_native/core/src/read/xls.rs +1 -1
  41. data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
  42. data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -19
  43. data/ext/imprimatur_native/core/src/read.rs +5 -1
  44. data/ext/imprimatur_native/core/src/script.rs +116 -0
  45. data/ext/imprimatur_native/core/src/style.rs +32 -0
  46. data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
  47. data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
  48. data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
  49. data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
  50. data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
  51. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
  52. data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
  53. data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
  54. data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
  55. data/ext/imprimatur_native/ffi/src/archive.rs +1 -3
  56. data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
  57. data/ext/imprimatur_native/ffi/src/document.rs +76 -1
  58. data/ext/imprimatur_native/ffi/src/lib.rs +40 -1
  59. data/ext/imprimatur_native/ffi/src/read.rs +8 -11
  60. data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
  61. data/lib/imprimatur/barcode.rb +78 -0
  62. data/lib/imprimatur/document.rb +46 -6
  63. data/lib/imprimatur/reader.rb +2 -2
  64. data/lib/imprimatur/spreadsheet.rb +151 -0
  65. data/lib/imprimatur/version.rb +1 -1
  66. data/lib/imprimatur/xlsx.rb +25 -4
  67. data/lib/imprimatur.rb +19 -3
  68. metadata +23 -2
  69. data/ext/imprimatur_native/core/src/test_support.rs +0 -250
@@ -4,11 +4,8 @@
4
4
 
5
5
  use std::fmt::Write as _;
6
6
 
7
- use crate::Result;
8
- use crate::read::Budget;
9
7
  use crate::read::content::Value;
10
8
  use crate::xlsx::DateTime;
11
- use crate::xml::reader::{Event, XmlReader};
12
9
 
13
10
  /// How a number format shows a number.
14
11
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -17,43 +14,24 @@ pub enum Shown {
17
14
  Date,
18
15
  Time,
19
16
  DateTime,
20
- Percent,
21
- }
22
-
23
- /// How each cell format (`cellXfs`, by index) shows numbers.
24
- pub fn styles(text: &str, budget: &Budget<'_>) -> Result<Vec<Shown>> {
25
- let mut xml = XmlReader::new(text, budget.max_depth());
26
- let (mut custom, mut shown, mut in_cell_formats) = (Vec::new(), Vec::new(), false);
27
- while let Some(event) = xml.next()? {
28
- match event {
29
- Event::Start(tag) => match tag.name() {
30
- "numFmt" => {
31
- if let (Some(id), Some(code)) = (tag.attr("numFmtId"), tag.attr("formatCode")) {
32
- custom.push((id.parse::<u32>().unwrap_or(u32::MAX), classify(&code)));
33
- }
34
- }
35
- "cellXfs" => in_cell_formats = true,
36
- "xf" if in_cell_formats => {
37
- let id =
38
- tag.attr("numFmtId").and_then(|id| id.parse::<u32>().ok()).unwrap_or(0);
39
- let custom =
40
- custom.iter().find(|&&(custom, _)| custom == id).map(|&(_, shown)| shown);
41
- shown.push(custom.unwrap_or_else(|| built_in(id)));
42
- }
43
- _ => {}
44
- },
45
- Event::End("cellXfs") => in_cell_formats = false,
46
- _ => {}
47
- }
48
- }
49
- Ok(shown)
17
+ /// A fixed number of decimals, thousands grouped with `,` or not, as a
18
+ /// percentage or not: `0.00`, `#,##0`, `0%`.
19
+ Fixed {
20
+ decimals: u8,
21
+ grouping: bool,
22
+ percent: bool,
23
+ },
50
24
  }
51
25
 
52
26
  /// Excel's built-in number formats (ECMA-376 Part 1, 18.8.30), with the
53
27
  /// East Asian date formats.
54
28
  pub const fn built_in(id: u32) -> Shown {
55
29
  match id {
56
- 9 | 10 => Shown::Percent,
30
+ 2 => fixed(2, false, false),
31
+ 3 | 37 | 38 => fixed(0, true, false),
32
+ 4 | 39 | 40 => fixed(2, true, false),
33
+ 9 => fixed(0, false, true),
34
+ 10 => fixed(2, false, true),
57
35
  14..=17 | 27..=31 | 34..=36 | 50..=58 => Shown::Date,
58
36
  18..=21 | 32 | 33 | 45..=47 => Shown::Time,
59
37
  22 => Shown::DateTime,
@@ -61,14 +39,26 @@ pub const fn built_in(id: u32) -> Shown {
61
39
  }
62
40
  }
63
41
 
42
+ pub const fn fixed(decimals: u8, grouping: bool, percent: bool) -> Shown {
43
+ Shown::Fixed { decimals, grouping, percent }
44
+ }
45
+
64
46
  /// What a custom format code shows, from its first section: date and time
65
- /// tokens, outside quoted text, escapes and `[...]` (colors, locales;
66
- /// `[h]` is elapsed time).
47
+ /// tokens, decimals, `,` grouping and `%`, outside quoted text, escapes and
48
+ /// `[...]` (colors, locales; `[h]` is elapsed time). Scientific formats
49
+ /// (`0.00E+00`) are shown as General.
67
50
  pub fn classify(code: &str) -> Shown {
68
51
  let (mut date, mut time, mut month, mut percent) = (false, false, false, false);
69
- let mut chars = code.chars();
52
+ let (mut digits, mut point, mut decimals, mut grouping, mut scientific) =
53
+ (false, false, 0_u8, false, false);
54
+ let mut chars = code.chars().peekable();
70
55
  while let Some(ch) = chars.next() {
71
56
  match ch.to_ascii_lowercase() {
57
+ '0' | '#' | '?' if point => decimals = decimals.saturating_add(1),
58
+ '0' | '#' | '?' => digits = true,
59
+ '.' => point = true,
60
+ ',' if digits && !point => grouping = true,
61
+ 'e' if matches!(chars.peek(), Some('+' | '-')) => scientific = true,
72
62
  ';' => break,
73
63
  '"' => {
74
64
  chars.by_ref().find(|&ch| ch == '"');
@@ -91,8 +81,8 @@ pub fn classify(code: &str) -> Shown {
91
81
  (true, true) => Shown::DateTime,
92
82
  (true, false) => Shown::Date,
93
83
  (false, true) => Shown::Time,
94
- (false, false) if percent => Shown::Percent,
95
- (false, false) => Shown::General,
84
+ (false, false) if scientific || !(decimals > 0 || grouping || percent) => Shown::General,
85
+ (false, false) => Shown::Fixed { decimals, grouping, percent },
96
86
  }
97
87
  }
98
88
 
@@ -130,15 +120,48 @@ pub fn push_number(
130
120
  let _ =
131
121
  write!(out, "{:02}:{:02}:{:02}", seconds / 3600, seconds / 60 % 60, seconds % 60);
132
122
  }
133
- Shown::Percent => {
134
- push_general(out, number * 100.0, scratch);
135
- out.push('%');
123
+ Shown::Fixed { decimals, grouping, percent } => {
124
+ let number = if percent { number * 100.0 } else { number };
125
+ push_fixed(out, number, decimals, grouping, scratch);
126
+ if percent {
127
+ out.push('%');
128
+ }
136
129
  }
137
130
  _ => push_general(out, number, scratch),
138
131
  }
139
132
  Value::Number(number)
140
133
  }
141
134
 
135
+ /// A number with `decimals` decimals, its thousands grouped with `,`,
136
+ /// rounded half away from zero as Excel does (`12.5` shows `13`, `1.005`
137
+ /// `1.01`): a few units in the last place toward the tie undo the binary
138
+ /// error that makes `1.005` `1.00499...`, which Rust would round down, as it
139
+ /// rounds an exact tie to even.
140
+ fn push_fixed(out: &mut String, number: f64, decimals: u8, grouping: bool, scratch: &mut String) {
141
+ let number = (number.abs() * 4.0 * f64::EPSILON).mul_add(number.signum(), number);
142
+ scratch.clear();
143
+ // Writing to a String cannot fail.
144
+ let _ = write!(scratch, "{number:.*}", usize::from(decimals));
145
+ if !grouping {
146
+ out.push_str(scratch);
147
+ return;
148
+ }
149
+ let (sign, digits) =
150
+ scratch.strip_prefix('-').map_or(("", scratch.as_str()), |rest| ("-", rest));
151
+ let (whole, fraction) = digits.split_once('.').map_or((digits, None), |(w, f)| (w, Some(f)));
152
+ out.push_str(sign);
153
+ for (index, digit) in whole.char_indices() {
154
+ if index > 0 && (whole.len() - index).is_multiple_of(3) {
155
+ out.push(',');
156
+ }
157
+ out.push(digit);
158
+ }
159
+ if let Some(fraction) = fraction {
160
+ out.push('.');
161
+ out.push_str(fraction);
162
+ }
163
+ }
164
+
142
165
  /// A number as Excel's General format shows it: 15 significant digits.
143
166
  pub fn push_general(out: &mut String, number: f64, scratch: &mut String) {
144
167
  #[expect(clippy::cast_possible_truncation, reason = "whole and under 1e15: exact in i64")]
@@ -120,8 +120,7 @@ impl Reader {
120
120
  self.sheets.push(Sheet {
121
121
  name: tag.attr("name").map(std::borrow::Cow::into_owned).unwrap_or_default(),
122
122
  hidden,
123
- cells: Grid::default(),
124
- comments: Vec::new(),
123
+ ..Sheet::default()
125
124
  });
126
125
  self.row = 0;
127
126
  }
@@ -0,0 +1,212 @@
1
+ //! Right-to-left text, from the order a PDF draws it to the order it reads.
2
+ //!
3
+ //! A PDF draws every line left to right, whatever its script: a Hebrew or
4
+ //! Arabic line comes out backwards, and so does an Arabic name inside an
5
+ //! English line. Each piece of a line gets the level the Unicode
6
+ //! bidirectional algorithm (UAX #9) gives it, even for left to right, odd
7
+ //! for right to left, and the runs are reversed the way a renderer reverses
8
+ //! them: reversed twice, a line reads as it was written. The page's own
9
+ //! direction stands in for the paragraph's, which a PDF does not record.
10
+
11
+ #[cfg(test)]
12
+ mod tests;
13
+
14
+ use std::ops::Range;
15
+
16
+ use crate::script::{Class, Counts, class};
17
+
18
+ /// Reusable buffers for [`reading_order`].
19
+ #[derive(Debug, Default)]
20
+ pub struct Scratch {
21
+ classes: Vec<Class>,
22
+ levels: Vec<u8>,
23
+ }
24
+
25
+ /// Combining marks: diacritics, and the vowel points and signs of the
26
+ /// right-to-left scripts. Drawn as glyphs of their own, they never start a
27
+ /// word.
28
+ pub const fn is_mark(ch: char) -> bool {
29
+ matches!(
30
+ ch,
31
+ '\u{0300}'..='\u{036F}'
32
+ // Hebrew points and cantillation.
33
+ | '\u{0591}'..='\u{05C7}'
34
+ | '\u{FB1E}'
35
+ // Arabic harakat, Quranic annotation, extended Arabic marks.
36
+ | '\u{0610}'..='\u{061A}'
37
+ | '\u{064B}'..='\u{065F}'
38
+ | '\u{0670}'
39
+ | '\u{06D6}'..='\u{06ED}'
40
+ | '\u{0898}'..='\u{089F}'
41
+ | '\u{08CA}'..='\u{08E1}'
42
+ | '\u{08E3}'..='\u{08FF}'
43
+ // Syriac, Thaana, N'Ko, Samaritan, Mandaic.
44
+ | '\u{0711}'
45
+ | '\u{0730}'..='\u{074A}'
46
+ | '\u{07A6}'..='\u{07B0}'
47
+ | '\u{07EB}'..='\u{07F3}'
48
+ | '\u{07FD}'
49
+ | '\u{0816}'..='\u{0819}'
50
+ | '\u{081B}'..='\u{0823}'
51
+ | '\u{0825}'..='\u{0827}'
52
+ | '\u{0829}'..='\u{082D}'
53
+ | '\u{0859}'..='\u{085B}'
54
+ | '\u{1AB0}'..='\u{1AFF}'
55
+ | '\u{20D0}'..='\u{20FF}'
56
+ | '\u{FE20}'..='\u{FE2F}'
57
+ // Hanifi Rohingya, Adlam.
58
+ | '\u{10D24}'..='\u{10D27}'
59
+ | '\u{1E944}'..='\u{1E94A}'
60
+ )
61
+ }
62
+
63
+ /// Whether a line reads right to left, from its letters and its page's
64
+ /// direction. On a right-to-left page a line does when it has a
65
+ /// right-to-left letter at all, or only figures (a table row reads from the
66
+ /// right); on a left-to-right page only when its letters are all, or nearly
67
+ /// all, right to left: an Arabic name in an English line stays in an
68
+ /// English line.
69
+ pub const fn line_right_to_left(line: Counts, page_right_to_left: bool) -> bool {
70
+ if page_right_to_left {
71
+ line.right > 0 || line.left == 0 && line.numbers > 0
72
+ } else {
73
+ line.right > 0 && line.right >= 4 * line.left
74
+ }
75
+ }
76
+
77
+ /// Puts a line's pieces, drawn left to right, in reading order. Lines
78
+ /// with nothing right to left in them are left as they are.
79
+ pub fn reading_order(pieces: &mut [&str], page_right_to_left: bool, scratch: &mut Scratch) {
80
+ let mut counts = Counts::default();
81
+ for piece in pieces.iter() {
82
+ counts.add(piece);
83
+ }
84
+ let right_to_left = line_right_to_left(counts, page_right_to_left);
85
+ if right_to_left || counts.right > 0 {
86
+ reorder(pieces, right_to_left, scratch);
87
+ }
88
+ }
89
+
90
+ /// UAX #9 on pieces in drawn order: classes, numbers (W2, W7, W4, W5),
91
+ /// neutrals (N1, N2), levels (I1, I2), then the runs reversed (L2).
92
+ fn reorder(pieces: &mut [&str], right_to_left: bool, scratch: &mut Scratch) {
93
+ let Scratch { classes, levels } = scratch;
94
+ classes.clear();
95
+ classes.extend(pieces.iter().map(|piece| {
96
+ piece.chars().map(class).find(|&class| class != Class::Neutral).unwrap_or(Class::Neutral)
97
+ }));
98
+ resolve_numbers(classes, right_to_left);
99
+ join_numbers(pieces, classes);
100
+ resolve_neutrals(classes, right_to_left);
101
+ levels.clear();
102
+ levels.extend(classes.iter().map(|class| match class {
103
+ Class::Right | Class::Arabic => 1,
104
+ Class::Number | Class::ArabicNumber => 2,
105
+ Class::Left | Class::Neutral => {
106
+ if right_to_left {
107
+ 2
108
+ } else {
109
+ 0
110
+ }
111
+ }
112
+ }));
113
+ // From the highest level to the lowest odd one, every run at that
114
+ // level or above reversed.
115
+ for level in [2, 1] {
116
+ let mut from = 0;
117
+ while let Some(run) = run(levels, from, |&piece| piece >= level) {
118
+ from = run.end;
119
+ pieces[run].reverse();
120
+ }
121
+ }
122
+ }
123
+
124
+ /// The next run of items, from `from` on, that `wanted` picks.
125
+ fn run<T>(items: &[T], from: usize, wanted: impl Fn(&T) -> bool) -> Option<Range<usize>> {
126
+ let start = from + items.get(from..)?.iter().position(&wanted)?;
127
+ let length = items[start..].iter().position(|item| !wanted(item));
128
+ Some(start..length.map_or(items.len(), |length| start + length))
129
+ }
130
+
131
+ /// What joins two digits into one number, Arabic numbers included:
132
+ /// `1,234.5`, `9:30`, `3/4`.
133
+ const SEPARATORS: [&str; 8] = [",", ".", ":", "/", "\u{00A0}", "\u{060C}", "\u{066B}", "\u{066C}"];
134
+
135
+ /// What joins two digits into one number in any but Arabic text:
136
+ /// `2024-03-15`, `+966`.
137
+ const SIGNS_BETWEEN: [&str; 3] = ["+", "-", "\u{2212}"];
138
+
139
+ /// Signs that go with the number next to them, in any but Arabic text:
140
+ /// `50%`, `$5`.
141
+ const SIGNS: [&str; 11] = ["%", "‰", "‱", "°", "$", "€", "£", "¥", "¢", "#", "\u{066A}"];
142
+
143
+ /// What kind of number each run of digits is. A number touching a letter
144
+ /// (`ISO9001`) goes with its word. In a right-to-left line, a number after
145
+ /// left-to-right text, the next letter to its left, is part of that text
146
+ /// (`ISO 9001`); in a left-to-right line, a number is part of a
147
+ /// right-to-left run when one follows it, the next letter to its right.
148
+ /// Digits following Arabic letters, in reading order, are Arabic numbers.
149
+ fn resolve_numbers(classes: &mut [Class], right_to_left: bool) {
150
+ let mut from = 0;
151
+ while let Some(Range { start, end: at }) = run(classes, from, |&class| class == Class::Number) {
152
+ from = at;
153
+ let glued = (start > 0 && classes[start - 1] == Class::Left)
154
+ || classes.get(at) == Some(&Class::Left);
155
+ let before = classes[..start].iter().rev().find(|class| class.strong());
156
+ let after = classes[at..].iter().find(|class| class.strong());
157
+ let left_to_right = if right_to_left {
158
+ before == Some(&Class::Left)
159
+ } else {
160
+ !after.is_some_and(|class| class.right_to_left())
161
+ };
162
+ if glued || left_to_right {
163
+ classes[start..at].fill(Class::Left);
164
+ } else if after == Some(&Class::Arabic) {
165
+ classes[start..at].fill(Class::ArabicNumber);
166
+ }
167
+ }
168
+ }
169
+
170
+ /// Separators between two digits, and signs next to one, as part of the
171
+ /// number, as far as its kind allows.
172
+ fn join_numbers(pieces: &[&str], classes: &mut [Class]) {
173
+ for at in 1..classes.len().saturating_sub(1) {
174
+ let (before, after) = (classes[at - 1], classes[at + 1]);
175
+ let joins = classes[at] == Class::Neutral
176
+ && before == after
177
+ && match before {
178
+ Class::Number => {
179
+ SEPARATORS.contains(&pieces[at]) || SIGNS_BETWEEN.contains(&pieces[at])
180
+ }
181
+ Class::ArabicNumber => SEPARATORS.contains(&pieces[at]),
182
+ _ => false,
183
+ };
184
+ if joins {
185
+ classes[at] = before;
186
+ }
187
+ }
188
+ for at in (0..classes.len()).chain((0..classes.len()).rev()) {
189
+ let next_to_number = (at > 0 && classes[at - 1] == Class::Number)
190
+ || classes.get(at + 1) == Some(&Class::Number);
191
+ if classes[at] == Class::Neutral && SIGNS.contains(&pieces[at]) && next_to_number {
192
+ classes[at] = Class::Number;
193
+ }
194
+ }
195
+ }
196
+
197
+ /// Spaces and punctuation between two pieces of one direction take it,
198
+ /// numbers counting as right to left; others take the line's.
199
+ fn resolve_neutrals(classes: &mut [Class], right_to_left: bool) {
200
+ let line = if right_to_left { Class::Right } else { Class::Left };
201
+ let side = |class: Class| {
202
+ if class.right_to_left() || class.number() { Class::Right } else { Class::Left }
203
+ };
204
+ let mut from = 0;
205
+ while let Some(Range { start, end: at }) = run(classes, from, |&class| class == Class::Neutral)
206
+ {
207
+ from = at;
208
+ let before = if start == 0 { line } else { side(classes[start - 1]) };
209
+ let after = classes.get(at).map_or(line, |&class| side(class));
210
+ classes[start..at].fill(if before == after { before } else { line });
211
+ }
212
+ }
@@ -2,15 +2,18 @@
2
2
  //! glyphs, paragraphs by the gaps between lines, headings by size, and
3
3
  //! the running headers and footers left out.
4
4
 
5
+ mod bidi;
5
6
  #[cfg(test)]
6
7
  mod tests;
7
8
 
8
9
  use std::collections::HashMap;
9
10
  use std::ops::Range;
10
11
 
12
+ use self::bidi::{Scratch, is_mark};
11
13
  use super::content::{Glyph, PageText};
12
14
  use crate::read::content::{Block, Bounds, Page, Placed, Text};
13
15
  use crate::read::span;
16
+ use crate::script::Counts;
14
17
 
15
18
  /// Share of the page height, at the top and the bottom, where running
16
19
  /// headers and footers live.
@@ -61,7 +64,9 @@ pub fn lines(page: &PageText) -> PageLines {
61
64
  height,
62
65
  images: u32::try_from(page.images).unwrap_or(u32::MAX),
63
66
  };
67
+ let right_to_left = right_to_left(page);
64
68
  let mut pieces: Vec<&str> = Vec::new();
69
+ let mut scratch = Scratch::default();
65
70
  let mut glyphs: Vec<(f64, f64, &Glyph)> = Vec::with_capacity(page.glyphs.len());
66
71
  for turns in 0..4_u8 {
67
72
  glyphs.clear();
@@ -89,7 +94,7 @@ pub fn lines(page: &PageText) -> PageLines {
89
94
  let row = &mut glyphs[start..end];
90
95
  row.sort_by(|a, b| a.0.total_cmp(&b.0));
91
96
  let added = out.lines.len();
92
- line(page, row, turns, &mut pieces, &mut out);
97
+ line(page, row, turns, right_to_left, &mut pieces, &mut scratch, &mut out);
93
98
  if let Some(line) = out.lines.get_mut(added)
94
99
  && turns == 0
95
100
  && (line.y > height * (1.0 - EDGE) || line.y < height * EDGE)
@@ -103,14 +108,31 @@ pub fn lines(page: &PageText) -> PageLines {
103
108
  out
104
109
  }
105
110
 
111
+ /// Whether a page reads right to left: more of its letters, those in the
112
+ /// top and bottom margins of upright text aside, are right to left.
113
+ fn right_to_left(page: &PageText) -> bool {
114
+ let height = page.size.1.max(1.0);
115
+ let mut counts = Counts::default();
116
+ for glyph in &page.glyphs {
117
+ if glyph.turns == 0 && (glyph.y > height * (1.0 - EDGE) || glyph.y < height * EDGE) {
118
+ continue;
119
+ }
120
+ counts.add(&page.text[glyph.text.start as usize..glyph.text.end as usize]);
121
+ }
122
+ counts.right_to_left()
123
+ }
124
+
106
125
  /// One line's text, appended to `out.text`: spaces where glyphs leave a
107
126
  /// word gap, a glyph drawn twice on itself (fake bold) kept once, and
108
- /// right-to-left lines in reading order. A line with no text is not added.
127
+ /// right-to-left text in reading order, in the page's direction. A line
128
+ /// with no text is not added.
109
129
  fn line<'p>(
110
130
  page: &'p PageText,
111
131
  row: &[(f64, f64, &Glyph)],
112
132
  turns: u8,
133
+ right_to_left: bool,
113
134
  pieces: &mut Vec<&'p str>,
135
+ scratch: &mut Scratch,
114
136
  out: &mut PageLines,
115
137
  ) {
116
138
  // Glyph texts and the spaces between them, left to right as drawn.
@@ -139,9 +161,7 @@ fn line<'p>(
139
161
  count += 1;
140
162
  previous = Some((x, glyph, glyph_text));
141
163
  }
142
- if right_to_left(pieces) {
143
- reorder(pieces);
144
- }
164
+ bidi::reading_order(pieces, right_to_left, scratch);
145
165
  // Spaces collapsed and trimmed on the way in.
146
166
  let start = out.text.len();
147
167
  for ch in pieces.iter().flat_map(|piece| piece.chars()) {
@@ -172,120 +192,6 @@ fn margin_key(text: &str) -> u64 {
172
192
  hash.max(1)
173
193
  }
174
194
 
175
- /// The strong direction of a character: `Some(true)` for the scripts
176
- /// written right to left, `Some(false)` for other letters and for digits
177
- /// (which keep their order within right-to-left text), `None` for spaces
178
- /// and punctuation.
179
- ///
180
- /// Right to left: Hebrew (and Yiddish, Ladino), Arabic (and Persian, Urdu,
181
- /// Pashto, Sindhi, Kurdish, Uyghur), Syriac, Thaana (Dhivehi), N'Ko,
182
- /// Samaritan, Mandaic, their presentation forms; Adlam (Fulani), Hanifi
183
- /// Rohingya, Mende Kikakui, Yezidi, Garay and the historic scripts
184
- /// (Phoenician, Aramaic, Sogdian...); and the right-to-left mark.
185
- fn direction(ch: char) -> Option<bool> {
186
- match ch {
187
- '\u{0590}'..='\u{08FF}'
188
- | '\u{200F}'
189
- | '\u{FB1D}'..='\u{FDFF}'
190
- | '\u{FE70}'..='\u{FEFC}'
191
- | '\u{10800}'..='\u{10FFF}'
192
- | '\u{1E800}'..='\u{1EFFF}' => Some(true),
193
- '\u{200E}' => Some(false),
194
- ch if ch.is_alphanumeric() => Some(false),
195
- _ => None,
196
- }
197
- }
198
-
199
- /// Combining marks: diacritics, and the vowel points and signs of the
200
- /// right-to-left scripts. Drawn as glyphs of their own, they never start a
201
- /// word.
202
- const fn is_mark(ch: char) -> bool {
203
- matches!(
204
- ch,
205
- '\u{0300}'..='\u{036F}'
206
- // Hebrew points and cantillation.
207
- | '\u{0591}'..='\u{05C7}'
208
- | '\u{FB1E}'
209
- // Arabic harakat, Quranic annotation, extended Arabic marks.
210
- | '\u{0610}'..='\u{061A}'
211
- | '\u{064B}'..='\u{065F}'
212
- | '\u{0670}'
213
- | '\u{06D6}'..='\u{06ED}'
214
- | '\u{0898}'..='\u{089F}'
215
- | '\u{08CA}'..='\u{08E1}'
216
- | '\u{08E3}'..='\u{08FF}'
217
- // Syriac, Thaana, N'Ko, Samaritan, Mandaic.
218
- | '\u{0711}'
219
- | '\u{0730}'..='\u{074A}'
220
- | '\u{07A6}'..='\u{07B0}'
221
- | '\u{07EB}'..='\u{07F3}'
222
- | '\u{07FD}'
223
- | '\u{0816}'..='\u{0819}'
224
- | '\u{081B}'..='\u{0823}'
225
- | '\u{0825}'..='\u{0827}'
226
- | '\u{0829}'..='\u{082D}'
227
- | '\u{0859}'..='\u{085B}'
228
- | '\u{1AB0}'..='\u{1AFF}'
229
- | '\u{20D0}'..='\u{20FF}'
230
- | '\u{FE20}'..='\u{FE2F}'
231
- // Hanifi Rohingya, Adlam.
232
- | '\u{10D24}'..='\u{10D27}'
233
- | '\u{1E944}'..='\u{1E94A}'
234
- )
235
- }
236
-
237
- fn piece_direction(piece: &str) -> Option<bool> {
238
- piece.chars().find_map(direction)
239
- }
240
-
241
- /// Whether more of the line's letters read right to left than left to right.
242
- fn right_to_left(pieces: &[&str]) -> bool {
243
- let (mut right, mut left) = (0_usize, 0_usize);
244
- for ch in pieces.iter().flat_map(|piece| piece.chars()) {
245
- match direction(ch) {
246
- Some(true) => right += 1,
247
- Some(false) if ch.is_alphabetic() => left += 1,
248
- _ => {}
249
- }
250
- }
251
- right > left
252
- }
253
-
254
- /// Puts a right-to-left line, drawn left to right, in reading order: runs
255
- /// of left-to-right text (Latin words, numbers) keep their order inside,
256
- /// everything else reads from the right. Spaces and punctuation between two
257
- /// left-to-right pieces belong to their run.
258
- fn reorder(pieces: &mut [&str]) {
259
- let directions: Vec<Option<bool>> = pieces.iter().map(|piece| piece_direction(piece)).collect();
260
- let left_to_right: Vec<bool> = (0..pieces.len())
261
- .map(|at| {
262
- directions[at].map_or_else(
263
- || {
264
- let before = directions[..at].iter().rev().find_map(|direction| *direction);
265
- let after = directions[at + 1..].iter().find_map(|direction| *direction);
266
- before == Some(false) && after == Some(false)
267
- },
268
- |rtl| !rtl,
269
- )
270
- })
271
- .collect();
272
- pieces.reverse();
273
- let mut at = 0;
274
- let total = pieces.len();
275
- while at < total {
276
- // Reversed, so a left-to-right run is now backwards: turn it back.
277
- if left_to_right[total - 1 - at] {
278
- let start = at;
279
- while at < total && left_to_right[total - 1 - at] {
280
- at += 1;
281
- }
282
- pieces[start..at].reverse();
283
- } else {
284
- at += 1;
285
- }
286
- }
287
- }
288
-
289
195
  /// The document's pages, from their lines: blocks of the lines that are
290
196
  /// not running headers or footers.
291
197
  pub fn pages(pages: &[PageLines]) -> Vec<Page> {
@@ -52,7 +52,7 @@ pub fn content(ole: &Ole<'_>, budget: &Budget<'_>) -> Result<Vec<Sheet>> {
52
52
  name: listed.name.clone(),
53
53
  hidden: listed.hidden,
54
54
  cells,
55
- comments: Vec::new(),
55
+ ..Sheet::default()
56
56
  });
57
57
  }
58
58
  Ok(sheets)