use std::collections::HashMap;
use crate::{ZplError, ZplResult};
use ab_glyph::{Font, FontArc, PxScale, ScaleFont};
const MONO_FONT_BYTES: &[u8] = include_bytes!("../assets/IosevkaTermSlab-Regular.ttf");
const SANS_FONT_BYTES: &[u8] = include_bytes!("../assets/TeXGyreHerosCn-Bold.otf");
const OCR_A_FONT_BYTES: &[u8] = include_bytes!("../assets/OCRA.ttf");
const OCR_B_FONT_BYTES: &[u8] = include_bytes!("../assets/OCRB.otf");
const FONT_MAP: &[char] = &[
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
];
const SCALABLE_CAP_RATIO: f32 = 0.75;
const DEFAULT_FONT_HEIGHT: u32 = 9;
const FALLBACK_CAP_RATIO: f32 = 0.7;
struct BitmapCell {
base_h: f32,
base_w: f32,
cell_w: f32,
baseline: f32,
}
fn bitmap_cell(font_char: char) -> Option<BitmapCell> {
let (base_h, base_w, cell_w, baseline) = match font_char {
'A' => (9.0, 5.0, 6.0, 7.0),
'B' => (11.0, 7.0, 9.0, 11.0),
'C' | 'D' => (18.0, 10.0, 12.0, 14.0),
'E' => (28.0, 15.0, 20.0, 23.0),
'F' => (26.0, 13.0, 16.0, 21.0),
'G' => (60.0, 40.0, 48.0, 48.0),
'H' => (21.0, 13.0, 19.0, 21.0),
_ => return None,
};
Some(BitmapCell {
base_h,
base_w,
cell_w,
baseline,
})
}
#[derive(Debug, Clone, Copy)]
struct FontMetrics {
units_per_em: f32,
height_unscaled: f32,
cap_height: f32,
advance: f32,
}
impl FontMetrics {
fn from_font(font: &FontArc) -> Self {
let units_per_em = font.units_per_em().unwrap_or(1000.0);
let cap_height = font
.outline(font.glyph_id('H'))
.map(|o| o.bounds.min.y.max(o.bounds.max.y))
.filter(|&v| v > 0.0)
.unwrap_or(units_per_em * FALLBACK_CAP_RATIO);
let advance = font.h_advance_unscaled(font.glyph_id('0'));
let advance = if advance > 0.0 {
advance
} else {
units_per_em * 0.5
};
Self {
units_per_em,
height_unscaled: font.height_unscaled(),
cap_height,
advance,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct TextLayout {
pub px: PxScale,
pub baseline: f32,
pub em_x: f32,
pub em_y: f32,
pub cell_h: f32,
}
pub(crate) fn interpretation_metrics(module_width: u32) -> (u32, u32) {
let module = module_width.max(1) as f32;
let text_h = (module * 36.0 / 5.0 / SCALABLE_CAP_RATIO).round() as u32;
let gap = ((module * 1.2).round() as u32).max(1);
(text_h, gap)
}
#[derive(Debug, Clone)]
pub struct FontManager {
font_map: HashMap<String, String>,
font_index: HashMap<String, FontArc>,
font_bytes: HashMap<String, Vec<u8>>,
font_metrics: HashMap<String, FontMetrics>,
}
impl Default for FontManager {
fn default() -> Self {
let mut current = Self {
font_map: HashMap::new(),
font_index: HashMap::new(),
font_bytes: HashMap::new(),
font_metrics: HashMap::new(),
};
let _ = current.register_font("Iosevka Term Slab", MONO_FONT_BYTES, 'A', 'Z');
let _ = current.register_font("TeX Gyre Heros Cn", SANS_FONT_BYTES, '0', '9');
let _ = current.register_font("OCR-A", OCR_A_FONT_BYTES, 'H', 'H');
let _ = current.register_font("OCR-B", OCR_B_FONT_BYTES, 'E', 'E');
current
}
}
impl FontManager {
pub fn get_font_bytes(&self, name: &str) -> Option<&[u8]> {
let font_name = self.font_map.get(name)?;
self.font_bytes.get(font_name).map(|v| v.as_slice())
}
pub fn get_font_name(&self, name: &str) -> Option<&str> {
self.font_map.get(name).map(|s| s.as_str())
}
pub fn get_font(&self, name: &str) -> Option<&FontArc> {
let font_name = self.font_map.get(name);
if let Some(font_name) = font_name {
self.font_index.get(font_name)
} else {
None
}
}
pub(crate) fn text_layout(
&self,
font_char: char,
height: Option<u32>,
width: Option<u32>,
) -> Option<(&FontArc, TextLayout)> {
let mut buf = [0; 4];
let key = font_char.encode_utf8(&mut buf);
let name = self.font_map.get(key).or_else(|| self.font_map.get("0"))?;
let font = self.font_index.get(name)?;
let metrics = self
.font_metrics
.get(name)
.copied()
.unwrap_or_else(|| FontMetrics::from_font(font));
let h = height.unwrap_or(DEFAULT_FONT_HEIGHT).max(1) as f32;
let (em_x, em_y, baseline, cell_h) = if let Some(cell) = bitmap_cell(font_char) {
let mag_h = (h / cell.base_h).round().max(1.0);
let mag_w = match width {
Some(w) if w > 0 => (w as f32 / cell.base_w).round().max(1.0),
_ => mag_h,
};
let cap_px = cell.baseline * mag_h;
let advance_px = cell.cell_w * mag_w;
let em_y = cap_px * metrics.units_per_em / metrics.cap_height;
let em_x = advance_px * metrics.units_per_em / metrics.advance;
(em_x, em_y, cap_px, cell.base_h * mag_h)
} else {
let cap_px = SCALABLE_CAP_RATIO * h;
let em_y = cap_px * metrics.units_per_em / metrics.cap_height;
let em_x = match width {
Some(w) if w > 0 => em_y * w as f32 / h,
_ => em_y,
};
(em_x, em_y, cap_px, h)
};
let px = PxScale {
x: em_x * metrics.height_unscaled / metrics.units_per_em,
y: em_y * metrics.height_unscaled / metrics.units_per_em,
};
Some((
font,
TextLayout {
px,
baseline,
em_x,
em_y,
cell_h,
},
))
}
pub(crate) fn measure_text(
&self,
font_char: char,
height: Option<u32>,
width: Option<u32>,
text: &str,
) -> u32 {
let Some((font, layout)) = self.text_layout(font_char, height, width) else {
return 0;
};
let scaled = font.as_scaled(layout.px);
let mut w = 0.0_f32;
let mut last = None;
for c in text.chars() {
let gid = font.glyph_id(c);
if let Some(prev) = last {
w += scaled.kern(prev, gid);
}
w += scaled.h_advance(gid);
last = Some(gid);
}
w.ceil() as u32
}
pub fn register_font(
&mut self,
name: &str,
bytes: &[u8],
from: char,
to: char,
) -> ZplResult<()> {
let font = FontArc::try_from_vec(bytes.to_vec())
.map_err(|_| ZplError::FontError("Invalid font data".into()))?;
self.font_metrics
.insert(name.to_string(), FontMetrics::from_font(&font));
self.font_index.insert(name.to_string(), font);
self.font_bytes.insert(name.to_string(), bytes.to_vec());
self.assign_font(name, from, to);
Ok(())
}
fn assign_font(&mut self, name: &str, from: char, to: char) {
let from_idx = FONT_MAP.iter().position(|&x| x == from);
let to_idx = FONT_MAP.iter().position(|&x| x == to);
if from_idx.is_none() || to_idx.is_none() {
return;
}
if let (Some(start), Some(end)) = (from_idx, to_idx)
&& start <= end
{
for key in &FONT_MAP[start..=end] {
self.font_map.insert(key.to_string(), name.to_string());
}
}
}
}