imprimatur 0.1.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 (98) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/Cargo.lock +835 -0
  4. data/Cargo.toml +44 -0
  5. data/LICENSE.txt +29 -0
  6. data/README.md +314 -0
  7. data/ext/imprimatur_native/cli/Cargo.toml +30 -0
  8. data/ext/imprimatur_native/cli/src/app.rs +183 -0
  9. data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
  10. data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
  11. data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
  12. data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
  13. data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
  14. data/ext/imprimatur_native/cli/src/cli.rs +71 -0
  15. data/ext/imprimatur_native/cli/src/main.rs +15 -0
  16. data/ext/imprimatur_native/core/Cargo.toml +33 -0
  17. data/ext/imprimatur_native/core/LICENSE +29 -0
  18. data/ext/imprimatur_native/core/README.md +105 -0
  19. data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
  20. data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
  21. data/ext/imprimatur_native/core/src/doc.rs +210 -0
  22. data/ext/imprimatur_native/core/src/docx.rs +343 -0
  23. data/ext/imprimatur_native/core/src/error.rs +113 -0
  24. data/ext/imprimatur_native/core/src/lib.rs +38 -0
  25. data/ext/imprimatur_native/core/src/opc.rs +51 -0
  26. data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
  27. data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
  28. data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
  29. data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
  30. data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
  31. data/ext/imprimatur_native/core/src/pdf.rs +84 -0
  32. data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
  33. data/ext/imprimatur_native/core/src/read/content.rs +266 -0
  34. data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
  35. data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
  36. data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
  37. data/ext/imprimatur_native/core/src/read/image.rs +233 -0
  38. data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
  39. data/ext/imprimatur_native/core/src/read/json.rs +172 -0
  40. data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
  41. data/ext/imprimatur_native/core/src/read/package.rs +191 -0
  42. data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
  43. data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
  44. data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
  45. data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
  46. data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
  47. data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
  48. data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
  49. data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
  50. data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
  51. data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
  52. data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
  53. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
  54. data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
  55. data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
  56. data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
  57. data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
  58. data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
  59. data/ext/imprimatur_native/core/src/read/span.rs +22 -0
  60. data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
  61. data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
  62. data/ext/imprimatur_native/core/src/read.rs +145 -0
  63. data/ext/imprimatur_native/core/src/style.rs +82 -0
  64. data/ext/imprimatur_native/core/src/test_support.rs +163 -0
  65. data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
  66. data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
  67. data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
  68. data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
  69. data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
  70. data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
  71. data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
  72. data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
  73. data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
  74. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
  75. data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
  76. data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
  77. data/ext/imprimatur_native/core/src/xml.rs +95 -0
  78. data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
  79. data/ext/imprimatur_native/core/src/zip.rs +161 -0
  80. data/ext/imprimatur_native/extconf.rb +45 -0
  81. data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
  82. data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
  83. data/ext/imprimatur_native/ffi/src/document.rs +138 -0
  84. data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
  85. data/ext/imprimatur_native/ffi/src/read.rs +182 -0
  86. data/ext/imprimatur_native/ffi/src/style.rs +85 -0
  87. data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
  88. data/lib/imprimatur/document.rb +127 -0
  89. data/lib/imprimatur/error.rb +22 -0
  90. data/lib/imprimatur/formula.rb +13 -0
  91. data/lib/imprimatur/native.rb +17 -0
  92. data/lib/imprimatur/reader.rb +59 -0
  93. data/lib/imprimatur/style.rb +31 -0
  94. data/lib/imprimatur/validation.rb +35 -0
  95. data/lib/imprimatur/version.rb +5 -0
  96. data/lib/imprimatur/xlsx.rb +183 -0
  97. data/lib/imprimatur.rb +111 -0
  98. metadata +157 -0
