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}