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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/Cargo.lock +124 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +123 -23
  6. data/ext/imprimatur_native/cli/Cargo.toml +2 -2
  7. data/ext/imprimatur_native/cli/src/cli/grep.rs +1 -1
  8. data/ext/imprimatur_native/cli/src/cli/markdown.rs +2 -2
  9. data/ext/imprimatur_native/cli/src/cli.rs +5 -5
  10. data/ext/imprimatur_native/cli/src/main.rs +1 -1
  11. data/ext/imprimatur_native/core/Cargo.toml +12 -2
  12. data/ext/imprimatur_native/core/README.md +15 -1
  13. data/ext/imprimatur_native/core/src/archive.rs +279 -0
  14. data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
  15. data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
  16. data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
  17. data/ext/imprimatur_native/core/src/doc.rs +86 -2
  18. data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
  19. data/ext/imprimatur_native/core/src/docx.rs +91 -28
  20. data/ext/imprimatur_native/core/src/error.rs +13 -1
  21. data/ext/imprimatur_native/core/src/font.rs +229 -0
  22. data/ext/imprimatur_native/core/src/lib.rs +5 -0
  23. data/ext/imprimatur_native/core/src/pdf/layout.rs +38 -15
  24. data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
  25. data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
  26. data/ext/imprimatur_native/core/src/pdf.rs +31 -3
  27. data/ext/imprimatur_native/core/src/read/content.rs +91 -3
  28. data/ext/imprimatur_native/core/src/read/json.rs +96 -7
  29. data/ext/imprimatur_native/core/src/read/markdown.rs +1 -1
  30. data/ext/imprimatur_native/core/src/read/{xlsx/numbers.rs → numbers.rs} +69 -45
  31. data/ext/imprimatur_native/core/src/read/ods.rs +356 -0
  32. data/ext/imprimatur_native/core/src/read/ole.rs +244 -0
  33. data/ext/imprimatur_native/core/src/read/package.rs +0 -5
  34. data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
  35. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
  36. data/ext/imprimatur_native/core/src/read/xls/strings.rs +156 -0
  37. data/ext/imprimatur_native/core/src/read/xls.rs +342 -0
  38. data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
  39. data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -20
  40. data/ext/imprimatur_native/core/src/read.rs +68 -21
  41. data/ext/imprimatur_native/core/src/script.rs +116 -0
  42. data/ext/imprimatur_native/core/src/style.rs +21 -0
  43. data/ext/imprimatur_native/core/src/test_support.rs +90 -2
  44. data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
  45. data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
  46. data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
  47. data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
  48. data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
  49. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
  50. data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
  51. data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
  52. data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
  53. data/ext/imprimatur_native/ffi/src/archive.rs +119 -0
  54. data/ext/imprimatur_native/ffi/src/blocking.rs +63 -0
  55. data/ext/imprimatur_native/ffi/src/document.rs +48 -1
  56. data/ext/imprimatur_native/ffi/src/lib.rs +52 -1
  57. data/ext/imprimatur_native/ffi/src/read.rs +9 -14
  58. data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
  59. data/lib/imprimatur/document.rb +30 -6
  60. data/lib/imprimatur/error.rb +4 -3
  61. data/lib/imprimatur/reader.rb +3 -3
  62. data/lib/imprimatur/spreadsheet.rb +151 -0
  63. data/lib/imprimatur/version.rb +1 -1
  64. data/lib/imprimatur/xlsx.rb +25 -4
  65. data/lib/imprimatur/zip.rb +127 -0
  66. data/lib/imprimatur.rb +18 -5
  67. metadata +23 -4