@@ -0,0 +1,315 @@
1
+ //! Fonts as text extraction needs them (ISO 32000-1, 9.5 to 9.10): how a
2
+ //! string splits into character codes, each code's text and width, and
3
+ //! whether the face is bold.
4
+
5
+ #[cfg(test)]
6
+ mod tests;
7
+
8
+ use std::ops::Range;
9
+
10
+ use super::cmap::{CMap, utf16};
11
+ use super::document::{Document, Resolved};
12
+ use super::glyphs::{BaseEncoding, encoded, named};
13
+ use super::object::{Dict, Object};
14
+ use crate::Result;
15
+ use crate::pdf::metrics::{HELVETICA, HELVETICA_BOLD};
16
+ use crate::read::span;
17
+
18
+ /// Widths in glyph space units: thousandths of the font size.
19
+ const DEFAULT_WIDTH: f64 = 500.0;
20
+
21
+ #[derive(Debug)]
22
+ enum Codes {
23
+ /// One byte per code; each code's text from the encoding.
24
+ Simple { text: Box<[Range<u32>; 256]>, widths: Box<[f64; 256]> },
25
+ /// Codes per the encoding `CMap` (two bytes for `Identity-H`).
26
+ Composite {
27
+ encoding: Option<CMap>,
28
+ identity: bool,
29
+ unicode: bool,
30
+ default: f64,
31
+ widths: Vec<(u32, u32, f64)>,
32
+ },
33
+ }
34
+
35
+ #[derive(Debug)]
36
+ pub struct Font {
37
+ pub bold: bool,
38
+ codes: Codes,
39
+ to_unicode: Option<CMap>,
40
+ /// The simple encoding's text, back to back.
41
+ text: String,
42
+ /// Glyph space to text space: 0.001, or a Type 3 font's matrix.
43
+ scale: f64,
44
+ }
45
+
46
+ impl Font {
47
+ /// A font from its dictionary. Never fails on a damaged font: what
48
+ /// cannot be read falls back to defaults.
49
+ pub fn load(document: &Document<'_>, dict: &Dict) -> Result<Self> {
50
+ let cmap = |object: &Object| -> Result<Option<CMap>> {
51
+ Ok(document.stream_data(object)?.map(|data| CMap::parse(&data)))
52
+ };
53
+ let to_unicode = dict.get(b"ToUnicode").map(cmap).transpose()?.flatten();
54
+ let base_font = document.entry(dict, b"BaseFont")?;
55
+ let base_font = base_font.as_deref().and_then(Object::as_name).unwrap_or_default();
56
+ let subtype = dict.get(b"Subtype").and_then(Object::as_name).unwrap_or_default();
57
+ // A composite font's widths and descriptor are its descendant's.
58
+ let descendant = if subtype == b"Type0" {
59
+ match document.entry(dict, b"DescendantFonts")?.as_deref() {
60
+ Some(Object::Array(fonts)) => fonts
61
+ .first()
62
+ .map(|font| document.resolve(font).map(Resolved::into_shared))
63
+ .transpose()?,
64
+ _ => None,
65
+ }
66
+ } else {
67
+ None
68
+ };
69
+ let empty = Dict::default();
70
+ let described = descendant.as_deref().and_then(Object::as_dict).unwrap_or(dict);
71
+ let descriptor = document.entry(described, b"FontDescriptor")?;
72
+ let descriptor = descriptor.as_deref().and_then(Object::as_dict).unwrap_or(&empty);
73
+ let flags = descriptor.get(b"Flags").and_then(Object::as_integer).unwrap_or(0);
74
+ let mut text = String::new();
75
+ let (codes, scale) = if subtype == b"Type0" {
76
+ let (encoding, identity, unicode) = match dict.get(b"Encoding") {
77
+ Some(Object::Name(name)) => (
78
+ None,
79
+ name.starts_with(b"Identity"),
80
+ name.windows(4).any(|part| part == b"UCS2" || part == b"TF16"),
81
+ ),
82
+ Some(object) => (cmap(object)?, false, false),
83
+ None => (None, true, false),
84
+ };
85
+ let default = document
86
+ .entry(described, b"DW")?
87
+ .and_then(|width| width.as_number())
88
+ .unwrap_or(1000.0);
89
+ let widths = match document.entry(described, b"W")?.as_deref() {
90
+ Some(Object::Array(items)) => cid_widths(document, items)?,
91
+ _ => Vec::new(),
92
+ };
93
+ (Codes::Composite { encoding, identity, unicode, default, widths }, 0.001)
94
+ } else {
95
+ let symbolic = flags & 4 != 0;
96
+ let table = simple_text(document, dict, base_font, subtype, symbolic, &mut text)?;
97
+ let scale = if subtype == b"Type3" {
98
+ let matrix = document.entry(dict, b"FontMatrix")?;
99
+ let first =
100
+ matrix.as_deref().and_then(Object::as_array).and_then(|matrix| matrix.first());
101
+ first
102
+ .and_then(Object::as_number)
103
+ .filter(|scale| *scale != 0.0)
104
+ .unwrap_or(0.001)
105
+ .abs()
106
+ } else {
107
+ 0.001
108
+ };
109
+ let widths = simple_widths(document, dict, base_font, descriptor)?;
110
+ (Codes::Simple { text: table, widths }, scale)
111
+ };
112
+ let weight = descriptor.get(b"FontWeight").and_then(Object::as_number).unwrap_or(0.0);
113
+ let bold = weight >= 600.0
114
+ || flags & 0x4_0000 != 0
115
+ || [&b"bold"[..], b"black", b"heavy", b"semibold", b"demi"].iter().any(|word| {
116
+ base_font.windows(word.len()).any(|part| part.eq_ignore_ascii_case(word))
117
+ });
118
+ Ok(Self { bold, codes, to_unicode, text, scale })
119
+ }
120
+
121
+ /// Calls `glyph` for each character code in `bytes`: its text (empty
122
+ /// when unknown), its width in text space units per unit of font size,
123
+ /// and whether it is the single-byte code 32 that word spacing applies to.
124
+ pub fn glyphs(
125
+ &self,
126
+ bytes: &[u8],
127
+ scratch: &mut String,
128
+ mut glyph: impl FnMut(&str, f64, bool),
129
+ ) {
130
+ let mut at = 0;
131
+ while at < bytes.len() {
132
+ let length = match &self.codes {
133
+ Codes::Simple { .. } => 1,
134
+ Codes::Composite { encoding: Some(cmap), .. } if cmap.has_spaces() => {
135
+ cmap.code_length(&bytes[at..])
136
+ }
137
+ Codes::Composite { .. } => self
138
+ .to_unicode
139
+ .as_ref()
140
+ .filter(|cmap| cmap.has_spaces())
141
+ .map_or(2, |cmap| cmap.code_length(&bytes[at..])),
142
+ };
143
+ let code_bytes = &bytes[at..(at + length).min(bytes.len())];
144
+ at += length;
145
+ let code = super::cmap::be(code_bytes);
146
+ scratch.clear();
147
+ let mapped = self.to_unicode.as_ref().is_some_and(|cmap| cmap.push_text(code, scratch));
148
+ let width = match &self.codes {
149
+ Codes::Simple { text, widths } => {
150
+ if !mapped {
151
+ let span = &text[code as usize & 0xFF];
152
+ scratch.push_str(span::slice(&self.text, span));
153
+ }
154
+ widths[code as usize & 0xFF]
155
+ }
156
+ Codes::Composite { encoding, identity, unicode, default, widths } => {
157
+ if !mapped && *unicode {
158
+ scratch.push_str(&utf16(code_bytes));
159
+ }
160
+ let cid = if *identity {
161
+ Some(code)
162
+ } else {
163
+ encoding.as_ref().and_then(|cmap| cmap.cid(code))
164
+ };
165
+ cid.and_then(|cid| {
166
+ widths
167
+ .iter()
168
+ .find(|&&(low, high, _)| (low..=high).contains(&cid))
169
+ .map(|&(_, _, width)| width)
170
+ })
171
+ .unwrap_or(*default)
172
+ }
173
+ };
174
+ glyph(scratch, width * self.scale, length == 1 && code == 32);
175
+ }
176
+ }
177
+ }
178
+
179
+ /// The text of each code of a simple font: its base encoding (named, or
180
+ /// the font's built-in one), then the `/Differences` over it.
181
+ fn simple_text(
182
+ document: &Document<'_>,
183
+ dict: &Dict,
184
+ base_font: &[u8],
185
+ subtype: &[u8],
186
+ symbolic: bool,
187
+ text: &mut String,
188
+ ) -> Result<Box<[Range<u32>; 256]>> {
189
+ let built_in = if base_font.windows(6).any(|part| part == b"Symbol") {
190
+ BaseEncoding::Symbol
191
+ } else if base_font.windows(7).any(|part| part == b"Dingbat") {
192
+ BaseEncoding::ZapfDingbats
193
+ } else if subtype == b"Type1" && !symbolic {
194
+ BaseEncoding::Standard
195
+ } else {
196
+ BaseEncoding::WinAnsi
197
+ };
198
+ let encoding = document.entry(dict, b"Encoding")?;
199
+ let (base, differences) = match encoding.as_deref() {
200
+ Some(Object::Name(name)) => (named_encoding(name).unwrap_or(built_in), None),
201
+ Some(Object::Dict(encoding)) => {
202
+ let base = encoding
203
+ .get(b"BaseEncoding")
204
+ .and_then(Object::as_name)
205
+ .and_then(named_encoding)
206
+ .unwrap_or(built_in);
207
+ (
208
+ base,
209
+ document
210
+ .entry(encoding, b"Differences")?
211
+ .and_then(|differences| differences.as_array().map(<[Object]>::to_vec)),
212
+ )
213
+ }
214
+ _ => (built_in, None),
215
+ };
216
+ let mut table: Box<[Range<u32>; 256]> = Box::new(std::array::from_fn(|_| 0..0));
217
+ let store = |text: &mut String, glyph: &str| span::append(text, |text| text.push_str(glyph));
218
+ for code in 0..=255_u8 {
219
+ table[usize::from(code)] = store(text, encoded(base, code));
220
+ }
221
+ let mut code = 0_usize;
222
+ for item in differences.unwrap_or_default() {
223
+ match item {
224
+ Object::Integer(start) => code = usize::try_from(start).unwrap_or(256),
225
+ Object::Name(name) => {
226
+ if code < 256 {
227
+ let glyph = std::str::from_utf8(&name).ok().and_then(named);
228
+ table[code] = store(text, glyph.as_deref().unwrap_or(""));
229
+ }
230
+ code += 1;
231
+ }
232
+ _ => {}
233
+ }
234
+ }
235
+ Ok(table)
236
+ }
237
+
238
+ const fn named_encoding(name: &[u8]) -> Option<BaseEncoding> {
239
+ match name {
240
+ b"WinAnsiEncoding" => Some(BaseEncoding::WinAnsi),
241
+ b"MacRomanEncoding" => Some(BaseEncoding::MacRoman),
242
+ b"StandardEncoding" | b"MacExpertEncoding" => Some(BaseEncoding::Standard),
243
+ _ => None,
244
+ }
245
+ }
246
+
247
+ /// `/FirstChar` and `/Widths`; the missing width, or the standard font's
248
+ /// widths, for codes they leave out.
249
+ fn simple_widths(
250
+ document: &Document<'_>,
251
+ dict: &Dict,
252
+ base_font: &[u8],
253
+ descriptor: &Dict,
254
+ ) -> Result<Box<[f64; 256]>> {
255
+ let missing =
256
+ descriptor.get(b"MissingWidth").and_then(Object::as_number).filter(|width| *width > 0.0);
257
+ let standard =
258
+ if base_font.windows(4).any(|part| part == b"Bold") { &HELVETICA_BOLD } else { &HELVETICA };
259
+ let courier = base_font.starts_with(b"Courier");
260
+ let mut widths: Box<[f64; 256]> = Box::new(std::array::from_fn(|code| {
261
+ missing.unwrap_or_else(|| {
262
+ if courier {
263
+ 600.0
264
+ } else {
265
+ f64::from(standard.widths[code]).max(if code == 32 {
266
+ 250.0
267
+ } else {
268
+ DEFAULT_WIDTH * 0.5
269
+ })
270
+ }
271
+ })
272
+ }));
273
+ let first =
274
+ document.entry(dict, b"FirstChar")?.and_then(|first| first.as_integer()).unwrap_or(0);
275
+ if let Some(listed) = document.entry(dict, b"Widths")?
276
+ && let Some(listed) = listed.as_array()
277
+ {
278
+ for (offset, width) in listed.iter().enumerate() {
279
+ let code = usize::try_from(first).unwrap_or(0) + offset;
280
+ if code < 256
281
+ && let Some(width) = document.resolve(width)?.as_number()
282
+ {
283
+ widths[code] = width;
284
+ }
285
+ }
286
+ }
287
+ Ok(widths)
288
+ }
289
+
290
+ /// A CID font's `/W`: `first [w1 w2 ...]` and `first last w` entries.
291
+ fn cid_widths(document: &Document<'_>, items: &[Object]) -> Result<Vec<(u32, u32, f64)>> {
292
+ let mut widths = Vec::new();
293
+ let mut at = 0;
294
+ while at + 1 < items.len() {
295
+ let first = items[at].as_integer().and_then(|first| u32::try_from(first).ok()).unwrap_or(0);
296
+ if let Some(list) = document.resolve(&items[at + 1])?.as_array() {
297
+ for (offset, width) in (0..).zip(list) {
298
+ if let Some(width) = width.as_number() {
299
+ widths.push((first + offset, first + offset, width));
300
+ }
301
+ }
302
+ at += 2;
303
+ } else {
304
+ let last = items[at + 1]
305
+ .as_integer()
306
+ .and_then(|last| u32::try_from(last).ok())
307
+ .unwrap_or(first);
308
+ if let Some(width) = items.get(at + 2).and_then(Object::as_number) {
309
+ widths.push((first, last, width));
310
+ }
311
+ at += 3;
312
+ }
313
+ }
314
+ Ok(widths)
315
+ }
@@ -0,0 +1,179 @@
1
+ //! Glyph names and simple-font encodings to text, from `glyphs.txt`
2
+ //! (generated by `tools/generate_pdf_glyphs.rb`), parsed once on first use.
3
+
4
+ #[cfg(test)]
5
+ mod tests;
6
+
7
+ use std::borrow::Cow;
8
+ use std::ops::Range;
9
+ use std::sync::LazyLock;
10
+
11
+ use crate::read::span;
12
+
13
+ static DATA: &str = include_str!("glyphs.txt");
14
+
15
+ /// The built-in encodings a simple font starts from (ISO 32000-1, Annex D).
16
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
17
+ pub enum BaseEncoding {
18
+ Standard,
19
+ WinAnsi,
20
+ MacRoman,
21
+ Symbol,
22
+ ZapfDingbats,
23
+ }
24
+
25
+ const SECTIONS: [&str; 6] =
26
+ ["STANDARD", "WIN_ANSI", "MAC_ROMAN", "SYMBOL", "ZAPF_DINGBATS", "PRESENTATION"];
27
+ const PRESENTATION: usize = 5;
28
+
29
+ struct Tables {
30
+ /// Every glyph's text, back to back; the tables hold spans of it.
31
+ text: String,
32
+ /// Sorted by name, as the file is.
33
+ names: Vec<(&'static str, Range<u32>)>,
34
+ encodings: [[Range<u32>; 256]; 5],
35
+ /// Presentation forms and their decompositions, sorted by code point.
36
+ presentation: Vec<(u32, Range<u32>)>,
37
+ }
38
+
39
+ static TABLES: LazyLock<Tables> = LazyLock::new(|| {
40
+ let mut tables = Tables {
41
+ text: String::with_capacity(20 * 1024),
42
+ names: Vec::with_capacity(4_600),
43
+ encodings: std::array::from_fn(|_| std::array::from_fn(|_| 0..0)),
44
+ presentation: Vec::with_capacity(1_000),
45
+ };
46
+ let mut section = None;
47
+ for line in DATA.lines() {
48
+ if let Some(name) = line.strip_prefix('@') {
49
+ section = SECTIONS.iter().position(|known| *known == name);
50
+ continue;
51
+ }
52
+ let Some((key, codes)) = line.split_once(';') else { continue };
53
+ let chars = codes
54
+ .split(' ')
55
+ .filter_map(|hex| u32::from_str_radix(hex, 16).ok().and_then(char::from_u32));
56
+ let span = span::append(&mut tables.text, |text| text.extend(chars));
57
+ match section {
58
+ None => tables.names.push((key, span)),
59
+ Some(PRESENTATION) => {
60
+ if let Ok(code) = u32::from_str_radix(key, 16) {
61
+ tables.presentation.push((code, span));
62
+ }
63
+ }
64
+ Some(index) => {
65
+ if let Ok(code) = key.parse::<u8>() {
66
+ tables.encodings[index][usize::from(code)] = span;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ tables
72
+ });
73
+
74
+ fn slice(span: &Range<u32>) -> &'static str {
75
+ span::slice(&TABLES.text, span)
76
+ }
77
+
78
+ /// The text of `code` in a base encoding; empty where it is undefined.
79
+ pub fn encoded(base: BaseEncoding, code: u8) -> &'static str {
80
+ slice(&TABLES.encodings[base as usize][usize::from(code)])
81
+ }
82
+
83
+ /// A presentation form's plain text (`fi` is `fi`, Arabic contextual forms
84
+ /// their letter), `None` for any other character.
85
+ pub fn decomposed(ch: char) -> Option<&'static str> {
86
+ if !matches!(ch, '\u{FB00}'..='\u{FDFF}' | '\u{FE70}'..='\u{FEFF}') {
87
+ return None;
88
+ }
89
+ let presentation = &TABLES.presentation;
90
+ let index = presentation.binary_search_by_key(&u32::from(ch), |&(code, _)| code).ok()?;
91
+ Some(slice(&presentation[index].1))
92
+ }
93
+
94
+ /// The text of a glyph name: the glyph lists, then the conventions for
95
+ /// names without an entry (ISO 32000-1, 9.10.2, and the AGL specification):
96
+ /// `uniXXXX...` and `uXXXX[XX]` code points, a `.suffix` dropped, and `_`
97
+ /// joining the components of a ligature.
98
+ pub fn named(name: &str) -> Option<Cow<'static, str>> {
99
+ if let Some(text) = listed(name) {
100
+ return Some(Cow::Borrowed(text));
101
+ }
102
+ let base = name.split_once('.').map_or(name, |(base, _)| base);
103
+ if base.is_empty() {
104
+ return None;
105
+ }
106
+ if base.contains('_') {
107
+ let mut text = String::new();
108
+ for part in base.split('_') {
109
+ text.push_str(&component(part)?);
110
+ }
111
+ return Some(Cow::Owned(text));
112
+ }
113
+ component(base)
114
+ }
115
+
116
+ /// A name's text in the glyph lists.
117
+ fn listed(name: &str) -> Option<&'static str> {
118
+ let names = &TABLES.names;
119
+ let index = names.binary_search_by(|(known, _)| known.as_bytes().cmp(name.as_bytes())).ok()?;
120
+ Some(slice(&names[index].1))
121
+ }
122
+
123
+ fn component(name: &str) -> Option<Cow<'static, str>> {
124
+ if let Some(text) = listed(name) {
125
+ return Some(Cow::Borrowed(text));
126
+ }
127
+ let hex = |digits: &str| {
128
+ u32::from_str_radix(digits, 16)
129
+ .ok()
130
+ .filter(|&code| !(0xD800..0xE000).contains(&code))
131
+ .and_then(char::from_u32)
132
+ };
133
+ if let Some(digits) = name.strip_prefix("uni")
134
+ && !digits.is_empty()
135
+ && digits.len().is_multiple_of(4)
136
+ && upper_hex(digits)
137
+ {
138
+ return digits
139
+ .as_bytes()
140
+ .chunks(4)
141
+ .map(|chunk| std::str::from_utf8(chunk).ok().and_then(hex))
142
+ .collect::<Option<String>>()
143
+ .map(Cow::Owned);
144
+ }
145
+ if let Some(digits) = name.strip_prefix('u')
146
+ && (4..=6).contains(&digits.len())
147
+ && upper_hex(digits)
148
+ {
149
+ return hex(digits).map(|ch| Cow::Owned(ch.to_string()));
150
+ }
151
+ None
152
+ }
153
+
154
+ /// Hexadecimal digits in upper case, as `uniXXXX` names spell them.
155
+ fn upper_hex(digits: &str) -> bool {
156
+ digits.bytes().all(|byte| byte.is_ascii_digit() || (b'A'..=b'F').contains(&byte))
157
+ }
158
+
159
+ /// Text fit for output: ligatures spelled out and presentation forms as
160
+ /// their letters (see [`decomposed`]), no-break and other spaces as
161
+ /// spaces, soft hyphens and control characters dropped, the private-use
162
+ /// bullets of Symbol and Wingdings fonts as bullets.
163
+ pub fn push_clean(out: &mut String, text: &str) {
164
+ for ch in text.chars() {
165
+ if let Some(plain) = decomposed(ch) {
166
+ out.push_str(plain);
167
+ continue;
168
+ }
169
+ match ch {
170
+ '\u{A0}' | '\u{2000}'..='\u{200A}' | '\u{202F}' | '\u{3000}' | '\t' => out.push(' '),
171
+ '\u{F0B7}' | '\u{F06C}' | '\u{F0A7}' | '\u{F0D8}' | '\u{F076}' | '\u{F0FC}' => {
172
+ out.push('•');
173
+ }
174
+ '\u{AD}' | '\u{200B}'..='\u{200F}' | '\u{FEFF}' | '\u{FFFD}' => {}
175
+ ch if ch.is_control() => {}
176
+ ch => out.push(ch),
177
+ }
178
+ }
179
+ }