Skip to main content

stet_fonts/
cff_parser.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! CFF (Compact Font Format) binary parser.
6//!
7//! Parses CFF binary data (Adobe TN#5176) into structured `CffFont` objects.
8//! CFF is a compact binary encoding for Type 1-style fonts using Type 2 charstrings.
9//!
10//! CFF data appears in PostScript via the FontSet resource mechanism:
11//!   FontSetInit /ProcSet findresource begin ... StartData
12//!
13//! This parser handles:
14//! - CFF Header, Name INDEX, Top DICT, String INDEX, Global Subr INDEX
15//! - CharStrings INDEX, charset, encoding, Private DICT, Local Subr INDEX
16//! - Both name-keyed and CID-keyed fonts
17//! - Predefined charsets (ISOAdobe, Expert, ExpertSubset) and encodings
18
19/// A parsed CFF font.
20pub struct CffFont {
21    /// Font name from Name INDEX.
22    pub name: String,
23    /// Font transformation matrix (default [0.001, 0, 0, 0.001, 0, 0]).
24    pub font_matrix: [f64; 6],
25    /// Font bounding box.
26    pub font_bbox: [f64; 4],
27    /// GID-indexed raw Type 2 charstring bytes.
28    pub char_strings: Vec<Vec<u8>>,
29    /// GID → glyph name.
30    pub charset: Vec<String>,
31    /// char_code (0–255) → GID.
32    pub encoding: Vec<u16>,
33    /// Default advance width from Private DICT.
34    pub default_width_x: f64,
35    /// Nominal advance width from Private DICT.
36    pub nominal_width_x: f64,
37    /// Local subroutines from Private DICT.
38    pub local_subrs: Vec<Vec<u8>>,
39    /// Global subroutines (shared across all fonts in FontSet).
40    pub global_subrs: Vec<Vec<u8>>,
41    /// Whether this is a CID-keyed font (ROS operator present).
42    pub is_cid: bool,
43    /// Per-FD Private dicts + subrs (CID only).
44    pub fd_array: Vec<FdEntry>,
45    /// GID → FD index (CID only).
46    pub fd_select: Vec<u8>,
47    /// Registry-Ordering-Supplement (CID only).
48    pub ros: Option<(String, String, i32)>,
49    /// CID → GID mapping for CID-keyed fonts.
50    /// In CID fonts, the charset encodes GID → CID; this is the reverse map.
51    pub cid_to_gid: Vec<u16>,
52}
53
54/// Per-FD entry for CID fonts (from FDArray).
55pub struct FdEntry {
56    /// Default advance width for this FD.
57    pub default_width_x: f64,
58    /// Nominal advance width for this FD.
59    pub nominal_width_x: f64,
60    /// Local subroutines for this FD.
61    pub local_subrs: Vec<Vec<u8>>,
62    /// Per-FD FontMatrix (None = use top-level FontMatrix).
63    pub font_matrix: Option<[f64; 6]>,
64}
65
66/// Parse CFF binary data into a list of `CffFont` objects.
67pub fn parse_cff(data: &[u8]) -> Result<Vec<CffFont>, String> {
68    if data.len() < 4 {
69        return Err("CFF data too short for header".into());
70    }
71
72    // Header
73    let major = data[0];
74    if major != 1 {
75        return Err(format!("Unsupported CFF major version: {major}"));
76    }
77    let hdr_size = data[2] as usize;
78    let mut offset = hdr_size;
79
80    // Name INDEX
81    let (name_index, off) = parse_index(data, offset)?;
82    offset = off;
83
84    // Top DICT INDEX
85    let (top_dict_index, off) = parse_index(data, offset)?;
86    offset = off;
87
88    // String INDEX
89    let (string_index, off) = parse_index(data, offset)?;
90    offset = off;
91
92    // Global Subr INDEX
93    let (global_subr_index, _off) = parse_index(data, offset)?;
94
95    let mut fonts = Vec::new();
96    for font_idx in 0..name_index.len() {
97        let mut font = CffFont {
98            name: String::from_utf8_lossy(&name_index[font_idx]).into_owned(),
99            font_matrix: [0.001, 0.0, 0.0, 0.001, 0.0, 0.0],
100            font_bbox: [0.0; 4],
101            char_strings: Vec::new(),
102            charset: Vec::new(),
103            encoding: vec![0u16; 256],
104            default_width_x: 0.0,
105            nominal_width_x: 0.0,
106            local_subrs: Vec::new(),
107            global_subrs: global_subr_index.clone(),
108            is_cid: false,
109            fd_array: Vec::new(),
110            fd_select: Vec::new(),
111            ros: None,
112            cid_to_gid: Vec::new(),
113        };
114
115        // Parse Top DICT
116        let top_dict = if font_idx < top_dict_index.len() {
117            parse_dict_data(&top_dict_index[font_idx])
118        } else {
119            Vec::new()
120        };
121
122        // FontMatrix (12,7)
123        if let Some(vals) = dict_get(&top_dict, DictOp::TwoByte(12, 7))
124            && vals.len() == 6
125        {
126            for (i, v) in vals.iter().enumerate() {
127                font.font_matrix[i] = *v;
128            }
129        }
130
131        // FontBBox (5)
132        if let Some(vals) = dict_get(&top_dict, DictOp::OneByte(5))
133            && vals.len() == 4
134        {
135            for (i, v) in vals.iter().enumerate() {
136                font.font_bbox[i] = *v;
137            }
138        }
139
140        // CID detection (ROS = 12,30)
141        if let Some(ros_ops) = dict_get(&top_dict, DictOp::TwoByte(12, 30)) {
142            font.is_cid = true;
143            if ros_ops.len() >= 3 {
144                let registry = get_sid_string(ros_ops[0] as u16, &string_index);
145                let ordering = get_sid_string(ros_ops[1] as u16, &string_index);
146                let supplement = ros_ops[2] as i32;
147                font.ros = Some((registry, ordering, supplement));
148            }
149        }
150
151        // CharStrings INDEX (op 17)
152        if let Some(vals) = dict_get(&top_dict, DictOp::OneByte(17))
153            && !vals.is_empty()
154        {
155            let cs_offset = vals[0] as usize;
156            if cs_offset > 0 && cs_offset < data.len() {
157                let (cs_items, _) = parse_index(data, cs_offset)?;
158                font.char_strings = cs_items;
159            }
160        }
161
162        let n_glyphs = font.char_strings.len();
163
164        // Charset (op 15)
165        let charset_val = dict_get(&top_dict, DictOp::OneByte(15))
166            .and_then(|v| v.first().copied())
167            .unwrap_or(0.0) as i32;
168        if charset_val <= 2 {
169            font.charset = get_predefined_charset(charset_val, n_glyphs, &string_index);
170        } else {
171            font.charset = parse_charset(data, charset_val as usize, n_glyphs, &string_index)?;
172        }
173
174        // Build CID→GID reverse mapping for CID-keyed fonts.
175        // In CID fonts, charset values are CID values (not SIDs).
176        if font.is_cid && charset_val > 2 {
177            font.cid_to_gid = build_cid_to_gid(data, charset_val as usize, n_glyphs)?;
178        }
179
180        // Encoding (only for name-keyed fonts, op 16)
181        if !font.is_cid {
182            let enc_val = dict_get(&top_dict, DictOp::OneByte(16))
183                .and_then(|v| v.first().copied())
184                .unwrap_or(0.0) as i32;
185            if enc_val <= 1 {
186                font.encoding = get_predefined_encoding(enc_val, &font.charset, &string_index);
187            } else {
188                font.encoding =
189                    parse_encoding(data, enc_val as usize, &font.charset, &string_index)?;
190            }
191        }
192
193        // Private DICT (op 18: [size, offset])
194        if let Some(priv_ops) = dict_get(&top_dict, DictOp::OneByte(18))
195            && priv_ops.len() >= 2
196            && let Some(priv_size) = dict_usize(priv_ops[0])
197            && let Some(priv_offset) = dict_usize(priv_ops[1])
198            && priv_size > 0
199            && priv_offset > 0
200            && let Some(priv_end) = priv_offset.checked_add(priv_size)
201            && let Some(priv_data) = data.get(priv_offset..priv_end)
202        {
203            let priv_dict = parse_dict_data(priv_data);
204
205            // defaultWidthX (op 20)
206            if let Some(vals) = dict_get(&priv_dict, DictOp::OneByte(20))
207                && let Some(&v) = vals.first()
208            {
209                font.default_width_x = v;
210            }
211
212            // nominalWidthX (op 21)
213            if let Some(vals) = dict_get(&priv_dict, DictOp::OneByte(21))
214                && let Some(&v) = vals.first()
215            {
216                font.nominal_width_x = v;
217            }
218
219            // Local Subr INDEX (op 19, offset relative to Private DICT start)
220            if let Some(vals) = dict_get(&priv_dict, DictOp::OneByte(19))
221                && let Some(&v) = vals.first()
222                && let Some(rel) = dict_usize(v)
223                && let Some(subr_abs_offset) = priv_offset.checked_add(rel)
224                && subr_abs_offset < data.len()
225            {
226                let (local_subrs, _) = parse_index(data, subr_abs_offset)?;
227                font.local_subrs = local_subrs;
228            }
229        }
230
231        // CID-specific: FDArray and FDSelect
232        if font.is_cid {
233            // FDArray (12,36)
234            if let Some(vals) = dict_get(&top_dict, DictOp::TwoByte(12, 36))
235                && let Some(&v) = vals.first()
236            {
237                let fda_offset = v as usize;
238                if fda_offset < data.len() {
239                    let (fd_dicts_raw, _) = parse_index(data, fda_offset)?;
240                    for fd_raw in &fd_dicts_raw {
241                        let fd_top = parse_dict_data(fd_raw);
242                        let mut fd_entry = FdEntry {
243                            default_width_x: 0.0,
244                            nominal_width_x: 0.0,
245                            local_subrs: Vec::new(),
246                            font_matrix: None,
247                        };
248
249                        // Check for FD-level FontMatrix
250                        if let Some(fm_vals) = dict_get(&fd_top, DictOp::TwoByte(12, 7))
251                            && fm_vals.len() == 6
252                        {
253                            fd_entry.font_matrix = Some([
254                                fm_vals[0], fm_vals[1], fm_vals[2], fm_vals[3], fm_vals[4],
255                                fm_vals[5],
256                            ]);
257                        }
258
259                        // Each FD has its own Private DICT
260                        if let Some(fd_priv_ops) = dict_get(&fd_top, DictOp::OneByte(18))
261                            && fd_priv_ops.len() >= 2
262                            && let Some(fd_priv_size) = dict_usize(fd_priv_ops[0])
263                            && let Some(fd_priv_offset) = dict_usize(fd_priv_ops[1])
264                            && fd_priv_size > 0
265                            && fd_priv_offset > 0
266                            && let Some(fd_priv_end) = fd_priv_offset.checked_add(fd_priv_size)
267                            && let Some(fd_priv_data) = data.get(fd_priv_offset..fd_priv_end)
268                        {
269                            let fd_priv_dict = parse_dict_data(fd_priv_data);
270
271                            if let Some(vals) = dict_get(&fd_priv_dict, DictOp::OneByte(20))
272                                && let Some(&v) = vals.first()
273                            {
274                                fd_entry.default_width_x = v;
275                            }
276                            if let Some(vals) = dict_get(&fd_priv_dict, DictOp::OneByte(21))
277                                && let Some(&v) = vals.first()
278                            {
279                                fd_entry.nominal_width_x = v;
280                            }
281
282                            // FD-level local subrs
283                            if let Some(vals) = dict_get(&fd_priv_dict, DictOp::OneByte(19))
284                                && let Some(&v) = vals.first()
285                                && let Some(rel) = dict_usize(v)
286                                && let Some(subr_abs) = fd_priv_offset.checked_add(rel)
287                                && subr_abs < data.len()
288                            {
289                                let (fd_local, _) = parse_index(data, subr_abs)?;
290                                fd_entry.local_subrs = fd_local;
291                            }
292                        }
293
294                        font.fd_array.push(fd_entry);
295                    }
296                }
297            }
298
299            // FDSelect (12,37)
300            if let Some(vals) = dict_get(&top_dict, DictOp::TwoByte(12, 37))
301                && let Some(&v) = vals.first()
302            {
303                let fds_offset = v as usize;
304                if fds_offset < data.len() {
305                    font.fd_select = parse_fd_select(data, fds_offset, n_glyphs)?;
306                }
307            }
308        }
309
310        fonts.push(font);
311    }
312
313    Ok(fonts)
314}
315
316// ---------------------------------------------------------------------------
317// INDEX Parsing
318// ---------------------------------------------------------------------------
319
320/// Parse a CFF INDEX structure. Returns (list of byte slices, offset after INDEX).
321fn parse_index(data: &[u8], offset: usize) -> Result<(Vec<Vec<u8>>, usize), String> {
322    if offset + 2 > data.len() {
323        return Err("INDEX: truncated count".into());
324    }
325    let count = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
326    let mut pos = offset + 2;
327
328    if count == 0 {
329        return Ok((Vec::new(), pos));
330    }
331
332    if pos >= data.len() {
333        return Err("INDEX: truncated offSize".into());
334    }
335    let off_size = data[pos] as usize;
336    pos += 1;
337
338    if off_size == 0 || off_size > 4 {
339        return Err(format!("INDEX: invalid offSize {off_size}"));
340    }
341
342    // Read count+1 offsets
343    let mut offsets = Vec::with_capacity(count + 1);
344    for _ in 0..=count {
345        if pos + off_size > data.len() {
346            return Err("INDEX: truncated offset".into());
347        }
348        let val = read_offset(data, pos, off_size);
349        offsets.push(val);
350        pos += off_size;
351    }
352
353    // Data starts at current pos; offsets are 1-based relative to byte before data
354    let data_start = pos - 1; // offsets[0] == 1 means first byte of data region
355    let mut items = Vec::with_capacity(count);
356    for i in 0..count {
357        let start = data_start + offsets[i];
358        let end = data_start + offsets[i + 1];
359        if end > data.len() || start > end {
360            return Err("INDEX: data out of bounds".into());
361        }
362        items.push(data[start..end].to_vec());
363    }
364
365    let end_offset = data_start + offsets[count];
366    Ok((items, end_offset))
367}
368
369/// Narrow a DICT operand to a `usize` offset or length.
370///
371/// DICT operands are `f64` because CFF permits a real-number operand wherever
372/// an integer is expected (TN#5176 §4), so a hostile font can put `1e30` — or
373/// a negative — where an offset belongs. `f64 as usize` *saturates*: those
374/// become `usize::MAX` and `0`, and `usize::MAX` then overflows the very next
375/// add. Reject anything outside what a CFF offset can actually address
376/// instead; `u32::MAX` is the ceiling because INDEX offSize is at most 4 bytes
377/// and a DICT integer operand is at most 32-bit.
378fn dict_usize(v: f64) -> Option<usize> {
379    if v.is_finite() && v >= 0.0 && v <= f64::from(u32::MAX) {
380        Some(v as usize)
381    } else {
382        None
383    }
384}
385
386/// Read an offset of `off_size` bytes (1–4), big-endian unsigned.
387fn read_offset(data: &[u8], offset: usize, off_size: usize) -> usize {
388    match off_size {
389        1 => data[offset] as usize,
390        2 => u16::from_be_bytes([data[offset], data[offset + 1]]) as usize,
391        3 => {
392            ((data[offset] as usize) << 16)
393                | ((data[offset + 1] as usize) << 8)
394                | (data[offset + 2] as usize)
395        }
396        4 => u32::from_be_bytes([
397            data[offset],
398            data[offset + 1],
399            data[offset + 2],
400            data[offset + 3],
401        ]) as usize,
402        _ => 0,
403    }
404}
405
406// ---------------------------------------------------------------------------
407// DICT Parsing
408// ---------------------------------------------------------------------------
409
410/// DICT operator key.
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412enum DictOp {
413    OneByte(u8),
414    TwoByte(u8, u8),
415}
416
417/// A DICT entry: operator key → operands.
418struct DictEntry {
419    op: DictOp,
420    operands: Vec<f64>,
421}
422
423/// Look up an operator's operands in a parsed DICT.
424fn dict_get(dict: &[DictEntry], op: DictOp) -> Option<&[f64]> {
425    dict.iter()
426        .find(|e| e.op == op)
427        .map(|e| e.operands.as_slice())
428}
429
430/// Parse CFF DICT binary data into a list of entries.
431fn parse_dict_data(data: &[u8]) -> Vec<DictEntry> {
432    let mut result = Vec::new();
433    let mut operands: Vec<f64> = Vec::new();
434    let mut i = 0;
435    let length = data.len();
436
437    while i < length {
438        let b0 = data[i];
439
440        if b0 <= 21 {
441            // Operator
442            let op = if b0 == 12 {
443                i += 1;
444                if i >= length {
445                    break;
446                }
447                DictOp::TwoByte(12, data[i])
448            } else {
449                DictOp::OneByte(b0)
450            };
451            result.push(DictEntry {
452                op,
453                operands: std::mem::take(&mut operands),
454            });
455            i += 1;
456        } else if b0 == 28 {
457            // 3-byte signed integer
458            if i + 2 >= length {
459                break;
460            }
461            let val = i16::from_be_bytes([data[i + 1], data[i + 2]]);
462            operands.push(val as f64);
463            i += 3;
464        } else if b0 == 29 {
465            // 5-byte signed integer
466            if i + 4 >= length {
467                break;
468            }
469            let val = i32::from_be_bytes([data[i + 1], data[i + 2], data[i + 3], data[i + 4]]);
470            operands.push(val as f64);
471            i += 5;
472        } else if b0 == 30 {
473            // BCD real
474            i += 1;
475            let mut chars = Vec::new();
476            while i < length {
477                let byte = data[i];
478                i += 1;
479                let n1 = (byte >> 4) & 0x0F;
480                let n2 = byte & 0x0F;
481
482                if !push_bcd_nibble(n1, &mut chars) {
483                    break;
484                }
485                if !push_bcd_nibble(n2, &mut chars) {
486                    break;
487                }
488            }
489            let s: String = chars.into_iter().collect();
490            operands.push(s.parse::<f64>().unwrap_or(0.0));
491        } else if (32..=246).contains(&b0) {
492            operands.push((b0 as i32 - 139) as f64);
493            i += 1;
494        } else if (247..=250).contains(&b0) {
495            if i + 1 >= length {
496                break;
497            }
498            let b1 = data[i + 1];
499            operands.push(((b0 as i32 - 247) * 256 + b1 as i32 + 108) as f64);
500            i += 2;
501        } else if (251..=254).contains(&b0) {
502            if i + 1 >= length {
503                break;
504            }
505            let b1 = data[i + 1];
506            operands.push((-(b0 as i32 - 251) * 256 - b1 as i32 - 108) as f64);
507            i += 2;
508        } else {
509            // Skip unknown bytes (255 not used in DICT data)
510            i += 1;
511        }
512    }
513
514    result
515}
516
517/// Push a BCD nibble character. Returns false on end-of-number (0xF).
518fn push_bcd_nibble(n: u8, chars: &mut Vec<char>) -> bool {
519    match n {
520        0..=9 => chars.push((b'0' + n) as char),
521        0x0A => chars.push('.'),
522        0x0B => chars.push('E'),
523        0x0C => {
524            chars.push('E');
525            chars.push('-');
526        }
527        0x0E => chars.push('-'),
528        0x0F => return false,
529        _ => {} // 0x0D reserved
530    }
531    true
532}
533
534// ---------------------------------------------------------------------------
535// SID Resolution
536// ---------------------------------------------------------------------------
537
538/// Resolve a String ID (SID) to its string.
539/// SID 0–390 are predefined standard strings.
540/// SID >= 391 indexes into the String INDEX (offset by 391).
541pub fn get_sid_string(sid: u16, string_index: &[Vec<u8>]) -> String {
542    if (sid as usize) < STANDARD_STRINGS.len() {
543        return STANDARD_STRINGS[sid as usize].to_string();
544    }
545    let idx = sid as usize - STANDARD_STRINGS.len();
546    if idx < string_index.len() {
547        String::from_utf8_lossy(&string_index[idx]).into_owned()
548    } else {
549        format!(".sid{sid}")
550    }
551}
552
553// ---------------------------------------------------------------------------
554// Charset Parsing
555// ---------------------------------------------------------------------------
556
557/// Parse a charset structure. GID 0 is always `.notdef`.
558fn parse_charset(
559    data: &[u8],
560    offset: usize,
561    n_glyphs: usize,
562    string_index: &[Vec<u8>],
563) -> Result<Vec<String>, String> {
564    let mut names = vec![".notdef".to_string()];
565    if n_glyphs <= 1 {
566        return Ok(names);
567    }
568
569    if offset >= data.len() {
570        return Err("charset: offset out of bounds".into());
571    }
572    let fmt = data[offset];
573    let mut pos = offset + 1;
574
575    match fmt {
576        0 => {
577            // Format 0: array of SIDs.
578            //
579            // `n_glyphs - 1` underflows usize when the font declares zero
580            // glyphs, which is a panic rather than an empty loop.
581            for _ in 0..n_glyphs.saturating_sub(1) {
582                if pos + 1 >= data.len() {
583                    break;
584                }
585                let sid = u16::from_be_bytes([data[pos], data[pos + 1]]);
586                pos += 2;
587                names.push(get_sid_string(sid, string_index));
588            }
589        }
590        1 => {
591            // Format 1: ranges with u8 nLeft
592            while names.len() < n_glyphs {
593                if pos + 2 >= data.len() {
594                    break;
595                }
596                let first_sid = u16::from_be_bytes([data[pos], data[pos + 1]]);
597                let n_left = data[pos + 2] as u16;
598                pos += 3;
599                // `first_sid + n_left` is a u16 add on two file-supplied
600                // values: a range starting near 0xFFFF overflows it. Compute
601                // the bound in u32 so the range is simply clipped at the SID
602                // space instead of wrapping (or panicking under overflow
603                // checks, which is how the fuzzer found this).
604                let last_sid = u32::from(first_sid) + u32::from(n_left);
605                for sid in u32::from(first_sid)..=last_sid.min(u32::from(u16::MAX)) {
606                    if names.len() >= n_glyphs {
607                        break;
608                    }
609                    names.push(get_sid_string(sid as u16, string_index));
610                }
611            }
612        }
613        2 => {
614            // Format 2: ranges with u16 nLeft
615            while names.len() < n_glyphs {
616                if pos + 3 >= data.len() {
617                    break;
618                }
619                let first_sid = u16::from_be_bytes([data[pos], data[pos + 1]]);
620                let n_left = u16::from_be_bytes([data[pos + 2], data[pos + 3]]);
621                pos += 4;
622                // `first_sid + n_left` is a u16 add on two file-supplied
623                // values: a range starting near 0xFFFF overflows it. Compute
624                // the bound in u32 so the range is simply clipped at the SID
625                // space instead of wrapping (or panicking under overflow
626                // checks, which is how the fuzzer found this).
627                let last_sid = u32::from(first_sid) + u32::from(n_left);
628                for sid in u32::from(first_sid)..=last_sid.min(u32::from(u16::MAX)) {
629                    if names.len() >= n_glyphs {
630                        break;
631                    }
632                    names.push(get_sid_string(sid as u16, string_index));
633                }
634            }
635        }
636        _ => return Err(format!("Unknown charset format: {fmt}")),
637    }
638
639    Ok(names)
640}
641
642/// Build a CID→GID reverse mapping from a CID-keyed CFF charset.
643/// In CID fonts, charset values are CID values. GID 0 always maps to CID 0.
644/// Returns a Vec where index = CID and value = GID.
645fn build_cid_to_gid(data: &[u8], offset: usize, n_glyphs: usize) -> Result<Vec<u16>, String> {
646    // Parse charset to get GID→CID pairs
647    let mut gid_to_cid: Vec<u16> = vec![0]; // GID 0 → CID 0
648    if n_glyphs <= 1 || offset >= data.len() {
649        return Ok(Vec::new());
650    }
651    let fmt = data[offset];
652    let mut pos = offset + 1;
653    match fmt {
654        0 => {
655            // `n_glyphs - 1` underflows usize when the font declares zero
656            // glyphs, which is a panic rather than an empty loop.
657            for _ in 0..n_glyphs.saturating_sub(1) {
658                if pos + 1 >= data.len() {
659                    break;
660                }
661                let cid = u16::from_be_bytes([data[pos], data[pos + 1]]);
662                pos += 2;
663                gid_to_cid.push(cid);
664            }
665        }
666        1 => {
667            while gid_to_cid.len() < n_glyphs {
668                if pos + 2 >= data.len() {
669                    break;
670                }
671                let first = u16::from_be_bytes([data[pos], data[pos + 1]]);
672                let n_left = data[pos + 2] as u16;
673                pos += 3;
674                // Same unchecked u16 range end as the charset parser above.
675                let last = u32::from(first) + u32::from(n_left);
676                for cid in u32::from(first)..=last.min(u32::from(u16::MAX)) {
677                    if gid_to_cid.len() >= n_glyphs {
678                        break;
679                    }
680                    gid_to_cid.push(cid as u16);
681                }
682            }
683        }
684        2 => {
685            while gid_to_cid.len() < n_glyphs {
686                if pos + 3 >= data.len() {
687                    break;
688                }
689                let first = u16::from_be_bytes([data[pos], data[pos + 1]]);
690                let n_left = u16::from_be_bytes([data[pos + 2], data[pos + 3]]);
691                pos += 4;
692                // Same unchecked u16 range end as the charset parser above.
693                let last = u32::from(first) + u32::from(n_left);
694                for cid in u32::from(first)..=last.min(u32::from(u16::MAX)) {
695                    if gid_to_cid.len() >= n_glyphs {
696                        break;
697                    }
698                    gid_to_cid.push(cid as u16);
699                }
700            }
701        }
702        _ => return Err(format!("Unknown charset format: {fmt}")),
703    }
704
705    // Find max CID to size the reverse map
706    let max_cid = gid_to_cid.iter().copied().max().unwrap_or(0) as usize;
707    let mut cid_to_gid = vec![0xFFFF_u16; max_cid + 1];
708    for (gid, &cid) in gid_to_cid.iter().enumerate() {
709        let cid_idx = cid as usize;
710        if cid_idx < cid_to_gid.len() {
711            cid_to_gid[cid_idx] = gid as u16;
712        }
713    }
714    Ok(cid_to_gid)
715}
716
717/// Return glyph names for a predefined charset ID.
718fn get_predefined_charset(
719    charset_id: i32,
720    n_glyphs: usize,
721    string_index: &[Vec<u8>],
722) -> Vec<String> {
723    let sids: &[u16] = match charset_id {
724        0 => &ISO_ADOBE_CHARSET,
725        1 => &EXPERT_CHARSET,
726        2 => &EXPERT_SUBSET_CHARSET,
727        _ => {
728            let mut names = vec![".notdef".to_string()];
729            for i in 1..n_glyphs {
730                names.push(format!(".gid{i}"));
731            }
732            return names;
733        }
734    };
735
736    let mut names = vec![".notdef".to_string()];
737    for &sid in sids.iter() {
738        if names.len() >= n_glyphs {
739            break;
740        }
741        names.push(get_sid_string(sid, string_index));
742    }
743    while names.len() < n_glyphs {
744        names.push(format!(".gid{}", names.len()));
745    }
746    names
747}
748
749// ---------------------------------------------------------------------------
750// Encoding Parsing
751// ---------------------------------------------------------------------------
752
753/// Parse an encoding structure. Returns 256-element Vec (code → GID).
754fn parse_encoding(
755    data: &[u8],
756    offset: usize,
757    charset: &[String],
758    string_index: &[Vec<u8>],
759) -> Result<Vec<u16>, String> {
760    let mut encoding = vec![0u16; 256];
761
762    // Build name→GID lookup
763    let name_to_gid: std::collections::HashMap<&str, u16> = charset
764        .iter()
765        .enumerate()
766        .map(|(gid, name)| (name.as_str(), gid as u16))
767        .collect();
768
769    if offset >= data.len() {
770        return Err("encoding: offset out of bounds".into());
771    }
772    let raw_format = data[offset];
773    let fmt = raw_format & 0x7F;
774    let has_supplement = (raw_format & 0x80) != 0;
775    let mut pos = offset + 1;
776
777    match fmt {
778        0 => {
779            if pos >= data.len() {
780                return Ok(encoding);
781            }
782            let n_codes = data[pos] as usize;
783            pos += 1;
784            for gid_minus_1 in 0..n_codes {
785                if pos >= data.len() {
786                    break;
787                }
788                let code = data[pos] as usize;
789                pos += 1;
790                let gid = (gid_minus_1 + 1) as u16;
791                if code < 256 {
792                    encoding[code] = gid;
793                }
794            }
795        }
796        1 => {
797            if pos >= data.len() {
798                return Ok(encoding);
799            }
800            let n_ranges = data[pos] as usize;
801            pos += 1;
802            let mut gid: u16 = 1;
803            for _ in 0..n_ranges {
804                if pos + 1 >= data.len() {
805                    break;
806                }
807                let first_code = data[pos] as usize;
808                let n_left = data[pos + 1] as usize;
809                pos += 2;
810                for off in 0..=n_left {
811                    let code = first_code + off;
812                    if code < 256 {
813                        encoding[code] = gid;
814                    }
815                    gid += 1;
816                }
817            }
818        }
819        _ => return Err(format!("Unknown encoding format: {fmt}")),
820    }
821
822    // Supplemental encoding
823    if has_supplement && pos < data.len() {
824        let n_sups = data[pos] as usize;
825        pos += 1;
826        for _ in 0..n_sups {
827            if pos + 2 >= data.len() {
828                break;
829            }
830            let code = data[pos] as usize;
831            let sid = u16::from_be_bytes([data[pos + 1], data[pos + 2]]);
832            pos += 3;
833            let name = get_sid_string(sid, string_index);
834            let gid = name_to_gid.get(name.as_str()).copied().unwrap_or(0);
835            if code < 256 {
836                encoding[code] = gid;
837            }
838        }
839    }
840
841    Ok(encoding)
842}
843
844/// Build encoding for predefined encoding IDs (0=Standard, 1=Expert).
845fn get_predefined_encoding(
846    encoding_id: i32,
847    charset: &[String],
848    string_index: &[Vec<u8>],
849) -> Vec<u16> {
850    let mut encoding = vec![0u16; 256];
851
852    let enc_map: &[(u8, u16)] = match encoding_id {
853        0 => &STANDARD_ENCODING_MAP,
854        1 => &EXPERT_ENCODING_MAP,
855        _ => return encoding,
856    };
857
858    // Build name→GID from charset
859    let name_to_gid: std::collections::HashMap<&str, u16> = charset
860        .iter()
861        .enumerate()
862        .map(|(gid, name)| (name.as_str(), gid as u16))
863        .collect();
864
865    // Map: code → SID → name → GID
866    for &(code, sid) in enc_map {
867        let name = get_sid_string(sid, string_index);
868        let gid = name_to_gid.get(name.as_str()).copied().unwrap_or(0);
869        encoding[code as usize] = gid;
870    }
871
872    encoding
873}
874
875// ---------------------------------------------------------------------------
876// FDSelect Parsing (CID fonts)
877// ---------------------------------------------------------------------------
878
879/// Parse FDSelect structure. Returns GID-indexed list of FD indices.
880fn parse_fd_select(data: &[u8], offset: usize, n_glyphs: usize) -> Result<Vec<u8>, String> {
881    if offset >= data.len() {
882        return Err("FDSelect: offset out of bounds".into());
883    }
884    let fmt = data[offset];
885    let mut pos = offset + 1;
886
887    match fmt {
888        0 => {
889            // Format 0: one byte per glyph
890            if pos + n_glyphs > data.len() {
891                return Err("FDSelect format 0: truncated data".into());
892            }
893            Ok(data[pos..pos + n_glyphs].to_vec())
894        }
895        3 => {
896            // Format 3: ranges
897            if pos + 1 >= data.len() {
898                return Err("FDSelect format 3: truncated".into());
899            }
900            let n_ranges = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
901            pos += 2;
902            let mut fd_select = vec![0u8; n_glyphs];
903
904            for i in 0..n_ranges {
905                if pos + 2 >= data.len() {
906                    break;
907                }
908                let first_gid = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
909                let fd = data[pos + 2];
910                pos += 3;
911
912                let next_first = if i + 1 < n_ranges && pos + 1 < data.len() {
913                    u16::from_be_bytes([data[pos], data[pos + 1]]) as usize
914                } else if pos + 1 < data.len() {
915                    // Sentinel
916                    u16::from_be_bytes([data[pos], data[pos + 1]]) as usize
917                } else {
918                    n_glyphs
919                };
920
921                for item in fd_select
922                    .iter_mut()
923                    .take(next_first.min(n_glyphs))
924                    .skip(first_gid)
925                {
926                    *item = fd;
927                }
928            }
929
930            Ok(fd_select)
931        }
932        _ => Err(format!("Unknown FDSelect format: {fmt}")),
933    }
934}
935
936// ---------------------------------------------------------------------------
937// Standard Strings (SID 0..390) — CFF Specification Appendix A
938// ---------------------------------------------------------------------------
939
940#[rustfmt::skip]
941const STANDARD_STRINGS: [&str; 391] = [
942    // SID 0-9
943    ".notdef", "space", "exclam", "quotedbl", "numbersign",
944    "dollar", "percent", "ampersand", "quoteright", "parenleft",
945    // SID 10-19
946    "parenright", "asterisk", "plus", "comma", "hyphen",
947    "period", "slash", "zero", "one", "two",
948    // SID 20-29
949    "three", "four", "five", "six", "seven",
950    "eight", "nine", "colon", "semicolon", "less",
951    // SID 30-39
952    "equal", "greater", "question", "at", "A",
953    "B", "C", "D", "E", "F",
954    // SID 40-49
955    "G", "H", "I", "J", "K",
956    "L", "M", "N", "O", "P",
957    // SID 50-59
958    "Q", "R", "S", "T", "U",
959    "V", "W", "X", "Y", "Z",
960    // SID 60-69
961    "bracketleft", "backslash", "bracketright", "asciicircum", "underscore",
962    "quoteleft", "a", "b", "c", "d",
963    // SID 70-79
964    "e", "f", "g", "h", "i",
965    "j", "k", "l", "m", "n",
966    // SID 80-89
967    "o", "p", "q", "r", "s",
968    "t", "u", "v", "w", "x",
969    // SID 90-99
970    "y", "z", "braceleft", "bar", "braceright",
971    "asciitilde", "exclamdown", "cent", "sterling", "fraction",
972    // SID 100-109
973    "yen", "florin", "section", "currency", "quotesingle",
974    "quotedblleft", "guillemotleft", "guilsinglleft", "guilsinglright", "fi",
975    // SID 110-119
976    "fl", "endash", "dagger", "daggerdbl", "periodcentered",
977    "paragraph", "bullet", "quotesinglbase", "quotedblbase", "quotedblright",
978    // SID 120-129
979    "guillemotright", "ellipsis", "perthousand", "questiondown", "grave",
980    "acute", "circumflex", "tilde", "macron", "breve",
981    // SID 130-139
982    "dotaccent", "dieresis", "ring", "cedilla", "hungarumlaut",
983    "ogonek", "caron", "emdash", "AE", "ordfeminine",
984    // SID 140-149
985    "Lslash", "Oslash", "OE", "ordmasculine", "ae",
986    "dotlessi", "lslash", "oslash", "oe", "germandbls",
987    // SID 150-159
988    "onesuperior", "logicalnot", "mu", "trademark", "Eth",
989    "onehalf", "plusminus", "Thorn", "onequarter", "divide",
990    // SID 160-169
991    "brokenbar", "degree", "thorn", "threequarters", "twosuperior",
992    "registered", "minus", "eth", "multiply", "threesuperior",
993    // SID 170-179
994    "copyright", "Aacute", "Acircumflex", "Adieresis", "Agrave",
995    "Aring", "Atilde", "Ccedilla", "Eacute", "Ecircumflex",
996    // SID 180-189
997    "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis",
998    "Igrave", "Ntilde", "Oacute", "Ocircumflex", "Odieresis",
999    // SID 190-199
1000    "Ograve", "Otilde", "Scaron", "Uacute", "Ucircumflex",
1001    "Udieresis", "Ugrave", "Yacute", "Ydieresis", "Zcaron",
1002    // SID 200-209
1003    "aacute", "acircumflex", "adieresis", "agrave", "aring",
1004    "atilde", "ccedilla", "eacute", "ecircumflex", "edieresis",
1005    // SID 210-219
1006    "egrave", "iacute", "icircumflex", "idieresis", "igrave",
1007    "ntilde", "oacute", "ocircumflex", "odieresis", "ograve",
1008    // SID 220-229
1009    "otilde", "scaron", "uacute", "ucircumflex", "udieresis",
1010    "ugrave", "yacute", "ydieresis", "zcaron", "exclamsmall",
1011    // SID 230-239
1012    "Hungarumlautsmall", "dollaroldstyle", "dollarsuperior", "ampersandsmall",
1013    "Acutesmall", "parenleftsuperior", "parenrightsuperior", "twodotenleader",
1014    "onedotenleader", "zerooldstyle",
1015    // SID 240-249
1016    "oneoldstyle", "twooldstyle", "threeoldstyle", "fouroldstyle",
1017    "fiveoldstyle", "sixoldstyle", "sevenoldstyle", "eightoldstyle",
1018    "nineoldstyle", "commasuperior",
1019    // SID 250-259
1020    "threequartersemdash", "periodsuperior", "questionsmall", "asuperior",
1021    "bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior",
1022    "lsuperior",
1023    // SID 260-269
1024    "msuperior", "nsuperior", "osuperior", "rsuperior", "ssuperior",
1025    "tsuperior", "ff", "ffi", "ffl", "parenleftinferior",
1026    // SID 270-279
1027    "parenrightinferior", "Circumflexsmall", "hyphensuperior", "Gravesmall",
1028    "Asmall", "Bsmall", "Csmall", "Dsmall", "Esmall", "Fsmall",
1029    // SID 280-289
1030    "Gsmall", "Hsmall", "Ismall", "Jsmall", "Ksmall",
1031    "Lsmall", "Msmall", "Nsmall", "Osmall", "Psmall",
1032    // SID 290-299
1033    "Qsmall", "Rsmall", "Ssmall", "Tsmall", "Usmall",
1034    "Vsmall", "Wsmall", "Xsmall", "Ysmall", "Zsmall",
1035    // SID 300-309
1036    "colonmonetary", "onefitted", "rupiah", "Tildesmall", "exclamdownsmall",
1037    "centoldstyle", "Lslashsmall", "Scaronsmall", "Zcaronsmall", "Dieresissmall",
1038    // SID 310-319
1039    "Brevesmall", "Caronsmall", "Dotaccentsmall", "Macronsmall", "figuredash",
1040    "hypheninferior", "Ogoneksmall", "Ringsmall", "Cedillasmall", "questiondownsmall",
1041    // SID 320-329
1042    "oneeighth", "threeeighths", "fiveeighths", "seveneighths", "onethird",
1043    "twothirds", "zerosuperior", "foursuperior", "fivesuperior", "sixsuperior",
1044    // SID 330-339
1045    "sevensuperior", "eightsuperior", "ninesuperior", "zeroinferior", "oneinferior",
1046    "twoinferior", "threeinferior", "fourinferior", "fiveinferior", "sixinferior",
1047    // SID 340-349
1048    "seveninferior", "eightinferior", "nineinferior", "centinferior", "dollarinferior",
1049    "periodinferior", "commainferior", "Agravesmall", "Aacutesmall", "Acircumflexsmall",
1050    // SID 350-359
1051    "Atildesmall", "Adieresissmall", "Aringsmall", "AEsmall", "Ccedillasmall",
1052    "Egravesmall", "Eacutesmall", "Ecircumflexsmall", "Edieresissmall", "Igravesmall",
1053    // SID 360-369
1054    "Iacutesmall", "Icircumflexsmall", "Idieresissmall", "Ethsmall", "Ntildesmall",
1055    "Ogravesmall", "Oacutesmall", "Ocircumflexsmall", "Otildesmall", "Odieresissmall",
1056    // SID 370-379
1057    "OEsmall", "Oslashsmall", "Ugravesmall", "Uacutesmall", "Ucircumflexsmall",
1058    "Udieresissmall", "Yacutesmall", "Thornsmall", "Ydieresissmall",
1059    "001.000", "001.001",
1060    // SID 380-390
1061    "001.002", "001.003", "Black", "Bold", "Book",
1062    "Light", "Medium", "Regular", "Roman", "Semibold",
1063];
1064
1065// ---------------------------------------------------------------------------
1066// Predefined Charsets
1067// ---------------------------------------------------------------------------
1068
1069/// ISOAdobe charset (charset ID 0) — SIDs for GID 1..228
1070#[rustfmt::skip]
1071const ISO_ADOBE_CHARSET: [u16; 228] = [
1072    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
1073    21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
1074    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
1075    61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
1076    81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100,
1077    101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
1078    121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140,
1079    141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160,
1080    161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180,
1081    181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200,
1082    201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220,
1083    221, 222, 223, 224, 225, 226, 227, 228,
1084];
1085
1086/// Expert charset (charset ID 1) — SIDs for GID 1..165
1087#[rustfmt::skip]
1088const EXPERT_CHARSET: [u16; 165] = [
1089    1, 229, 230, 231, 232, 233, 234, 235, 236, 237,
1090    238, 13, 14, 15, 99, 239, 240, 241, 242, 243,
1091    244, 245, 246, 247, 248, 27, 28, 249, 250, 251,
1092    252, 253, 254, 255, 256, 257, 258, 259, 260, 261,
1093    262, 263, 264, 265, 266, 109, 110, 267, 268, 269,
1094    270, 271, 272, 273, 274, 275, 276, 277, 278, 279,
1095    280, 281, 282, 283, 284, 285, 286, 287, 288, 289,
1096    290, 291, 292, 293, 294, 295, 296, 297, 298, 299,
1097    300, 301, 302, 303, 304, 305, 306, 307, 308, 309,
1098    310, 311, 312, 313, 314, 315, 316, 317, 318, 158,
1099    155, 163, 319, 320, 321, 322, 323, 324, 325, 326,
1100    150, 164, 169, 327, 328, 329, 330, 331, 332, 333,
1101    334, 335, 336, 337, 338, 339, 340, 341, 342, 343,
1102    344, 345, 346, 347, 348, 349, 350, 351, 352, 353,
1103    354, 355, 356, 357, 358, 359, 360, 361, 362, 363,
1104    364, 365, 366, 367, 368, 369, 370, 371, 372, 373,
1105    374, 375, 376, 377, 378,
1106];
1107
1108/// ExpertSubset charset (charset ID 2) — SIDs for GID 1..86
1109#[rustfmt::skip]
1110const EXPERT_SUBSET_CHARSET: [u16; 86] = [
1111    1, 231, 232, 235, 236, 237, 238, 13, 14, 15,
1112    99, 239, 240, 241, 242, 243, 244, 245, 246, 247,
1113    248, 27, 28, 249, 250, 251, 253, 254, 255, 256,
1114    257, 258, 259, 260, 261, 262, 263, 264, 265, 266,
1115    109, 110, 267, 268, 269, 270, 272, 300, 301, 302,
1116    305, 314, 315, 158, 155, 163, 320, 321, 322, 323,
1117    324, 325, 326, 150, 164, 169, 327, 328, 329, 330,
1118    331, 332, 333, 334, 335, 336, 337, 338, 339, 340,
1119    341, 342, 343, 344, 345, 346,
1120];
1121
1122// ---------------------------------------------------------------------------
1123// Predefined Encodings
1124// ---------------------------------------------------------------------------
1125
1126/// Standard Encoding — (code, SID) pairs for non-zero entries.
1127#[rustfmt::skip]
1128const STANDARD_ENCODING_MAP: [(u8, u16); 149] = [
1129    (32, 1), (33, 2), (34, 3), (35, 4), (36, 5), (37, 6), (38, 7), (39, 8),
1130    (40, 9), (41, 10), (42, 11), (43, 12), (44, 13), (45, 14), (46, 15), (47, 16),
1131    (48, 17), (49, 18), (50, 19), (51, 20), (52, 21), (53, 22), (54, 23), (55, 24),
1132    (56, 25), (57, 26), (58, 27), (59, 28), (60, 29), (61, 30), (62, 31), (63, 32),
1133    (64, 33), (65, 34), (66, 35), (67, 36), (68, 37), (69, 38), (70, 39), (71, 40),
1134    (72, 41), (73, 42), (74, 43), (75, 44), (76, 45), (77, 46), (78, 47), (79, 48),
1135    (80, 49), (81, 50), (82, 51), (83, 52), (84, 53), (85, 54), (86, 55), (87, 56),
1136    (88, 57), (89, 58), (90, 59), (91, 60), (92, 61), (93, 62), (94, 63), (95, 64),
1137    (96, 65), (97, 66), (98, 67), (99, 68), (100, 69), (101, 70), (102, 71),
1138    (103, 72), (104, 73), (105, 74), (106, 75), (107, 76), (108, 77), (109, 78),
1139    (110, 79), (111, 80), (112, 81), (113, 82), (114, 83), (115, 84), (116, 85),
1140    (117, 86), (118, 87), (119, 88), (120, 89), (121, 90), (122, 91), (123, 92),
1141    (124, 93), (125, 94), (126, 95),
1142    (161, 96), (162, 97), (163, 98), (164, 99), (165, 100), (166, 101),
1143    (167, 102), (168, 103), (169, 104), (170, 105), (171, 106), (172, 107),
1144    (173, 108), (174, 109), (175, 110), (177, 111), (178, 112), (179, 113),
1145    (180, 114), (182, 115), (183, 116), (184, 117), (185, 118), (186, 119),
1146    (187, 120), (188, 121), (189, 122), (191, 123), (193, 124), (194, 125),
1147    (195, 126), (196, 127), (197, 128), (198, 129), (199, 130), (200, 131),
1148    (202, 132), (203, 133), (205, 134), (206, 135), (207, 136), (208, 137),
1149    (225, 138), (227, 139), (232, 140), (233, 141), (234, 142), (235, 143),
1150    (241, 144), (245, 145), (248, 146), (249, 147), (250, 148), (251, 149),
1151];
1152
1153/// Expert Encoding — (code, SID) pairs for non-zero entries.
1154#[rustfmt::skip]
1155pub const EXPERT_ENCODING_MAP: [(u8, u16); 165] = [
1156    (32, 1), (33, 229), (34, 230), (36, 231), (37, 232), (38, 233), (39, 234),
1157    (40, 235), (41, 236), (42, 237), (43, 238), (44, 13), (45, 14), (46, 15),
1158    (47, 99), (48, 239), (49, 240), (50, 241), (51, 242), (52, 243), (53, 244),
1159    (54, 245), (55, 246), (56, 247), (57, 248), (58, 27), (59, 28), (60, 249),
1160    (61, 250), (62, 251), (63, 252), (64, 253), (65, 254), (66, 255), (67, 256),
1161    (68, 257), (69, 258), (70, 259), (71, 260), (72, 261), (73, 262), (74, 263),
1162    (75, 264), (76, 265), (77, 266), (78, 109), (79, 110), (80, 267), (81, 268),
1163    (82, 269), (83, 270), (84, 271), (85, 272), (86, 273), (87, 274), (88, 275),
1164    (89, 276), (90, 277), (91, 278), (92, 279), (93, 280), (94, 281), (95, 282),
1165    (96, 283), (97, 284), (98, 285), (99, 286), (100, 287), (101, 288), (102, 289),
1166    (103, 290), (104, 291), (105, 292), (106, 293), (107, 294), (108, 295),
1167    (109, 296), (110, 297), (111, 298), (112, 299), (113, 300), (114, 301),
1168    (115, 302), (116, 303), (117, 304), (118, 305), (119, 306), (120, 307),
1169    (121, 308), (122, 309), (123, 310), (124, 311), (125, 312), (126, 313),
1170    (161, 314), (162, 315), (163, 316), (164, 317), (165, 318), (166, 158),
1171    (167, 155), (168, 163), (169, 319), (170, 320), (171, 321), (172, 322),
1172    (173, 323), (174, 324), (175, 325), (176, 326), (177, 150), (178, 164),
1173    (179, 169), (180, 327), (181, 328), (182, 329), (183, 330), (184, 331),
1174    (185, 332), (186, 333), (187, 334), (188, 335), (189, 336), (190, 337),
1175    (191, 338), (192, 339), (193, 340), (194, 341), (195, 342), (196, 343),
1176    (197, 344), (198, 345), (199, 346), (200, 347), (201, 348), (202, 349),
1177    (203, 350), (204, 351), (205, 352), (206, 353), (207, 354), (208, 355),
1178    (209, 356), (210, 357), (211, 358), (212, 359), (213, 360), (214, 361),
1179    (215, 362), (216, 363), (217, 364), (218, 365), (219, 366), (220, 367),
1180    (221, 368), (222, 369), (223, 370), (224, 371), (225, 372), (226, 373),
1181    (227, 374), (228, 375), (229, 376), (230, 377), (231, 378),
1182];
1183
1184#[cfg(test)]
1185mod tests {
1186    use super::*;
1187
1188    #[test]
1189    fn test_sid_resolution() {
1190        let string_index = vec![b"CustomGlyph".to_vec()];
1191        assert_eq!(get_sid_string(0, &string_index), ".notdef");
1192        assert_eq!(get_sid_string(34, &string_index), "A");
1193        assert_eq!(get_sid_string(391, &string_index), "CustomGlyph");
1194        assert_eq!(get_sid_string(999, &string_index), ".sid999");
1195    }
1196
1197    #[test]
1198    fn test_dict_number_encoding() {
1199        // 32-246 range: value = b0 - 139
1200        let data = [139u8, 15]; // operand 0, then operator 15 (charset)
1201        let entries = parse_dict_data(&data);
1202        assert_eq!(entries.len(), 1);
1203        assert_eq!(entries[0].operands, vec![0.0]);
1204
1205        // 247-250 range
1206        let data = [247u8, 0, 15]; // (247-247)*256 + 0 + 108 = 108
1207        let entries = parse_dict_data(&data);
1208        assert_eq!(entries[0].operands, vec![108.0]);
1209
1210        // 251-254 range
1211        let data = [251u8, 0, 15]; // -(251-251)*256 - 0 - 108 = -108
1212        let entries = parse_dict_data(&data);
1213        assert_eq!(entries[0].operands, vec![-108.0]);
1214    }
1215
1216    #[test]
1217    fn test_empty_index() {
1218        // count = 0
1219        let data = [0u8, 0];
1220        let (items, off) = parse_index(&data, 0).unwrap();
1221        assert!(items.is_empty());
1222        assert_eq!(off, 2);
1223    }
1224
1225    #[test]
1226    fn test_predefined_charset_iso_adobe() {
1227        let names = get_predefined_charset(0, 5, &[]);
1228        assert_eq!(names[0], ".notdef");
1229        assert_eq!(names[1], "space");
1230        assert_eq!(names[2], "exclam");
1231        assert_eq!(names.len(), 5);
1232    }
1233}