use resvg::tiny_skia;
use resvg::usvg;
pub fn render_svg_glyph(
face_data: &[u8],
glyph_id: u16,
px_size: u16,
) -> Option<oxitext_core::ColorBitmap> {
let face = ttf_parser::Face::parse(face_data, 0).ok()?;
let gid = ttf_parser::GlyphId(glyph_id);
let svg_doc = face.glyph_svg_image(gid)?;
render_svg_bytes(svg_doc.data, px_size)
}
pub fn render_svg_bytes(svg_data: &[u8], px_size: u16) -> Option<oxitext_core::ColorBitmap> {
if px_size == 0 {
return None;
}
let options = usvg::Options::default();
let tree = usvg::Tree::from_data(svg_data, &options).ok()?;
let size = tree.size();
let svg_w = size.width();
let svg_h = size.height();
if svg_w <= 0.0 || svg_h <= 0.0 {
return None;
}
let scale_x = px_size as f32 / svg_w;
let scale_y = px_size as f32 / svg_h;
let scale = scale_x.min(scale_y);
if !scale.is_finite() || scale <= 0.0 {
return None;
}
let out_w = ((svg_w * scale).round() as u32).max(1);
let out_h = ((svg_h * scale).round() as u32).max(1);
let mut pixmap = tiny_skia::Pixmap::new(out_w, out_h)?;
resvg::render(
&tree,
tiny_skia::Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
let rgba = pixmap.take_demultiplied();
Some(oxitext_core::ColorBitmap {
width: out_w,
height: out_h,
rgba,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_svg_glyph_returns_none_for_non_svg_font() {
let font_data = include_bytes!("../../../tests/fixtures/test-font.ttf");
let result = render_svg_glyph(font_data, 1, 32);
assert!(
result.is_none(),
"plain TTF has no SVG table; expected None"
);
}
#[test]
fn test_svg_glyph_empty_font_returns_none() {
let result = render_svg_glyph(&[], 0, 32);
assert!(result.is_none());
}
#[test]
fn test_svg_bytes_zero_size_returns_none() {
let minimal_svg = br#"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"/>"#;
let result = render_svg_bytes(minimal_svg, 0);
assert!(result.is_none());
}
#[test]
fn test_render_svg_parse() {
let svg_src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
<rect width="10" height="10" fill="red"/>
</svg>"#;
let bm = render_svg_bytes(svg_src, 20);
let bm = bm.expect("resvg should render a minimal SVG to a ColorBitmap");
assert_eq!(bm.width, 20, "expected width 20, got {}", bm.width);
assert_eq!(bm.height, 20, "expected height 20, got {}", bm.height);
assert_eq!(
bm.rgba.len(),
(bm.width * bm.height * 4) as usize,
"RGBA buffer length must equal width * height * 4"
);
let has_red_pixel = bm.rgba.chunks_exact(4).any(|px| px[3] > 0 && px[0] > 0);
assert!(
has_red_pixel,
"rendered red square should contain at least one opaque red pixel"
);
}
#[test]
fn test_render_svg_aspect_ratio() {
let svg_src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10">
<rect width="20" height="10" fill="blue"/>
</svg>"#;
let bm = render_svg_bytes(svg_src, 40).expect("resvg should render a wide SVG");
assert_eq!(bm.width, 40, "expected width 40, got {}", bm.width);
assert_eq!(bm.height, 20, "expected height 20, got {}", bm.height);
assert_eq!(
bm.rgba.len(),
(bm.width * bm.height * 4) as usize,
"RGBA buffer length must equal width * height * 4"
);
}
#[test]
fn test_render_svg_bytes_invalid_data_returns_none() {
let result = render_svg_bytes(b"not svg data at all", 32);
assert!(result.is_none());
}
}