Skip to main content

stet_core/
system_font_loader.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! System font loading: TTF/OTF → PostScript font dictionaries.
6//!
7//! Loads TrueType (.ttf) and OpenType (.otf) fonts from the filesystem,
8//! constructing Type 42 font dicts (for TrueType outlines) or delegating
9//! to the CFF parser (for CFF outlines).
10
11use std::collections::HashMap;
12use std::path::Path;
13
14use crate::cff_parser;
15use crate::context::Context;
16use crate::dict::DictKey;
17use crate::object::{ObjFlags, PsObject, PsValue};
18use crate::system_fonts::{
19    extract_ps_name_from_name_table, parse_cmap_table, parse_post_table, unicode_to_glyph_name,
20};
21use crate::truetype::{find_table, get_units_per_em, read_i16};
22
23/// Load an OTF file with CFF outlines.
24///
25/// Extracts the CFF table, parses it, and registers fonts via the CFF pipeline.
26/// Returns true on success.
27pub fn load_otf_cff(ctx: &mut Context, path: &Path) -> bool {
28    let data = match std::fs::read(path) {
29        Ok(d) => d,
30        Err(_) => return false,
31    };
32
33    // Find CFF table
34    let (cff_offset, cff_length) = match find_table(&data, b"CFF ") {
35        Some(t) => t,
36        None => return false,
37    };
38
39    if cff_offset + cff_length > data.len() {
40        return false;
41    }
42
43    let cff_data = &data[cff_offset..cff_offset + cff_length];
44
45    let cff_fonts = match cff_parser::parse_cff(cff_data) {
46        Ok(f) => f,
47        Err(_) => return false,
48    };
49
50    // Register in global VM
51    let saved_vm_mode = ctx.vm_alloc_mode;
52    ctx.vm_alloc_mode = true;
53
54    // Store raw CFF binary for PDF embedding
55    let sl = ctx.save_stack.current_level();
56    let cs = ctx.save_stack.last_save_id();
57    let cff_str_entity = ctx.strings.allocate_from_with(cff_data, sl, true, cs);
58    let cff_data_len = cff_data.len() as u32;
59
60    let mut success = true;
61    for cff_font in &cff_fonts {
62        if register_cff_font(ctx, cff_font, cff_str_entity, cff_data_len).is_err() {
63            success = false;
64            break;
65        }
66    }
67
68    ctx.vm_alloc_mode = saved_vm_mode;
69    success
70}
71
72/// Load a TrueType font and register it as a Type 42 PostScript font.
73///
74/// Returns true on success.
75pub fn load_ttf(ctx: &mut Context, path: &Path) -> bool {
76    let data = match std::fs::read(path) {
77        Ok(d) => d,
78        Err(_) => return false,
79    };
80
81    load_ttf_from_data(ctx, &data)
82}
83
84/// Load a TrueType font from raw data and register as Type 42.
85pub fn load_ttf_from_data(ctx: &mut Context, data: &[u8]) -> bool {
86    if data.len() < 12 {
87        return false;
88    }
89
90    // Extract font name from name table
91    let font_name = match extract_ps_name_from_name_table(data) {
92        Some(n) => n,
93        None => return false,
94    };
95
96    // Parse head table for unitsPerEm and bbox
97    let units_per_em = get_units_per_em(data);
98    let em_scale = 1.0 / units_per_em as f64;
99
100    let (x_min, y_min, x_max, y_max) = get_font_bbox(data);
101    let bbox = [
102        x_min as f64 * em_scale,
103        y_min as f64 * em_scale,
104        x_max as f64 * em_scale,
105        y_max as f64 * em_scale,
106    ];
107
108    // Parse cmap for Unicode→GID mapping
109    let unicode_to_gid = match parse_cmap_table(data) {
110        Some(m) => m,
111        None => return false,
112    };
113
114    // Parse post table for GID→name mapping (optional)
115    let gid_to_name = parse_post_table(data);
116
117    // Build glyph name resolution: GID → name
118    // Priority: post table → AGL reverse → uniXXXX
119    let resolve_glyph_name = |gid: u16, unicode: Option<u32>| -> String {
120        // Try post table first
121        if let Some(ref post_map) = gid_to_name
122            && let Some(name) = post_map.get(&gid)
123        {
124            return name.clone();
125        }
126        // Try AGL reverse mapping
127        if let Some(cp) = unicode {
128            if let Some(name) = unicode_to_glyph_name(cp) {
129                return name.to_string();
130            }
131            return format!("uni{:04X}", cp);
132        }
133        format!("glyph{}", gid)
134    };
135
136    // Build Encoding (256 entries) and CharStrings
137    let mut encoding = vec![".notdef".to_string(); 256];
138    let mut charstrings: HashMap<String, u16> = HashMap::new();
139    charstrings.insert(".notdef".to_string(), 0);
140
141    // Map Unicode 0-255 to Encoding slots
142    for code in 0u32..256 {
143        if let Some(&gid) = unicode_to_gid.get(&code) {
144            let name = resolve_glyph_name(gid, Some(code));
145            encoding[code as usize] = name.clone();
146            charstrings.insert(name, gid);
147        }
148    }
149
150    // Add all other mapped glyphs to CharStrings (for glyphshow)
151    for (&unicode, &gid) in &unicode_to_gid {
152        if unicode >= 256 {
153            let name = resolve_glyph_name(gid, Some(unicode));
154            charstrings.entry(name).or_insert(gid);
155        }
156    }
157
158    // Register in global VM
159    let saved_vm_mode = ctx.vm_alloc_mode;
160    ctx.vm_alloc_mode = true;
161
162    let success = build_type42_font(ctx, &font_name, &bbox, &encoding, &charstrings, data);
163
164    ctx.vm_alloc_mode = saved_vm_mode;
165    success
166}
167
168/// Build and register a Type 42 font dictionary.
169/// All allocations use global VM (PLRM: fonts always in global VM).
170fn build_type42_font(
171    ctx: &mut Context,
172    font_name: &str,
173    bbox: &[f64; 4],
174    encoding: &[String],
175    charstrings: &HashMap<String, u16>,
176    font_data: &[u8],
177) -> bool {
178    let sl = ctx.save_stack.current_level();
179    let cs = ctx.save_stack.last_save_id();
180
181    let font_entity = ctx.dicts.allocate_with(16, b"Type42", sl, true, cs);
182
183    // FontType = 42
184    ctx.dicts.put(
185        font_entity,
186        DictKey::Name(ctx.name_cache.n_font_type),
187        PsObject::int(42),
188    );
189
190    // FontName
191    let font_name_id = ctx.names.intern(font_name.as_bytes());
192    ctx.dicts.put(
193        font_entity,
194        DictKey::Name(ctx.name_cache.n_font_name),
195        PsObject::name_lit(font_name_id),
196    );
197
198    // FontMatrix = [1 0 0 1 0 0] (identity — unitsPerEm handled by renderer)
199    let fm_items = [
200        PsObject::real(1.0),
201        PsObject::real(0.0),
202        PsObject::real(0.0),
203        PsObject::real(1.0),
204        PsObject::real(0.0),
205        PsObject::real(0.0),
206    ];
207    let fm_entity = ctx.arrays.allocate_from_with(&fm_items, sl, true, cs);
208    ctx.dicts.put(
209        font_entity,
210        DictKey::Name(ctx.name_cache.n_font_matrix),
211        PsObject::array(fm_entity, 6),
212    );
213
214    // FontBBox
215    let bb_items = [
216        PsObject::real(bbox[0]),
217        PsObject::real(bbox[1]),
218        PsObject::real(bbox[2]),
219        PsObject::real(bbox[3]),
220    ];
221    let bb_entity = ctx.arrays.allocate_from_with(&bb_items, sl, true, cs);
222    ctx.dicts.put(
223        font_entity,
224        DictKey::Name(ctx.name_cache.n_font_bbox),
225        PsObject::array(bb_entity, 4),
226    );
227
228    // PaintType = 0
229    ctx.dicts.put(
230        font_entity,
231        DictKey::Name(ctx.name_cache.n_paint_type),
232        PsObject::int(0),
233    );
234
235    // Encoding — 256-element array of name objects
236    let enc_entity = ctx.arrays.allocate_with(256, sl, true, cs);
237    for (i, name) in encoding.iter().enumerate() {
238        let name_id = ctx.names.intern(name.as_bytes());
239        ctx.arrays
240            .set_element(enc_entity, i as u32, PsObject::name_lit(name_id));
241    }
242    ctx.dicts.put(
243        font_entity,
244        DictKey::Name(ctx.name_cache.n_encoding),
245        PsObject::array(enc_entity, 256),
246    );
247
248    // CharStrings — dict mapping glyph name → GID (integer)
249    let cs_entity =
250        ctx.dicts
251            .allocate_with(charstrings.len().max(10), b"CharStrings", sl, true, cs);
252    for (name, &gid) in charstrings {
253        let name_id = ctx.names.intern(name.as_bytes());
254        ctx.dicts
255            .put(cs_entity, DictKey::Name(name_id), PsObject::int(gid as i32));
256    }
257    ctx.dicts.put(
258        font_entity,
259        DictKey::Name(ctx.name_cache.n_char_strings),
260        PsObject::dict(cs_entity),
261    );
262
263    // sfnts — single-element array containing the entire font file as a string
264    let sfnts_entity = ctx.arrays.allocate_with(1, sl, true, cs);
265    let str_entity = ctx.strings.allocate_from_with(font_data, sl, true, cs);
266    ctx.arrays.set_element(
267        sfnts_entity,
268        0,
269        PsObject::string(str_entity, font_data.len() as u32),
270    );
271    let sfnts_key = DictKey::Name(ctx.names.intern(b"sfnts"));
272    ctx.dicts
273        .put(font_entity, sfnts_key, PsObject::array(sfnts_entity, 1));
274
275    // _unitsPerEm (internal, for renderer)
276    let upem_key = DictKey::Name(ctx.names.intern(b"_unitsPerEm"));
277    let upem = get_units_per_em(font_data);
278    ctx.dicts
279        .put(font_entity, upem_key, PsObject::int(upem as i32));
280
281    // FID
282    let fid = ctx.next_fid;
283    ctx.next_fid += 1;
284    ctx.dicts.put(
285        font_entity,
286        DictKey::Name(ctx.name_cache.n_fid),
287        PsObject {
288            value: PsValue::FontID(fid),
289            flags: ObjFlags::literal(),
290        },
291    );
292
293    // Register via definefont if available, otherwise direct FontDirectory insert
294    let font_dict_obj = PsObject::dict(font_entity);
295
296    let definefont_id = ctx.names.intern(b"definefont");
297    let key = DictKey::Name(definefont_id);
298    if let Some(op_obj) = ctx.dict_load(&key)
299        && let PsValue::Operator(opcode) = op_obj.value
300    {
301        if ctx.o_stack.push(PsObject::name_lit(font_name_id)).is_err() {
302            return false;
303        }
304        if ctx.o_stack.push(font_dict_obj).is_err() {
305            return false;
306        }
307        let func = ctx.operators[opcode.0 as usize].func;
308        if func(ctx).is_err() {
309            return false;
310        }
311        let _ = ctx.o_stack.pop();
312    } else {
313        // Fallback: register directly in FontDirectory
314        ctx.dicts.put(
315            ctx.font_directory,
316            DictKey::Name(font_name_id),
317            font_dict_obj,
318        );
319    }
320
321    true
322}
323
324/// Extract font bounding box from the head table.
325fn get_font_bbox(font_data: &[u8]) -> (i16, i16, i16, i16) {
326    if let Some((head_off, _)) = find_table(font_data, b"head")
327        && head_off + 54 <= font_data.len()
328    {
329        let x_min = read_i16(font_data, head_off + 36);
330        let y_min = read_i16(font_data, head_off + 38);
331        let x_max = read_i16(font_data, head_off + 40);
332        let y_max = read_i16(font_data, head_off + 42);
333        return (x_min, y_min, x_max, y_max);
334    }
335    (0, 0, 1000, 1000)
336}
337
338/// Register a CFF font (reused from cff_ops pattern).
339/// All allocations use global VM (PLRM: fonts always in global VM).
340fn register_cff_font(
341    ctx: &mut Context,
342    cff_font: &cff_parser::CffFont,
343    cff_str_entity: crate::object::EntityId,
344    cff_data_len: u32,
345) -> Result<(), crate::error::PsError> {
346    let sl = ctx.save_stack.current_level();
347    let cs = ctx.save_stack.last_save_id();
348
349    let font_entity = ctx.dicts.allocate_with(16, b"CFF", sl, true, cs);
350
351    // FontType = 2
352    ctx.dicts.put(
353        font_entity,
354        DictKey::Name(ctx.name_cache.n_font_type),
355        PsObject::int(2),
356    );
357
358    // FontName
359    let font_name_id = ctx.names.intern(cff_font.name.as_bytes());
360    ctx.dicts.put(
361        font_entity,
362        DictKey::Name(ctx.name_cache.n_font_name),
363        PsObject::name_lit(font_name_id),
364    );
365
366    // FontMatrix
367    let fm_entity = ctx.arrays.allocate_with(6, sl, true, cs);
368    for (i, &val) in cff_font.font_matrix.iter().enumerate() {
369        ctx.arrays
370            .set_element(fm_entity, i as u32, PsObject::real(val));
371    }
372    ctx.dicts.put(
373        font_entity,
374        DictKey::Name(ctx.name_cache.n_font_matrix),
375        PsObject::array(fm_entity, 6),
376    );
377
378    // FontBBox
379    let bbox_entity = ctx.arrays.allocate_with(4, sl, true, cs);
380    for (i, &val) in cff_font.font_bbox.iter().enumerate() {
381        ctx.arrays
382            .set_element(bbox_entity, i as u32, PsObject::real(val));
383    }
384    ctx.dicts.put(
385        font_entity,
386        DictKey::Name(ctx.name_cache.n_font_bbox),
387        PsObject::array(bbox_entity, 4),
388    );
389
390    // Encoding
391    let enc_entity = ctx.arrays.allocate_with(256, sl, true, cs);
392    for code in 0..256u32 {
393        let gid = cff_font.encoding[code as usize] as usize;
394        let glyph_name = if gid < cff_font.charset.len() {
395            &cff_font.charset[gid]
396        } else {
397            ".notdef"
398        };
399        let name_id = ctx.names.intern(glyph_name.as_bytes());
400        ctx.arrays
401            .set_element(enc_entity, code, PsObject::name_lit(name_id));
402    }
403    ctx.dicts.put(
404        font_entity,
405        DictKey::Name(ctx.name_cache.n_encoding),
406        PsObject::array(enc_entity, 256),
407    );
408
409    // CharStrings
410    let cs_entity = ctx.dicts.allocate_with(16, b"CFF", sl, true, cs);
411    for (gid, cs_data) in cff_font.char_strings.iter().enumerate() {
412        let str_entity = ctx.strings.allocate_from_with(cs_data, sl, true, cs);
413        let cs_obj = PsObject::string(str_entity, cs_data.len() as u32);
414
415        if gid < cff_font.charset.len() {
416            let glyph_name = &cff_font.charset[gid];
417            let name_id = ctx.names.intern(glyph_name.as_bytes());
418            ctx.dicts.put(cs_entity, DictKey::Name(name_id), cs_obj);
419        }
420
421        if cff_font.is_cid {
422            ctx.dicts.put(cs_entity, DictKey::Int(gid as i64), cs_obj);
423        }
424    }
425    ctx.dicts.put(
426        font_entity,
427        DictKey::Name(ctx.name_cache.n_char_strings),
428        PsObject::dict(cs_entity),
429    );
430
431    // Private dictionary
432    let priv_entity = ctx.dicts.allocate_with(16, b"CFF", sl, true, cs);
433    ctx.dicts.put(
434        priv_entity,
435        DictKey::Name(ctx.names.intern(b"defaultWidthX")),
436        PsObject::real(cff_font.default_width_x),
437    );
438    ctx.dicts.put(
439        priv_entity,
440        DictKey::Name(ctx.names.intern(b"nominalWidthX")),
441        PsObject::real(cff_font.nominal_width_x),
442    );
443
444    if !cff_font.local_subrs.is_empty() {
445        let subrs_len = cff_font.local_subrs.len() as u32;
446        let subrs_entity = ctx.arrays.allocate_with(subrs_len as usize, sl, true, cs);
447        for (i, subr_data) in cff_font.local_subrs.iter().enumerate() {
448            let str_entity = ctx.strings.allocate_from_with(subr_data, sl, true, cs);
449            ctx.arrays.set_element(
450                subrs_entity,
451                i as u32,
452                PsObject::string(str_entity, subr_data.len() as u32),
453            );
454        }
455        ctx.dicts.put(
456            priv_entity,
457            DictKey::Name(ctx.name_cache.n_subrs),
458            PsObject {
459                value: PsValue::Array {
460                    entity: subrs_entity,
461                    start: 0,
462                    len: subrs_len,
463                },
464                flags: ObjFlags::literal_composite(),
465            },
466        );
467    }
468    ctx.dicts.put(
469        font_entity,
470        DictKey::Name(ctx.name_cache.n_private),
471        PsObject::dict(priv_entity),
472    );
473
474    // Global subroutines
475    if !cff_font.global_subrs.is_empty() {
476        let gs_key = DictKey::Name(ctx.names.intern(b"_cff_global_subrs"));
477        let gs_len = cff_font.global_subrs.len() as u32;
478        let gs_entity = ctx.arrays.allocate_with(gs_len as usize, sl, true, cs);
479        for (i, subr_data) in cff_font.global_subrs.iter().enumerate() {
480            let str_entity = ctx.strings.allocate_from_with(subr_data, sl, true, cs);
481            ctx.arrays.set_element(
482                gs_entity,
483                i as u32,
484                PsObject::string(str_entity, subr_data.len() as u32),
485            );
486        }
487        ctx.dicts.put(
488            font_entity,
489            gs_key,
490            PsObject {
491                value: PsValue::Array {
492                    entity: gs_entity,
493                    start: 0,
494                    len: gs_len,
495                },
496                flags: ObjFlags::literal_composite(),
497            },
498        );
499    }
500
501    // Store raw CFF binary for PDF embedding
502    let cff_data_key = DictKey::Name(ctx.names.intern(b"_CFFData"));
503    ctx.dicts.put(
504        font_entity,
505        cff_data_key,
506        PsObject::string(cff_str_entity, cff_data_len),
507    );
508
509    // FID
510    let fid = ctx.next_fid;
511    ctx.next_fid += 1;
512    ctx.dicts.put(
513        font_entity,
514        DictKey::Name(ctx.name_cache.n_fid),
515        PsObject {
516            value: PsValue::FontID(fid),
517            flags: ObjFlags::literal(),
518        },
519    );
520
521    // Register via definefont if available, otherwise direct FontDirectory insert
522    let font_dict_obj = PsObject::dict(font_entity);
523
524    let definefont_id = ctx.names.intern(b"definefont");
525    let key = DictKey::Name(definefont_id);
526    if let Some(op_obj) = ctx.dict_load(&key)
527        && let PsValue::Operator(opcode) = op_obj.value
528    {
529        ctx.o_stack.push(PsObject::name_lit(font_name_id))?;
530        ctx.o_stack.push(font_dict_obj)?;
531        let func = ctx.operators[opcode.0 as usize].func;
532        func(ctx)?;
533        let _ = ctx.o_stack.pop();
534    } else {
535        ctx.dicts.put(
536            ctx.font_directory,
537            DictKey::Name(font_name_id),
538            font_dict_obj,
539        );
540    }
541
542    Ok(())
543}
544
545/// Detect whether a font file contains TrueType or CFF outlines.
546pub fn is_cff_font(data: &[u8]) -> bool {
547    data.len() >= 4 && &data[0..4] == b"OTTO"
548}
549
550/// Load a binary font file (TTF or OTF), auto-detecting the format.
551pub fn load_binary_font(ctx: &mut Context, path: &Path) -> bool {
552    let data = match std::fs::read(path) {
553        Ok(d) => d,
554        Err(_) => return false,
555    };
556
557    if data.len() < 4 {
558        return false;
559    }
560
561    if is_cff_font(&data) {
562        // OTF with CFF outlines
563        load_otf_cff(ctx, path)
564    } else {
565        // TrueType outlines (TTF or OTF with glyf table)
566        load_ttf_from_data(ctx, &data)
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn test_is_cff_font() {
576        assert!(is_cff_font(b"OTTO\x00\x00\x00\x00"));
577        assert!(!is_cff_font(b"\x00\x01\x00\x00"));
578        assert!(!is_cff_font(b"tru"));
579    }
580
581    #[test]
582    fn test_load_system_ttf() {
583        // Try to load a real system TrueType font
584        let test_fonts = [
585            "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
586            "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
587        ];
588        for path_str in &test_fonts {
589            let path = Path::new(path_str);
590            if path.exists() {
591                let mut ctx = Context::new();
592                let ok = load_ttf(&mut ctx, path);
593                assert!(ok, "Failed to load {}", path_str);
594
595                // Verify font was registered in FontDirectory
596                let name = extract_ps_name_from_name_table(&std::fs::read(path).unwrap());
597                if let Some(font_name) = name {
598                    let name_id = ctx.names.intern(font_name.as_bytes());
599                    let key = DictKey::Name(name_id);
600                    assert!(
601                        ctx.dicts.get(ctx.font_directory, &key).is_some(),
602                        "Font '{}' not in FontDirectory",
603                        font_name
604                    );
605                }
606                return;
607            }
608        }
609        eprintln!("Skipping TTF loading test — no test font found");
610    }
611
612    #[test]
613    fn test_load_system_otf() {
614        // Try to load a real OTF with CFF
615        let test_fonts = ["/usr/share/fonts/opentype/urw-base35/C059-Italic.otf"];
616        for path_str in &test_fonts {
617            let path = Path::new(path_str);
618            if path.exists() {
619                let mut ctx = Context::new();
620                let ok = load_otf_cff(&mut ctx, path);
621                assert!(ok, "Failed to load CFF from {}", path_str);
622                return;
623            }
624        }
625        eprintln!("Skipping OTF loading test — no test font found");
626    }
627}