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