use resvg::{tiny_skia, usvg};
use crate::font::DEFAULT_FONT;
pub(crate) const MAX_PNG_AREA_PIXELS: u64 = 64_000_000;
pub fn svg_to_png(svg: &str, scale: f32) -> Result<Vec<u8>, String> {
svg_to_png_with_font(svg, scale, DEFAULT_FONT)
}
pub fn svg_to_png_with_font(svg: &str, scale: f32, font_bytes: &[u8]) -> Result<Vec<u8>, String> {
ttf_parser::Face::parse(font_bytes, 0).map_err(|e| format!("フォント解析失敗: {e}"))?;
let scale = if scale > 0.0 { scale } else { 1.0 };
let mut opt = usvg::Options::default();
opt.fontdb_mut().load_font_data(font_bytes.to_vec());
let tree = usvg::Tree::from_str(svg, &opt).map_err(|e| format!("SVG 解析失敗: {e}"))?;
let size = tree.size();
let w = (size.width() * scale).round().max(1.0) as u32;
let h = (size.height() * scale).round().max(1.0) as u32;
let area = w as u64 * h as u64;
if area > MAX_PNG_AREA_PIXELS {
return Err(format!(
"PNG 解像度 {w}×{h} px ({area} ピクセル) が上限 {MAX_PNG_AREA_PIXELS} px² を超えています"
));
}
let mut pixmap = tiny_skia::Pixmap::new(w, h)
.ok_or_else(|| format!("Pixmap 確保失敗: 寸法 {w}x{h} が無効です"))?;
let transform = tiny_skia::Transform::from_scale(scale, scale);
resvg::render(&tree, transform, &mut pixmap.as_mut());
pixmap
.encode_png()
.map_err(|e| format!("PNG エンコード失敗: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
const MIN_SVG: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"10\"><rect x=\"0\" y=\"0\" width=\"20\" height=\"10\" fill=\"#36a2eb\"/></svg>";
#[test]
fn rasterizes_to_valid_png() {
let png = svg_to_png(MIN_SVG, 1.0).unwrap();
assert_eq!(&png[0..4], &[0x89, b'P', b'N', b'G']);
assert!(png.len() > 50);
}
#[test]
fn scale_increases_size() {
let small = svg_to_png(MIN_SVG, 1.0).unwrap();
let large = svg_to_png(MIN_SVG, 3.0).unwrap();
assert!(large.len() > small.len(), "scaled PNG should be larger");
}
#[test]
fn non_positive_scale_falls_back() {
let png = svg_to_png(MIN_SVG, 0.0).unwrap();
assert_eq!(&png[0..4], &[0x89, b'P', b'N', b'G']);
}
#[test]
fn png_is_byte_deterministic() {
let a = svg_to_png(MIN_SVG, 1.0).unwrap();
let b = svg_to_png(MIN_SVG, 1.0).unwrap();
assert_eq!(a, b);
}
#[test]
fn renders_text_with_bundled_font() {
let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"30\"><text x=\"0\" y=\"20\" font-family=\"Noto Sans JP, sans-serif\" font-size=\"16\" fill=\"#000000\">売上</text></svg>";
let png = svg_to_png(svg, 1.0).unwrap();
assert_eq!(&png[0..4], &[0x89, b'P', b'N', b'G']);
}
#[test]
fn with_font_rasterizes_to_valid_png() {
use crate::font::DEFAULT_FONT;
let png = svg_to_png_with_font(MIN_SVG, 1.0, DEFAULT_FONT).unwrap();
assert_eq!(&png[0..4], &[0x89, b'P', b'N', b'G']);
}
#[test]
fn with_invalid_font_is_err() {
let err = svg_to_png_with_font(MIN_SVG, 1.0, b"not a font");
assert!(err.is_err());
}
#[test]
fn oversized_png_area_is_err() {
let huge_svg =
r#"<svg xmlns="http://www.w3.org/2000/svg" width="8001" height="8001"></svg>"#;
let err = svg_to_png(huge_svg, 1.0);
assert!(err.is_err(), "oversized PNG should be rejected: {err:?}");
assert!(
err.unwrap_err().contains("上限"),
"error message should mention the limit"
);
}
#[test]
fn near_limit_png_area_is_ok() {
let near_svg =
r#"<svg xmlns="http://www.w3.org/2000/svg" width="7999" height="7999"></svg>"#;
let result = svg_to_png(near_svg, 1.0);
assert!(result.is_ok(), "near-limit PNG should succeed: {result:?}");
}
}