use crate::compose::RgbaBitmap;
use crate::face::Face;
use crate::Error;
use oxideav_core::VideoFrame;
#[derive(Debug, Clone)]
pub struct ColorGlyphBitmap {
pub bitmap: RgbaBitmap,
pub bearing_x: i32,
pub bearing_y: i32,
pub advance: u32,
pub ppem: u8,
}
impl Face {
pub fn has_color_bitmaps(&self) -> bool {
match self.kind() {
crate::FaceKind::Otf => false,
crate::FaceKind::Ttf => self.with_font(|f| f.has_color_bitmaps()).unwrap_or(false),
}
}
pub fn color_strike_sizes(&self) -> Vec<(u8, u8)> {
match self.kind() {
crate::FaceKind::Otf => Vec::new(),
crate::FaceKind::Ttf => self
.with_font(|f| f.color_strike_sizes())
.unwrap_or_default(),
}
}
pub fn raster_color_glyph(
&self,
glyph_id: u16,
size_px: f32,
) -> Result<Option<ColorGlyphBitmap>, Error> {
if size_px <= 0.0 || !size_px.is_finite() {
return Err(Error::InvalidSize);
}
if self.kind() != crate::FaceKind::Ttf {
return Ok(None);
}
let target_ppem = size_px.round().clamp(1.0, 255.0) as u8;
let bitmap_descriptor = self.with_font(|f| {
f.glyph_color_bitmap(glyph_id, target_ppem).map(|cb| {
(
cb.png_bytes.to_vec(),
cb.width,
cb.height,
cb.bearing_x,
cb.bearing_y,
cb.advance,
cb.ppem,
)
})
})?;
let (png_bytes, _meta_w, _meta_h, bx, by, adv, ppem) = match bitmap_descriptor {
Some(t) => t,
None => return Ok(None),
};
let frame = match oxideav_png::decode_png_to_frame(&png_bytes, None) {
Ok(f) => f,
Err(_) => return Ok(None),
};
let (png_w, png_h) = match read_png_dimensions(&png_bytes) {
Some(d) => d,
None => return Ok(None),
};
let palette = read_png_palette(&png_bytes);
let bitmap = videoframe_to_rgba(&frame, png_w, png_h, palette.as_ref());
Ok(Some(ColorGlyphBitmap {
bitmap,
bearing_x: bx as i32,
bearing_y: by as i32,
advance: adv as u32,
ppem,
}))
}
pub fn raster_color_glyph_at(
&self,
glyph_id: u16,
size_px: f32,
) -> Result<Option<ColorGlyphBitmap>, Error> {
let native = match self.raster_color_glyph(glyph_id, size_px)? {
Some(c) => c,
None => return Ok(None),
};
if native.bitmap.is_empty() || native.ppem == 0 {
return Ok(Some(native));
}
let strike_scale = size_px / native.ppem as f32;
let new_w = (native.bitmap.width as f32 * strike_scale).round().max(1.0) as u32;
let new_h = (native.bitmap.height as f32 * strike_scale)
.round()
.max(1.0) as u32;
let resampled = native.bitmap.resample_bilinear(new_w, new_h);
let new_bx = (native.bearing_x as f32 * strike_scale).round() as i32;
let new_by = (native.bearing_y as f32 * strike_scale).round() as i32;
let new_adv = (native.advance as f32 * strike_scale).round().max(0.0) as u32;
let reported_ppem = size_px.round().clamp(1.0, 255.0) as u8;
Ok(Some(ColorGlyphBitmap {
bitmap: resampled,
bearing_x: new_bx,
bearing_y: new_by,
advance: new_adv,
ppem: reported_ppem,
}))
}
}
fn videoframe_to_rgba(
frame: &VideoFrame,
width: u32,
height: u32,
palette: Option<&PngPalette>,
) -> RgbaBitmap {
if frame.planes.is_empty() || width == 0 || height == 0 {
return RgbaBitmap::default();
}
let plane = &frame.planes[0];
if plane.stride == 0 || plane.data.is_empty() {
return RgbaBitmap::default();
}
let w = width as usize;
let h = height as usize;
if plane.stride % w != 0 {
return RgbaBitmap::default();
}
let bpp = plane.stride / w;
if !(1..=4).contains(&bpp) {
return RgbaBitmap::default();
}
if plane.data.len() < plane.stride * h {
return RgbaBitmap::default();
}
let mut out = RgbaBitmap::new(width, height);
for row in 0..h {
for col in 0..w {
let src_off = row * plane.stride + col * bpp;
let dst_off = (row * w + col) * 4;
match bpp {
4 => {
out.data[dst_off] = plane.data[src_off];
out.data[dst_off + 1] = plane.data[src_off + 1];
out.data[dst_off + 2] = plane.data[src_off + 2];
out.data[dst_off + 3] = plane.data[src_off + 3];
}
3 => {
out.data[dst_off] = plane.data[src_off];
out.data[dst_off + 1] = plane.data[src_off + 1];
out.data[dst_off + 2] = plane.data[src_off + 2];
out.data[dst_off + 3] = 255;
}
2 => {
let y = plane.data[src_off];
let a = plane.data[src_off + 1];
out.data[dst_off] = y;
out.data[dst_off + 1] = y;
out.data[dst_off + 2] = y;
out.data[dst_off + 3] = a;
}
1 => {
let idx = plane.data[src_off];
if let Some(p) = palette {
let rgba = p.lookup(idx);
out.data[dst_off] = rgba[0];
out.data[dst_off + 1] = rgba[1];
out.data[dst_off + 2] = rgba[2];
out.data[dst_off + 3] = rgba[3];
} else {
out.data[dst_off] = idx;
out.data[dst_off + 1] = idx;
out.data[dst_off + 2] = idx;
out.data[dst_off + 3] = 255;
}
}
_ => unreachable!(),
}
}
}
out
}
#[derive(Debug, Clone)]
struct PngPalette {
entries: Vec<[u8; 4]>,
}
impl PngPalette {
fn lookup(&self, idx: u8) -> [u8; 4] {
self.entries
.get(idx as usize)
.copied()
.unwrap_or([0, 0, 0, 0])
}
}
fn read_png_palette(bytes: &[u8]) -> Option<PngPalette> {
if bytes.len() < 8 || &bytes[0..8] != b"\x89PNG\r\n\x1a\n" {
return None;
}
let mut off = 8usize;
let mut plte: Option<&[u8]> = None;
let mut trns: Option<&[u8]> = None;
while off + 12 <= bytes.len() {
let len = u32::from_be_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
as usize;
let chunk_end = off.checked_add(8).and_then(|x| x.checked_add(len))?;
let total_end = chunk_end.checked_add(4)?;
if total_end > bytes.len() {
break;
}
let chunk_type = &bytes[off + 4..off + 8];
let chunk_data = &bytes[off + 8..chunk_end];
match chunk_type {
b"PLTE" => plte = Some(chunk_data),
b"tRNS" => trns = Some(chunk_data),
b"IDAT" => {
break;
}
b"IEND" => break,
_ => {}
}
off = total_end;
}
let plte = plte?;
if plte.is_empty() || plte.len() % 3 != 0 || plte.len() > 256 * 3 {
return None;
}
let n = plte.len() / 3;
let mut entries: Vec<[u8; 4]> = Vec::with_capacity(n);
for i in 0..n {
entries.push([plte[i * 3], plte[i * 3 + 1], plte[i * 3 + 2], 255]);
}
if let Some(t) = trns {
let m = t.len().min(n);
for (i, &alpha) in t.iter().take(m).enumerate() {
entries[i][3] = alpha;
}
}
Some(PngPalette { entries })
}
fn read_png_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
if bytes.len() < 24 {
return None;
}
if &bytes[0..8] != b"\x89PNG\r\n\x1a\n" {
return None;
}
if &bytes[12..16] != b"IHDR" {
return None;
}
let w = u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
let h = u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
if w == 0 || h == 0 {
return None;
}
Some((w, h))
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::{VideoFrame, VideoPlane};
#[test]
fn videoframe_rgba8_to_bitmap() {
let frame = VideoFrame {
pts: None,
planes: vec![VideoPlane {
stride: 8, data: vec![
255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 64, 255, 255, 0, 255, ],
}],
};
let bm = videoframe_to_rgba(&frame, 2, 2, None);
assert_eq!(bm.width, 2);
assert_eq!(bm.height, 2);
assert_eq!(bm.get(0, 0), [255, 0, 0, 255]);
assert_eq!(bm.get(1, 0), [0, 255, 0, 128]);
assert_eq!(bm.get(0, 1), [0, 0, 255, 64]);
assert_eq!(bm.get(1, 1), [255, 255, 0, 255]);
}
#[test]
fn videoframe_rgb24_to_bitmap_fills_opaque_alpha() {
let frame = VideoFrame {
pts: None,
planes: vec![VideoPlane {
stride: 6, data: vec![
255, 0, 0, 0, 255, 0, ],
}],
};
let bm = videoframe_to_rgba(&frame, 2, 1, None);
assert_eq!(bm.width, 2);
assert_eq!(bm.height, 1);
assert_eq!(bm.get(0, 0), [255, 0, 0, 255]);
assert_eq!(bm.get(1, 0), [0, 255, 0, 255]);
}
#[test]
fn empty_videoframe_returns_empty_bitmap() {
let frame = VideoFrame {
pts: None,
planes: vec![],
};
let bm = videoframe_to_rgba(&frame, 0, 0, None);
assert!(bm.is_empty());
}
#[test]
fn read_png_dimensions_extracts_ihdr() {
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(b"\x89PNG\r\n\x1a\n");
buf.extend_from_slice(&13u32.to_be_bytes());
buf.extend_from_slice(b"IHDR");
buf.extend_from_slice(&96u32.to_be_bytes());
buf.extend_from_slice(&109u32.to_be_bytes());
buf.extend_from_slice(&[8, 6, 0, 0, 0]);
buf.extend_from_slice(&[0; 4]);
let dim = read_png_dimensions(&buf).expect("ihdr");
assert_eq!(dim, (96, 109));
let mut bad = buf.clone();
bad[0] = 0;
assert!(read_png_dimensions(&bad).is_none());
assert!(read_png_dimensions(&buf[..16]).is_none());
}
#[test]
fn read_png_palette_extracts_plte_and_trns() {
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(b"\x89PNG\r\n\x1a\n");
buf.extend_from_slice(&13u32.to_be_bytes());
buf.extend_from_slice(b"IHDR");
buf.extend_from_slice(&8u32.to_be_bytes()); buf.extend_from_slice(&8u32.to_be_bytes()); buf.extend_from_slice(&[8, 3, 0, 0, 0]); buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&9u32.to_be_bytes()); buf.extend_from_slice(b"PLTE");
buf.extend_from_slice(&[
255, 0, 0, 0, 255, 0, 0, 0, 255, ]);
buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&2u32.to_be_bytes()); buf.extend_from_slice(b"tRNS");
buf.extend_from_slice(&[255, 128]);
buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(b"IDAT");
buf.extend_from_slice(&[0u8; 4]);
let pal = read_png_palette(&buf).expect("palette");
assert_eq!(pal.entries.len(), 3);
assert_eq!(pal.lookup(0), [255, 0, 0, 255]);
assert_eq!(pal.lookup(1), [0, 255, 0, 128]);
assert_eq!(pal.lookup(2), [0, 0, 255, 255]);
assert_eq!(pal.lookup(3), [0, 0, 0, 0]);
let mut nopal = Vec::new();
nopal.extend_from_slice(b"\x89PNG\r\n\x1a\n");
nopal.extend_from_slice(&13u32.to_be_bytes());
nopal.extend_from_slice(b"IHDR");
nopal.extend_from_slice(&[0u8; 13]);
nopal.extend_from_slice(&[0u8; 4]);
assert!(
read_png_palette(&nopal).is_none(),
"palette-less PNG must return None"
);
let mut bad = buf.clone();
bad[0] = 0;
assert!(read_png_palette(&bad).is_none());
}
#[test]
fn videoframe_pal8_to_bitmap_via_palette() {
let frame = VideoFrame {
pts: None,
planes: vec![VideoPlane {
stride: 2, data: vec![
0, 1, 2, 1, ],
}],
};
let pal = PngPalette {
entries: vec![
[255, 0, 0, 255], [0, 255, 0, 128], [0, 0, 255, 255], ],
};
let bm = videoframe_to_rgba(&frame, 2, 2, Some(&pal));
assert_eq!(bm.get(0, 0), [255, 0, 0, 255]);
assert_eq!(bm.get(1, 0), [0, 255, 0, 128]);
assert_eq!(bm.get(0, 1), [0, 0, 255, 255]);
assert_eq!(bm.get(1, 1), [0, 255, 0, 128]);
}
#[test]
fn videoframe_gray8_to_bitmap_without_palette() {
let frame = VideoFrame {
pts: None,
planes: vec![VideoPlane {
stride: 2,
data: vec![100, 200, 50, 25],
}],
};
let bm = videoframe_to_rgba(&frame, 2, 2, None);
assert_eq!(bm.get(0, 0), [100, 100, 100, 255]);
assert_eq!(bm.get(1, 0), [200, 200, 200, 255]);
assert_eq!(bm.get(0, 1), [50, 50, 50, 255]);
assert_eq!(bm.get(1, 1), [25, 25, 25, 255]);
}
}