Skip to main content

pdfrum_font/cid/
mod.rs

1//! Composite (Type0) fonts: a multi-byte code, through a CMap, to a CID, to a
2//! glyph.
3//!
4//! This is the one font kind whose load can genuinely **fail** — four ways,
5//! all of which mean "the resource is not there" and cause the content-stream
6//! interpreter to skip text using the font.
7
8mod glyph;
9mod gsub;
10mod transform;
11
12pub use transform::{CidTransform, cid_transform_to_float, japan1_transform};
13
14use crate::descriptor::{self, FontDescriptor};
15use crate::glyphs::{Charmap, Face, GlyphSource};
16use crate::subst::{self, CodePage, FontRequest, SubstFont, SubstitutionOptions};
17use crate::tounicode::ToUnicode;
18use crate::widths::CidWidths;
19use crate::{CharCode, CharItem, Cid, Error, FontCache, FontId, Gid, names, widths};
20use pdfrum_cmap::{CMap, CidCoding, CidSet};
21use pdfrum_common::kurbo::Rect;
22use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
23use pdfrum_object::{Dict, Object, Resolve};
24use smallvec::SmallVec;
25
26pub use crate::widths::VerticalMetrics;
27
28/// How a CID becomes a glyph index.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum CidToGid {
31    /// `/CIDToGIDMap /Identity`, or the absence of the key for a font where
32    /// CID and GID coincide.
33    Identity,
34    /// A `/CIDToGIDMap` stream: a big-endian `u16` table indexed by CID.
35    Stream(Box<[u8]>),
36    /// Neither: the glyph is found through a charmap instead.
37    ViaCharmap,
38}
39
40/// Which flavour of descendant font.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum CidFontKind {
43    /// `CIDFontType0` — CFF outlines, where CID *is* GID for an embedded font.
44    Type1,
45    /// `CIDFontType2` — TrueType outlines.
46    TrueType,
47}
48
49/// A composite font.
50#[derive(Debug)]
51pub struct Type0Font {
52    /// This font's identity, for glyph-cache keys.
53    pub(crate) id: FontId,
54    /// The CMap that splits bytes into codes and maps them to CIDs.
55    pub(crate) cmap: CMap,
56    /// Where glyphs come from.
57    pub(crate) glyphs: GlyphSource,
58    /// The CID collection, from the CMap or from `/CIDSystemInfo`.
59    pub(crate) charset: CidSet,
60    /// How a CID becomes a glyph.
61    pub(crate) cid_to_gid: CidToGid,
62    /// `/W` and `/DW`.
63    pub(crate) widths: CidWidths,
64    /// `/W2` and `/DW2`, only for a vertical font.
65    pub(crate) vertical: Option<VerticalMetrics>,
66    /// The `/ToUnicode` CMap.
67    pub(crate) to_unicode: Option<ToUnicode>,
68    /// The descendant's `/FontDescriptor`.
69    pub(crate) descriptor: FontDescriptor,
70    /// What substitution decided.
71    pub(crate) subst: Option<SubstFont>,
72    /// Which flavour of descendant.
73    pub(crate) kind: CidFontKind,
74    /// Whether a usable font program was embedded.
75    pub(crate) embedded: bool,
76    /// The descendant's `/BaseFont`.
77    pub(crate) base_font_name: Vec<u8>,
78    /// Adobe's CourierStd, whose CIDs are offset from the standard encoding by
79    /// 31 — a hard-coded rescue with no general rule behind it.
80    pub(crate) adobe_courier_std: bool,
81    /// The GB2312 rescue path, which fixes ASCII widths.
82    #[cfg(test)]
83    pub(crate) ansi_widths_fixed: bool,
84    /// Vertical substitution, parsed from `GSUB` on first use.
85    gsub: gsub::VerticalSubst,
86}
87
88impl Type0Font {
89    /// The CID a character code maps to.
90    #[must_use]
91    pub(crate) fn cid_from_charcode(&self, code: CharCode) -> Cid {
92        self.cmap.cid(code)
93    }
94
95    /// The glyph a character code selects, or `None` for "draw nothing".
96    ///
97    /// Also reports whether a vertical form was substituted, which suppresses
98    /// the Japan1 transform downstream.
99    #[must_use]
100    pub(crate) fn glyph_from_charcode(&self, code: CharCode) -> (Option<Gid>, bool) {
101        glyph::resolve(self, code)
102    }
103
104    /// The advance width for a character code, in 1000/em units.
105    #[must_use]
106    pub(crate) fn char_width(&self, code: CharCode) -> f32 {
107        self.widths.width(code, self.cid_from_charcode(code))
108    }
109
110    /// The vertical advance for a character code.
111    #[must_use]
112    pub(crate) fn vert_width(&self, code: CharCode) -> f32 {
113        match &self.vertical {
114            Some(v) => v.width(self.cid_from_charcode(code)),
115            None => -1000.0,
116        }
117    }
118
119    /// The vertical origin for a character code, in 1000/em units.
120    ///
121    /// Absent a `/W2` record this is **half the horizontal width** and
122    /// `DW2[0]`, which is why the horizontal table is consulted here.
123    #[must_use]
124    pub(crate) fn vert_origin(&self, code: CharCode) -> (f32, f32) {
125        let cid = self.cid_from_charcode(code);
126        match &self.vertical {
127            Some(v) => v.origin(cid, &self.widths),
128            None => ((self.widths.width(code, cid) / 2.0).trunc(), 880.0),
129        }
130    }
131
132    /// The Unicode a code stands for, `/ToUnicode` first.
133    #[must_use]
134    pub(crate) fn unicode_from_charcode(&self, code: CharCode) -> SmallVec<[char; 2]> {
135        if let Some(tu) = &self.to_unicode {
136            let chars = tu.lookup(code);
137            if !chars.is_empty() {
138                return chars;
139            }
140        }
141        match self.scalar_unicode(code) {
142            0 => SmallVec::new(),
143            u => char::from_u32(u32::from(u))
144                .map(|c| SmallVec::from_slice(&[c]))
145                .unwrap_or_default(),
146        }
147    }
148
149    /// The *scalar* Unicode derivation, which does **not** consult
150    /// `/ToUnicode`.
151    ///
152    /// The CMap's coding scheme decides: a UCS-2 or UTF-16 CMap means the
153    /// character code simply *is* the Unicode, while a CID-coded one goes
154    /// through the collection's table.
155    #[must_use]
156    pub(crate) fn scalar_unicode(&self, code: CharCode) -> u16 {
157        match self.cmap.coding() {
158            CidCoding::Ucs2 | CidCoding::Utf16 => return (code.0 & 0xffff) as u16,
159            CidCoding::Cid => {
160                if !pdfrum_cmap::has_cid2unicode(self.charset) {
161                    return 0;
162                }
163                let cid = Cid((code.0 & 0xffff) as u16);
164                return pdfrum_cmap::unicode_from_cid(self.charset, cid).map_or(0, |c| c as u16);
165            }
166            _ => {}
167        }
168        if pdfrum_cmap::has_cid2unicode(self.charset) && self.cmap.is_loaded() {
169            let cid = self.cid_from_charcode(code);
170            return pdfrum_cmap::unicode_from_cid(self.charset, cid).map_or(0, |c| c as u16);
171        }
172        // The non-Windows tail: only the four CJK registries have a static
173        // map to walk backwards through.
174        if !self.cmap.has_static_map() {
175            return 0;
176        }
177        let cid = self.cid_from_charcode(code);
178        if cid.0 == 0 {
179            return 0;
180        }
181        pdfrum_cmap::unicode_from_cid(self.charset, cid).map_or(0, |c| c as u16)
182    }
183
184    /// The character code for a Unicode, or 0 (`CharCodeFromUnicode`).
185    #[must_use]
186    pub(crate) fn charcode_from_unicode(&self, unicode: char) -> CharCode {
187        if let Some(tu) = &self.to_unicode {
188            let c = tu.reverse(unicode);
189            if c.0 != 0 {
190                return c;
191            }
192        }
193        match self.cmap.coding() {
194            CidCoding::Unknown => return CharCode(0),
195            CidCoding::Ucs2 | CidCoding::Utf16 => return CharCode(unicode as u32),
196            CidCoding::Cid => {
197                if !pdfrum_cmap::has_cid2unicode(self.charset) {
198                    return CharCode(0);
199                }
200                // The C++ scans all 65 536 CIDs linearly; the cmap crate has
201                // the same scan behind a name.
202                for cid in 0..=u16::MAX {
203                    if pdfrum_cmap::unicode_from_cid(self.charset, Cid(cid)) == Some(unicode) {
204                        return CharCode(u32::from(cid));
205                    }
206                }
207            }
208            _ => {}
209        }
210        if (unicode as u32) < 0x80 {
211            return CharCode(unicode as u32);
212        }
213        if self.cmap.coding() == CidCoding::Cid {
214            return CharCode(0);
215        }
216        pdfrum_cmap::charcode_from_unicode(&self.cmap, unicode)
217    }
218
219    /// Whether codes can be turned into Unicode at all (`IsUnicodeCompatible`).
220    #[must_use]
221    pub(crate) fn is_unicode_compatible(&self) -> bool {
222        if pdfrum_cmap::has_cid2unicode(self.charset) && self.cmap.is_loaded() {
223            return true;
224        }
225        self.cmap.coding() != CidCoding::Unknown
226    }
227
228    /// Whether the font writes vertically.
229    #[must_use]
230    pub(crate) fn is_vertical(&self) -> bool {
231        self.cmap.is_vertical()
232    }
233
234    /// The bounding box for a code, in 1000/em units, with the Japan1
235    /// transform applied when it applies.
236    #[must_use]
237    pub(crate) fn char_bbox(&self, code: CharCode) -> Rect {
238        let (gid, vertical) = self.glyph_from_charcode(code);
239        let Some(gid) = gid else { return Rect::ZERO };
240        let Some(bbox) = self.glyphs.glyph_bbox(gid) else {
241            return Rect::ZERO;
242        };
243        // The transform rotates an upright glyph into a vertical one — so a
244        // glyph GSUB *already* substituted must not be rotated again.
245        if vertical {
246            return bbox;
247        }
248        match self.japan1_transform(code) {
249            Some(t) => transform::apply(t, bbox),
250            None => bbox,
251        }
252    }
253
254    /// The Japan1 vertical transform for a code, when one applies.
255    ///
256    /// Only for a **non-embedded** Adobe-Japan1 font: an embedded one is
257    /// expected to carry its own vertical forms.
258    #[must_use]
259    pub(crate) fn japan1_transform(&self, code: CharCode) -> Option<CidTransform> {
260        if self.charset != CidSet::Japan1 || self.embedded {
261            return None;
262        }
263        japan1_transform(self.cid_from_charcode(code))
264    }
265
266    /// Build one [`CharItem`].
267    pub(crate) fn char_item(&self, code: CharCode) -> CharItem {
268        let (gid, vertical_glyph) = self.glyph_from_charcode(code);
269        CharItem {
270            code,
271            cid: Some(self.cid_from_charcode(code)),
272            gid,
273            unicode: self.unicode_from_charcode(code),
274            width: if self.is_vertical() {
275                self.vert_width(code)
276            } else {
277                self.char_width(code)
278            },
279            vertical_glyph,
280        }
281    }
282
283    fn gsub(&self) -> &gsub::VerticalSubst {
284        &self.gsub
285    }
286}
287
288/// The CMap an `/Encoding` stream's `/UseCMap` key names, or `None` when it
289/// has none.
290///
291/// The key may be a name — the built-in CMap of that name — or a stream
292/// holding another CMap program, which is read the same way the `/Encoding`
293/// stream itself was. Anything else is not an inheritance and is ignored.
294//
295// [oracle-bug] `grep -rn 'usecmap|UseCMap' core/fpdfapi/` over the oracle
296// returns exactly one line — `cpdf_cmapparser.cpp:61`, an empty
297// `} else if (word == "usecmap") {` — so the `/UseCMap` *dictionary* key is
298// not read anywhere in PDFium at all, and neither of the two inheritance
299// channels ISO 32000-1 §9.7.5.3 defines exists. A CMap stream that says
300// `/UseCMap /GBK-EUC-H` and overrides a handful of codes therefore decodes
301// every inherited code to CID 0 rather than to the base map's answer: total
302// loss, not degradation. pdf.js reads the key, and gives it precedence over
303// the program's own `usecmap` operator — `cmap.js:639-643` takes the embedded
304// operand only `if (!useCMap && embeddedUseCMap)` — then merges child-wins in
305// `extendCMap` (:650-669). We do the same; `pdfrum_cmap::inherit_from` is the
306// call that overrides whatever the program named.
307fn use_cmap_parent(
308    dict: &Dict,
309    r: &impl Resolve,
310    limits: &Limits,
311    diags: &mut Diagnostics,
312) -> Option<CMap> {
313    if let Some(stream) = dict.stream(names::USE_CMAP, r) {
314        let bytes = pdfrum_filters::decode_chain(&stream, 0, r, limits, diags).data;
315        // Depth 1: this is already one link down from the `/Encoding` stream.
316        // A parent naming its own `/UseCMap` is not followed further, which
317        // is what keeps a stream that names itself from recursing.
318        return Some(pdfrum_cmap::parse_embedded(&bytes, limits, diags));
319    }
320    let resolved = dict.get(names::USE_CMAP, r)?;
321    let name = resolved.as_name()?;
322    Some(pdfrum_cmap::from_encoding_name(name, diags))
323}
324
325/// Load a Type0 font.
326///
327/// # Errors
328///
329/// The four cases PDFium treats as "this resource does not exist": a
330/// `/DescendantFonts` that is missing or does not hold exactly one element, a
331/// first element that is not a dictionary, a missing `/Encoding`, and an
332/// `/Encoding` that is neither a name nor a stream.
333pub(crate) fn load(
334    dict: &Dict,
335    r: &impl Resolve,
336    cache: &FontCache,
337    opts: &SubstitutionOptions,
338    limits: &Limits,
339    diags: &mut Diagnostics,
340) -> Result<Type0Font, Error> {
341    let descendants = dict
342        .array(names::DESCENDANT_FONTS, r)
343        .ok_or(Error::BadDescendantFonts)?;
344    if descendants.len() != 1 {
345        return Err(Error::BadDescendantFonts);
346    }
347    let cid_dict = descendants.dict_at(0, r).ok_or(Error::BadDescendantFonts)?;
348
349    let base_font_name = cid_dict
350        .name(names::BASE_FONT)
351        .map(|n| n.as_bytes().to_vec())
352        .unwrap_or_default();
353
354    let encoding = dict.raw(names::ENCODING).ok_or(Error::BadCidEncoding)?;
355    let kind = match cid_dict.name(names::SUBTYPE).map(|n| n.as_bytes().to_vec()) {
356        Some(s) if s == b"CIDFontType0" => CidFontKind::Type1,
357        _ => CidFontKind::TrueType,
358    };
359
360    // An `/Encoding` must be a name or a stream. A dictionary, an array or a
361    // number fails the load outright.
362    let cmap = match encoding {
363        Object::Name(name) => pdfrum_cmap::from_encoding_name(name, diags),
364        Object::Stream(_) | Object::Ref(_) => {
365            if let Some(stream) = dict.stream(names::ENCODING, r) {
366                let bytes = pdfrum_filters::decode_chain(&stream, 0, r, limits, diags).data;
367                let cmap = pdfrum_cmap::parse_embedded(&bytes, limits, diags);
368                match use_cmap_parent(&stream.dict, r, limits, diags) {
369                    Some(parent) => pdfrum_cmap::inherit_from(cmap, parent, 0, limits, diags),
370                    None => cmap,
371                }
372            } else {
373                // A reference is only usable if it resolves to a stream or a
374                // name; anything else is one of the four fatal cases.
375                let resolved = dict.get(names::ENCODING, r);
376                match resolved.as_ref().and_then(|o| o.as_name()) {
377                    Some(name) => pdfrum_cmap::from_encoding_name(name, diags),
378                    None => return Err(Error::BadCidEncoding),
379                }
380            }
381        }
382        _ => return Err(Error::BadCidEncoding),
383    };
384
385    Ok(build(
386        &cid_dict,
387        dict,
388        cmap,
389        kind,
390        base_font_name,
391        r,
392        cache,
393        opts,
394        limits,
395        diags,
396        false,
397    ))
398}
399
400/// The GB2312 rescue path, reached only through the Chinese-name special case
401/// of the font-type dispatch.
402///
403/// Charset GB1 with the predefined `GBK-EUC-H` CMap, and fixed ASCII widths.
404pub(crate) fn load_gb2312(
405    dict: &Dict,
406    r: &impl Resolve,
407    cache: &FontCache,
408    opts: &SubstitutionOptions,
409    limits: &Limits,
410    diags: &mut Diagnostics,
411) -> Result<Type0Font, Error> {
412    let cmap = pdfrum_cmap::predefined(&pdfrum_object::Name::from("GBK-EUC-H"))
413        .ok_or(Error::BadCidEncoding)?;
414    let base_font_name = dict
415        .name(names::BASE_FONT)
416        .map(|n| n.as_bytes().to_vec())
417        .unwrap_or_default();
418    Ok(build(
419        dict,
420        dict,
421        cmap,
422        CidFontKind::TrueType,
423        base_font_name,
424        r,
425        cache,
426        opts,
427        limits,
428        diags,
429        true,
430    ))
431}
432
433// The ladder below reads as one sequence — each step's inputs come from the
434// step above it — and splitting it into pieces would hide the order the
435// decisions have to be taken in.
436#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
437fn build(
438    cid_dict: &Dict,
439    font_dict: &Dict,
440    cmap: CMap,
441    kind: CidFontKind,
442    base_font_name: Vec<u8>,
443    r: &impl Resolve,
444    cache: &FontCache,
445    opts: &SubstitutionOptions,
446    limits: &Limits,
447    diags: &mut Diagnostics,
448    gb2312: bool,
449) -> Type0Font {
450    // Adobe's CourierStd, whose CIDs sit 31 codes below the standard
451    // encoding. There is no general rule here — four literal names.
452    let adobe_courier_std = matches!(
453        base_font_name.as_slice(),
454        b"CourierStd" | b"CourierStd-Bold" | b"CourierStd-BoldOblique" | b"CourierStd-Oblique"
455    );
456
457    let desc = cid_dict.dict(names::FONT_DESCRIPTOR, r);
458    let mut descriptor = FontDescriptor::default();
459    if let Some(d) = &desc {
460        descriptor = descriptor::load(d, r);
461    }
462    let (mut glyphs, mut embedded) =
463        crate::simple::load_font_program(desc.as_ref(), r, limits, diags);
464
465    // The collection: what the CMap says, or what `/CIDSystemInfo` says.
466    let mut charset = if gb2312 { CidSet::Gb1 } else { cmap.charset() };
467    if charset == CidSet::Unknown
468        && let Some(info) = cid_dict.dict(names::CID_SYSTEM_INFO, r)
469        && let Some(ordering) = info.byte_string(names::ORDERING, r)
470    {
471        charset = pdfrum_cmap::charset_from_ordering(&ordering);
472    }
473
474    let mut widths_table = CidWidths::load(cid_dict, r, diags);
475    if gb2312 {
476        widths_table.set_ansi_widths_fixed();
477    }
478
479    let mut subst_font = None;
480    if !embedded {
481        let request = FontRequest {
482            name: base_font_name.clone(),
483            is_truetype: kind == CidFontKind::TrueType,
484            flags: descriptor.flags,
485            // The C++ multiplies the stem by five here rather than using the
486            // descriptor's own weight estimate, saturating to 400.
487            weight: descriptor
488                .stem_v
489                .checked_mul(5)
490                .filter(|w| *w > 0)
491                .unwrap_or(400),
492            italic_angle: descriptor.italic_angle,
493            code_page: CodePage::for_cid_set(charset),
494            vertical: cmap.is_vertical(),
495        };
496        let s = substitute(&request, opts, diags);
497        glyphs = s.glyphs;
498        subst_font = Some(s.subst);
499    }
500    if !glyphs.is_some() {
501        embedded = false;
502    }
503
504    // `/CIDToGIDMap` is read non-resolving for the name form: PDFium checks
505    // the direct object's type before resolving, so an indirect `/Identity`
506    // reads as absent.
507    let cid_to_gid = match cid_dict.raw(names::CID_TO_GID_MAP) {
508        Some(Object::Name(n)) if n.as_bytes() == b"Identity" && embedded => CidToGid::Identity,
509        Some(Object::Stream(_) | Object::Ref(_)) => {
510            match cid_dict.stream(names::CID_TO_GID_MAP, r) {
511                Some(s) => {
512                    let bytes = pdfrum_filters::decode_chain(&s, 0, r, limits, diags).data;
513                    // Two bytes per CID: a table shorter than the face has
514                    // glyphs leaves the tail unmapped rather than erroring.
515                    if bytes.len() < glyphs.num_glyphs() as usize * 2 {
516                        diags.record(Severity::Suspicious, DiagKind::CidToGidStreamShort, None);
517                    }
518                    CidToGid::Stream(bytes.into_boxed_slice())
519                }
520                None => CidToGid::ViaCharmap,
521            }
522        }
523        _ => CidToGid::ViaCharmap,
524    };
525
526    let vertical = if cmap.is_vertical() {
527        Some(widths::VerticalMetrics::load(cid_dict, r, diags))
528    } else {
529        None
530    };
531
532    let to_unicode = crate::simple::load_to_unicode(font_dict, r, limits, diags);
533
534    let metrics = match &glyphs {
535        GlyphSource::Fontations(f) => f.metrics(),
536        GlyphSource::Type1(f) => Some(descriptor::FaceMetrics {
537            upem: f.units_per_em(),
538            bbox_left: f.bbox().x0 as i64,
539            bbox_top: f.bbox().y1 as i64,
540            bbox_right: f.bbox().x1 as i64,
541            bbox_bottom: f.bbox().y0 as i64,
542            ascender: f.bbox().y1 as i64,
543            descender: f.bbox().y0 as i64,
544        }),
545        GlyphSource::None => None,
546    };
547    descriptor::check_font_metrics(&mut descriptor, metrics, |_| Rect::ZERO);
548
549    let gsub = if cmap.is_vertical() {
550        gsub::VerticalSubst::parse(&glyphs, diags)
551    } else {
552        gsub::VerticalSubst::none()
553    };
554
555    Type0Font {
556        id: cache.next_id(),
557        cmap,
558        glyphs,
559        charset,
560        cid_to_gid,
561        widths: widths_table,
562        vertical,
563        to_unicode,
564        descriptor,
565        subst: subst_font,
566        kind,
567        embedded,
568        base_font_name,
569        adobe_courier_std,
570        #[cfg(test)]
571        ansi_widths_fixed: gb2312,
572        gsub,
573    }
574}
575
576fn substitute(
577    request: &FontRequest,
578    opts: &SubstitutionOptions,
579    diags: &mut Diagnostics,
580) -> subst::Substitution {
581    subst::resolve_with_options(request, opts, diags)
582}
583
584/// Choose the charmap a CID font drives (`UseCIDCharmap`).
585///
586/// Three rungs: the **legacy** charmap the coding scheme names, then Unicode,
587/// then whatever charmap comes first. A CJK font that carries its national
588/// encoding's own subtable is driven through *that*, with character codes
589/// passed straight in — which is why the embedded glyph ladder branches on
590/// whether the chosen charmap is Unicode before deciding what to look up.
591///
592/// Note **Korea asks for Johab**, encoding id 6, not Wansung's 5. A font
593/// carrying only a Wansung subtable therefore falls through to Unicode, which
594/// looks like an oversight and is what the oracle does.
595pub(crate) fn cid_charmap(glyphs: &GlyphSource, coding: CidCoding) -> Charmap {
596    let charmaps = glyphs.charmaps();
597
598    // Rung 1 — the national encoding, as a Windows-platform subtable.
599    if let Some(wanted) = legacy_encoding_id(coding)
600        && let Some(i) = charmaps
601            .iter()
602            .position(|c| c.platform == 3 && c.encoding == wanted)
603    {
604        return Charmap::Index(i);
605    }
606    // Rung 2 — Unicode.
607    if let Some(i) = charmaps.iter().position(|c| c.is_unicode()) {
608        return Charmap::Index(i);
609    }
610    // Rung 3 — anything at all.
611    if charmaps.is_empty() {
612        Charmap::None
613    } else {
614        Charmap::Index(0)
615    }
616}
617
618/// The `cmap` encoding id a CID coding scheme's legacy charmap carries, on the
619/// Windows platform.
620///
621/// The ids are the `TT_MS_ID_*` values FreeType maps its `FT_ENCODING_*`
622/// constants onto: Shift-JIS 2, GB2312 3, Big5 4, Johab 6.
623fn legacy_encoding_id(coding: CidCoding) -> Option<u16> {
624    Some(match coding {
625        CidCoding::Gb => 3,
626        CidCoding::Big5 => 4,
627        CidCoding::Jis => 2,
628        CidCoding::Korea => 6,
629        // Every other scheme asks for Unicode outright, which rung 2 covers.
630        CidCoding::Unknown | CidCoding::Ucs2 | CidCoding::Cid | CidCoding::Utf16 => return None,
631    })
632}
633
634/// Read a `Face` out of a glyph source, for the GSUB reader.
635pub(crate) fn face_of(glyphs: &GlyphSource) -> Option<&Face> {
636    match glyphs {
637        GlyphSource::Fontations(f) => Some(f),
638        GlyphSource::Type1(_) | GlyphSource::None => None,
639    }
640}
641
642#[cfg(test)]
643#[path = "cid_tests.rs"]
644mod tests;