@@ -0,0 +1,244 @@
1
+ //! Compound files (MS-CFB): the container legacy Office documents are, a
2
+ //! small FAT file system inside one file. Reading a stream of the root
3
+ //! storage by name is all the readers need.
4
+
5
+ #[cfg(test)]
6
+ mod tests;
7
+
8
+ use crate::read::Budget;
9
+ use crate::{Error, Result};
10
+
11
+ /// The first eight bytes of every compound file.
12
+ pub const SIGNATURE: [u8; 8] = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
13
+ /// Sector numbers above this are markers: end of chain, free, FAT, DIFAT.
14
+ const MAX_SECTOR: u32 = 0xFFFF_FFFA;
15
+ /// No sibling or child in the directory tree.
16
+ const NO_STREAM: u32 = 0xFFFF_FFFF;
17
+ const MINI_SECTOR: usize = 64;
18
+ const ENTRY: usize = 128;
19
+
20
+ /// A compound file's allocation tables and the root storage's entries.
21
+ pub struct Ole<'a> {
22
+ data: &'a [u8],
23
+ sector: usize,
24
+ fat: Vec<u32>,
25
+ mini_fat: Vec<u32>,
26
+ mini_stream: Vec<u8>,
27
+ mini_cutoff: u64,
28
+ /// The root storage's children: name, whether a stream, start, size.
29
+ entries: Vec<(String, bool, u32, u64)>,
30
+ }
31
+
32
+ fn malformed(reason: &str) -> Error {
33
+ Error::malformed("compound file", reason)
34
+ }
35
+
36
+ /// An allocation table's sector numbers.
37
+ fn words(bytes: &[u8]) -> impl Iterator<Item = u32> + '_ {
38
+ bytes.as_chunks::<4>().0.iter().map(|word| u32::from_le_bytes(*word))
39
+ }
40
+
41
+ fn u16_at(bytes: &[u8], at: usize) -> Option<u16> {
42
+ Some(u16::from_le_bytes(bytes.get(at..at.checked_add(2)?)?.try_into().ok()?))
43
+ }
44
+
45
+ fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
46
+ Some(u32::from_le_bytes(bytes.get(at..at.checked_add(4)?)?.try_into().ok()?))
47
+ }
48
+
49
+ impl<'a> Ole<'a> {
50
+ /// Reads the header, the allocation tables and the directory.
51
+ ///
52
+ /// # Errors
53
+ /// [`Error::Malformed`] for a file that is not a compound file or whose
54
+ /// tables point outside it or in circles.
55
+ pub fn open(data: &'a [u8], budget: &Budget<'_>) -> Result<Self> {
56
+ if !data.starts_with(&SIGNATURE) || data.len() < 512 {
57
+ return Err(malformed("no compound file header"));
58
+ }
59
+ let header = |at| u32_at(data, at).ok_or_else(|| malformed("a short header"));
60
+ let sector = match u16_at(data, 0x1E) {
61
+ Some(9) => 512,
62
+ Some(12) => 4096,
63
+ _ => return Err(malformed("an unknown sector size")),
64
+ };
65
+ let mut ole = Self {
66
+ data,
67
+ sector,
68
+ fat: Vec::new(),
69
+ mini_fat: Vec::new(),
70
+ mini_stream: Vec::new(),
71
+ mini_cutoff: u64::from(header(0x38)?),
72
+ entries: Vec::new(),
73
+ };
74
+ // The FAT's own sectors: 109 listed in the header, the rest in a
75
+ // chain of DIFAT sectors, each ending with the next one's number.
76
+ let mut fat_sectors: Vec<u32> = (0..109)
77
+ .filter_map(|index| u32_at(data, 0x4C + index * 4))
78
+ .filter(|&s| s <= MAX_SECTOR)
79
+ .collect();
80
+ let mut next = header(0x44)?;
81
+ let per_sector = sector / 4;
82
+ for _ in 0..=data.len() / sector {
83
+ if next > MAX_SECTOR {
84
+ break;
85
+ }
86
+ let bytes = ole.sector_bytes(next)?;
87
+ fat_sectors.extend(
88
+ (0..per_sector - 1)
89
+ .filter_map(|index| u32_at(bytes, index * 4))
90
+ .filter(|&s| s <= MAX_SECTOR),
91
+ );
92
+ next = u32_at(bytes, (per_sector - 1) * 4).unwrap_or(NO_STREAM);
93
+ }
94
+ budget.inflate(fat_sectors.len().saturating_mul(sector))?;
95
+ for number in fat_sectors {
96
+ budget.tick()?;
97
+ let bytes = ole.sector_bytes(number)?;
98
+ ole.fat.extend(words(bytes));
99
+ }
100
+ let directory = ole.chain(header(0x30)?, None, budget)?;
101
+ let first_mini_fat = header(0x3C)?;
102
+ if first_mini_fat <= MAX_SECTOR {
103
+ let table = ole.chain(first_mini_fat, None, budget)?;
104
+ ole.mini_fat = words(&table).collect();
105
+ }
106
+ let root = directory.get(..ENTRY).ok_or_else(|| malformed("no root entry"))?;
107
+ if root[0x42] != 5 {
108
+ return Err(malformed("the first entry is not the root"));
109
+ }
110
+ // The whole chain: some writers give the root the size of the one
111
+ // stream in it, short of the last mini sector it fills.
112
+ let (start, _) = location(root, sector);
113
+ ole.mini_stream =
114
+ if start <= MAX_SECTOR { ole.chain(start, None, budget)? } else { Vec::new() };
115
+ ole.entries = children(&directory, sector, budget)?;
116
+ Ok(ole)
117
+ }
118
+
119
+ /// The bytes of the stream `name` in the root storage, its name
120
+ /// compared regardless of case; `None` when there is none.
121
+ ///
122
+ /// # Errors
123
+ /// [`Error::Malformed`] for a stream whose sectors are missing or loop.
124
+ pub fn stream(&self, name: &str, budget: &Budget<'_>) -> Result<Option<Vec<u8>>> {
125
+ let Some(&(_, _, start, size)) = self
126
+ .entries
127
+ .iter()
128
+ .find(|(entry, stream, ..)| *stream && entry.eq_ignore_ascii_case(name))
129
+ else {
130
+ return Ok(None);
131
+ };
132
+ if size < self.mini_cutoff {
133
+ return self.mini_chain(start, size, budget).map(Some);
134
+ }
135
+ self.chain(start, Some(size), budget).map(Some)
136
+ }
137
+
138
+ fn sector_bytes(&self, number: u32) -> Result<&'a [u8]> {
139
+ let start = (number as usize + 1)
140
+ .checked_mul(self.sector)
141
+ .ok_or_else(|| malformed("a sector out of range"))?;
142
+ self.data
143
+ .get(start..start + self.sector)
144
+ .ok_or_else(|| malformed("a sector past the end of the file"))
145
+ }
146
+
147
+ /// The sectors chained from `start`, cut to `size` when given.
148
+ fn chain(&self, start: u32, size: Option<u64>, budget: &Budget<'_>) -> Result<Vec<u8>> {
149
+ let limit = size.map_or(usize::MAX, |size| usize::try_from(size).unwrap_or(usize::MAX));
150
+ if limit != usize::MAX && limit > self.data.len() {
151
+ return Err(malformed("a stream larger than the file"));
152
+ }
153
+ let mut out = Vec::with_capacity(limit.min(self.data.len()));
154
+ let mut seen = vec![false; self.fat.len()];
155
+ let mut next = start;
156
+ while next <= MAX_SECTOR && out.len() < limit {
157
+ budget.tick()?;
158
+ let visited =
159
+ seen.get_mut(next as usize).ok_or_else(|| malformed("a sector beyond the FAT"))?;
160
+ if std::mem::replace(visited, true) {
161
+ return Err(malformed("a sector chain that loops"));
162
+ }
163
+ out.extend_from_slice(self.sector_bytes(next)?);
164
+ next = self.fat[next as usize];
165
+ }
166
+ out.truncate(limit);
167
+ budget.inflate(out.len())?;
168
+ Ok(out)
169
+ }
170
+
171
+ /// A small stream, kept in the root's mini stream in 64-byte sectors.
172
+ fn mini_chain(&self, start: u32, size: u64, budget: &Budget<'_>) -> Result<Vec<u8>> {
173
+ let limit = usize::try_from(size).unwrap_or(usize::MAX);
174
+ let mut out = Vec::with_capacity(limit.min(self.mini_stream.len()));
175
+ let mut seen = vec![false; self.mini_fat.len()];
176
+ let mut next = start;
177
+ while next <= MAX_SECTOR && out.len() < limit {
178
+ budget.tick()?;
179
+ let visited = seen
180
+ .get_mut(next as usize)
181
+ .ok_or_else(|| malformed("a sector beyond the mini FAT"))?;
182
+ if std::mem::replace(visited, true) {
183
+ return Err(malformed("a mini sector chain that loops"));
184
+ }
185
+ let at = next as usize * MINI_SECTOR;
186
+ let bytes = self
187
+ .mini_stream
188
+ .get(at..at + MINI_SECTOR)
189
+ .ok_or_else(|| malformed("a mini sector past the mini stream"))?;
190
+ out.extend_from_slice(bytes);
191
+ next = self.mini_fat[next as usize];
192
+ }
193
+ out.truncate(limit);
194
+ budget.inflate(out.len())?;
195
+ Ok(out)
196
+ }
197
+ }
198
+
199
+ /// An entry's first sector and size; files of 512-byte sectors (version 3)
200
+ /// leave the size's high half undefined.
201
+ fn location(entry: &[u8], sector: usize) -> (u32, u64) {
202
+ let start = u32_at(entry, 0x74).unwrap_or(NO_STREAM);
203
+ let low = u64::from(u32_at(entry, 0x78).unwrap_or(0));
204
+ let high = if sector == 512 { 0 } else { u64::from(u32_at(entry, 0x7C).unwrap_or(0)) };
205
+ (start, high << 32 | low)
206
+ }
207
+
208
+ /// The root storage's children, from the red-black tree under its child
209
+ /// pointer: name, whether a stream, first sector, size.
210
+ fn children(
211
+ directory: &[u8],
212
+ sector: usize,
213
+ budget: &Budget<'_>,
214
+ ) -> Result<Vec<(String, bool, u32, u64)>> {
215
+ let entry = |index: u32| directory.get(index as usize * ENTRY..(index as usize + 1) * ENTRY);
216
+ let mut found = Vec::new();
217
+ let mut pending = vec![u32_at(directory, 0x4C).unwrap_or(NO_STREAM)];
218
+ let mut visits = 0_usize;
219
+ while let Some(index) = pending.pop() {
220
+ if index == NO_STREAM {
221
+ continue;
222
+ }
223
+ visits += 1;
224
+ if visits > directory.len() / ENTRY {
225
+ return Err(malformed("a directory tree that loops"));
226
+ }
227
+ budget.tick()?;
228
+ let Some(bytes) = entry(index) else {
229
+ return Err(malformed("a directory entry past the end"));
230
+ };
231
+ let length = usize::from(u16_at(bytes, 0x40).unwrap_or(0)).min(64).saturating_sub(2);
232
+ let units: Vec<u16> = bytes[..length]
233
+ .as_chunks::<2>()
234
+ .0
235
+ .iter()
236
+ .map(|unit| u16::from_le_bytes(*unit))
237
+ .collect();
238
+ let (start, size) = location(bytes, sector);
239
+ found.push((String::from_utf16_lossy(&units), bytes[0x42] == 2, start, size));
240
+ pending.push(u32_at(bytes, 0x44).unwrap_or(NO_STREAM));
241
+ pending.push(u32_at(bytes, 0x48).unwrap_or(NO_STREAM));
242
+ }
243
+ Ok(found)
244
+ }
@@ -86,11 +86,6 @@ impl<'a> Package<'a> {
86
86
  self.text(&part.target)?.map_or_else(|| Ok(T::default()), |text| parse(&text))
87
87
  }
88
88
 
89
- /// The parts' names.
90
- pub fn parts(&self) -> impl Iterator<Item = &'a str> + '_ {
91
- self.zip.names()
92
- }
93
-
94
89
  /// A part's bytes, `None` when the package has no such part.
95
90
  pub fn part(&self, name: &str) -> Result<Option<Vec<u8>>> {
96
91
  self.zip.read(name, self.budget)
@@ -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> {