Skip to main content

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}