Skip to main content

zpl_forge/engine/
font.rs

1use std::collections::HashMap;
2
3use crate::{ZplError, ZplResult};
4use ab_glyph::{Font, FontArc, PxScale, ScaleFont};
5
6/// Default fallback font bytes embedded in the binary.
7/// This guarantees the library runs on any OS/Platform without C dependencies.
8const MONO_FONT_BYTES: &[u8] = include_bytes!("../assets/IosevkaTermSlab-Regular.ttf");
9
10/// High-fidelity default scalable sans font embedded in the binary
11/// (TeX Gyre Heros Condensed, a free Helvetica-metric clone derived from
12/// URW Nimbus Sans). Used for font identifiers '0'-'9' and standard
13/// scalable text fields.
14const SANS_FONT_BYTES: &[u8] = include_bytes!("../assets/TeXGyreHerosCn-Bold.otf");
15
16/// Default OCR-A font bytes embedded in the binary.
17/// Used for font identifier 'H'.
18const OCR_A_FONT_BYTES: &[u8] = include_bytes!("../assets/OCRA.ttf");
19
20/// Default OCR-B font bytes embedded in the binary (Schwarz/Wagner free
21/// digitization, distributed without limitation). Used for font identifier 'E'.
22const OCR_B_FONT_BYTES: &[u8] = include_bytes!("../assets/OCRB.otf");
23
24/// List of valid ZPL font identifiers (A-Z and 0-9).
25const FONT_MAP: &[char] = &[
26    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
27    'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
28];
29
30/// Zebra scalable fonts (e.g. `^A0`): capital letters span ~75% of the `^A`
31/// height with the cap top sitting exactly on the field origin. Calibrated
32/// against Labelary renders across `^A` sizes 30-190.
33const SCALABLE_CAP_RATIO: f32 = 0.75;
34
35/// Default `^A` height in dots when none was specified (ZPL font A default).
36const DEFAULT_FONT_HEIGHT: u32 = 9;
37
38/// Fallback cap height when a font has no 'H' outline, in em units.
39const FALLBACK_CAP_RATIO: f32 = 0.7;
40
41/// Geometry of a Zebra built-in bitmap font cell at 203 dpi, in dots.
42///
43/// `base_h`/`base_w` are the glyph matrix, `cell_w` is the horizontal advance
44/// (matrix width + intercharacter gap) and `baseline` is the distance from the
45/// cell top to the baseline, which equals the capital-letter height.
46/// Values from the ZPL II programming guide font matrices.
47struct BitmapCell {
48    base_h: f32,
49    base_w: f32,
50    cell_w: f32,
51    baseline: f32,
52}
53
54/// Zebra bitmap font matrices for identifiers A-H. Other identifiers are
55/// treated as scalable fonts.
56fn bitmap_cell(font_char: char) -> Option<BitmapCell> {
57    let (base_h, base_w, cell_w, baseline) = match font_char {
58        'A' => (9.0, 5.0, 6.0, 7.0),
59        'B' => (11.0, 7.0, 9.0, 11.0),
60        'C' | 'D' => (18.0, 10.0, 12.0, 14.0),
61        'E' => (28.0, 15.0, 20.0, 23.0),
62        'F' => (26.0, 13.0, 16.0, 21.0),
63        'G' => (60.0, 40.0, 48.0, 48.0),
64        'H' => (21.0, 13.0, 19.0, 21.0),
65        _ => return None,
66    };
67    Some(BitmapCell {
68        base_h,
69        base_w,
70        cell_w,
71        baseline,
72    })
73}
74
75/// Normalization metrics extracted once per registered font, in font units.
76#[derive(Debug, Clone, Copy)]
77struct FontMetrics {
78    /// Units per em.
79    units_per_em: f32,
80    /// `ascent - descent` (what an `ab_glyph::PxScale` maps to its `y` value).
81    height_unscaled: f32,
82    /// Capital-letter height, measured from the 'H' outline.
83    cap_height: f32,
84    /// Representative advance width (digit '0'), used to fit monospace cells.
85    advance: f32,
86}
87
88impl FontMetrics {
89    fn from_font(font: &FontArc) -> Self {
90        let units_per_em = font.units_per_em().unwrap_or(1000.0);
91        // Outline bounds come in screen-style order (min.y holds the glyph
92        // top in font units), so take the larger of the two y values.
93        let cap_height = font
94            .outline(font.glyph_id('H'))
95            .map(|o| o.bounds.min.y.max(o.bounds.max.y))
96            .filter(|&v| v > 0.0)
97            .unwrap_or(units_per_em * FALLBACK_CAP_RATIO);
98        let advance = font.h_advance_unscaled(font.glyph_id('0'));
99        let advance = if advance > 0.0 {
100            advance
101        } else {
102            units_per_em * 0.5
103        };
104        Self {
105            units_per_em,
106            height_unscaled: font.height_unscaled(),
107            cap_height,
108            advance,
109        }
110    }
111}
112
113/// Resolved scaling for one ZPL text field, in dots.
114///
115/// Both backends must consume this instead of building an `ab_glyph::PxScale`
116/// from the raw `^A` parameters: `PxScale` maps `ascent - descent` (not the
117/// em) to its `y` value, which shrinks glyphs and misplaces the baseline.
118#[derive(Debug, Clone, Copy)]
119pub(crate) struct TextLayout {
120    /// Rasterization scale for `ab_glyph`/`imageproc`.
121    pub px: PxScale,
122    /// Distance from the top of the ZPL character cell to the baseline.
123    /// Capital letters render from the cell top down to this line.
124    pub baseline: f32,
125    /// Em size in dots, horizontal (vector backends: PDF text matrix).
126    pub em_x: f32,
127    /// Em size in dots, vertical.
128    pub em_y: f32,
129    /// Total character-cell height in dots (rotation anchors, reverse boxes).
130    pub cell_h: f32,
131}
132
133/// Interpretation-line geometry for 1-D barcodes: `(font_height, gap)` in
134/// dots, both proportional to the module width as rendered by Labelary
135/// (digit ink measured 36 dots tall, 6 dots below the bars, at `^BY5`).
136pub(crate) fn interpretation_metrics(module_width: u32) -> (u32, u32) {
137    let module = module_width.max(1) as f32;
138    let text_h = (module * 36.0 / 5.0 / SCALABLE_CAP_RATIO).round() as u32;
139    let gap = ((module * 1.2).round() as u32).max(1);
140    (text_h, gap)
141}
142
143/// Manages fonts and their mapping to ZPL font identifiers.
144///
145/// This structure tracks registered fonts and maps them to the single-character
146/// identifiers used in ZPL commands (e.g., '^A0', '^AA').
147#[derive(Debug, Clone)]
148pub struct FontManager {
149    /// Maps ZPL font identifiers (as Strings) to internal font names.
150    font_map: HashMap<String, String>,
151    /// Stores the actual font data indexed by internal font names.
152    font_index: HashMap<String, FontArc>,
153    /// Stores the raw TTF/OTF bytes indexed by internal font names.
154    font_bytes: HashMap<String, Vec<u8>>,
155    /// Normalization metrics per internal font name, computed at registration.
156    font_metrics: HashMap<String, FontMetrics>,
157}
158
159impl Default for FontManager {
160    /// Creates a `FontManager` with high-fidelity, lightweight open-source fonts
161    /// registered for their respective identifiers.
162    ///
163    /// - TeX Gyre Heros Cn (GUST Font License) is registered for scalable/sans identifiers ('0' to '9').
164    /// - Iosevka Term Slab (SIL Open Font License) is registered for monospace/slab identifiers ('A' to 'Z').
165    /// - OCR-A is registered for OCR-A identifier ('H').
166    /// - OCR-B is registered for OCR-B identifier ('E').
167    fn default() -> Self {
168        let mut current = Self {
169            font_map: HashMap::new(),
170            font_index: HashMap::new(),
171            font_bytes: HashMap::new(),
172            font_metrics: HashMap::new(),
173        };
174
175        // Register default fonts for their respective alphanumeric ZPL identifiers
176        let _ = current.register_font("Iosevka Term Slab", MONO_FONT_BYTES, 'A', 'Z');
177        let _ = current.register_font("TeX Gyre Heros Cn", SANS_FONT_BYTES, '0', '9');
178        let _ = current.register_font("OCR-A", OCR_A_FONT_BYTES, 'H', 'H');
179        let _ = current.register_font("OCR-B", OCR_B_FONT_BYTES, 'E', 'E');
180
181        current
182    }
183}
184
185impl FontManager {
186    /// Retrieves the raw TTF/OTF bytes for a font by its ZPL identifier.
187    ///
188    /// This is used by backends that need the raw font data (e.g., PDF embedding).
189    pub fn get_font_bytes(&self, name: &str) -> Option<&[u8]> {
190        let font_name = self.font_map.get(name)?;
191        self.font_bytes.get(font_name).map(|v| v.as_slice())
192    }
193
194    /// Returns the internal font name mapped to a ZPL identifier.
195    pub fn get_font_name(&self, name: &str) -> Option<&str> {
196        self.font_map.get(name).map(|s| s.as_str())
197    }
198
199    /// Retrieves a font by its ZPL identifier.
200    ///
201    /// # Arguments
202    /// * `name` - The ZPL font identifier (e.g., "0", "A").
203    pub fn get_font(&self, name: &str) -> Option<&FontArc> {
204        let font_name = self.font_map.get(name);
205        if let Some(font_name) = font_name {
206            self.font_index.get(font_name)
207        } else {
208            None
209        }
210    }
211
212    /// Resolves a ZPL font identifier (falling back to font '0') and computes
213    /// the Zebra-calibrated [`TextLayout`] for the given `^A` height/width.
214    ///
215    /// Bitmap identifiers (A-H) use integer cell magnification like real
216    /// printers; every other identifier uses the scalable-font model.
217    pub(crate) fn text_layout(
218        &self,
219        font_char: char,
220        height: Option<u32>,
221        width: Option<u32>,
222    ) -> Option<(&FontArc, TextLayout)> {
223        let mut buf = [0; 4];
224        let key = font_char.encode_utf8(&mut buf);
225        let name = self.font_map.get(key).or_else(|| self.font_map.get("0"))?;
226        let font = self.font_index.get(name)?;
227        let metrics = self
228            .font_metrics
229            .get(name)
230            .copied()
231            .unwrap_or_else(|| FontMetrics::from_font(font));
232
233        let h = height.unwrap_or(DEFAULT_FONT_HEIGHT).max(1) as f32;
234
235        let (em_x, em_y, baseline, cell_h) = if let Some(cell) = bitmap_cell(font_char) {
236            // Bitmap fonts magnify a fixed dot matrix by integer factors.
237            let mag_h = (h / cell.base_h).round().max(1.0);
238            let mag_w = match width {
239                Some(w) if w > 0 => (w as f32 / cell.base_w).round().max(1.0),
240                _ => mag_h,
241            };
242            let cap_px = cell.baseline * mag_h;
243            let advance_px = cell.cell_w * mag_w;
244            let em_y = cap_px * metrics.units_per_em / metrics.cap_height;
245            let em_x = advance_px * metrics.units_per_em / metrics.advance;
246            (em_x, em_y, cap_px, cell.base_h * mag_h)
247        } else {
248            // Scalable fonts: caps span SCALABLE_CAP_RATIO of the ^A height.
249            let cap_px = SCALABLE_CAP_RATIO * h;
250            let em_y = cap_px * metrics.units_per_em / metrics.cap_height;
251            let em_x = match width {
252                Some(w) if w > 0 => em_y * w as f32 / h,
253                _ => em_y,
254            };
255            (em_x, em_y, cap_px, h)
256        };
257
258        // ab_glyph's PxScale maps (ascent - descent) to its value, so convert
259        // the em sizes through the font's own vertical extent.
260        let px = PxScale {
261            x: em_x * metrics.height_unscaled / metrics.units_per_em,
262            y: em_y * metrics.height_unscaled / metrics.units_per_em,
263        };
264
265        Some((
266            font,
267            TextLayout {
268                px,
269                baseline,
270                em_x,
271                em_y,
272                cell_h,
273            },
274        ))
275    }
276
277    /// Measures the advance width of `text` in dots for the given `^A` spec.
278    /// Single source of truth for every backend and for `^FB` wrapping.
279    pub(crate) fn measure_text(
280        &self,
281        font_char: char,
282        height: Option<u32>,
283        width: Option<u32>,
284        text: &str,
285    ) -> u32 {
286        let Some((font, layout)) = self.text_layout(font_char, height, width) else {
287            return 0;
288        };
289        let scaled = font.as_scaled(layout.px);
290        let mut w = 0.0_f32;
291        let mut last = None;
292        for c in text.chars() {
293            let gid = font.glyph_id(c);
294            if let Some(prev) = last {
295                w += scaled.kern(prev, gid);
296            }
297            w += scaled.h_advance(gid);
298            last = Some(gid);
299        }
300        w.ceil() as u32
301    }
302
303    /// Registers a new font and maps it to a range of ZPL identifiers.
304    ///
305    /// Custom fonts must be in TrueType (`.ttf`) or OpenType (`.otf`) format.
306    /// Once registered, the font can be used in ZPL commands like `^A` or `^CF`
307    /// by referencing the assigned identifiers.
308    ///
309    /// # Arguments
310    /// * `name` - An internal name for the font.
311    /// * `bytes` - The raw TrueType/OpenType font data.
312    /// * `from` - The starting ZPL identifier in the range (A-Z, 0-9).
313    /// * `to` - The ending ZPL identifier in the range (A-Z, 0-9).
314    ///
315    /// # Errors
316    /// Returns an error if the font data is invalid.
317    ///
318    /// # Example
319    ///
320    /// ```rust
321    /// use zpl_forge::FontManager;
322    ///
323    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
324    /// let mut font_manager = FontManager::default();
325    ///
326    /// // Load your font file bytes
327    /// // let font_bytes = std::fs::read("fonts/IosevkaTermSlab-Regular.ttf")?;
328    ///
329    /// // Register it for a range of ZPL identifiers (e.g., from 'A' to 'Z')
330    /// // font_manager.register_font("Iosevka Term Slab", &font_bytes, 'A', 'Z')?;
331    /// # Ok(())
332    /// # }
333    /// ```
334    pub fn register_font(
335        &mut self,
336        name: &str,
337        bytes: &[u8],
338        from: char,
339        to: char,
340    ) -> ZplResult<()> {
341        let font = FontArc::try_from_vec(bytes.to_vec())
342            .map_err(|_| ZplError::FontError("Invalid font data".into()))?;
343        self.font_metrics
344            .insert(name.to_string(), FontMetrics::from_font(&font));
345        self.font_index.insert(name.to_string(), font);
346        self.font_bytes.insert(name.to_string(), bytes.to_vec());
347        self.assign_font(name, from, to);
348        Ok(())
349    }
350
351    /// Internal helper to assign a registered font to a range of ZPL identifiers.
352    fn assign_font(&mut self, name: &str, from: char, to: char) {
353        let from_idx = FONT_MAP.iter().position(|&x| x == from);
354        let to_idx = FONT_MAP.iter().position(|&x| x == to);
355
356        if from_idx.is_none() || to_idx.is_none() {
357            return;
358        }
359
360        if let (Some(start), Some(end)) = (from_idx, to_idx)
361            && start <= end
362        {
363            for key in &FONT_MAP[start..=end] {
364                self.font_map.insert(key.to_string(), name.to_string());
365            }
366        }
367    }
368}