#[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> {
use ttf_parser::RasterImageFormat as Rif;
let face = ttf_parser::Face::parse(face_data, 0).ok()?;
let gid = ttf_parser::GlyphId(glyph_id);
let img = face.glyph_raster_image(gid, u16::from(target_ppem))?;
let w = u32::from(img.width);
let h = u32::from(img.height);
let rgba = match img.format {
Rif::PNG => {
let (bw, bh, data) = decode_png_to_bitmap(img.data)?;
return Some(oxitext_core::ColorBitmap {
width: bw,
height: bh,
rgba: data,
});
}
Rif::BitmapMono => unpack_mono(img.data, w, h, false),
Rif::BitmapMonoPacked => unpack_mono(img.data, w, h, true),
Rif::BitmapGray2 => unpack_gray2(img.data, w, h, false),
Rif::BitmapGray2Packed => unpack_gray2(img.data, w, h, true),
Rif::BitmapGray4 => unpack_gray4(img.data, w, h, false),
Rif::BitmapGray4Packed => unpack_gray4(img.data, w, h, true),
Rif::BitmapGray8 => unpack_gray8(img.data, w, h),
Rif::BitmapPremulBgra32 => unpack_bgra32(img.data, w, h),
}?;
Some(oxitext_core::ColorBitmap {
width: w,
height: h,
rgba,
})
}
pub fn render_cbdt_glyph(
face_data: &[u8],
glyph_id: u16,
px_size: u16,
) -> Option<crate::color::ColorGlyphBitmap> {
use ttf_parser::RasterImageFormat as Rif;
let face = ttf_parser::Face::parse(face_data, 0).ok()?;
let gid = ttf_parser::GlyphId(glyph_id);
let img = face.glyph_raster_image(gid, px_size)?;
let w = u32::from(img.width);
let h = u32::from(img.height);
let rgba = match img.format {
Rif::PNG => {
let (bw, bh, data) = decode_png_to_bitmap(img.data)?;
return Some(crate::color::ColorGlyphBitmap {
width: bw,
height: bh,
rgba: data,
});
}
Rif::BitmapMono => unpack_mono(img.data, w, h, false),
Rif::BitmapMonoPacked => unpack_mono(img.data, w, h, true),
Rif::BitmapGray2 => unpack_gray2(img.data, w, h, false),
Rif::BitmapGray2Packed => unpack_gray2(img.data, w, h, true),
Rif::BitmapGray4 => unpack_gray4(img.data, w, h, false),
Rif::BitmapGray4Packed => unpack_gray4(img.data, w, h, true),
Rif::BitmapGray8 => unpack_gray8(img.data, w, h),
Rif::BitmapPremulBgra32 => unpack_bgra32(img.data, w, h),
}?;
Some(crate::color::ColorGlyphBitmap {
width: w,
height: h,
rgba,
})
}
fn unpack_mono(data: &[u8], width: u32, height: u32, packed: bool) -> Option<Vec<u8>> {
let pixel_count = width.checked_mul(height)? as usize;
let mut rgba = Vec::with_capacity(pixel_count * 4);
if packed {
let total_bits = pixel_count;
let required_bytes = total_bits.div_ceil(8);
if data.len() < required_bytes {
return None;
}
for i in 0..pixel_count {
let byte_idx = i / 8;
let bit_shift = 7 - (i % 8); let bit = (data[byte_idx] >> bit_shift) & 1;
if bit == 1 {
rgba.extend_from_slice(&[0, 0, 0, 255]);
} else {
rgba.extend_from_slice(&[0, 0, 0, 0]);
}
}
} else {
let row_bytes = (width as usize).div_ceil(8);
let required_bytes = row_bytes.checked_mul(height as usize)?;
if data.len() < required_bytes {
return None;
}
for row in 0..height as usize {
let row_start = row * row_bytes;
for col in 0..width as usize {
let byte_idx = row_start + col / 8;
let bit_shift = 7 - (col % 8);
let bit = (data[byte_idx] >> bit_shift) & 1;
if bit == 1 {
rgba.extend_from_slice(&[0, 0, 0, 255]);
} else {
rgba.extend_from_slice(&[0, 0, 0, 0]);
}
}
}
}
Some(rgba)
}
fn unpack_gray2(data: &[u8], width: u32, height: u32, packed: bool) -> Option<Vec<u8>> {
let pixel_count = width.checked_mul(height)? as usize;
let mut rgba = Vec::with_capacity(pixel_count * 4);
if packed {
let required_bytes = (pixel_count * 2).div_ceil(8);
if data.len() < required_bytes {
return None;
}
for i in 0..pixel_count {
let bit_pos = i * 2;
let byte_idx = bit_pos / 8;
let bit_shift = 6 - (bit_pos % 8); let val = (data[byte_idx] >> bit_shift) & 0b11;
let alpha = val * 85;
rgba.extend_from_slice(&[0, 0, 0, alpha]);
}
} else {
let row_bytes = ((width as usize) * 2).div_ceil(8);
let required_bytes = row_bytes.checked_mul(height as usize)?;
if data.len() < required_bytes {
return None;
}
for row in 0..height as usize {
let row_start = row * row_bytes;
for col in 0..width as usize {
let bit_pos = col * 2;
let byte_idx = row_start + bit_pos / 8;
let bit_shift = 6 - (bit_pos % 8);
let val = (data[byte_idx] >> bit_shift) & 0b11;
let alpha = val * 85;
rgba.extend_from_slice(&[0, 0, 0, alpha]);
}
}
}
Some(rgba)
}
fn unpack_gray4(data: &[u8], width: u32, height: u32, packed: bool) -> Option<Vec<u8>> {
let pixel_count = width.checked_mul(height)? as usize;
let mut rgba = Vec::with_capacity(pixel_count * 4);
if packed {
let required_bytes = (pixel_count * 4).div_ceil(8);
if data.len() < required_bytes {
return None;
}
for i in 0..pixel_count {
let byte_idx = i / 2;
let val = if i % 2 == 0 {
(data[byte_idx] >> 4) & 0x0F } else {
data[byte_idx] & 0x0F
};
let alpha = val * 17;
rgba.extend_from_slice(&[0, 0, 0, alpha]);
}
} else {
let row_bytes = (width as usize).div_ceil(2);
let required_bytes = row_bytes.checked_mul(height as usize)?;
if data.len() < required_bytes {
return None;
}
for row in 0..height as usize {
let row_start = row * row_bytes;
for col in 0..width as usize {
let byte_idx = row_start + col / 2;
let val = if col % 2 == 0 {
(data[byte_idx] >> 4) & 0x0F
} else {
data[byte_idx] & 0x0F
};
let alpha = val * 17;
rgba.extend_from_slice(&[0, 0, 0, alpha]);
}
}
}
Some(rgba)
}
fn unpack_gray8(data: &[u8], width: u32, height: u32) -> Option<Vec<u8>> {
let pixel_count = width.checked_mul(height)? as usize;
if data.len() < pixel_count {
return None;
}
let mut rgba = Vec::with_capacity(pixel_count * 4);
for &alpha in &data[..pixel_count] {
rgba.extend_from_slice(&[0, 0, 0, alpha]);
}
Some(rgba)
}
fn unpack_bgra32(data: &[u8], width: u32, height: u32) -> Option<Vec<u8>> {
let pixel_count = width.checked_mul(height)? as usize;
let required_bytes = pixel_count.checked_mul(4)?;
if data.len() < required_bytes {
return None;
}
let mut rgba = Vec::with_capacity(required_bytes);
for chunk in data[..required_bytes].chunks(4) {
let b_pre = chunk[0];
let g_pre = chunk[1];
let r_pre = chunk[2];
let a = chunk[3];
if a == 0 {
rgba.extend_from_slice(&[0, 0, 0, 0]);
} else {
let a_u32 = u32::from(a);
let r = ((u32::from(r_pre) * 255 + a_u32 / 2) / a_u32) as u8;
let g = ((u32::from(g_pre) * 255 + a_u32 / 2) / a_u32) as u8;
let b = ((u32::from(b_pre) * 255 + a_u32 / 2) / a_u32) as u8;
rgba.extend_from_slice(&[r, g, b, a]);
}
}
Some(rgba)
}
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 => {
}
}
}
#[test]
fn unpack_mono_row_padded_2x2() {
let data: &[u8] = &[0xC0, 0x00];
let result = unpack_mono(data, 2, 2, false).expect("should succeed");
assert_eq!(result.len(), 16, "2x2 RGBA = 16 bytes");
assert_eq!(&result[0..4], &[0, 0, 0, 255]);
assert_eq!(&result[4..8], &[0, 0, 0, 255]);
assert_eq!(&result[8..12], &[0, 0, 0, 0]);
assert_eq!(&result[12..16], &[0, 0, 0, 0]);
}
#[test]
fn unpack_mono_packed_4x1() {
let data: &[u8] = &[0xA0];
let result = unpack_mono(data, 4, 1, true).expect("should succeed");
assert_eq!(result.len(), 16, "4x1 RGBA = 16 bytes");
assert_eq!(&result[0..4], &[0, 0, 0, 255], "pixel 0: black");
assert_eq!(&result[4..8], &[0, 0, 0, 0], "pixel 1: transparent");
assert_eq!(&result[8..12], &[0, 0, 0, 255], "pixel 2: black");
assert_eq!(&result[12..16], &[0, 0, 0, 0], "pixel 3: transparent");
}
#[test]
fn unpack_mono_too_short_returns_none() {
assert!(unpack_mono(&[0xFF], 4, 4, false).is_none());
}
#[test]
fn unpack_gray2_row_padded_2x1() {
let data: &[u8] = &[0xB0];
let result = unpack_gray2(data, 2, 1, false).expect("should succeed");
assert_eq!(result.len(), 8, "2x1 RGBA = 8 bytes");
assert_eq!(&result[0..4], &[0, 0, 0, 170], "pixel 0: alpha=170");
assert_eq!(&result[4..8], &[0, 0, 0, 255], "pixel 1: alpha=255");
}
#[test]
fn unpack_gray2_packed_4x1() {
let data: &[u8] = &[0b0001_1011];
let result = unpack_gray2(data, 4, 1, true).expect("should succeed");
assert_eq!(result.len(), 16, "4x1 RGBA = 16 bytes");
assert_eq!(&result[0..4], &[0, 0, 0, 0], "pixel 0: alpha=0");
assert_eq!(&result[4..8], &[0, 0, 0, 85], "pixel 1: alpha=85");
assert_eq!(&result[8..12], &[0, 0, 0, 170], "pixel 2: alpha=170");
assert_eq!(&result[12..16], &[0, 0, 0, 255], "pixel 3: alpha=255");
}
#[test]
fn unpack_gray2_too_short_returns_none() {
assert!(unpack_gray2(&[0xFF], 8, 1, true).is_none());
}
#[test]
fn unpack_gray4_row_padded_2x1() {
let data: &[u8] = &[0x5F];
let result = unpack_gray4(data, 2, 1, false).expect("should succeed");
assert_eq!(result.len(), 8, "2x1 RGBA = 8 bytes");
assert_eq!(&result[0..4], &[0, 0, 0, 85], "pixel 0: alpha=85");
assert_eq!(&result[4..8], &[0, 0, 0, 255], "pixel 1: alpha=255");
}
#[test]
fn unpack_gray4_packed_2x1() {
let data: &[u8] = &[0x0A];
let result = unpack_gray4(data, 2, 1, true).expect("should succeed");
assert_eq!(result.len(), 8, "2x1 RGBA = 8 bytes");
assert_eq!(&result[0..4], &[0, 0, 0, 0], "pixel 0: alpha=0");
assert_eq!(&result[4..8], &[0, 0, 0, 170], "pixel 1: alpha=170");
}
#[test]
fn unpack_gray4_too_short_returns_none() {
assert!(unpack_gray4(&[0xFF], 4, 1, false).is_none());
}
#[test]
fn unpack_gray8_2x2() {
let data: &[u8] = &[0x00, 0x80, 0xC0, 0xFF];
let result = unpack_gray8(data, 2, 2).expect("should succeed");
assert_eq!(result.len(), 16, "2x2 RGBA = 16 bytes");
assert_eq!(&result[0..4], &[0, 0, 0, 0x00]);
assert_eq!(&result[4..8], &[0, 0, 0, 0x80]);
assert_eq!(&result[8..12], &[0, 0, 0, 0xC0]);
assert_eq!(&result[12..16], &[0, 0, 0, 0xFF]);
}
#[test]
fn unpack_gray8_too_short_returns_none() {
assert!(unpack_gray8(&[0x00, 0x80, 0xC0], 2, 2).is_none());
}
#[test]
fn unpack_bgra32_opaque_and_transparent() {
let data: &[u8] = &[0, 0, 255, 255, 0, 0, 0, 0];
let result = unpack_bgra32(data, 2, 1).expect("should succeed");
assert_eq!(result.len(), 8, "2x1 RGBA = 8 bytes");
assert_eq!(&result[0..4], &[255, 0, 0, 255], "pixel 0: opaque red");
assert_eq!(&result[4..8], &[0, 0, 0, 0], "pixel 1: transparent");
}
#[test]
fn unpack_bgra32_half_transparent_green() {
let data: &[u8] = &[0, 128, 0, 128];
let result = unpack_bgra32(data, 1, 1).expect("should succeed");
assert_eq!(result.len(), 4, "1x1 RGBA = 4 bytes");
assert_eq!(result[0], 0, "R=0");
assert_eq!(result[1], 255, "G=255");
assert_eq!(result[2], 0, "B=0");
assert_eq!(result[3], 128, "A=128");
}
#[test]
fn unpack_bgra32_too_short_returns_none() {
assert!(unpack_bgra32(&[0, 0, 255, 255], 2, 1).is_none());
}
}