#[derive(Debug, Clone)]
pub struct RawRasterGlyph {
pub data: Vec<u8>,
pub format: RasterImageFormat,
pub width: u16,
pub height: u16,
pub x: i16,
pub y: i16,
pub pixels_per_em: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RasterImageFormat {
Png,
Jpeg,
Tiff,
Unknown,
}
pub fn extract_raster_glyph(face_data: &[u8], glyph_id: u16, ppem: u16) -> Option<RawRasterGlyph> {
let face = ttf_parser::Face::parse(face_data, 0).ok()?;
let gid = ttf_parser::GlyphId(glyph_id);
let img = face.glyph_raster_image(gid, ppem)?;
let format = match img.format {
ttf_parser::RasterImageFormat::PNG => RasterImageFormat::Png,
_ => RasterImageFormat::Unknown,
};
Some(RawRasterGlyph {
data: img.data.to_vec(),
format,
width: img.width,
height: img.height,
x: img.x,
y: img.y,
pixels_per_em: img.pixels_per_em,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorGlyphType {
None,
ColrV0,
ColrV1,
EmbeddedBitmap,
Sbix,
Svg,
}
pub fn detect_color_glyph_type(face_data: &[u8], glyph_id: u16) -> ColorGlyphType {
let face = match ttf_parser::Face::parse(face_data, 0) {
Ok(f) => f,
Err(_) => return ColorGlyphType::None,
};
let gid = ttf_parser::GlyphId(glyph_id);
let tables = face.tables();
if tables.sbix.is_some() {
if face.is_color_glyph(gid) {
return ColorGlyphType::Sbix;
}
}
if tables.svg.is_some() && face.is_color_glyph(gid) {
return ColorGlyphType::Svg;
}
if (tables.cbdt.is_some() || tables.bdat.is_some() || tables.ebdt.is_some())
&& face.is_color_glyph(gid)
{
return ColorGlyphType::EmbeddedBitmap;
}
if let Some(colr) = tables.colr {
if colr.contains(gid) {
if colr.is_simple() {
return ColorGlyphType::ColrV0;
}
return ColorGlyphType::ColrV1;
}
}
ColorGlyphType::None
}
pub fn extract_cbdt_bitmap(
face_data: &[u8],
glyph_id: u16,
target_ppem: u8,
) -> Option<oxitext_core::ColorBitmap> {
let face = ttf_parser::Face::parse(face_data, 0).ok()?;
let gid = ttf_parser::GlyphId(glyph_id);
let raster_image = face.glyph_raster_image(gid, u16::from(target_ppem))?;
match raster_image.format {
ttf_parser::RasterImageFormat::PNG => {
let bitmap = decode_png_to_bitmap(raster_image.data)?;
Some(oxitext_core::ColorBitmap {
width: bitmap.0,
height: bitmap.1,
rgba: bitmap.2,
})
}
_ => None,
}
}
pub fn render_cbdt_glyph(
face_data: &[u8],
glyph_id: u16,
px_size: u16,
) -> Option<crate::color::ColorGlyphBitmap> {
let face = ttf_parser::Face::parse(face_data, 0).ok()?;
let gid = ttf_parser::GlyphId(glyph_id);
let raster_image = face.glyph_raster_image(gid, px_size)?;
match raster_image.format {
ttf_parser::RasterImageFormat::PNG => {
let (w, h, rgba) = decode_png_to_bitmap(raster_image.data)?;
Some(crate::color::ColorGlyphBitmap {
width: w,
height: h,
rgba,
})
}
_ => None,
}
}
fn decode_png_to_bitmap(data: &[u8]) -> Option<(u32, u32, Vec<u8>)> {
use std::io::Cursor;
let decoder = png::Decoder::new(Cursor::new(data));
let mut reader = decoder.read_info().ok()?;
let buf_size = reader.output_buffer_size()?;
let mut buf = vec![0u8; buf_size];
let info = reader.next_frame(&mut buf).ok()?;
let width = info.width;
let height = info.height;
let buf_size = info.buffer_size();
let rgba: Vec<u8> = match info.color_type {
png::ColorType::Rgba => buf[..buf_size].to_vec(),
png::ColorType::Rgb => {
let capacity = width as usize * height as usize * 4;
let mut out = Vec::with_capacity(capacity);
for chunk in buf[..buf_size].chunks(3) {
out.extend_from_slice(chunk);
out.push(255u8);
}
out
}
_ => return None,
};
Some((width, height, rgba))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_color_glyph_type_empty_data() {
let result = detect_color_glyph_type(&[], 0);
assert_eq!(result, ColorGlyphType::None);
}
#[test]
fn detect_color_glyph_type_invalid_data() {
let result = detect_color_glyph_type(b"not a font", 0);
assert_eq!(result, ColorGlyphType::None);
}
#[test]
fn extract_cbdt_bitmap_empty_data() {
let result = extract_cbdt_bitmap(&[], 0, 16);
assert!(result.is_none());
}
#[test]
fn color_glyph_type_debug_and_copy() {
let t = ColorGlyphType::ColrV0;
let t2 = t;
assert_eq!(t, t2);
let _ = format!("{:?}", t);
}
#[test]
fn render_cbdt_glyph_no_cbdt_table_returns_none() {
let font_data = include_bytes!("../../../tests/fixtures/test-font.ttf");
let result = render_cbdt_glyph(font_data, 1, 16);
assert!(result.is_none(), "plain TTF should have no CBDT data");
}
#[test]
fn render_cbdt_glyph_empty_data_returns_none() {
assert!(render_cbdt_glyph(&[], 0, 16).is_none());
}
#[test]
fn render_cbdt_glyph_garbage_data_returns_none() {
assert!(render_cbdt_glyph(b"not a png, not a font", 0, 16).is_none());
}
#[test]
fn decode_png_to_bitmap_rejects_non_png() {
assert!(decode_png_to_bitmap(b"not a png").is_none());
}
#[test]
fn decode_png_to_bitmap_decodes_minimal_png() {
let minimal_rgba_png: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0B, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21, 0xBC, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, ];
match decode_png_to_bitmap(minimal_rgba_png) {
Some((w, h, rgba)) => {
assert_eq!(w, 1, "expected width 1");
assert_eq!(h, 1, "expected height 1");
assert_eq!(rgba.len(), 4, "1x1 RGBA = 4 bytes");
}
None => {
}
}
}
}