Skip to main content

stet_core/
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//! Font loading pipeline.
6//!
7//! Loads Type 1 font files from disk, parses them, and creates PostScript
8//! font dictionary objects. Includes font name substitution table.
9
10use crate::context::Context;
11use crate::dict::DictKey;
12use crate::object::{PsObject, PsValue};
13
14// Re-export font substitution data from stet-fonts for backward compatibility.
15pub use stet_fonts::{FONT_SUBSTITUTIONS, find_substitution};
16
17/// Try to load a font by name, using the substitution table.
18/// Checks embedded files first (for WASM), then falls back to disk.
19pub fn load_font_file(
20    font_name: &str,
21    resource_path: &str,
22    files: &crate::file_store::FileStore,
23) -> Option<Vec<u8>> {
24    // Try embedded files first (for WASM builds)
25    let direct_path = format!("{}/{}.t1", resource_path, font_name);
26    if let Some(data) = files.get_embedded_file(&direct_path) {
27        return Some(data.to_vec());
28    }
29    if let Some(urw_name) = find_substitution(font_name) {
30        let sub_path = format!("{}/{}.t1", resource_path, urw_name);
31        if let Some(data) = files.get_embedded_file(&sub_path) {
32            return Some(data.to_vec());
33        }
34    }
35
36    // Fall back to disk I/O
37    if let Ok(data) = std::fs::read(&direct_path) {
38        return Some(data);
39    }
40    if let Some(urw_name) = find_substitution(font_name) {
41        let sub_path = format!("{}/{}.t1", resource_path, urw_name);
42        if let Ok(data) = std::fs::read(&sub_path) {
43            return Some(data);
44        }
45    }
46
47    None
48}
49
50/// Load a Type 1 font from data and register it as a PostScript dict in the context.
51/// Returns the font dict PsObject.
52pub fn load_type1_font(ctx: &mut Context, font_data: &[u8]) -> Result<PsObject, String> {
53    let font = crate::type1_parser::parse_type1(font_data)?;
54
55    let save_level = ctx.save_stack.current_level();
56    let global = ctx.vm_alloc_mode;
57    let created = ctx.save_stack.last_save_id();
58
59    // Create the font dictionary (respects current VM allocation mode)
60    let font_dict =
61        ctx.dicts
62            .allocate_with(30, font.font_name.as_bytes(), save_level, global, created);
63
64    // /FontName → name object
65    let name_id = ctx.names.intern(font.font_name.as_bytes());
66    ctx.dicts.put(
67        font_dict,
68        DictKey::Name(ctx.name_cache.n_font_name),
69        PsObject::name_lit(name_id),
70    );
71
72    // /FontType → Int(1)
73    ctx.dicts.put(
74        font_dict,
75        DictKey::Name(ctx.name_cache.n_font_type),
76        PsObject::int(1),
77    );
78
79    // /FontMatrix → array of 6 reals
80    let fm_items: Vec<PsObject> = font
81        .font_matrix
82        .iter()
83        .map(|&v| PsObject::real(v))
84        .collect();
85    let fm_entity = ctx
86        .arrays
87        .allocate_from_with(&fm_items, save_level, global, created);
88    ctx.dicts.put(
89        font_dict,
90        DictKey::Name(ctx.name_cache.n_font_matrix),
91        PsObject::array(fm_entity, 6),
92    );
93
94    // /FontBBox → array of 4 reals
95    let bb_items: Vec<PsObject> = font.font_bbox.iter().map(|&v| PsObject::real(v)).collect();
96    let bb_entity = ctx
97        .arrays
98        .allocate_from_with(&bb_items, save_level, global, created);
99    ctx.dicts.put(
100        font_dict,
101        DictKey::Name(ctx.name_cache.n_font_bbox),
102        PsObject::array(bb_entity, 4),
103    );
104
105    // /PaintType → Int
106    ctx.dicts.put(
107        font_dict,
108        DictKey::Name(ctx.name_cache.n_paint_type),
109        PsObject::int(font.paint_type),
110    );
111
112    // /Encoding → array of 256 name objects
113    let enc_items: Vec<PsObject> = font
114        .encoding
115        .iter()
116        .map(|name| {
117            let id = ctx.names.intern(name.as_bytes());
118            PsObject::name_lit(id)
119        })
120        .collect();
121    let enc_entity = ctx
122        .arrays
123        .allocate_from_with(&enc_items, save_level, global, created);
124    ctx.dicts.put(
125        font_dict,
126        DictKey::Name(ctx.name_cache.n_encoding),
127        PsObject::array(enc_entity, 256),
128    );
129
130    // /CharStrings → dict mapping glyph names to string objects (encrypted bytes)
131    let cs_dict = ctx.dicts.allocate_with(
132        font.charstrings.len().max(10),
133        b"CharStrings",
134        save_level,
135        global,
136        created,
137    );
138    for (glyph_name, bytes) in &font.charstrings {
139        let glyph_name_id = ctx.names.intern(glyph_name.as_bytes());
140        let str_entity = ctx
141            .strings
142            .allocate_from_with(bytes, save_level, global, created);
143        let str_obj = PsObject::string(str_entity, bytes.len() as u32);
144        ctx.dicts
145            .put(cs_dict, DictKey::Name(glyph_name_id), str_obj);
146    }
147    ctx.dicts.put(
148        font_dict,
149        DictKey::Name(ctx.name_cache.n_char_strings),
150        PsObject::dict(cs_dict),
151    );
152
153    // /Private → dict with lenIV and Subrs (standard Type 1 structure)
154    let priv_dict = ctx
155        .dicts
156        .allocate_with(10, b"Private", save_level, global, created);
157    ctx.dicts.put(
158        priv_dict,
159        DictKey::Name(ctx.name_cache.n_len_iv),
160        PsObject::int(font.len_iv as i32),
161    );
162
163    // /Subrs → array of string objects (encrypted subroutine bytes) inside Private
164    let subr_items: Vec<PsObject> = font
165        .subrs
166        .iter()
167        .map(|bytes| {
168            let entity = ctx
169                .strings
170                .allocate_from_with(bytes, save_level, global, created);
171            PsObject::string(entity, bytes.len() as u32)
172        })
173        .collect();
174    let subrs_entity = ctx
175        .arrays
176        .allocate_from_with(&subr_items, save_level, global, created);
177    ctx.dicts.put(
178        priv_dict,
179        DictKey::Name(ctx.name_cache.n_subrs),
180        PsObject::array(subrs_entity, font.subrs.len() as u32),
181    );
182
183    ctx.dicts.put(
184        font_dict,
185        DictKey::Name(ctx.name_cache.n_private),
186        PsObject::dict(priv_dict),
187    );
188
189    // /FID → unique font ID
190    let fid = ctx.next_fid;
191    ctx.next_fid += 1;
192    ctx.dicts.put(
193        font_dict,
194        DictKey::Name(ctx.name_cache.n_fid),
195        PsObject::int(fid),
196    );
197
198    // Register in FontDirectory under the font name
199    let font_obj = PsObject::dict(font_dict);
200    let font_directory = ctx.font_directory;
201    ctx.dict_put_cow(font_directory, DictKey::Name(name_id), font_obj);
202
203    Ok(font_obj)
204}
205
206/// Try to find a font by name: check FontDirectory first, then load from disk.
207pub fn find_font(ctx: &mut Context, name_bytes: &[u8]) -> Result<PsObject, String> {
208    let name_id = ctx.names.intern(name_bytes);
209    let key = DictKey::Name(name_id);
210
211    // Check FontDirectory
212    if let Some(font_obj) = ctx.dicts.get(ctx.font_directory, &key) {
213        return Ok(font_obj);
214    }
215
216    // Try to load from disk
217    let font_name = String::from_utf8_lossy(name_bytes);
218    let resource_path = ctx
219        .font_resource_path
220        .clone()
221        .ok_or_else(|| format!("Font '{}' not found and no resource path set", font_name))?;
222
223    let font_data = load_font_file(&font_name, &resource_path, &ctx.files)
224        .ok_or_else(|| format!("Font '{}' not found in {}", font_name, resource_path))?;
225
226    let font_obj = load_type1_font(ctx, &font_data)?;
227
228    // The font may have been registered under a different name (the actual font name
229    // from the .t1 file). We need to also register under the requested name.
230    let actual_name = get_font_name(ctx, font_obj);
231    if actual_name.as_deref() != Some(&*font_name) {
232        let font_directory = ctx.font_directory;
233        ctx.dict_put_cow(font_directory, key, font_obj);
234    }
235
236    Ok(font_obj)
237}
238
239/// Extract /FontName from a font dict object.
240fn get_font_name(ctx: &Context, font_obj: PsObject) -> Option<String> {
241    if let PsValue::Dict(entity) = font_obj.value
242        && let Some(name_obj) = ctx
243            .dicts
244            .get(entity, &DictKey::Name(ctx.name_cache.n_font_name))
245        && let PsValue::Name(id) = name_obj.value
246    {
247        return Some(String::from_utf8_lossy(ctx.names.get_bytes(id)).to_string());
248    }
249    None
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_font_substitution() {
258        assert_eq!(find_substitution("Helvetica"), Some("NimbusSans-Regular"));
259        assert_eq!(
260            find_substitution("Times-Roman"),
261            Some("NimbusRoman-Regular")
262        );
263        assert_eq!(find_substitution("Courier"), Some("NimbusMonoPS-Regular"));
264        assert_eq!(find_substitution("Symbol"), Some("StandardSymbolsPS"));
265        assert_eq!(find_substitution("NoSuchFont"), None);
266    }
267
268    #[test]
269    fn test_load_real_font() {
270        let font_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
271            .join("../../resources/Font/NimbusSans-Regular.t1");
272        if !font_path.exists() {
273            eprintln!("Skipping test — font file not found");
274            return;
275        }
276
277        let mut ctx = Context::new();
278        let font_data = std::fs::read(&font_path).unwrap();
279        let font_obj = load_type1_font(&mut ctx, &font_data).unwrap();
280
281        // Verify it's a dict
282        assert!(matches!(font_obj.value, PsValue::Dict(_)));
283
284        // Verify FontName
285        if let PsValue::Dict(entity) = font_obj.value {
286            let name_obj = ctx
287                .dicts
288                .get(entity, &DictKey::Name(ctx.name_cache.n_font_name))
289                .unwrap();
290            if let PsValue::Name(id) = name_obj.value {
291                assert_eq!(ctx.names.get_bytes(id), b"NimbusSans-Regular");
292            }
293
294            // Verify FontType = 1
295            let type_obj = ctx
296                .dicts
297                .get(entity, &DictKey::Name(ctx.name_cache.n_font_type))
298                .unwrap();
299            assert_eq!(type_obj.as_i32(), Some(1));
300
301            // Verify CharStrings is a dict with entries
302            let cs_obj = ctx
303                .dicts
304                .get(entity, &DictKey::Name(ctx.name_cache.n_char_strings))
305                .unwrap();
306            if let PsValue::Dict(cs_entity) = cs_obj.value {
307                assert!(ctx.dicts.length(cs_entity) > 100);
308            } else {
309                panic!("CharStrings should be a dict");
310            }
311        }
312
313        // Verify it's registered in FontDirectory
314        let name_id = ctx.names.intern(b"NimbusSans-Regular");
315        assert!(
316            ctx.dicts
317                .get(ctx.font_directory, &DictKey::Name(name_id))
318                .is_some()
319        );
320    }
321}