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 bitmap = videoframe_to_rgba(&frame, png_w, png_h);
Ok(Some(ColorGlyphBitmap {
bitmap,
bearing_x: bx as i32,
bearing_y: by as i32,
advance: adv as u32,
ppem,
}))
}
}
fn videoframe_to_rgba(frame: &VideoFrame, width: u32, height: u32) -> 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 y = plane.data[src_off];
out.data[dst_off] = y;
out.data[dst_off + 1] = y;
out.data[dst_off + 2] = y;
out.data[dst_off + 3] = 255;
}
_ => unreachable!(),
}
}
}
out
}
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);
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);
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);
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());
}
}