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
|
@@ -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
|
|
127
|
+
/// right-to-left text in reading order, in the page's direction. A line
|
|
128
|
+
/// with no text is not added.
|
|
109
129
|
fn line<'p>(
|
|
110
130
|
page: &'p PageText,
|
|
111
131
|
row: &[(f64, f64, &Glyph)],
|
|
112
132
|
turns: u8,
|
|
133
|
+
right_to_left: bool,
|
|
113
134
|
pieces: &mut Vec<&'p str>,
|
|
135
|
+
scratch: &mut Scratch,
|
|
114
136
|
out: &mut PageLines,
|
|
115
137
|
) {
|
|
116
138
|
// Glyph texts and the spaces between them, left to right as drawn.
|
|
@@ -139,9 +161,7 @@ fn line<'p>(
|
|
|
139
161
|
count += 1;
|
|
140
162
|
previous = Some((x, glyph, glyph_text));
|
|
141
163
|
}
|
|
142
|
-
|
|
143
|
-
reorder(pieces);
|
|
144
|
-
}
|
|
164
|
+
bidi::reading_order(pieces, right_to_left, scratch);
|
|
145
165
|
// Spaces collapsed and trimmed on the way in.
|
|
146
166
|
let start = out.text.len();
|
|
147
167
|
for ch in pieces.iter().flat_map(|piece| piece.chars()) {
|
|
@@ -172,120 +192,6 @@ fn margin_key(text: &str) -> u64 {
|
|
|
172
192
|
hash.max(1)
|
|
173
193
|
}
|
|
174
194
|
|
|
175
|
-
/// The strong direction of a character: `Some(true)` for the scripts
|
|
176
|
-
/// written right to left, `Some(false)` for other letters and for digits
|
|
177
|
-
/// (which keep their order within right-to-left text), `None` for spaces
|
|
178
|
-
/// and punctuation.
|
|
179
|
-
///
|
|
180
|
-
/// Right to left: Hebrew (and Yiddish, Ladino), Arabic (and Persian, Urdu,
|
|
181
|
-
/// Pashto, Sindhi, Kurdish, Uyghur), Syriac, Thaana (Dhivehi), N'Ko,
|
|
182
|
-
/// Samaritan, Mandaic, their presentation forms; Adlam (Fulani), Hanifi
|
|
183
|
-
/// Rohingya, Mende Kikakui, Yezidi, Garay and the historic scripts
|
|
184
|
-
/// (Phoenician, Aramaic, Sogdian...); and the right-to-left mark.
|
|
185
|
-
fn direction(ch: char) -> Option<bool> {
|
|
186
|
-
match ch {
|
|
187
|
-
'\u{0590}'..='\u{08FF}'
|
|
188
|
-
| '\u{200F}'
|
|
189
|
-
| '\u{FB1D}'..='\u{FDFF}'
|
|
190
|
-
| '\u{FE70}'..='\u{FEFC}'
|
|
191
|
-
| '\u{10800}'..='\u{10FFF}'
|
|
192
|
-
| '\u{1E800}'..='\u{1EFFF}' => Some(true),
|
|
193
|
-
'\u{200E}' => Some(false),
|
|
194
|
-
ch if ch.is_alphanumeric() => Some(false),
|
|
195
|
-
_ => None,
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/// Combining marks: diacritics, and the vowel points and signs of the
|
|
200
|
-
/// right-to-left scripts. Drawn as glyphs of their own, they never start a
|
|
201
|
-
/// word.
|
|
202
|
-
const fn is_mark(ch: char) -> bool {
|
|
203
|
-
matches!(
|
|
204
|
-
ch,
|
|
205
|
-
'\u{0300}'..='\u{036F}'
|
|
206
|
-
// Hebrew points and cantillation.
|
|
207
|
-
| '\u{0591}'..='\u{05C7}'
|
|
208
|
-
| '\u{FB1E}'
|
|
209
|
-
// Arabic harakat, Quranic annotation, extended Arabic marks.
|
|
210
|
-
| '\u{0610}'..='\u{061A}'
|
|
211
|
-
| '\u{064B}'..='\u{065F}'
|
|
212
|
-
| '\u{0670}'
|
|
213
|
-
| '\u{06D6}'..='\u{06ED}'
|
|
214
|
-
| '\u{0898}'..='\u{089F}'
|
|
215
|
-
| '\u{08CA}'..='\u{08E1}'
|
|
216
|
-
| '\u{08E3}'..='\u{08FF}'
|
|
217
|
-
// Syriac, Thaana, N'Ko, Samaritan, Mandaic.
|
|
218
|
-
| '\u{0711}'
|
|
219
|
-
| '\u{0730}'..='\u{074A}'
|
|
220
|
-
| '\u{07A6}'..='\u{07B0}'
|
|
221
|
-
| '\u{07EB}'..='\u{07F3}'
|
|
222
|
-
| '\u{07FD}'
|
|
223
|
-
| '\u{0816}'..='\u{0819}'
|
|
224
|
-
| '\u{081B}'..='\u{0823}'
|
|
225
|
-
| '\u{0825}'..='\u{0827}'
|
|
226
|
-
| '\u{0829}'..='\u{082D}'
|
|
227
|
-
| '\u{0859}'..='\u{085B}'
|
|
228
|
-
| '\u{1AB0}'..='\u{1AFF}'
|
|
229
|
-
| '\u{20D0}'..='\u{20FF}'
|
|
230
|
-
| '\u{FE20}'..='\u{FE2F}'
|
|
231
|
-
// Hanifi Rohingya, Adlam.
|
|
232
|
-
| '\u{10D24}'..='\u{10D27}'
|
|
233
|
-
| '\u{1E944}'..='\u{1E94A}'
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
fn piece_direction(piece: &str) -> Option<bool> {
|
|
238
|
-
piece.chars().find_map(direction)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/// Whether more of the line's letters read right to left than left to right.
|
|
242
|
-
fn right_to_left(pieces: &[&str]) -> bool {
|
|
243
|
-
let (mut right, mut left) = (0_usize, 0_usize);
|
|
244
|
-
for ch in pieces.iter().flat_map(|piece| piece.chars()) {
|
|
245
|
-
match direction(ch) {
|
|
246
|
-
Some(true) => right += 1,
|
|
247
|
-
Some(false) if ch.is_alphabetic() => left += 1,
|
|
248
|
-
_ => {}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
right > left
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/// Puts a right-to-left line, drawn left to right, in reading order: runs
|
|
255
|
-
/// of left-to-right text (Latin words, numbers) keep their order inside,
|
|
256
|
-
/// everything else reads from the right. Spaces and punctuation between two
|
|
257
|
-
/// left-to-right pieces belong to their run.
|
|
258
|
-
fn reorder(pieces: &mut [&str]) {
|
|
259
|
-
let directions: Vec<Option<bool>> = pieces.iter().map(|piece| piece_direction(piece)).collect();
|
|
260
|
-
let left_to_right: Vec<bool> = (0..pieces.len())
|
|
261
|
-
.map(|at| {
|
|
262
|
-
directions[at].map_or_else(
|
|
263
|
-
|| {
|
|
264
|
-
let before = directions[..at].iter().rev().find_map(|direction| *direction);
|
|
265
|
-
let after = directions[at + 1..].iter().find_map(|direction| *direction);
|
|
266
|
-
before == Some(false) && after == Some(false)
|
|
267
|
-
},
|
|
268
|
-
|rtl| !rtl,
|
|
269
|
-
)
|
|
270
|
-
})
|
|
271
|
-
.collect();
|
|
272
|
-
pieces.reverse();
|
|
273
|
-
let mut at = 0;
|
|
274
|
-
let total = pieces.len();
|
|
275
|
-
while at < total {
|
|
276
|
-
// Reversed, so a left-to-right run is now backwards: turn it back.
|
|
277
|
-
if left_to_right[total - 1 - at] {
|
|
278
|
-
let start = at;
|
|
279
|
-
while at < total && left_to_right[total - 1 - at] {
|
|
280
|
-
at += 1;
|
|
281
|
-
}
|
|
282
|
-
pieces[start..at].reverse();
|
|
283
|
-
} else {
|
|
284
|
-
at += 1;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
195
|
/// The document's pages, from their lines: blocks of the lines that are
|
|
290
196
|
/// not running headers or footers.
|
|
291
197
|
pub fn pages(pages: &[PageLines]) -> Vec<Page> {
|