use std::{borrow::Cow, sync::Arc};
use dashmap::DashMap;
use image::RgbaImage;
use crate::{
layout::style::{Color, ImageScalingAlgorithm},
rendering::{fast_resize, unpremultiply_alpha},
};
use thiserror::Error;
pub type ImageResult = Result<Arc<ImageSource>, ImageResourceError>;
#[derive(Debug, Clone)]
pub enum ImageSource {
#[cfg(feature = "svg")]
Svg {
source: Arc<str>,
tree: Box<resvg::usvg::Tree>,
},
Bitmap(RgbaImage),
}
pub type PersistentImageStore = DashMap<String, Arc<ImageSource>>;
impl From<RgbaImage> for ImageSource {
fn from(bitmap: RgbaImage) -> Self {
ImageSource::Bitmap(bitmap)
}
}
impl ImageSource {
pub fn size(&self) -> (f32, f32) {
match self {
#[cfg(feature = "svg")]
ImageSource::Svg { tree, .. } => (tree.size().width(), tree.size().height()),
ImageSource::Bitmap(bitmap) => (bitmap.width() as f32, bitmap.height() as f32),
}
}
pub fn render_to_rgba_image<'i>(
&'i self,
width: u32,
height: u32,
image_rendering: ImageScalingAlgorithm,
current_color: Color,
) -> Result<Cow<'i, RgbaImage>, ImageResourceError> {
#[cfg(not(feature = "svg"))]
let _ = current_color;
match self {
ImageSource::Bitmap(bitmap) => {
if bitmap.width() == width && bitmap.height() == height {
return Ok(Cow::Borrowed(bitmap));
}
Ok(Cow::Owned(fast_resize(
bitmap,
width,
height,
image_rendering,
)?))
}
#[cfg(feature = "svg")]
ImageSource::Svg { source, tree } => {
use resvg::{
tiny_skia::Pixmap,
usvg::{Options, Transform, Tree},
};
let options = Options {
style_sheet: Some(format!("svg {{ color: {current_color}; }}")),
image_rendering: image_rendering.into(),
..Default::default()
};
let reparsed_tree =
Tree::from_str(source, &options).map_err(ImageResourceError::SvgParseError)?;
let mut pixmap = Pixmap::new(width, height).ok_or(ImageResourceError::InvalidPixmapSize)?;
let original_size = tree.size();
let sx = width as f32 / original_size.width();
let sy = height as f32 / original_size.height();
resvg::render(
&reparsed_tree,
Transform::from_scale(sx, sy),
&mut pixmap.as_mut(),
);
let mut image = RgbaImage::from_raw(width, height, pixmap.take())
.ok_or(ImageResourceError::MismatchedBufferSize)?;
for pixel in bytemuck::cast_slice_mut::<u8, [u8; 4]>(image.as_mut()) {
unpremultiply_alpha(pixel);
}
Ok(Cow::Owned(image))
}
}
}
}
pub fn load_image_source_from_bytes(bytes: &[u8]) -> ImageResult {
#[cfg(feature = "svg")]
{
use std::str::from_utf8;
if let Ok(text) = from_utf8(bytes)
&& is_svg_like(text)
{
return parse_svg_str(text);
}
}
let img = image::load_from_memory(bytes).map_err(ImageResourceError::DecodeError)?;
Ok(Arc::new(img.into_rgba8().into()))
}
pub(crate) fn is_svg_like(src: &str) -> bool {
src.contains("<svg") && src.contains("xmlns")
}
#[cfg(feature = "svg")]
fn strip_unsupported_svg_text_nodes(src: &str) -> String {
use std::ops::Range;
use roxmltree::{Document, Node};
fn merge_ranges(mut ranges: Vec<Range<usize>>) -> Vec<Range<usize>> {
ranges.sort_by_key(|range| (range.start, range.end));
let mut merged: Vec<Range<usize>> = Vec::with_capacity(ranges.len());
for range in ranges {
if let Some(last) = merged.last_mut()
&& range.start <= last.end
{
last.end = last.end.max(range.end);
} else {
merged.push(range);
}
}
merged
}
let Ok(document) = Document::parse(src) else {
return src.to_owned();
};
let ranges = document
.descendants()
.filter(Node::is_element)
.filter_map(|node| {
let name = node.tag_name().name();
if name == "text" || name == "tspan" {
Some(node.range())
} else {
None
}
})
.collect::<Vec<_>>();
if ranges.is_empty() {
return src.to_owned();
}
let merged_ranges = merge_ranges(ranges);
let mut stripped = String::with_capacity(src.len());
let mut cursor = 0;
for range in merged_ranges {
if range.start > cursor {
stripped.push_str(&src[cursor..range.start]);
}
cursor = cursor.max(range.end);
}
if cursor < src.len() {
stripped.push_str(&src[cursor..]);
}
stripped
}
#[cfg(feature = "svg")]
pub fn parse_svg_str(src: &str) -> ImageResult {
use resvg::usvg::Tree;
let sanitized_svg = strip_unsupported_svg_text_nodes(src);
let tree = Tree::from_str(&sanitized_svg, &Default::default())
.map_err(ImageResourceError::SvgParseError)?;
Ok(Arc::new(ImageSource::Svg {
source: Arc::from(sanitized_svg),
tree: Box::new(tree),
}))
}
#[derive(Debug, Error)]
pub enum ImageResourceError {
#[error("An error occurred while decoding the image data: {0}")]
DecodeError(#[from] image::ImageError),
#[error("The image data URI is in an invalid format")]
InvalidDataUriFormat,
#[error("The image data URI is malformed and cannot be parsed")]
MalformedDataUri,
#[cfg(feature = "svg")]
#[error("An error occurred while parsing an SVG image: {0}")]
SvgParseError(#[from] resvg::usvg::Error),
#[cfg(not(feature = "svg"))]
#[error("SVG parsing is not supported in this build")]
SvgParseNotSupported,
#[error("The image source is unknown")]
Unknown,
#[error("The pixmap size is invalid")]
InvalidPixmapSize,
#[error("The buffer size does not match the target image size")]
MismatchedBufferSize,
#[error("An error occurred while resizing the image: {0}")]
ResizeError(#[from] fast_image_resize::ResizeError),
}
#[cfg(test)]
mod tests {
use image::Rgba;
use super::*;
fn rgba_at(image: &RgbaImage, x: u32, y: u32) -> [u8; 4] {
image.get_pixel(x, y).0
}
#[cfg(feature = "svg")]
#[test]
fn svg_current_color_changes_output() -> Result<(), ImageResourceError> {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4"><rect x="0" y="0" width="4" height="4" fill="currentColor"/></svg>"#;
let image = parse_svg_str(svg)?;
let red = image
.render_to_rgba_image(4, 4, ImageScalingAlgorithm::Auto, Color::from_rgb(0xFF0000))?
.into_owned();
let blue = image
.render_to_rgba_image(4, 4, ImageScalingAlgorithm::Auto, Color::from_rgb(0x0000FF))?
.into_owned();
assert_ne!(rgba_at(&red, 2, 2), rgba_at(&blue, 2, 2));
Ok(())
}
#[cfg(feature = "svg")]
#[test]
fn svg_current_color_applies_alpha() -> Result<(), ImageResourceError> {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4"><rect x="0" y="0" width="4" height="4" fill="currentColor"/></svg>"#;
let image = parse_svg_str(svg)?;
let color = Color([255, 0, 0, 128]);
let rendered = image
.render_to_rgba_image(4, 4, ImageScalingAlgorithm::Auto, color)?
.into_owned();
let alpha = rgba_at(&rendered, 2, 2)[3];
assert!((alpha as i16 - 128).abs() <= 1);
Ok(())
}
#[cfg(feature = "svg")]
#[test]
fn svg_fixed_fill_is_not_affected_by_current_color() -> Result<(), ImageResourceError> {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4"><rect x="0" y="0" width="4" height="4" fill="#ff0000"/></svg>"##;
let image = parse_svg_str(svg)?;
let first = image
.render_to_rgba_image(4, 4, ImageScalingAlgorithm::Auto, Color::from_rgb(0x00FF00))?
.into_owned();
let second = image
.render_to_rgba_image(4, 4, ImageScalingAlgorithm::Auto, Color::from_rgb(0x0000FF))?
.into_owned();
assert_eq!(first.as_raw(), second.as_raw());
Ok(())
}
#[cfg(feature = "svg")]
#[test]
fn parse_svg_str_strips_text_and_tspan_nodes() -> Result<(), ImageResourceError> {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><rect x="0" y="0" width="20" height="20" fill="#ff0000"/><text x="2" y="10">hello <tspan>world</tspan></text><g><tspan>orphan</tspan></g></svg>"##;
let image = parse_svg_str(svg)?;
let ImageSource::Svg { source, .. } = image.as_ref() else {
unreachable!()
};
assert!(source.contains("<rect"));
assert!(!source.contains("<text"));
assert!(!source.contains("<tspan"));
Ok(())
}
#[test]
fn bitmap_is_not_affected_by_current_color() -> Result<(), ImageResourceError> {
let mut bitmap = RgbaImage::new(2, 2);
bitmap.put_pixel(0, 0, Rgba([12, 34, 56, 200]));
bitmap.put_pixel(1, 0, Rgba([78, 90, 12, 255]));
let image = ImageSource::Bitmap(bitmap);
let first = image
.render_to_rgba_image(2, 2, ImageScalingAlgorithm::Auto, Color::from_rgb(0xFF0000))?
.into_owned();
let second = image
.render_to_rgba_image(2, 2, ImageScalingAlgorithm::Auto, Color::from_rgb(0x0000FF))?
.into_owned();
assert_eq!(first.as_raw(), second.as_raw());
Ok(())
}
#[test]
fn bitmap_resize_smoke_for_scaling_algorithm() -> Result<(), ImageResourceError> {
let mut bitmap = RgbaImage::new(2, 2);
bitmap.put_pixel(0, 0, Rgba([255, 0, 0, 255]));
bitmap.put_pixel(1, 0, Rgba([0, 255, 0, 255]));
bitmap.put_pixel(0, 1, Rgba([0, 0, 255, 255]));
bitmap.put_pixel(1, 1, Rgba([255, 255, 255, 255]));
let image = ImageSource::Bitmap(bitmap);
let resized = image
.render_to_rgba_image(4, 4, ImageScalingAlgorithm::Pixelated, Color::black())?
.into_owned();
assert_eq!(resized.width(), 4);
assert_eq!(resized.height(), 4);
Ok(())
}
}