oxidize_pdf/text/encoding.rs
1#[derive(Debug, Clone, Copy, PartialEq)]
2pub enum TextEncoding {
3 StandardEncoding,
4 MacRomanEncoding,
5 WinAnsiEncoding,
6 PdfDocEncoding,
7}
8
9impl TextEncoding {
10 /// Strict encoding: returns `Err(char)` for the first codepoint the
11 /// encoding cannot represent. Unlike [`encode`], does NOT silently
12 /// substitute `?` for unmappable characters — callers that must refuse
13 /// to emit corrupt output (e.g. form-field appearance streams) use this
14 /// path to produce explicit errors instead of silent garbage.
15 ///
16 /// Contract per encoding:
17 /// - `WinAnsiEncoding`: every Unicode codepoint in the Windows-1252
18 /// repertoire is representable; every other codepoint fails.
19 /// - `MacRomanEncoding`: same idea over the Mac Roman repertoire.
20 /// - `StandardEncoding` / `PdfDocEncoding`: accepts only the ASCII
21 /// range `0x00..=0x7F` (the safe lowest-common-denominator — the
22 /// lossy UTF-8 passthrough used by `encode` would silently produce
23 /// garbage for non-ASCII here).
24 pub fn encode_strict(&self, text: &str) -> Result<Vec<u8>, char> {
25 let mut out = Vec::with_capacity(text.len());
26 for ch in text.chars() {
27 match self {
28 TextEncoding::WinAnsiEncoding => match winansi_encode_char(ch) {
29 Some(b) => out.push(b),
30 None => return Err(ch),
31 },
32 TextEncoding::MacRomanEncoding => match macroman_encode_char(ch) {
33 Some(b) => out.push(b),
34 None => return Err(ch),
35 },
36 TextEncoding::StandardEncoding | TextEncoding::PdfDocEncoding => {
37 if (ch as u32) <= 0x7F {
38 out.push(ch as u8);
39 } else {
40 return Err(ch);
41 }
42 }
43 }
44 }
45 Ok(out)
46 }
47
48 pub fn encode(&self, text: &str) -> Vec<u8> {
49 match self {
50 TextEncoding::StandardEncoding | TextEncoding::PdfDocEncoding => {
51 // For now, use UTF-8 encoding
52 text.bytes().collect()
53 }
54 TextEncoding::WinAnsiEncoding => {
55 // Convert UTF-8 to Windows-1252
56 let mut result = Vec::new();
57 for ch in text.chars() {
58 match ch as u32 {
59 // ASCII range
60 0x00..=0x7F => result.push(ch as u8),
61 // Latin-1 Supplement that overlaps with Windows-1252
62 0xA0..=0xFF => result.push(ch as u8),
63 // Special mappings for Windows-1252
64 0x20AC => result.push(0x80), // Euro sign
65 0x201A => result.push(0x82), // Single low quotation mark
66 0x0192 => result.push(0x83), // Latin small letter f with hook
67 0x201E => result.push(0x84), // Double low quotation mark
68 0x2026 => result.push(0x85), // Horizontal ellipsis
69 0x2020 => result.push(0x86), // Dagger
70 0x2021 => result.push(0x87), // Double dagger
71 0x02C6 => result.push(0x88), // Circumflex accent
72 0x2030 => result.push(0x89), // Per mille sign
73 0x0160 => result.push(0x8A), // Latin capital letter S with caron
74 0x2039 => result.push(0x8B), // Single left angle quotation mark
75 0x0152 => result.push(0x8C), // Latin capital ligature OE
76 0x017D => result.push(0x8E), // Latin capital letter Z with caron
77 0x2018 => result.push(0x91), // Left single quotation mark
78 0x2019 => result.push(0x92), // Right single quotation mark
79 0x201C => result.push(0x93), // Left double quotation mark
80 0x201D => result.push(0x94), // Right double quotation mark
81 0x2022 => result.push(0x95), // Bullet
82 0x2013 => result.push(0x96), // En dash
83 0x2014 => result.push(0x97), // Em dash
84 0x02DC => result.push(0x98), // Small tilde
85 0x2122 => result.push(0x99), // Trade mark sign
86 0x0161 => result.push(0x9A), // Latin small letter s with caron
87 0x203A => result.push(0x9B), // Single right angle quotation mark
88 0x0153 => result.push(0x9C), // Latin small ligature oe
89 0x017E => result.push(0x9E), // Latin small letter z with caron
90 0x0178 => result.push(0x9F), // Latin capital letter Y with diaeresis
91 // Default: use question mark for unmapped characters
92 _ => result.push(b'?'),
93 }
94 }
95 result
96 }
97 TextEncoding::MacRomanEncoding => {
98 // Convert UTF-8 to Mac Roman encoding
99 let mut result = Vec::new();
100 for ch in text.chars() {
101 match ch as u32 {
102 // ASCII range
103 0x00..=0x7F => result.push(ch as u8),
104 // Mac Roman specific mappings
105 0x00C4 => result.push(0x80), // Latin capital letter A with diaeresis
106 0x00C5 => result.push(0x81), // Latin capital letter A with ring above
107 0x00C7 => result.push(0x82), // Latin capital letter C with cedilla
108 0x00C9 => result.push(0x83), // Latin capital letter E with acute
109 0x00D1 => result.push(0x84), // Latin capital letter N with tilde
110 0x00D6 => result.push(0x85), // Latin capital letter O with diaeresis
111 0x00DC => result.push(0x86), // Latin capital letter U with diaeresis
112 0x00E1 => result.push(0x87), // Latin small letter a with acute
113 0x00E0 => result.push(0x88), // Latin small letter a with grave
114 0x00E2 => result.push(0x89), // Latin small letter a with circumflex
115 0x00E4 => result.push(0x8A), // Latin small letter a with diaeresis
116 0x00E3 => result.push(0x8B), // Latin small letter a with tilde
117 0x00E5 => result.push(0x8C), // Latin small letter a with ring above
118 0x00E7 => result.push(0x8D), // Latin small letter c with cedilla
119 0x00E9 => result.push(0x8E), // Latin small letter e with acute
120 0x00E8 => result.push(0x8F), // Latin small letter e with grave
121 0x00EA => result.push(0x90), // Latin small letter e with circumflex
122 0x00EB => result.push(0x91), // Latin small letter e with diaeresis
123 0x00ED => result.push(0x92), // Latin small letter i with acute
124 0x00EC => result.push(0x93), // Latin small letter i with grave
125 0x00EE => result.push(0x94), // Latin small letter i with circumflex
126 0x00EF => result.push(0x95), // Latin small letter i with diaeresis
127 0x00F1 => result.push(0x96), // Latin small letter n with tilde
128 0x00F3 => result.push(0x97), // Latin small letter o with acute
129 0x00F2 => result.push(0x98), // Latin small letter o with grave
130 0x00F4 => result.push(0x99), // Latin small letter o with circumflex
131 0x00F6 => result.push(0x9A), // Latin small letter o with diaeresis
132 0x00F5 => result.push(0x9B), // Latin small letter o with tilde
133 0x00FA => result.push(0x9C), // Latin small letter u with acute
134 0x00F9 => result.push(0x9D), // Latin small letter u with grave
135 0x00FB => result.push(0x9E), // Latin small letter u with circumflex
136 0x00FC => result.push(0x9F), // Latin small letter u with diaeresis
137 0x2020 => result.push(0xA0), // Dagger
138 0x00B0 => result.push(0xA1), // Degree sign
139 0x00A2 => result.push(0xA2), // Cent sign
140 0x00A3 => result.push(0xA3), // Pound sign
141 0x00A7 => result.push(0xA4), // Section sign
142 0x2022 => result.push(0xA5), // Bullet
143 0x00B6 => result.push(0xA6), // Pilcrow sign
144 0x00DF => result.push(0xA7), // Latin small letter sharp s
145 0x00AE => result.push(0xA8), // Registered sign
146 0x00A9 => result.push(0xA9), // Copyright sign
147 0x2122 => result.push(0xAA), // Trade mark sign
148 0x00B4 => result.push(0xAB), // Acute accent
149 0x00A8 => result.push(0xAC), // Diaeresis
150 0x2260 => result.push(0xAD), // Not equal to
151 0x00C6 => result.push(0xAE), // Latin capital letter AE
152 0x00D8 => result.push(0xAF), // Latin capital letter O with stroke
153 // Default: use question mark for unmapped characters
154 _ => result.push(b'?'),
155 }
156 }
157 result
158 }
159 }
160 }
161
162 pub fn decode(&self, data: &[u8]) -> String {
163 match self {
164 TextEncoding::StandardEncoding | TextEncoding::PdfDocEncoding => {
165 // For now, assume UTF-8
166 String::from_utf8_lossy(data).to_string()
167 }
168 TextEncoding::WinAnsiEncoding => {
169 // Decode Windows-1252 to UTF-8
170 let mut result = String::new();
171 for &byte in data {
172 let ch = match byte {
173 // ASCII range
174 0x00..=0x7F => byte as char,
175 // Windows-1252 specific mappings
176 0x80 => '\u{20AC}', // Euro sign
177 0x82 => '\u{201A}', // Single low quotation mark
178 0x83 => '\u{0192}', // Latin small letter f with hook
179 0x84 => '\u{201E}', // Double low quotation mark
180 0x85 => '\u{2026}', // Horizontal ellipsis
181 0x86 => '\u{2020}', // Dagger
182 0x87 => '\u{2021}', // Double dagger
183 0x88 => '\u{02C6}', // Circumflex accent
184 0x89 => '\u{2030}', // Per mille sign
185 0x8A => '\u{0160}', // Latin capital letter S with caron
186 0x8B => '\u{2039}', // Single left angle quotation mark
187 0x8C => '\u{0152}', // Latin capital ligature OE
188 0x8E => '\u{017D}', // Latin capital letter Z with caron
189 0x91 => '\u{2018}', // Left single quotation mark
190 0x92 => '\u{2019}', // Right single quotation mark
191 0x93 => '\u{201C}', // Left double quotation mark
192 0x94 => '\u{201D}', // Right double quotation mark
193 0x95 => '\u{2022}', // Bullet
194 0x96 => '\u{2013}', // En dash
195 0x97 => '\u{2014}', // Em dash
196 0x98 => '\u{02DC}', // Small tilde
197 0x99 => '\u{2122}', // Trade mark sign
198 0x9A => '\u{0161}', // Latin small letter s with caron
199 0x9B => '\u{203A}', // Single right angle quotation mark
200 0x9C => '\u{0153}', // Latin small ligature oe
201 0x9E => '\u{017E}', // Latin small letter z with caron
202 0x9F => '\u{0178}', // Latin capital letter Y with diaeresis
203 // Latin-1 range that overlaps with Windows-1252
204 0xA0..=0xFF => char::from_u32(byte as u32).unwrap_or('?'),
205 // Undefined bytes
206 _ => '?',
207 };
208 result.push(ch);
209 }
210 result
211 }
212 TextEncoding::MacRomanEncoding => {
213 // Decode Mac Roman to UTF-8
214 let mut result = String::new();
215 for &byte in data {
216 let ch = match byte {
217 // ASCII range
218 0x00..=0x7F => byte as char,
219 // Mac Roman specific mappings
220 0x80 => '\u{00C4}', // Latin capital letter A with diaeresis
221 0x81 => '\u{00C5}', // Latin capital letter A with ring above
222 0x82 => '\u{00C7}', // Latin capital letter C with cedilla
223 0x83 => '\u{00C9}', // Latin capital letter E with acute
224 0x84 => '\u{00D1}', // Latin capital letter N with tilde
225 0x85 => '\u{00D6}', // Latin capital letter O with diaeresis
226 0x86 => '\u{00DC}', // Latin capital letter U with diaeresis
227 0x87 => '\u{00E1}', // Latin small letter a with acute
228 0x88 => '\u{00E0}', // Latin small letter a with grave
229 0x89 => '\u{00E2}', // Latin small letter a with circumflex
230 0x8A => '\u{00E4}', // Latin small letter a with diaeresis
231 0x8B => '\u{00E3}', // Latin small letter a with tilde
232 0x8C => '\u{00E5}', // Latin small letter a with ring above
233 0x8D => '\u{00E7}', // Latin small letter c with cedilla
234 0x8E => '\u{00E9}', // Latin small letter e with acute
235 0x8F => '\u{00E8}', // Latin small letter e with grave
236 0x90 => '\u{00EA}', // Latin small letter e with circumflex
237 0x91 => '\u{00EB}', // Latin small letter e with diaeresis
238 0x92 => '\u{00ED}', // Latin small letter i with acute
239 0x93 => '\u{00EC}', // Latin small letter i with grave
240 0x94 => '\u{00EE}', // Latin small letter i with circumflex
241 0x95 => '\u{00EF}', // Latin small letter i with diaeresis
242 0x96 => '\u{00F1}', // Latin small letter n with tilde
243 0x97 => '\u{00F3}', // Latin small letter o with acute
244 0x98 => '\u{00F2}', // Latin small letter o with grave
245 0x99 => '\u{00F4}', // Latin small letter o with circumflex
246 0x9A => '\u{00F6}', // Latin small letter o with diaeresis
247 0x9B => '\u{00F5}', // Latin small letter o with tilde
248 0x9C => '\u{00FA}', // Latin small letter u with acute
249 0x9D => '\u{00F9}', // Latin small letter u with grave
250 0x9E => '\u{00FB}', // Latin small letter u with circumflex
251 0x9F => '\u{00FC}', // Latin small letter u with diaeresis
252 0xA0 => '\u{2020}', // Dagger
253 0xA1 => '\u{00B0}', // Degree sign
254 0xA2 => '\u{00A2}', // Cent sign
255 0xA3 => '\u{00A3}', // Pound sign
256 0xA4 => '\u{00A7}', // Section sign
257 0xA5 => '\u{2022}', // Bullet
258 0xA6 => '\u{00B6}', // Pilcrow sign
259 0xA7 => '\u{00DF}', // Latin small letter sharp s
260 0xA8 => '\u{00AE}', // Registered sign
261 0xA9 => '\u{00A9}', // Copyright sign
262 0xAA => '\u{2122}', // Trade mark sign
263 0xAB => '\u{00B4}', // Acute accent
264 0xAC => '\u{00A8}', // Diaeresis
265 0xAD => '\u{2260}', // Not equal to
266 0xAE => '\u{00C6}', // Latin capital letter AE
267 0xAF => '\u{00D8}', // Latin capital letter O with stroke
268 0xB0 => '\u{221E}', // Infinity
269 0xB1 => '\u{00B1}', // Plus-minus sign
270 0xB2 => '\u{2264}', // Less-than or equal to
271 0xB3 => '\u{2265}', // Greater-than or equal to
272 0xB4 => '\u{00A5}', // Yen sign
273 0xB5 => '\u{00B5}', // Micro sign
274 0xB6 => '\u{2202}', // Partial differential
275 0xB7 => '\u{2211}', // N-ary summation
276 0xB8 => '\u{220F}', // N-ary product
277 0xB9 => '\u{03C0}', // Greek small letter pi
278 0xBA => '\u{222B}', // Integral
279 0xBB => '\u{00AA}', // Feminine ordinal indicator
280 0xBC => '\u{00BA}', // Masculine ordinal indicator
281 0xBD => '\u{03A9}', // Greek capital letter omega
282 0xBE => '\u{00E6}', // Latin small letter ae
283 0xBF => '\u{00F8}', // Latin small letter o with stroke
284 0xC0 => '\u{00BF}', // Inverted question mark
285 0xC1 => '\u{00A1}', // Inverted exclamation mark
286 0xC2 => '\u{00AC}', // Not sign
287 0xC3 => '\u{221A}', // Square root
288 0xC4 => '\u{0192}', // Latin small letter f with hook
289 0xC5 => '\u{2248}', // Almost equal to
290 0xC6 => '\u{2206}', // Increment
291 0xC7 => '\u{00AB}', // Left-pointing double angle quotation mark
292 0xC8 => '\u{00BB}', // Right-pointing double angle quotation mark
293 0xC9 => '\u{2026}', // Horizontal ellipsis
294 0xCA => '\u{00A0}', // No-break space
295 0xCB => '\u{00C0}', // Latin capital letter A with grave
296 0xCC => '\u{00C3}', // Latin capital letter A with tilde
297 0xCD => '\u{00D5}', // Latin capital letter O with tilde
298 0xCE => '\u{0152}', // Latin capital ligature OE
299 0xCF => '\u{0153}', // Latin small ligature oe
300 0xD0 => '\u{2013}', // En dash
301 0xD1 => '\u{2014}', // Em dash
302 0xD2 => '\u{201C}', // Left double quotation mark
303 0xD3 => '\u{201D}', // Right double quotation mark
304 0xD4 => '\u{2018}', // Left single quotation mark
305 0xD5 => '\u{2019}', // Right single quotation mark
306 0xD6 => '\u{00F7}', // Division sign
307 0xD7 => '\u{25CA}', // Lozenge
308 0xD8 => '\u{00FF}', // Latin small letter y with diaeresis
309 0xD9 => '\u{0178}', // Latin capital letter Y with diaeresis
310 0xDA => '\u{2044}', // Fraction slash
311 0xDB => '\u{20AC}', // Euro sign
312 0xDC => '\u{2039}', // Single left-pointing angle quotation mark
313 0xDD => '\u{203A}', // Single right-pointing angle quotation mark
314 0xDE => '\u{FB01}', // Latin small ligature fi
315 0xDF => '\u{FB02}', // Latin small ligature fl
316 0xE0 => '\u{2021}', // Double dagger
317 0xE1 => '\u{00B7}', // Middle dot
318 0xE2 => '\u{201A}', // Single low-9 quotation mark
319 0xE3 => '\u{201E}', // Double low-9 quotation mark
320 0xE4 => '\u{2030}', // Per mille sign
321 0xE5 => '\u{00C2}', // Latin capital letter A with circumflex
322 0xE6 => '\u{00CA}', // Latin capital letter E with circumflex
323 0xE7 => '\u{00C1}', // Latin capital letter A with acute
324 0xE8 => '\u{00CB}', // Latin capital letter E with diaeresis
325 0xE9 => '\u{00C8}', // Latin capital letter E with grave
326 0xEA => '\u{00CD}', // Latin capital letter I with acute
327 0xEB => '\u{00CE}', // Latin capital letter I with circumflex
328 0xEC => '\u{00CF}', // Latin capital letter I with diaeresis
329 0xED => '\u{00CC}', // Latin capital letter I with grave
330 0xEE => '\u{00D3}', // Latin capital letter O with acute
331 0xEF => '\u{00D4}', // Latin capital letter O with circumflex
332 0xF0 => '\u{F8FF}', // Apple logo
333 0xF1 => '\u{00D2}', // Latin capital letter O with grave
334 0xF2 => '\u{00DA}', // Latin capital letter U with acute
335 0xF3 => '\u{00DB}', // Latin capital letter U with circumflex
336 0xF4 => '\u{00D9}', // Latin capital letter U with grave
337 0xF5 => '\u{0131}', // Latin small letter dotless i
338 0xF6 => '\u{02C6}', // Modifier letter circumflex accent
339 0xF7 => '\u{02DC}', // Small tilde
340 0xF8 => '\u{00AF}', // Macron
341 0xF9 => '\u{02D8}', // Breve
342 0xFA => '\u{02D9}', // Dot above
343 0xFB => '\u{02DA}', // Ring above
344 0xFC => '\u{00B8}', // Cedilla
345 0xFD => '\u{02DD}', // Double acute accent
346 0xFE => '\u{02DB}', // Ogonek
347 0xFF => '\u{02C7}', // Caron
348 };
349 result.push(ch);
350 }
351 result
352 }
353 }
354 }
355}
356
357/// Encode a single Unicode character as a Windows-1252 (PDF WinAnsi) byte.
358///
359/// Returns `None` for any codepoint outside the Windows-1252 repertoire.
360/// Mirrors the mapping used inline by `TextEncoding::encode` so that the
361/// strict and lossy paths stay in lock-step.
362pub fn winansi_encode_char(ch: char) -> Option<u8> {
363 match ch as u32 {
364 0x00..=0x7F => Some(ch as u8),
365 0xA0..=0xFF => Some(ch as u8),
366 0x20AC => Some(0x80), // €
367 0x201A => Some(0x82), // ‚
368 0x0192 => Some(0x83), // ƒ
369 0x201E => Some(0x84), // „
370 0x2026 => Some(0x85), // …
371 0x2020 => Some(0x86), // †
372 0x2021 => Some(0x87), // ‡
373 0x02C6 => Some(0x88), // ˆ
374 0x2030 => Some(0x89), // ‰
375 0x0160 => Some(0x8A), // Š
376 0x2039 => Some(0x8B), // ‹
377 0x0152 => Some(0x8C), // Œ
378 0x017D => Some(0x8E), // Ž
379 0x2018 => Some(0x91), // '
380 0x2019 => Some(0x92), // '
381 0x201C => Some(0x93), // "
382 0x201D => Some(0x94), // "
383 0x2022 => Some(0x95), // •
384 0x2013 => Some(0x96), // –
385 0x2014 => Some(0x97), // —
386 0x02DC => Some(0x98), // ˜
387 0x2122 => Some(0x99), // ™
388 0x0161 => Some(0x9A), // š
389 0x203A => Some(0x9B), // ›
390 0x0153 => Some(0x9C), // œ
391 0x017E => Some(0x9E), // ž
392 0x0178 => Some(0x9F), // Ÿ
393 _ => None,
394 }
395}
396
397/// Decode a Windows-1252 (PDF WinAnsi) byte to its Unicode character.
398///
399/// Exact inverse of [`winansi_encode_char`] over the defined code points. The
400/// `0x00..=0x7F` and `0xA0..=0xFF` ranges decode identity (ASCII / Latin-1);
401/// the `0x80..=0x9F` window carries the Windows-1252 specials. The five
402/// unassigned code points (0x81, 0x8D, 0x8F, 0x90, 0x9D) decode to the
403/// same-valued C1 control character (matching `byte as char`); they carry no
404/// glyph in WinAnsi-encoded fonts.
405pub fn winansi_decode_char(byte: u8) -> char {
406 match byte {
407 0x80 => '\u{20AC}', // €
408 0x82 => '\u{201A}', // ‚
409 0x83 => '\u{0192}', // ƒ
410 0x84 => '\u{201E}', // „
411 0x85 => '\u{2026}', // …
412 0x86 => '\u{2020}', // †
413 0x87 => '\u{2021}', // ‡
414 0x88 => '\u{02C6}', // ˆ
415 0x89 => '\u{2030}', // ‰
416 0x8A => '\u{0160}', // Š
417 0x8B => '\u{2039}', // ‹
418 0x8C => '\u{0152}', // Œ
419 0x8E => '\u{017D}', // Ž
420 0x91 => '\u{2018}', // '
421 0x92 => '\u{2019}', // '
422 0x93 => '\u{201C}', // "
423 0x94 => '\u{201D}', // "
424 0x95 => '\u{2022}', // •
425 0x96 => '\u{2013}', // –
426 0x97 => '\u{2014}', // —
427 0x98 => '\u{02DC}', // ˜
428 0x99 => '\u{2122}', // ™
429 0x9A => '\u{0161}', // š
430 0x9B => '\u{203A}', // ›
431 0x9C => '\u{0153}', // œ
432 0x9E => '\u{017E}', // ž
433 0x9F => '\u{0178}', // Ÿ
434 _ => byte as char,
435 }
436}
437
438/// Encode a single Unicode character as a Mac Roman byte.
439///
440/// Mirrors the table used by `TextEncoding::encode` for MacRoman. Returns
441/// `None` for codepoints outside the encoding's repertoire. Kept only for
442/// symmetry with `winansi_encode_char`; callers outside this module should
443/// prefer `TextEncoding::encode_strict`.
444pub fn macroman_encode_char(ch: char) -> Option<u8> {
445 match ch as u32 {
446 0x00..=0x7F => Some(ch as u8),
447 0x00C4 => Some(0x80),
448 0x00C5 => Some(0x81),
449 0x00C7 => Some(0x82),
450 0x00C9 => Some(0x83),
451 0x00D1 => Some(0x84),
452 0x00D6 => Some(0x85),
453 0x00DC => Some(0x86),
454 0x00E1 => Some(0x87),
455 0x00E0 => Some(0x88),
456 0x00E2 => Some(0x89),
457 0x00E4 => Some(0x8A),
458 0x00E3 => Some(0x8B),
459 0x00E5 => Some(0x8C),
460 0x00E7 => Some(0x8D),
461 0x00E9 => Some(0x8E),
462 0x00E8 => Some(0x8F),
463 0x00EA => Some(0x90),
464 0x00EB => Some(0x91),
465 0x00ED => Some(0x92),
466 0x00EC => Some(0x93),
467 0x00EE => Some(0x94),
468 0x00EF => Some(0x95),
469 0x00F1 => Some(0x96),
470 0x00F3 => Some(0x97),
471 0x00F2 => Some(0x98),
472 0x00F4 => Some(0x99),
473 0x00F6 => Some(0x9A),
474 0x00F5 => Some(0x9B),
475 0x00FA => Some(0x9C),
476 0x00F9 => Some(0x9D),
477 0x00FB => Some(0x9E),
478 0x00FC => Some(0x9F),
479 0x2020 => Some(0xA0),
480 0x00B0 => Some(0xA1),
481 0x00A2 => Some(0xA2),
482 0x00A3 => Some(0xA3),
483 0x00A7 => Some(0xA4),
484 0x2022 => Some(0xA5),
485 0x00B6 => Some(0xA6),
486 0x00DF => Some(0xA7),
487 0x00AE => Some(0xA8),
488 0x00A9 => Some(0xA9),
489 0x2122 => Some(0xAA),
490 0x00B4 => Some(0xAB),
491 0x00A8 => Some(0xAC),
492 0x2260 => Some(0xAD),
493 0x00C6 => Some(0xAE),
494 0x00D8 => Some(0xAF),
495 _ => None,
496 }
497}
498
499/// Escape `bytes` as the body of a PDF literal-string `(...)` show-text
500/// payload, suitable for direct use as the payload of `Op::ShowText`.
501///
502/// Produces `Vec<u8>` (vs `escape_pdf_string_literal`'s `String`) and
503/// uses the same six named escapes as that helper plus the octal
504/// fallback for everything else. The two helpers are kept in lock-step
505/// by sharing the named-escape table; only the output container differs.
506///
507/// - `(`, `)`, `\\` → backslash-prefixed.
508/// - `\n`, `\r`, `\t`, `\x08` (`BS`), `\x0C` (`FF`) → named two-character
509/// escapes per ISO 32000-1 §7.9.2 Table 3.
510/// - Printable ASCII `0x20..=0x7E` → passed through verbatim.
511/// - Everything else (including the WinAnsi high range `0x80..=0xFF`)
512/// → three-digit octal `\NNN`. The octal form keeps eight-bit bytes
513/// intact through 7-bit-safe intermediaries and matches ISO 32000-1
514/// §7.9.2.
515///
516/// Used by `text::build_show_text_op` to consolidate the encoding +
517/// escape pipeline between `TextContext::write` and
518/// `TextFlowContext::write_wrapped` (issue #240). The initial capacity
519/// (`bytes.len() * 4`) is the upper bound (every byte expanding to the
520/// four-char `\NNN` octal form); for the common ASCII-dominant case it
521/// is an over-allocation but avoids realloc cascades on
522/// multibyte-heavy text (French, German, typographic glyphs).
523pub(crate) fn escape_show_text_literal_bytes(bytes: &[u8]) -> Vec<u8> {
524 let mut buf = Vec::with_capacity(bytes.len() * 4);
525 for &b in bytes {
526 match b {
527 b'(' => buf.extend_from_slice(b"\\("),
528 b')' => buf.extend_from_slice(b"\\)"),
529 b'\\' => buf.extend_from_slice(b"\\\\"),
530 b'\n' => buf.extend_from_slice(b"\\n"),
531 b'\r' => buf.extend_from_slice(b"\\r"),
532 b'\t' => buf.extend_from_slice(b"\\t"),
533 b'\x08' => buf.extend_from_slice(b"\\b"),
534 b'\x0C' => buf.extend_from_slice(b"\\f"),
535 0x20..=0x7E => buf.push(b),
536 _ => {
537 use std::io::Write as _;
538 write!(&mut buf, "\\{b:03o}").expect("write to Vec<u8> never fails");
539 }
540 }
541 }
542 buf
543}
544
545/// Emit the UTF-8 bytes `input` as a PDF string-literal body, escaping
546/// characters that have special meaning inside `(...)` and writing bytes
547/// above 0x7F as three-digit octal escapes. Does NOT wrap the output in
548/// parentheses — callers compose that around it.
549///
550/// Used by appearance-stream emitters so the bytes of a WinAnsi-encoded
551/// value survive the serialisation round-trip intact (avoids interference
552/// from 8-bit-unsafe channels).
553pub fn escape_pdf_string_literal(input: &[u8]) -> String {
554 let mut out = String::with_capacity(input.len());
555 for &b in input {
556 match b {
557 b'\\' => out.push_str("\\\\"),
558 b'(' => out.push_str("\\("),
559 b')' => out.push_str("\\)"),
560 b'\n' => out.push_str("\\n"),
561 b'\r' => out.push_str("\\r"),
562 b'\t' => out.push_str("\\t"),
563 b'\x08' => out.push_str("\\b"),
564 b'\x0C' => out.push_str("\\f"),
565 0x20..=0x7E => out.push(b as char),
566 _ => {
567 // Three-digit octal escape keeps 8-bit bytes intact through
568 // any 7-bit-safe intermediary.
569 //
570 // Infallibility invariant for the three `from_digit` calls:
571 // - Each extracted nibble is `x & 0x07`, always in `0..=7`.
572 // - Radix 8 — `char::from_digit(n, 8)` is documented to
573 // return `Some(_)` iff `n < 8`.
574 // Therefore every `.unwrap()` below cannot observe `None`;
575 // no input byte reaches this branch.
576 out.push('\\');
577 out.push(char::from_digit(((b >> 6) & 0x07) as u32, 8).unwrap());
578 out.push(char::from_digit(((b >> 3) & 0x07) as u32, 8).unwrap());
579 out.push(char::from_digit((b & 0x07) as u32, 8).unwrap());
580 }
581 }
582 }
583 out
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 #[test]
591 fn winansi_decode_char_is_inverse_of_encode() {
592 // For every byte that maps to a defined WinAnsi glyph, decode then encode
593 // must round-trip back to the same byte.
594 for byte in 0u8..=255 {
595 let ch = winansi_decode_char(byte);
596 if let Some(reencoded) = winansi_encode_char(ch) {
597 assert_eq!(reencoded, byte, "round-trip failed for byte {byte:#04x}");
598 }
599 }
600 }
601
602 #[test]
603 fn winansi_decode_char_specific_points() {
604 assert_eq!(winansi_decode_char(0x80), '€');
605 assert_eq!(winansi_decode_char(0x97), '—'); // emdash
606 assert_eq!(winansi_decode_char(0x92), '\u{2019}'); // right single quote
607 assert_eq!(winansi_decode_char(0xED), 'í'); // Latin-1 identity
608 assert_eq!(winansi_decode_char(b'A'), 'A'); // ASCII identity
609 // Unassigned Windows-1252 code points decode to their C1 control char.
610 assert_eq!(winansi_decode_char(0x81), '\u{0081}');
611 }
612
613 /// `escape_show_text_literal_bytes` must use the SAME named escapes
614 /// as `escape_pdf_string_literal` for the six controls listed in
615 /// ISO 32000-1 §7.9.2 Table 3 (`\\b`, `\\f`, `\\n`, `\\r`, `\\t`,
616 /// plus the literal backslash). Without this guard the two helpers
617 /// can drift again — pre-fix, `escape_show_text_literal_bytes`
618 /// emitted `\\010` and `\\014` for `BS`/`FF` while the sibling
619 /// emitted `\\b`/`\\f`.
620 #[test]
621 fn escape_show_text_literal_bytes_named_escape_parity() {
622 // Each byte must produce the exact named-escape form, byte-for-byte.
623 let cases: &[(u8, &[u8])] = &[
624 (b'(', b"\\("),
625 (b')', b"\\)"),
626 (b'\\', b"\\\\"),
627 (b'\n', b"\\n"),
628 (b'\r', b"\\r"),
629 (b'\t', b"\\t"),
630 (b'\x08', b"\\b"),
631 (b'\x0C', b"\\f"),
632 ];
633 for (input, expected) in cases {
634 let got = escape_show_text_literal_bytes(&[*input]);
635 assert_eq!(
636 got,
637 *expected,
638 "byte 0x{input:02X} must escape as {:?}, got {:?}",
639 std::str::from_utf8(expected).unwrap(),
640 String::from_utf8_lossy(&got),
641 );
642 }
643 }
644
645 /// The octal fallback must emit three digits for every byte in
646 /// `0x80..=0xFF` (Windows-1252 high range) and for any sub-`0x20`
647 /// control byte not covered by a named escape (e.g. `0x01`).
648 #[test]
649 fn escape_show_text_literal_bytes_octal_fallback_three_digits() {
650 // 0x80 → "\\200", 0xFF → "\\377", 0x01 → "\\001"
651 assert_eq!(escape_show_text_literal_bytes(&[0x80]), b"\\200");
652 assert_eq!(escape_show_text_literal_bytes(&[0xFF]), b"\\377");
653 assert_eq!(escape_show_text_literal_bytes(&[0x01]), b"\\001");
654 }
655
656 /// Printable ASCII (`0x20..=0x7E`) must pass through unchanged
657 /// (single byte each). Guard against a future change that adds
658 /// unnecessary escapes to this range.
659 #[test]
660 fn escape_show_text_literal_bytes_printable_ascii_passthrough() {
661 let printable: Vec<u8> = (0x20u8..=0x7E)
662 .filter(|b| !matches!(b, b'(' | b')' | b'\\'))
663 .collect();
664 let got = escape_show_text_literal_bytes(&printable);
665 assert_eq!(got, printable);
666 }
667
668 #[test]
669 fn test_text_encoding_variants() {
670 let encodings = [
671 TextEncoding::StandardEncoding,
672 TextEncoding::MacRomanEncoding,
673 TextEncoding::WinAnsiEncoding,
674 TextEncoding::PdfDocEncoding,
675 ];
676
677 for encoding in &encodings {
678 assert_eq!(*encoding, *encoding);
679 }
680
681 assert_ne!(
682 TextEncoding::StandardEncoding,
683 TextEncoding::WinAnsiEncoding
684 );
685 }
686
687 #[test]
688 fn test_standard_encoding_basic_ascii() {
689 let encoding = TextEncoding::StandardEncoding;
690 let text = "Hello World!";
691
692 let encoded = encoding.encode(text);
693 let decoded = encoding.decode(&encoded);
694 assert_eq!(decoded, text);
695 }
696
697 /// Parity contract: `encode_strict(ch)` and `encode(ch)` must agree on
698 /// each Unicode codepoint in the Basic Multilingual Plane (BMP) — the
699 /// strict path rejects a char iff the lossy path would have emitted the
700 /// substitute `?` (0x3F) for it. Any divergence means the two code
701 /// paths got out of sync (which is a real risk because the tables are
702 /// duplicated: one inline in `encode`, one in the `*_encode_char`
703 /// helper called by `encode_strict`).
704 ///
705 /// This is NOT a completeness test: if both paths fail to encode a
706 /// valid codepoint (gap in both tables), parity still holds. Callers
707 /// who need completeness must extend both tables in lock-step.
708 ///
709 /// The `?` character (U+003F) is the one legitimate case where
710 /// `encode` would emit byte 0x3F as the actual encoded result; the
711 /// strict path also emits 0x3F for it. Handled as a special case
712 /// below so the test logic stays readable.
713 #[test]
714 fn test_encode_strict_matches_encode_across_bmp() {
715 for encoding in [
716 TextEncoding::WinAnsiEncoding,
717 TextEncoding::MacRomanEncoding,
718 ] {
719 let mut divergences: Vec<(u32, Vec<u8>, Result<Vec<u8>, char>)> = Vec::new();
720
721 for cp in 0u32..=0xFFFF {
722 let Some(ch) = char::from_u32(cp) else {
723 continue;
724 };
725 let s: String = std::iter::once(ch).collect();
726 let strict = encoding.encode_strict(&s);
727 let lossy = encoding.encode(&s);
728
729 match &strict {
730 Ok(bytes) => {
731 // Strict succeeded → lossy must produce the same
732 // bytes (that's the definition of agreement).
733 if bytes != &lossy {
734 divergences.push((cp, lossy.clone(), strict.clone()));
735 }
736 }
737 Err(_) => {
738 // Strict rejected → lossy must be the substitute
739 // byte 0x3F (single byte). If lossy produced
740 // something else, the two tables disagree.
741 //
742 // Special case: `?` itself (U+003F) is ASCII and
743 // both paths return [0x3F], but strict returns
744 // Ok — handled by the Ok arm above, never reaches
745 // here.
746 if lossy != vec![b'?'] {
747 divergences.push((cp, lossy.clone(), strict.clone()));
748 }
749 }
750 }
751 }
752
753 assert!(
754 divergences.is_empty(),
755 "{:?} encoding: strict/lossy disagreement on {} codepoints. \
756 First 5: {:?}",
757 encoding,
758 divergences.len(),
759 &divergences[..divergences.len().min(5)],
760 );
761 }
762 }
763
764 #[test]
765 fn test_win_ansi_encoding_special_chars() {
766 let encoding = TextEncoding::WinAnsiEncoding;
767
768 // Test Euro sign
769 let text = "€100";
770 let encoded = encoding.encode(text);
771 assert_eq!(encoded[0], 0x80);
772 let decoded = encoding.decode(&encoded);
773 assert_eq!(decoded, text);
774
775 // Test other special characters
776 let text2 = "Hello—World"; // Em dash
777 let encoded2 = encoding.encode(text2);
778 let decoded2 = encoding.decode(&encoded2);
779 assert_eq!(decoded2, text2);
780 }
781
782 #[test]
783 fn test_mac_roman_encoding_special_chars() {
784 let encoding = TextEncoding::MacRomanEncoding;
785
786 // Test accented characters
787 let text = "café";
788 let encoded = encoding.encode(text);
789 assert_eq!(encoded[3], 0x8E); // é
790 let decoded = encoding.decode(&encoded);
791 assert_eq!(decoded, text);
792
793 // Test Apple logo (special Mac character)
794 let apple_bytes = vec![0xF0];
795 let decoded_apple = encoding.decode(&apple_bytes);
796 assert_eq!(decoded_apple, "\u{F8FF}");
797
798 // Test various accented characters
799 let text2 = "Zürich";
800 let encoded2 = encoding.encode(text2);
801 assert_eq!(encoded2[1], 0x9F); // ü
802 let decoded2 = encoding.decode(&encoded2);
803 assert_eq!(decoded2, text2);
804 }
805
806 #[test]
807 fn test_pdf_doc_encoding() {
808 let encoding = TextEncoding::PdfDocEncoding;
809 let text = "PDF Document";
810
811 let encoded = encoding.encode(text);
812 let decoded = encoding.decode(&encoded);
813
814 assert_eq!(text, decoded);
815 }
816
817 #[test]
818 fn test_pdf_doc_encoding_basic_ascii() {
819 let encoding = TextEncoding::PdfDocEncoding;
820 let text = "Hello World!";
821
822 let encoded = encoding.encode(text);
823 let decoded = encoding.decode(&encoded);
824
825 assert_eq!(text, decoded);
826 }
827
828 #[test]
829 fn test_mac_roman_encoding_basic_ascii() {
830 let encoding = TextEncoding::MacRomanEncoding;
831 let text = "Hello World!";
832
833 let encoded = encoding.encode(text);
834 let decoded = encoding.decode(&encoded);
835
836 assert_eq!(text, decoded);
837 }
838
839 #[test]
840 fn test_win_ansi_encoding_basic_ascii() {
841 let encoding = TextEncoding::WinAnsiEncoding;
842 let text = "Hello World!";
843
844 let encoded = encoding.encode(text);
845 let decoded = encoding.decode(&encoded);
846
847 assert_eq!(text, decoded);
848 }
849
850 #[test]
851 fn test_win_ansi_encoding_special_characters() {
852 let encoding = TextEncoding::WinAnsiEncoding;
853
854 // Test Euro sign
855 let euro_text = "€";
856 let encoded = encoding.encode(euro_text);
857 assert_eq!(encoded, vec![0x80]);
858 let decoded = encoding.decode(&encoded);
859 assert_eq!(decoded, euro_text);
860
861 // Test em dash
862 let dash_text = "—";
863 let encoded = encoding.encode(dash_text);
864 assert_eq!(encoded, vec![0x97]);
865 let decoded = encoding.decode(&encoded);
866 assert_eq!(decoded, dash_text);
867
868 // Test single low quotation mark
869 let quote_text = "‚";
870 let encoded = encoding.encode(quote_text);
871 assert_eq!(encoded, vec![0x82]);
872 let decoded = encoding.decode(&encoded);
873 assert_eq!(decoded, quote_text);
874 }
875
876 #[test]
877 fn test_win_ansi_encoding_latin_supplement() {
878 let encoding = TextEncoding::WinAnsiEncoding;
879 let text = "café";
880
881 let encoded = encoding.encode(text);
882 let decoded = encoding.decode(&encoded);
883
884 assert_eq!(text, decoded);
885 }
886
887 #[test]
888 fn test_win_ansi_encoding_unmapped_character() {
889 let encoding = TextEncoding::WinAnsiEncoding;
890
891 // Use a character that's not in Windows-1252
892 let text = "❤"; // Heart emoji
893 let encoded = encoding.encode(text);
894 assert_eq!(encoded, vec![b'?']); // Should be replaced with ?
895
896 let decoded = encoding.decode(&encoded);
897 assert_eq!(decoded, "?");
898 }
899
900 #[test]
901 fn test_win_ansi_encoding_round_trip_special_chars() {
902 let encoding = TextEncoding::WinAnsiEncoding;
903
904 let special_chars = [
905 ("€", 0x80), // Euro sign
906 ("‚", 0x82), // Single low quotation mark
907 ("ƒ", 0x83), // Latin small letter f with hook
908 ("„", 0x84), // Double low quotation mark
909 ("…", 0x85), // Horizontal ellipsis
910 ("†", 0x86), // Dagger
911 ("‡", 0x87), // Double dagger
912 ("‰", 0x89), // Per mille sign
913 ("\u{2018}", 0x91), // Left single quotation mark
914 ("\u{2019}", 0x92), // Right single quotation mark
915 ("\u{201C}", 0x93), // Left double quotation mark
916 ("\u{201D}", 0x94), // Right double quotation mark
917 ("•", 0x95), // Bullet
918 ("–", 0x96), // En dash
919 ("—", 0x97), // Em dash
920 ("™", 0x99), // Trade mark sign
921 ];
922
923 for (text, expected_byte) in &special_chars {
924 let encoded = encoding.encode(text);
925 assert_eq!(encoded, vec![*expected_byte], "Failed for character {text}");
926
927 let decoded = encoding.decode(&encoded);
928 assert_eq!(decoded, *text, "Round trip failed for character {text}");
929 }
930 }
931
932 #[test]
933 fn test_encoding_equality() {
934 assert_eq!(
935 TextEncoding::StandardEncoding,
936 TextEncoding::StandardEncoding
937 );
938 assert_eq!(TextEncoding::WinAnsiEncoding, TextEncoding::WinAnsiEncoding);
939
940 assert_ne!(
941 TextEncoding::StandardEncoding,
942 TextEncoding::WinAnsiEncoding
943 );
944 assert_ne!(TextEncoding::MacRomanEncoding, TextEncoding::PdfDocEncoding);
945 }
946
947 #[test]
948 fn test_encoding_debug() {
949 let encoding = TextEncoding::WinAnsiEncoding;
950 let debug_str = format!("{encoding:?}");
951 assert_eq!(debug_str, "WinAnsiEncoding");
952 }
953
954 #[test]
955 fn test_encoding_clone() {
956 let encoding1 = TextEncoding::PdfDocEncoding;
957 let encoding2 = encoding1;
958 assert_eq!(encoding1, encoding2);
959 }
960
961 #[test]
962 fn test_encoding_copy() {
963 let encoding1 = TextEncoding::StandardEncoding;
964 let encoding2 = encoding1; // Copy semantics
965 assert_eq!(encoding1, encoding2);
966
967 // Both variables should still be usable
968 assert_eq!(encoding1, TextEncoding::StandardEncoding);
969 assert_eq!(encoding2, TextEncoding::StandardEncoding);
970 }
971
972 #[test]
973 fn test_empty_string_encoding() {
974 for encoding in &[
975 TextEncoding::StandardEncoding,
976 TextEncoding::MacRomanEncoding,
977 TextEncoding::WinAnsiEncoding,
978 TextEncoding::PdfDocEncoding,
979 ] {
980 let encoded = encoding.encode("");
981 assert!(encoded.is_empty());
982
983 let decoded = encoding.decode(&[]);
984 assert!(decoded.is_empty());
985 }
986 }
987
988 #[test]
989 fn test_win_ansi_decode_undefined_bytes() {
990 let encoding = TextEncoding::WinAnsiEncoding;
991
992 // Test some undefined bytes in Windows-1252 (0x81, 0x8D, 0x8F, 0x90, 0x9D)
993 let undefined_bytes = [0x81, 0x8D, 0x8F, 0x90, 0x9D];
994
995 for &byte in &undefined_bytes {
996 let decoded = encoding.decode(&[byte]);
997 assert_eq!(
998 decoded, "?",
999 "Undefined byte 0x{byte:02X} should decode to '?'"
1000 );
1001 }
1002 }
1003
1004 #[test]
1005 fn test_win_ansi_ascii_range() {
1006 let encoding = TextEncoding::WinAnsiEncoding;
1007
1008 // Test ASCII range (0x00-0x7F)
1009 for byte in 0x20..=0x7E {
1010 // Printable ASCII
1011 let text = char::from(byte).to_string();
1012 let encoded = encoding.encode(&text);
1013 assert_eq!(encoded, vec![byte]);
1014
1015 let decoded = encoding.decode(&encoded);
1016 assert_eq!(decoded, text);
1017 }
1018 }
1019
1020 #[test]
1021 fn test_win_ansi_latin1_overlap() {
1022 let encoding = TextEncoding::WinAnsiEncoding;
1023
1024 // Test Latin-1 range that overlaps with Windows-1252 (0xA0-0xFF)
1025 let test_chars = "¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ";
1026
1027 let encoded = encoding.encode(test_chars);
1028 let decoded = encoding.decode(&encoded);
1029
1030 assert_eq!(decoded, test_chars);
1031 }
1032
1033 #[test]
1034 fn test_mac_roman_encode_special_characters() {
1035 let encoding = TextEncoding::MacRomanEncoding;
1036
1037 // Test Mac Roman specific mappings
1038 let test_cases = [
1039 ("Ä", 0x80), // A with diaeresis
1040 ("Å", 0x81), // A with ring
1041 ("Ç", 0x82), // C with cedilla
1042 ("É", 0x83), // E with acute
1043 ("Ñ", 0x84), // N with tilde
1044 ("Ö", 0x85), // O with diaeresis
1045 ("Ü", 0x86), // U with diaeresis
1046 ("á", 0x87), // a with acute
1047 ("à", 0x88), // a with grave
1048 ("â", 0x89), // a with circumflex
1049 ("ä", 0x8A), // a with diaeresis
1050 ("ã", 0x8B), // a with tilde
1051 ("å", 0x8C), // a with ring
1052 ("ç", 0x8D), // c with cedilla
1053 ("é", 0x8E), // e with acute
1054 ("è", 0x8F), // e with grave
1055 ("ê", 0x90), // e with circumflex
1056 ("ë", 0x91), // e with diaeresis
1057 ("í", 0x92), // i with acute
1058 ("ì", 0x93), // i with grave
1059 ("î", 0x94), // i with circumflex
1060 ("ï", 0x95), // i with diaeresis
1061 ("ñ", 0x96), // n with tilde
1062 ("ó", 0x97), // o with acute
1063 ("ò", 0x98), // o with grave
1064 ("ô", 0x99), // o with circumflex
1065 ("ö", 0x9A), // o with diaeresis
1066 ("õ", 0x9B), // o with tilde
1067 ("ú", 0x9C), // u with acute
1068 ("ù", 0x9D), // u with grave
1069 ("û", 0x9E), // u with circumflex
1070 ("ü", 0x9F), // u with diaeresis
1071 ];
1072
1073 for (text, expected_byte) in &test_cases {
1074 let encoded = encoding.encode(text);
1075 assert_eq!(
1076 encoded,
1077 vec![*expected_byte],
1078 "Failed encoding {text} (U+{:04X})",
1079 text.chars().next().unwrap() as u32
1080 );
1081 }
1082 }
1083
1084 #[test]
1085 fn test_mac_roman_encode_symbols() {
1086 let encoding = TextEncoding::MacRomanEncoding;
1087
1088 let test_cases = [
1089 ("†", 0xA0), // Dagger
1090 ("°", 0xA1), // Degree sign
1091 ("¢", 0xA2), // Cent sign
1092 ("£", 0xA3), // Pound sign
1093 ("§", 0xA4), // Section sign
1094 ("•", 0xA5), // Bullet
1095 ("¶", 0xA6), // Pilcrow sign
1096 ("ß", 0xA7), // Sharp s
1097 ("®", 0xA8), // Registered sign
1098 ("©", 0xA9), // Copyright sign
1099 ("™", 0xAA), // Trade mark sign
1100 ("´", 0xAB), // Acute accent
1101 ("¨", 0xAC), // Diaeresis
1102 ("≠", 0xAD), // Not equal to
1103 ("Æ", 0xAE), // AE ligature
1104 ("Ø", 0xAF), // O with stroke
1105 ];
1106
1107 for (text, expected_byte) in &test_cases {
1108 let encoded = encoding.encode(text);
1109 assert_eq!(encoded, vec![*expected_byte], "Failed encoding {text}");
1110 }
1111 }
1112
1113 #[test]
1114 fn test_mac_roman_decode_extended_range() {
1115 let encoding = TextEncoding::MacRomanEncoding;
1116
1117 // Test extended range (0xB0-0xFF)
1118 let test_cases: Vec<(u8, char)> = vec![
1119 (0xB0, '∞'), // Infinity
1120 (0xB1, '±'), // Plus-minus
1121 (0xB2, '≤'), // Less-than or equal
1122 (0xB3, '≥'), // Greater-than or equal
1123 (0xB4, '¥'), // Yen sign
1124 (0xB5, 'µ'), // Micro sign
1125 (0xB6, '∂'), // Partial differential
1126 (0xB7, '∑'), // Summation
1127 (0xB8, '∏'), // Product
1128 (0xB9, 'π'), // Pi
1129 (0xBA, '∫'), // Integral
1130 (0xBB, 'ª'), // Feminine ordinal
1131 (0xBC, 'º'), // Masculine ordinal
1132 (0xBD, 'Ω'), // Omega
1133 (0xBE, 'æ'), // ae ligature
1134 (0xBF, 'ø'), // o with stroke
1135 (0xC0, '¿'), // Inverted question mark
1136 (0xC1, '¡'), // Inverted exclamation
1137 (0xC2, '¬'), // Not sign
1138 (0xC3, '√'), // Square root
1139 (0xC4, 'ƒ'), // f with hook
1140 (0xC5, '≈'), // Almost equal
1141 (0xC6, '∆'), // Increment
1142 (0xC7, '«'), // Left double angle quote
1143 (0xC8, '»'), // Right double angle quote
1144 (0xC9, '…'), // Horizontal ellipsis
1145 (0xCA, '\u{00A0}'), // No-break space
1146 (0xCB, 'À'), // A with grave
1147 (0xCC, 'Ã'), // A with tilde
1148 (0xCD, 'Õ'), // O with tilde
1149 (0xCE, 'Œ'), // OE ligature
1150 (0xCF, 'œ'), // oe ligature
1151 ];
1152
1153 for (byte, expected_char) in test_cases {
1154 let decoded = encoding.decode(&[byte]);
1155 assert_eq!(
1156 decoded.chars().next().unwrap(),
1157 expected_char,
1158 "Failed decoding byte 0x{byte:02X}"
1159 );
1160 }
1161 }
1162
1163 #[test]
1164 fn test_mac_roman_decode_high_range() {
1165 let encoding = TextEncoding::MacRomanEncoding;
1166
1167 let test_cases: Vec<(u8, char)> = vec![
1168 (0xD0, '\u{2013}'), // En dash
1169 (0xD1, '\u{2014}'), // Em dash
1170 (0xD2, '\u{201C}'), // Left double quote
1171 (0xD3, '\u{201D}'), // Right double quote
1172 (0xD4, '\u{2018}'), // Left single quote
1173 (0xD5, '\u{2019}'), // Right single quote
1174 (0xD6, '\u{00F7}'), // Division sign
1175 (0xD7, '\u{25CA}'), // Lozenge
1176 (0xD8, '\u{00FF}'), // y with diaeresis
1177 (0xD9, '\u{0178}'), // Y with diaeresis
1178 (0xDA, '\u{2044}'), // Fraction slash
1179 (0xDB, '\u{20AC}'), // Euro sign
1180 (0xDC, '\u{2039}'), // Single left angle quote
1181 (0xDD, '\u{203A}'), // Single right angle quote
1182 (0xDE, '\u{FB01}'), // fi ligature
1183 (0xDF, '\u{FB02}'), // fl ligature
1184 (0xE0, '\u{2021}'), // Double dagger
1185 (0xE1, '\u{00B7}'), // Middle dot
1186 (0xE2, '\u{201A}'), // Single low quote
1187 (0xE3, '\u{201E}'), // Double low quote
1188 (0xE4, '\u{2030}'), // Per mille sign
1189 (0xE5, '\u{00C2}'), // A with circumflex
1190 (0xE6, '\u{00CA}'), // E with circumflex
1191 (0xE7, '\u{00C1}'), // A with acute
1192 (0xE8, '\u{00CB}'), // E with diaeresis
1193 (0xE9, '\u{00C8}'), // E with grave
1194 (0xEA, '\u{00CD}'), // I with acute
1195 (0xEB, '\u{00CE}'), // I with circumflex
1196 (0xEC, '\u{00CF}'), // I with diaeresis
1197 (0xED, '\u{00CC}'), // I with grave
1198 (0xEE, '\u{00D3}'), // O with acute
1199 (0xEF, '\u{00D4}'), // O with circumflex
1200 ];
1201
1202 for (byte, expected_char) in test_cases {
1203 let decoded = encoding.decode(&[byte]);
1204 assert_eq!(
1205 decoded.chars().next().unwrap(),
1206 expected_char,
1207 "Failed decoding byte 0x{byte:02X}"
1208 );
1209 }
1210 }
1211
1212 #[test]
1213 fn test_mac_roman_decode_final_range() {
1214 let encoding = TextEncoding::MacRomanEncoding;
1215
1216 let test_cases: Vec<(u8, char)> = vec![
1217 (0xF0, '\u{F8FF}'), // Apple logo
1218 (0xF1, 'Ò'), // O with grave
1219 (0xF2, 'Ú'), // U with acute
1220 (0xF3, 'Û'), // U with circumflex
1221 (0xF4, 'Ù'), // U with grave
1222 (0xF5, 'ı'), // Dotless i
1223 (0xF6, 'ˆ'), // Circumflex modifier
1224 (0xF7, '˜'), // Small tilde
1225 (0xF8, '¯'), // Macron
1226 (0xF9, '˘'), // Breve
1227 (0xFA, '˙'), // Dot above
1228 (0xFB, '˚'), // Ring above
1229 (0xFC, '¸'), // Cedilla
1230 (0xFD, '˝'), // Double acute
1231 (0xFE, '˛'), // Ogonek
1232 (0xFF, 'ˇ'), // Caron
1233 ];
1234
1235 for (byte, expected_char) in test_cases {
1236 let decoded = encoding.decode(&[byte]);
1237 assert_eq!(
1238 decoded.chars().next().unwrap(),
1239 expected_char,
1240 "Failed decoding byte 0x{byte:02X}"
1241 );
1242 }
1243 }
1244
1245 #[test]
1246 fn test_mac_roman_unmapped_character() {
1247 let encoding = TextEncoding::MacRomanEncoding;
1248
1249 // Use a character that's not in Mac Roman
1250 let text = "❤"; // Heart emoji
1251 let encoded = encoding.encode(text);
1252 assert_eq!(encoded, vec![b'?']);
1253 }
1254
1255 #[test]
1256 fn test_win_ansi_encode_all_special_mappings() {
1257 let encoding = TextEncoding::WinAnsiEncoding;
1258
1259 let test_cases = [
1260 ("\u{0160}", 0x8A), // S with caron
1261 ("\u{0152}", 0x8C), // OE ligature
1262 ("\u{017D}", 0x8E), // Z with caron
1263 ("\u{0161}", 0x9A), // s with caron
1264 ("\u{0153}", 0x9C), // oe ligature
1265 ("\u{017E}", 0x9E), // z with caron
1266 ("\u{0178}", 0x9F), // Y with diaeresis
1267 ("\u{02C6}", 0x88), // Circumflex
1268 ("\u{02DC}", 0x98), // Small tilde
1269 ("\u{2039}", 0x8B), // Single left angle quote
1270 ("\u{203A}", 0x9B), // Single right angle quote
1271 ];
1272
1273 for (text, expected_byte) in &test_cases {
1274 let encoded = encoding.encode(text);
1275 assert_eq!(
1276 encoded,
1277 vec![*expected_byte],
1278 "Failed encoding {text} (U+{:04X})",
1279 text.chars().next().unwrap() as u32
1280 );
1281 }
1282 }
1283
1284 #[test]
1285 fn test_long_text_encoding_roundtrip() {
1286 let encodings = [
1287 TextEncoding::StandardEncoding,
1288 TextEncoding::WinAnsiEncoding,
1289 TextEncoding::MacRomanEncoding,
1290 TextEncoding::PdfDocEncoding,
1291 ];
1292
1293 let long_text = "The quick brown fox jumps over the lazy dog. 0123456789!@#$%^&*()";
1294
1295 for encoding in &encodings {
1296 let encoded = encoding.encode(long_text);
1297 let decoded = encoding.decode(&encoded);
1298 assert_eq!(decoded, long_text, "Failed for {encoding:?}");
1299 }
1300 }
1301
1302 #[test]
1303 fn test_win_ansi_decode_all_special_bytes() {
1304 let encoding = TextEncoding::WinAnsiEncoding;
1305
1306 // Test all defined special bytes
1307 let test_cases: Vec<(u8, char)> = vec![
1308 (0x80, '\u{20AC}'), // Euro sign
1309 (0x82, '\u{201A}'), // Single low quotation mark
1310 (0x83, '\u{0192}'), // f with hook
1311 (0x84, '\u{201E}'), // Double low quotation mark
1312 (0x85, '\u{2026}'), // Horizontal ellipsis
1313 (0x86, '\u{2020}'), // Dagger
1314 (0x87, '\u{2021}'), // Double dagger
1315 (0x88, '\u{02C6}'), // Circumflex accent
1316 (0x89, '\u{2030}'), // Per mille sign
1317 (0x8A, '\u{0160}'), // S with caron
1318 (0x8B, '\u{2039}'), // Single left angle quote
1319 (0x8C, '\u{0152}'), // OE ligature
1320 (0x8E, '\u{017D}'), // Z with caron
1321 (0x91, '\u{2018}'), // Left single quotation mark
1322 (0x92, '\u{2019}'), // Right single quotation mark
1323 (0x93, '\u{201C}'), // Left double quotation mark
1324 (0x94, '\u{201D}'), // Right double quotation mark
1325 (0x95, '\u{2022}'), // Bullet
1326 (0x96, '\u{2013}'), // En dash
1327 (0x97, '\u{2014}'), // Em dash
1328 (0x98, '\u{02DC}'), // Small tilde
1329 (0x99, '\u{2122}'), // Trade mark sign
1330 (0x9A, '\u{0161}'), // s with caron
1331 (0x9B, '\u{203A}'), // Single right angle quote
1332 (0x9C, '\u{0153}'), // oe ligature
1333 (0x9E, '\u{017E}'), // z with caron
1334 (0x9F, '\u{0178}'), // Y with diaeresis
1335 ];
1336
1337 for (byte, expected_char) in test_cases {
1338 let decoded = encoding.decode(&[byte]);
1339 assert_eq!(
1340 decoded.chars().next().unwrap(),
1341 expected_char,
1342 "Failed decoding byte 0x{byte:02X}"
1343 );
1344 }
1345 }
1346}