Skip to main content

otf_pdf_doc/
text.rs

1//! Text strings (ISO 32000-2 §7.9.2.2): the encoding used by metadata,
2//! outline titles, annotation text and other human-readable strings.
3
4/// Decodes a text string: UTF-16BE with a BOM, UTF-8 with a BOM (PDF 2.0),
5/// otherwise PDFDocEncoding. UTF-16LE with a BOM, which some writers emit,
6/// is accepted too. Undecodable units become U+FFFD.
7pub 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
20/// Encodes a text string: PDFDocEncoding when every character has a byte
21/// there, otherwise UTF-16BE with a byte order mark (works in every version).
22pub 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        // A PDFDocEncoding string must not start like a byte order mark.
26        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
41/// The PDFDocEncoding byte for `c`, if there is one.
42fn 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    // A language escape (U+001B ... U+001B) carries a language tag, not text.
57    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
72/// One PDFDocEncoding byte (Annex D.2).
73pub 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        // A surrogate pair.
122        assert_eq!(
123            decode_text_string(&[0xfe, 0xff, 0xd8, 0x3d, 0xde, 0x00]),
124            "😀"
125        );
126        // A lone surrogate and an odd trailing byte.
127        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}