1pub fn decode_text_string(bytes: &[u8]) -> String {
8 if let Some(rest) = bytes.strip_prefix(&[0xfe, 0xff]) {
9 return utf16(rest, u16::from_be_bytes);
10 }
11 if let Some(rest) = bytes.strip_prefix(&[0xff, 0xfe]) {
12 return utf16(rest, u16::from_le_bytes);
13 }
14 if let Some(rest) = bytes.strip_prefix(&[0xef, 0xbb, 0xbf]) {
15 return String::from_utf8_lossy(rest).into_owned();
16 }
17 bytes.iter().map(|&b| pdf_doc_char(b)).collect()
18}
19
20pub fn encode_text_string(text: &str) -> Vec<u8> {
23 let doc: Option<Vec<u8>> = text.chars().map(pdf_doc_byte).collect();
24 match doc {
25 Some(bytes)
27 if !bytes.starts_with(&[0xfe, 0xff]) && !bytes.starts_with(&[0xef, 0xbb, 0xbf]) =>
28 {
29 bytes
30 }
31 _ => {
32 let mut out = vec![0xfe, 0xff];
33 for unit in text.encode_utf16() {
34 out.extend_from_slice(&unit.to_be_bytes());
35 }
36 out
37 }
38 }
39}
40
41fn pdf_doc_byte(c: char) -> Option<u8> {
43 if c == '\u{fffd}' {
44 return None;
45 }
46 (0u8..=255).find(|&b| pdf_doc_char(b) == c)
47}
48
49fn utf16(bytes: &[u8], unit: fn([u8; 2]) -> u16) -> String {
50 let units = bytes.chunks(2).map(|c| {
51 unit([
52 c.first().copied().unwrap_or(0),
53 c.get(1).copied().unwrap_or(0),
54 ])
55 });
56 let mut out = String::new();
58 let mut in_escape = false;
59 for c in char::decode_utf16(units) {
60 let c = c.unwrap_or(char::REPLACEMENT_CHARACTER);
61 if c == '\u{1b}' {
62 in_escape = !in_escape;
63 continue;
64 }
65 if !in_escape {
66 out.push(c);
67 }
68 }
69 out
70}
71
72pub fn pdf_doc_char(b: u8) -> char {
74 const LOW: [char; 8] = [
75 '\u{02d8}', '\u{02c7}', '\u{02c6}', '\u{02d9}', '\u{02dd}', '\u{02db}', '\u{02da}',
76 '\u{02dc}',
77 ];
78 const HIGH: [char; 33] = [
79 '\u{2022}', '\u{2020}', '\u{2021}', '\u{2026}', '\u{2014}', '\u{2013}', '\u{0192}',
80 '\u{2044}', '\u{2039}', '\u{203a}', '\u{2212}', '\u{2030}', '\u{201e}', '\u{201c}',
81 '\u{201d}', '\u{2018}', '\u{2019}', '\u{201a}', '\u{2122}', '\u{fb01}', '\u{fb02}',
82 '\u{0141}', '\u{0152}', '\u{0160}', '\u{0178}', '\u{017d}', '\u{0131}', '\u{0142}',
83 '\u{0153}', '\u{0161}', '\u{017e}', '\u{fffd}', '\u{20ac}',
84 ];
85 match b {
86 0x18..=0x1f => LOW
87 .get(usize::from(b - 0x18))
88 .copied()
89 .unwrap_or('\u{fffd}'),
90 0x7f | 0xad => '\u{fffd}',
91 0x80..=0xa0 => HIGH
92 .get(usize::from(b - 0x80))
93 .copied()
94 .unwrap_or('\u{fffd}'),
95 _ => char::from(b),
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn pdf_doc_encoding() {
105 assert_eq!(decode_text_string(b"Hello"), "Hello");
106 assert_eq!(decode_text_string(&[0x80, 0x92, 0xa0, 0xe9]), "•™€é");
107 assert_eq!(decode_text_string(&[0x18, 0x1f]), "˘˜");
108 assert_eq!(
109 decode_text_string(&[0x7f, 0x9f, 0xad]),
110 "\u{fffd}\u{fffd}\u{fffd}"
111 );
112 }
113
114 #[test]
115 fn utf16_be_and_le() {
116 assert_eq!(
117 decode_text_string(&[0xfe, 0xff, 0x00, 0x41, 0x04, 0x14]),
118 "AД"
119 );
120 assert_eq!(decode_text_string(&[0xff, 0xfe, 0x41, 0x00]), "A");
121 assert_eq!(
123 decode_text_string(&[0xfe, 0xff, 0xd8, 0x3d, 0xde, 0x00]),
124 "😀"
125 );
126 assert_eq!(decode_text_string(&[0xfe, 0xff, 0xd8, 0x3d]), "\u{fffd}");
128 assert_eq!(
129 decode_text_string(&[0xfe, 0xff, 0x00, 0x41, 0x00]),
130 "A\u{0}"
131 );
132 }
133
134 #[test]
135 fn language_escape_is_dropped() {
136 let bytes = [
137 0xfe, 0xff, 0x00, 0x1b, 0x00, b'e', 0x00, b'n', 0x00, 0x1b, 0x00, b'H',
138 ];
139 assert_eq!(decode_text_string(&bytes), "H");
140 }
141
142 #[test]
143 fn encoding_round_trips() {
144 for text in [
145 "Hello",
146 "café €",
147 "Titre Δ",
148 "日本語",
149 "",
150 "þÿ",
151 "\u{1f600}",
152 ] {
153 assert_eq!(
154 decode_text_string(&encode_text_string(text)),
155 text,
156 "{text}"
157 );
158 }
159 assert_eq!(encode_text_string("A€"), vec![b'A', 0xa0]);
160 assert_eq!(&encode_text_string("Δ")[..2], &[0xfe, 0xff]);
161 }
162
163 #[test]
164 fn utf8_bom() {
165 assert_eq!(decode_text_string("\u{feff}ünï".as_bytes()), "ünï");
166 }
167}