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