use yog_data::Schema;
use yog_graphics::Op;
#[derive(Schema, Debug, Clone, PartialEq, Default)]
pub struct Font {
pub units_per_em: u32,
pub ascent: i32,
pub descent: i32,
#[data(default)]
pub line_gap: i32,
pub glyphs: Vec<Glyph>,
pub chars: Vec<CharGlyph>,
#[data(default)]
pub ligatures: Vec<Ligature>,
#[data(default)]
pub machines: Vec<Machine>,
#[data(default)]
pub kerning: Vec<Kern>,
pub fit: Option<Fit>,
}
#[derive(Schema, Debug, Clone, PartialEq, Default)]
pub struct Glyph {
#[data(default)]
pub name: String,
#[data(default)]
pub advance: i32,
#[data(default)]
pub ops: Vec<Op>,
pub entry: Option<[i32; 2]>,
pub exit: Option<[i32; 2]>,
}
#[derive(Schema, Debug, Clone, Copy, PartialEq)]
pub struct CharGlyph {
pub char: u32,
pub glyph: u32,
}
#[derive(Schema, Debug, Clone, PartialEq)]
pub struct Ligature {
pub glyphs: Vec<u32>,
pub glyph: u32,
}
#[derive(Schema, Debug, Clone, PartialEq, Default)]
pub struct Machine {
pub classes: Vec<GlyphClass>,
pub transitions: Vec<Transition>,
}
#[derive(Schema, Debug, Clone, PartialEq)]
pub struct GlyphClass {
pub class: u32,
pub glyphs: Vec<u32>,
}
#[derive(Schema, Debug, Clone, Copy, PartialEq)]
pub struct Transition {
pub state: u32,
pub class: u32,
pub next: u32,
pub glyph: Option<u32>,
}
#[derive(Schema, Debug, Clone, Copy, PartialEq)]
pub struct Kern {
pub left: u32,
pub right: u32,
pub x: i32,
}
#[derive(Schema, Debug, Clone, Copy, PartialEq)]
pub struct Fit {
pub height: f32,
pub center: f32,
}
impl Font {
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if self.units_per_em == 0 {
errors.push("units_per_em must be greater than zero".to_string());
}
if self.glyphs.is_empty() {
errors.push("a font must have at least one glyph".to_string());
}
self.validate_chars(&mut errors);
self.validate_ligatures(&mut errors);
for (i, machine) in self.machines.iter().enumerate() {
self.validate_machine(i, machine, &mut errors);
}
self.validate_kerning(&mut errors);
if let Some(fit) = self.fit
&& (!fit.height.is_finite() || fit.height <= 0.0 || !fit.center.is_finite())
{
errors.push("fit.height must be positive and fit values must be finite".to_string());
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn has_glyph(&self, glyph: u32) -> bool {
(glyph as usize) < self.glyphs.len()
}
fn validate_chars(&self, errors: &mut Vec<String>) {
for (i, m) in self.chars.iter().enumerate() {
if char::from_u32(m.char).is_none() {
errors.push(format!("chars[{i}].char is not a Unicode scalar value"));
}
if !self.has_glyph(m.glyph) {
errors.push(format!(
"chars[{i}].glyph {} does not name a glyph",
m.glyph
));
}
}
}
fn validate_ligatures(&self, errors: &mut Vec<String>) {
for (i, ligature) in self.ligatures.iter().enumerate() {
if ligature.glyphs.len() < 2 {
errors.push(format!("ligatures[{i}] must contain at least two glyphs"));
}
for &glyph in ligature
.glyphs
.iter()
.chain(std::iter::once(&ligature.glyph))
{
if !self.has_glyph(glyph) {
errors.push(format!("ligatures[{i}] references missing glyph {glyph}"));
}
}
}
}
fn validate_machine(&self, i: usize, machine: &Machine, errors: &mut Vec<String>) {
let mut classes = std::collections::HashSet::new();
let mut members = std::collections::HashSet::new();
for class in &machine.classes {
if !classes.insert(class.class) {
errors.push(format!("machines[{i}] repeats class {}", class.class));
}
for &glyph in &class.glyphs {
if !self.has_glyph(glyph) {
errors.push(format!(
"machines[{i}] class {} references missing glyph {glyph}",
class.class
));
}
if !members.insert(glyph) {
errors.push(format!(
"machines[{i}] assigns glyph {glyph} to more than one class"
));
}
}
}
let mut transitions = std::collections::HashSet::new();
for t in &machine.transitions {
if !classes.contains(&t.class) {
errors.push(format!(
"machines[{i}] transition references missing class {}",
t.class
));
}
if !transitions.insert((t.state, t.class)) {
errors.push(format!(
"machines[{i}] repeats transition from state {} for class {}",
t.state, t.class
));
}
if let Some(glyph) = t.glyph
&& !self.has_glyph(glyph)
{
errors.push(format!(
"machines[{i}] transition references missing glyph {glyph}"
));
}
}
}
fn validate_kerning(&self, errors: &mut Vec<String>) {
let mut kerning = std::collections::HashSet::new();
for kern in &self.kerning {
if !self.has_glyph(kern.left) || !self.has_glyph(kern.right) {
errors.push(format!(
"kerning pair ({}, {}) references a missing glyph",
kern.left, kern.right
));
}
if !kerning.insert((kern.left, kern.right)) {
errors.push(format!(
"kerning pair ({}, {}) is repeated",
kern.left, kern.right
));
}
}
}
pub fn from_bytes(bytes: &[u8]) -> Result<Font, String> {
let font = yog_data::read_bytes::<Font>(bytes).map_err(|e| format!("{e:?}"))?;
font.validate().map_err(|errors| errors.join("; "))?;
Ok(font)
}
pub fn to_bytes(&self) -> Vec<u8> {
yog_data::binary::encode(&self.write())
}
pub fn glyph_of(&self, c: char) -> Option<u32> {
let c = c as u32;
self.chars.iter().find(|m| m.char == c).map(|m| m.glyph)
}
}