use oxifont_core::{FontFace as _, GlyphOutline as FontOutline};
use oxifont_parser::ParsedFace;
use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Transform};
use crate::backend::{RasterBackend, RasterOutput};
pub struct OxifontRaster;
impl OxifontRaster {
pub fn new() -> Self {
Self
}
}
impl Default for OxifontRaster {
fn default() -> Self {
Self::new()
}
}
impl RasterBackend for OxifontRaster {
fn rasterize(&self, face_data: &[u8], glyph_id: u16, px_size: f32) -> RasterOutput {
rasterize_glyph(face_data, glyph_id, px_size).unwrap_or_else(|| RasterOutput {
width: 0,
height: 0,
coverage: Vec::new(),
advance_x: 0.0,
advance_y: 0.0,
bearing_x: 0,
bearing_y: 0,
})
}
}
fn rasterize_glyph(face_data: &[u8], glyph_id: u16, px_size: f32) -> Option<RasterOutput> {
let face = ParsedFace::parse(face_data.to_vec(), 0).ok()?;
let units_per_em = face.units_per_em() as f32;
if units_per_em <= 0.0 || px_size <= 0.0 {
return None;
}
let scale = px_size / units_per_em;
let advance_x = face.advance_width(glyph_id).unwrap_or(0) as f32 * scale;
let outlines = face.outline(glyph_id)?;
if outlines.is_empty() {
return Some(RasterOutput {
width: 0,
height: 0,
coverage: Vec::new(),
advance_x,
advance_y: 0.0,
bearing_x: 0,
bearing_y: 0,
});
}
let (min_x, min_y, max_x, max_y) = bbox_from_outlines(&outlines);
if min_x >= max_x || min_y >= max_y {
return Some(RasterOutput {
width: 0,
height: 0,
coverage: Vec::new(),
advance_x,
advance_y: 0.0,
bearing_x: 0,
bearing_y: 0,
});
}
let w = ((max_x - min_x) * scale).ceil() as u32 + 2;
let h = ((max_y - min_y) * scale).ceil() as u32 + 2;
if w == 0 || h == 0 {
return None;
}
let path = build_path(&outlines, min_x, max_y, scale)?;
let mut pixmap = Pixmap::new(w, h)?;
let mut paint = Paint::default();
paint.set_color_rgba8(255, 255, 255, 255);
paint.anti_alias = true;
pixmap.as_mut().fill_path(
&path,
&paint,
FillRule::Winding,
Transform::identity(),
None,
);
let coverage: Vec<u8> = pixmap.data().chunks_exact(4).map(|px| px[3]).collect();
let bearing_x = (min_x * scale).floor() as i32 - 1;
let bearing_y = (max_y * scale).ceil() as i32 + 1;
Some(RasterOutput {
width: w as usize,
height: h as usize,
coverage,
advance_x,
advance_y: 0.0,
bearing_x,
bearing_y,
})
}
fn build_path(
outlines: &[FontOutline],
min_x: f32,
max_y: f32,
scale: f32,
) -> Option<tiny_skia::Path> {
let mut pb = PathBuilder::new();
for cmd in outlines {
match cmd {
FontOutline::MoveTo { x, y } => {
pb.move_to((x - min_x) * scale + 1.0, (max_y - y) * scale + 1.0);
}
FontOutline::LineTo { x, y } => {
pb.line_to((x - min_x) * scale + 1.0, (max_y - y) * scale + 1.0);
}
FontOutline::QuadTo { cx, cy, x, y } => {
pb.quad_to(
(cx - min_x) * scale + 1.0,
(max_y - cy) * scale + 1.0,
(x - min_x) * scale + 1.0,
(max_y - y) * scale + 1.0,
);
}
FontOutline::CubicTo {
cx1,
cy1,
cx2,
cy2,
x,
y,
} => {
pb.cubic_to(
(cx1 - min_x) * scale + 1.0,
(max_y - cy1) * scale + 1.0,
(cx2 - min_x) * scale + 1.0,
(max_y - cy2) * scale + 1.0,
(x - min_x) * scale + 1.0,
(max_y - y) * scale + 1.0,
);
}
FontOutline::Close => {
pb.close();
}
}
}
pb.finish()
}
fn bbox_from_outlines(outlines: &[FontOutline]) -> (f32, f32, f32, f32) {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for cmd in outlines {
match cmd {
FontOutline::MoveTo { x, y } | FontOutline::LineTo { x, y } => {
update_bbox(&mut min_x, &mut min_y, &mut max_x, &mut max_y, *x, *y);
}
FontOutline::QuadTo { cx, cy, x, y } => {
update_bbox(&mut min_x, &mut min_y, &mut max_x, &mut max_y, *cx, *cy);
update_bbox(&mut min_x, &mut min_y, &mut max_x, &mut max_y, *x, *y);
}
FontOutline::CubicTo {
cx1,
cy1,
cx2,
cy2,
x,
y,
} => {
update_bbox(&mut min_x, &mut min_y, &mut max_x, &mut max_y, *cx1, *cy1);
update_bbox(&mut min_x, &mut min_y, &mut max_x, &mut max_y, *cx2, *cy2);
update_bbox(&mut min_x, &mut min_y, &mut max_x, &mut max_y, *x, *y);
}
FontOutline::Close => {}
}
}
(min_x, min_y, max_x, max_y)
}
#[inline]
fn update_bbox(min_x: &mut f32, min_y: &mut f32, max_x: &mut f32, max_y: &mut f32, x: f32, y: f32) {
if x < *min_x {
*min_x = x;
}
if y < *min_y {
*min_y = y;
}
if x > *max_x {
*max_x = x;
}
if y > *max_y {
*max_y = y;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::RasterBackend;
use std::path::Path;
fn load_test_font() -> Vec<u8> {
let fixture =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/test-font.ttf");
if fixture.exists() {
return std::fs::read(&fixture).expect("read fixture font");
}
let candidates = [
"/Library/Fonts/Arial Unicode.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
];
for p in &candidates {
if Path::new(p).exists() {
return std::fs::read(p).expect("read system font");
}
}
panic!("no test font found — add tests/fixtures/test-font.ttf");
}
fn glyph_id_for_char(font: &[u8], c: char) -> u16 {
let face = ParsedFace::parse(font.to_vec(), 0).expect("font must parse");
face.glyph_for_char(c).expect("glyph for char must exist")
}
#[test]
fn oxifont_raster_produces_bitmap_for_visible_glyph() {
let font = load_test_font();
let gid = glyph_id_for_char(&font, 'A');
let raster = OxifontRaster::new();
let out = raster.rasterize(&font, gid, 16.0);
assert!(out.width > 0, "visible glyph should have non-zero width");
assert!(out.height > 0, "visible glyph should have non-zero height");
assert_eq!(out.coverage.len(), out.width * out.height);
}
#[test]
fn oxifont_raster_has_nonzero_coverage() {
let font = load_test_font();
let gid = glyph_id_for_char(&font, 'A');
let raster = OxifontRaster::new();
let out = raster.rasterize(&font, gid, 24.0);
assert!(
out.coverage.iter().any(|&p| p > 0),
"rasterized 'A' should have non-zero coverage"
);
}
#[test]
fn oxifont_raster_invalid_font_returns_zero_output() {
let raster = OxifontRaster::new();
let out = raster.rasterize(b"not a font", 36, 16.0);
assert_eq!(out.width, 0);
assert_eq!(out.height, 0);
}
#[test]
fn oxifont_raster_larger_size_produces_larger_bitmap() {
let font = load_test_font();
let gid = glyph_id_for_char(&font, 'A');
let raster = OxifontRaster::new();
let small = raster.rasterize(&font, gid, 8.0);
let large = raster.rasterize(&font, gid, 32.0);
assert!(large.width >= small.width);
assert!(large.height >= small.height);
}
#[test]
fn oxifont_raster_advance_width_is_positive_for_visible_glyph() {
let font = load_test_font();
let gid = glyph_id_for_char(&font, 'A');
let raster = OxifontRaster::new();
let out = raster.rasterize(&font, gid, 16.0);
assert!(out.advance_x > 0.0);
}
#[test]
fn oxifont_raster_rasterize_full_roundtrips() {
let font = load_test_font();
let gid = glyph_id_for_char(&font, 'B');
let raster = OxifontRaster::new();
let result = raster.rasterize_full(&font, gid, 20.0);
assert!(!result.is_empty());
}
}