use std::io::Result;
use crate::TerminalSize;
pub fn render_svg_to_png_scaled(svg: &[u8], scale: f32) -> Result<Vec<u8>> {
implementation::render_svg_to_png(svg, scale)
}
pub fn render_svg_to_png_for_terminal(svg: &[u8], terminal_size: TerminalSize) -> Result<Vec<u8>> {
let scale = implementation::native_size(svg)
.and_then(|(width, height)| scale_to_fill_terminal(width, height, terminal_size))
.unwrap_or(1.0);
implementation::render_svg_to_png(svg, scale)
}
fn scale_to_fill_terminal(width: u32, height: u32, size: TerminalSize) -> Option<f32> {
if width == 0 || height == 0 {
return None;
}
let cell = size.cell.filter(|cell| cell.x > 0 && cell.y > 0)?;
let rows = (height as f64 / cell.y as f64).ceil().max(1.0);
let mut target_height = rows * cell.y as f64;
let mut target_width = width as f64 * target_height / height as f64;
if let Some(pixels) = size.pixels {
if target_width > pixels.x as f64 {
target_width = (pixels.x as f64).max(1.0);
target_height = height as f64 * target_width / width as f64;
}
}
Some((target_height / height as f64) as f32)
}
#[cfg(test)]
mod tests {
use super::scale_to_fill_terminal;
use crate::terminal::PixelSize;
use crate::TerminalSize;
#[test]
fn scale_to_fill_terminal_rounds_up_to_whole_cell_rows() {
let size = TerminalSize {
cell: Some(PixelSize::from_xy((20, 30))),
..TerminalSize::default()
};
let scale = scale_to_fill_terminal(24, 24, size).unwrap();
assert!((scale - 30.0 / 24.0).abs() < 1e-6, "scale was {scale}");
}
#[test]
fn scale_to_fill_terminal_caps_width_to_window_pixels() {
let size = TerminalSize {
cell: Some(PixelSize::from_xy((10, 20))),
pixels: Some(PixelSize::from_xy((100, 200))),
..TerminalSize::default()
};
let scale = scale_to_fill_terminal(1000, 10, size).unwrap();
assert!((scale - 0.1).abs() < 1e-6, "scale was {scale}");
}
#[test]
fn scale_to_fill_terminal_none_without_cell_size() {
assert_eq!(
scale_to_fill_terminal(24, 24, TerminalSize::default()),
None
);
}
#[test]
fn scale_to_fill_terminal_none_for_zero_sized_image() {
let size = TerminalSize {
cell: Some(PixelSize::from_xy((20, 30))),
..TerminalSize::default()
};
assert_eq!(scale_to_fill_terminal(0, 24, size), None);
assert_eq!(scale_to_fill_terminal(24, 0, size), None);
}
#[test]
#[cfg(all(feature = "svg", feature = "image-processing"))]
fn render_svg_to_png_for_terminal_rasterizes_at_target_resolution() {
use image::GenericImageView;
let svg = br#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="red"/></svg>"#;
let size = TerminalSize {
cell: Some(PixelSize::from_xy((10, 20))),
pixels: Some(PixelSize::from_xy((800, 480))),
..TerminalSize::default()
};
let native = super::render_svg_to_png_scaled(svg, 1.0).unwrap();
let native_dims = image::load_from_memory(&native).unwrap().dimensions();
assert_eq!(native_dims, (24, 24));
let fitted = super::render_svg_to_png_for_terminal(svg, size).unwrap();
let fitted_dims = image::load_from_memory(&fitted).unwrap().dimensions();
assert_eq!(fitted_dims, (40, 40));
}
}
#[cfg(feature = "svg")]
mod implementation {
use std::error::Error;
use std::fmt::Display;
use std::sync::{Arc, OnceLock};
use resvg::tiny_skia::{IntSize, Pixmap, Transform};
use resvg::usvg::{self, Tree};
use usvg::fontdb;
#[derive(Debug)]
pub enum RenderSvgError {
ParseError(usvg::Error),
FailedToCreatePixmap(IntSize),
EncodePngError(Box<dyn Error + Send + Sync>),
}
impl Display for RenderSvgError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RenderSvgError::ParseError(error) => write!(f, "Failed to parse SVG: {error}"),
RenderSvgError::FailedToCreatePixmap(int_size) => {
write!(f, "Failed to create pixmap of size {int_size:?}")
}
RenderSvgError::EncodePngError(error) => {
write!(f, "Failed to encode pixmap to PNG image: {error}")
}
}
}
}
impl std::error::Error for RenderSvgError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
RenderSvgError::ParseError(error) => Some(error),
RenderSvgError::FailedToCreatePixmap(_) => None,
RenderSvgError::EncodePngError(error) => Some(error.as_ref()),
}
}
}
impl From<RenderSvgError> for std::io::Error {
fn from(value: RenderSvgError) -> Self {
std::io::Error::other(value)
}
}
impl From<usvg::Error> for RenderSvgError {
fn from(value: usvg::Error) -> Self {
Self::ParseError(value)
}
}
static FONTS: OnceLock<Arc<fontdb::Database>> = OnceLock::new();
fn parse_svg(svg: &[u8]) -> Result<Tree, RenderSvgError> {
let fonts = FONTS.get_or_init(|| {
let mut fontdb = fontdb::Database::new();
fontdb.load_system_fonts();
Arc::new(fontdb)
});
let options = usvg::Options {
fontdb: fonts.clone(),
..Default::default()
};
Ok(usvg::Tree::from_data(svg, &options)?)
}
fn render_svg_to_png_with_resvg(svg: &[u8], scale: f32) -> Result<Vec<u8>, RenderSvgError> {
let tree = parse_svg(svg)?;
let native_size = tree.size().to_int_size();
let width = ((native_size.width() as f32) * scale).round().max(1.0) as u32;
let height = ((native_size.height() as f32) * scale).round().max(1.0) as u32;
let size = IntSize::from_wh(width, height).unwrap_or(native_size);
let mut pixmap = Pixmap::new(size.width(), size.height())
.ok_or(RenderSvgError::FailedToCreatePixmap(size))?;
resvg::render(
&tree,
Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
pixmap
.encode_png()
.map_err(|err| RenderSvgError::EncodePngError(Box::new(err)))
}
pub fn render_svg_to_png(svg: &[u8], scale: f32) -> std::io::Result<Vec<u8>> {
render_svg_to_png_with_resvg(svg, scale).map_err(Into::into)
}
pub fn native_size(svg: &[u8]) -> Option<(u32, u32)> {
let size = parse_svg(svg).ok()?.size().to_int_size();
Some((size.width(), size.height()))
}
}
#[cfg(not(feature = "svg"))]
mod implementation {
use std::io::{Error, ErrorKind, Result};
pub fn render_svg_to_png(_svg: &[u8], _scale: f32) -> Result<Vec<u8>> {
Err(Error::new(
ErrorKind::Unsupported,
"SVG rendering not enabled in this build",
))
}
pub fn native_size(_svg: &[u8]) -> Option<(u32, u32)> {
None
}
}