Skip to main content

pdfrum_font/subst/
charset.rs

1//! Charsets, code pages, and the pitch-family bits.
2//!
3//! Windows vocabulary that PDFium carries everywhere, because the
4//! font-selection API it was written against was Windows'. On Linux it
5//! survives as the language a font request is phrased in.
6
7use crate::FontFlags;
8
9/// A Windows charset, as `FX_Charset`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
11pub enum Charset {
12    /// Western European — the default, and where a font with no better claim
13    /// lands.
14    #[default]
15    Ansi,
16    /// No charset stated. Distinct from `Ansi`: it makes every installed face
17    /// eligible rather than only the ANSI ones.
18    Default,
19    /// Symbolic, meaning the font's own character set.
20    Symbol,
21    /// Japanese.
22    ShiftJis,
23    /// Korean (Wansung).
24    Hangul,
25    /// Simplified Chinese.
26    ChineseSimplified,
27    /// Traditional Chinese.
28    ChineseTraditional,
29    /// Korean (Johab). Notably **not** CJK for the purpose of the former working note step 8.
30    Johab,
31    /// Greek.
32    Greek,
33    /// Turkish.
34    Turkish,
35    /// Vietnamese.
36    Vietnamese,
37    /// Hebrew.
38    Hebrew,
39    /// Arabic.
40    Arabic,
41    /// Baltic.
42    Baltic,
43    /// Cyrillic.
44    Cyrillic,
45    /// Thai.
46    Thai,
47    /// Central and Eastern European.
48    EasternEuropean,
49    /// OEM.
50    Oem,
51}
52
53impl Charset {
54    /// Is this one of the four charsets the substitution ladder treats as CJK?
55    ///
56    /// **Johab is excluded**, even though it is Korean, and so are the
57    /// Macintosh CJK charsets. The set is exactly what `FX_CharSetIsCJK`
58    /// names, and it decides whether Branch A of the former working note takes its CJK arm.
59    #[must_use]
60    pub(crate) fn is_cjk(self) -> bool {
61        matches!(
62            self,
63            Self::ChineseSimplified | Self::ChineseTraditional | Self::Hangul | Self::ShiftJis
64        )
65    }
66
67    /// The charset a code page implies.
68    #[must_use]
69    pub(crate) fn from_code_page(cp: CodePage) -> Self {
70        match cp {
71            CodePage::ShiftJis => Self::ShiftJis,
72            CodePage::ChineseSimplified => Self::ChineseSimplified,
73            CodePage::Hangul => Self::Hangul,
74            CodePage::ChineseTraditional => Self::ChineseTraditional,
75            CodePage::DefAnsi | CodePage::Utf16Le => Self::Ansi,
76        }
77    }
78}
79
80/// The code pages a CID collection maps to (`kCharsetCodePages`).
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
82pub enum CodePage {
83    /// The system default, 0.
84    #[default]
85    DefAnsi,
86    /// 932, Japanese.
87    ShiftJis,
88    /// 936, Simplified Chinese.
89    ChineseSimplified,
90    /// 949, Korean.
91    Hangul,
92    /// 950, Traditional Chinese.
93    ChineseTraditional,
94    /// 1200, UTF-16LE — the Adobe-Identity collection's.
95    Utf16Le,
96}
97
98impl CodePage {
99    /// The code page a CID collection is written in.
100    #[must_use]
101    pub fn for_cid_set(set: pdfrum_cmap::CidSet) -> Self {
102        use pdfrum_cmap::CidSet;
103        match set {
104            CidSet::Unknown => Self::DefAnsi,
105            CidSet::Gb1 => Self::ChineseSimplified,
106            CidSet::Cns1 => Self::ChineseTraditional,
107            CidSet::Japan1 => Self::ShiftJis,
108            CidSet::Korea1 => Self::Hangul,
109            CidSet::Unicode => Self::Utf16Le,
110        }
111    }
112}
113
114/// The pitch-family bits a font request carries.
115///
116/// A bit set rather than an enum: a request can be both serif and fixed-pitch,
117/// and the scoring function tests each bit separately.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
119pub struct PitchFamily(pub u32);
120
121impl PitchFamily {
122    /// Every glyph the same width.
123    pub const FIXED: u32 = 1;
124    /// Serifed.
125    pub const ROMAN: u32 = 16;
126    /// Cursive.
127    pub const SCRIPT: u32 = 64;
128
129    /// Is the bit set?
130    #[must_use]
131    pub const fn has(self, bit: u32) -> bool {
132        self.0 & bit != 0
133    }
134
135    /// Derive from `/Flags` (`GetPitchFamilyFromFlags`).
136    #[must_use]
137    pub fn from_flags(flags: FontFlags) -> Self {
138        let mut p = 0;
139        if flags.contains(FontFlags::SERIF) {
140            p |= Self::ROMAN;
141        }
142        if flags.contains(FontFlags::SCRIPT) {
143            p |= Self::SCRIPT;
144        }
145        if flags.contains(FontFlags::FIXED_PITCH) {
146            p |= Self::FIXED;
147        }
148        Self(p)
149    }
150
151    /// Derive from a standard-font index (`GetPitchFamilyFromBaseFont`).
152    ///
153    /// The four Couriers are fixed; Times, Symbol and ZapfDingbats are Roman;
154    /// the Helveticas are neither.
155    #[must_use]
156    pub fn from_standard_font(f: super::StandardFont) -> Self {
157        let i = f.index();
158        if i < 4 {
159            Self(Self::FIXED)
160        } else if i >= 8 {
161            Self(Self::ROMAN)
162        } else {
163            Self(0)
164        }
165    }
166}
167
168/// Guess a charset from a single character (`GetCharSetFromUnicode`).
169///
170/// An **ordered** first-hit ladder, and the order has consequences: General
171/// Punctuation is claimed by Simplified Chinese, and everything below U+007F
172/// is ANSI so that ASCII never drags in a CJK face.
173// The ASCII arm shares the wildcard's body but not its meaning, and it cannot
174// be folded into it: it has to be *first* so that ASCII is claimed before the
175// General Punctuation and half-width ranges below can take it.
176#[allow(clippy::match_same_arms)]
177#[must_use]
178pub fn charset_from_unicode(u: u32) -> Charset {
179    match u {
180        // "Avoid a CJK font to show ASCII" — the C++'s own comment.
181        0..=0x7E => Charset::Ansi,
182        0x4E00..=0x9FA5 | 0xE7C7..=0xE7F3 | 0x3000..=0x303F | 0x2000..=0x206F => {
183            Charset::ChineseSimplified
184        }
185        0x3040..=0x309F | 0x30A0..=0x30FF | 0x31F0..=0x31FF | 0xFF00..=0xFFEF => Charset::ShiftJis,
186        0xAC00..=0xD7AF | 0x1100..=0x11FF | 0x3130..=0x318F => Charset::Hangul,
187        0x0E00..=0x0E7F => Charset::Thai,
188        0x0370..=0x03FF | 0x1F00..=0x1FFF => Charset::Greek,
189        0x0600..=0x06FF | 0xFB50..=0xFEFC => Charset::Arabic,
190        0x0590..=0x05FF => Charset::Hebrew,
191        0x0400..=0x04FF => Charset::Cyrillic,
192        0x0100..=0x024F => Charset::EasternEuropean,
193        0x1E00..=0x1EFF => Charset::Vietnamese,
194        _ => Charset::Ansi,
195    }
196}
197
198#[cfg(all(feature = "system-fonts", not(target_arch = "wasm32")))]
199/// The `OS/2` code-page-range bit a charset corresponds to, for reading a
200/// face's charsets out of its own tables (`cfx_folderfontinfo.cpp`).
201#[must_use]
202pub fn charset_for_code_page_bit(bit: u32) -> Option<Charset> {
203    Some(match bit {
204        1 => Charset::EasternEuropean,
205        2 => Charset::Cyrillic,
206        3 => Charset::Greek,
207        4 => Charset::Turkish,
208        5 => Charset::Hebrew,
209        6 => Charset::Arabic,
210        7 => Charset::Baltic,
211        8 => Charset::Vietnamese,
212        16 => Charset::Thai,
213        17 => Charset::ShiftJis,
214        18 => Charset::ChineseSimplified,
215        19 => Charset::Hangul,
216        20 => Charset::ChineseTraditional,
217        21 => Charset::Johab,
218        30 => Charset::Oem,
219        31 => Charset::Symbol,
220        _ => return None,
221    })
222}
223
224/// A default face name per charset (`kDefaultTTFMap`), taking the Linux
225/// spellings the oracle's build uses.
226#[must_use]
227#[cfg(test)]
228pub fn default_face_name(charset: Charset) -> &'static str {
229    match charset {
230        Charset::Ansi => "Helvetica",
231        Charset::ChineseSimplified => "SimSun",
232        Charset::ChineseTraditional => "MingLiU",
233        Charset::ShiftJis => "MS Gothic",
234        Charset::Hangul => "Batang",
235        Charset::Cyrillic | Charset::EasternEuropean | Charset::Arabic => "Arial",
236        _ => "Arial Unicode MS",
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn only_four_charsets_count_as_cjk() {
246        for c in [
247            Charset::ChineseSimplified,
248            Charset::ChineseTraditional,
249            Charset::Hangul,
250            Charset::ShiftJis,
251        ] {
252            assert!(c.is_cjk(), "{c:?}");
253        }
254        // Johab is Korean and deliberately excluded.
255        assert!(!Charset::Johab.is_cjk());
256        for c in [
257            Charset::Ansi,
258            Charset::Symbol,
259            Charset::Thai,
260            Charset::Default,
261        ] {
262            assert!(!c.is_cjk(), "{c:?}");
263        }
264    }
265
266    #[test]
267    fn each_cid_collection_maps_to_its_code_page() {
268        use pdfrum_cmap::CidSet;
269        assert_eq!(CodePage::for_cid_set(CidSet::Unknown), CodePage::DefAnsi);
270        assert_eq!(
271            CodePage::for_cid_set(CidSet::Gb1),
272            CodePage::ChineseSimplified
273        );
274        assert_eq!(
275            CodePage::for_cid_set(CidSet::Cns1),
276            CodePage::ChineseTraditional
277        );
278        assert_eq!(CodePage::for_cid_set(CidSet::Japan1), CodePage::ShiftJis);
279        assert_eq!(CodePage::for_cid_set(CidSet::Korea1), CodePage::Hangul);
280        assert_eq!(CodePage::for_cid_set(CidSet::Unicode), CodePage::Utf16Le);
281    }
282
283    #[test]
284    fn the_charset_ladder_is_ordered_and_ascii_wins_first() {
285        assert_eq!(charset_from_unicode(0x41), Charset::Ansi);
286        assert_eq!(charset_from_unicode(0x7E), Charset::Ansi);
287        // 0x7F itself falls out of the first arm and lands on the default.
288        assert_eq!(charset_from_unicode(0x7F), Charset::Ansi);
289        assert_eq!(charset_from_unicode(0x4E00), Charset::ChineseSimplified);
290        // General Punctuation is claimed by Simplified Chinese, not by any
291        // Latin charset — an ordering consequence, not an obvious rule.
292        assert_eq!(charset_from_unicode(0x2014), Charset::ChineseSimplified);
293        assert_eq!(charset_from_unicode(0x3042), Charset::ShiftJis);
294        assert_eq!(charset_from_unicode(0xAC00), Charset::Hangul);
295        assert_eq!(charset_from_unicode(0x0E01), Charset::Thai);
296        assert_eq!(charset_from_unicode(0x03B1), Charset::Greek);
297        assert_eq!(charset_from_unicode(0x0627), Charset::Arabic);
298        assert_eq!(charset_from_unicode(0x05D0), Charset::Hebrew);
299        assert_eq!(charset_from_unicode(0x0410), Charset::Cyrillic);
300        assert_eq!(charset_from_unicode(0x0100), Charset::EasternEuropean);
301        assert_eq!(charset_from_unicode(0x1E00), Charset::Vietnamese);
302        assert_eq!(charset_from_unicode(0x10000), Charset::Ansi);
303    }
304
305    #[test]
306    fn pitch_families_derive_from_both_sources() {
307        use super::super::StandardFont;
308        assert_eq!(
309            PitchFamily::from_standard_font(StandardFont::Courier).0,
310            PitchFamily::FIXED
311        );
312        assert_eq!(
313            PitchFamily::from_standard_font(StandardFont::Helvetica).0,
314            0
315        );
316        assert_eq!(
317            PitchFamily::from_standard_font(StandardFont::Times).0,
318            PitchFamily::ROMAN
319        );
320        // Symbol and Dingbats are at index 12 and 13, so both are Roman.
321        assert_eq!(
322            PitchFamily::from_standard_font(StandardFont::Symbol).0,
323            PitchFamily::ROMAN
324        );
325
326        let flags = FontFlags::SERIF | FontFlags::FIXED_PITCH;
327        let p = PitchFamily::from_flags(flags);
328        assert!(p.has(PitchFamily::ROMAN));
329        assert!(p.has(PitchFamily::FIXED));
330        assert!(!p.has(PitchFamily::SCRIPT));
331    }
332
333    #[cfg(all(feature = "system-fonts", not(target_arch = "wasm32")))]
334    #[test]
335    fn code_page_bits_map_to_charsets() {
336        assert_eq!(charset_for_code_page_bit(17), Some(Charset::ShiftJis));
337        assert_eq!(charset_for_code_page_bit(31), Some(Charset::Symbol));
338        assert_eq!(charset_for_code_page_bit(0), None);
339        assert_eq!(charset_for_code_page_bit(29), None);
340    }
341
342    #[test]
343    fn default_face_names_match_the_linux_table() {
344        assert_eq!(default_face_name(Charset::Ansi), "Helvetica");
345        assert_eq!(default_face_name(Charset::ChineseSimplified), "SimSun");
346        assert_eq!(default_face_name(Charset::ShiftJis), "MS Gothic");
347        assert_eq!(default_face_name(Charset::Symbol), "Arial Unicode MS");
348    }
349}