use data_url::DataUrl;
use taffy::{AvailableSpace, CompactLength, Layout, MaybeResolve, Size};
use crate::{
Result,
layout::{
inline::InlineContentKind,
node::{ImageData, ImageSourceInput, Node, NodeKind, NodeStyleLayers},
style::{Length, Style, StyleDeclaration},
},
rendering::{Canvas, RenderContext, draw_image},
resources::image::{ImageResourceError, ImageResult, ImageSource, is_svg_like},
};
pub(crate) fn image_resource_url(image: &ImageData) -> Option<&str> {
match &image.src {
ImageSourceInput::Url(src) if src.starts_with("https://") || src.starts_with("http://") => {
Some(src.as_ref())
}
_ => None,
}
}
pub(crate) fn take_image_style_layers(
node: &mut Node,
width: Option<f32>,
height: Option<f32>,
) -> NodeStyleLayers {
let mut preset = node.metadata.preset.take();
if width.is_some() || height.is_some() {
let preset_style = preset.get_or_insert_with(Style::default);
if let Some(width) = width {
preset_style.push(StyleDeclaration::width(Length::Px(width)), false);
}
if let Some(height) = height {
preset_style.push(StyleDeclaration::height(Length::Px(height)), false);
}
}
NodeStyleLayers {
preset,
author_tw: node.metadata.tw.take(),
inline: node.metadata.style.take(),
dir: node.metadata.dir.take(),
}
}
pub(crate) fn image_inline_content(kind: &NodeKind) -> Option<InlineContentKind<'_>> {
matches!(kind, NodeKind::Image(_)).then_some(InlineContentKind::Box)
}
pub(crate) fn measure_image_node(
image: &ImageData,
context: &RenderContext,
available_space: Size<AvailableSpace>,
known_dimensions: Size<Option<f32>>,
style: &taffy::Style,
) -> Size<f32> {
let Ok(image_source) = image.src.resolve(context) else {
return Size::zero();
};
let intrinsic_size = match &image_source {
#[cfg(feature = "svg")]
ImageSource::Svg(svg) => Size {
width: svg.tree.size().width(),
height: svg.tree.size().height(),
},
ImageSource::Gif(gif) => {
let frame = gif.frame_at_time(context.time);
Size {
width: frame.width() as f32,
height: frame.height() as f32,
}
}
ImageSource::Bitmap(bitmap) => Size {
width: bitmap.width() as f32,
height: bitmap.height() as f32,
},
};
let intrinsic_aspect_ratio =
(intrinsic_size.height != 0.0).then_some(intrinsic_size.width / intrinsic_size.height);
let preferred_size = match (image.width, image.height) {
(Some(width), Some(height)) => Size { width, height },
(Some(width), None) => Size {
width,
height: intrinsic_aspect_ratio
.map(|ratio| width / ratio)
.unwrap_or(intrinsic_size.height),
},
(None, Some(height)) => Size {
width: intrinsic_aspect_ratio
.map(|ratio| height * ratio)
.unwrap_or(intrinsic_size.width),
height,
},
(None, None) => intrinsic_size,
}
.map(|value| value * context.sizing.viewport.device_pixel_ratio);
let style_known_dimensions = Size {
width: resolve_style_size_axis(style.size.width, available_space.width, context),
height: resolve_style_size_axis(style.size.height, available_space.height, context),
};
if let Size {
width: Some(width),
height: Some(height),
} = style_known_dimensions
{
return Size { width, height };
}
let known_dimensions = Size {
width: style_known_dimensions.width.or(known_dimensions.width),
height: style_known_dimensions.height.or(known_dimensions.height),
};
let known_dimensions = if style.size.width.is_auto()
&& style.size.height.is_auto()
&& known_dimensions.width.is_none()
&& known_dimensions.height.is_none()
&& matches!(
available_space.height,
AvailableSpace::MinContent | AvailableSpace::MaxContent
) {
Size {
width: available_space.width.into_option(),
height: None,
}
} else {
known_dimensions
};
let aspect_ratio = style.aspect_ratio.or_else(|| {
(preferred_size.height != 0.0).then_some(preferred_size.width / preferred_size.height)
});
let known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio);
if let Size {
width: Some(width),
height: Some(height),
} = known_dimensions
{
return Size { width, height };
}
preferred_size
}
fn resolve_style_size_axis(
size: taffy::Dimension,
available: AvailableSpace,
context: &RenderContext,
) -> Option<f32> {
match size.tag() {
CompactLength::AUTO_TAG => None,
CompactLength::LENGTH_TAG => Some(size.value()),
CompactLength::PERCENT_TAG => available.into_option(),
_ => size.maybe_resolve(available.into_option(), |val, basis| {
context.sizing.calc_arena.resolve_calc_value(val, basis)
}),
}
}
pub(crate) fn draw_image_node_content(
image: &ImageData,
context: &RenderContext,
canvas: &mut Canvas,
layout: Layout,
) -> Result<()> {
let Ok(image_source) = image.src.resolve(context) else {
return Ok(());
};
draw_image(&image_source, context, canvas, layout)?;
Ok(())
}
const DATA_URI_PREFIX: &str = "data:";
fn parse_data_uri_image(src: &str) -> ImageResult {
let url = DataUrl::process(src).map_err(|_| ImageResourceError::InvalidDataUriFormat)?;
let (data, _) = url
.decode_to_vec()
.map_err(|_| ImageResourceError::InvalidDataUriFormat)?;
ImageSource::from_bytes(&data)
}
pub(crate) fn resolve_image(src: &str, context: &RenderContext) -> ImageResult {
if src.starts_with(DATA_URI_PREFIX) {
return parse_data_uri_image(src);
}
if is_svg_like(src) {
#[cfg(feature = "svg")]
return ImageSource::from_bytes(src.as_bytes());
#[cfg(not(feature = "svg"))]
return Err(ImageResourceError::SvgParseNotSupported);
}
if let Some(img) = context.fetched_resources.get(src) {
return Ok(img.clone());
}
if let Some(img) = context.global.persistent_image_store.get(src) {
return Ok(img);
}
Err(ImageResourceError::Unknown)
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use image::RgbaImage;
use serde_json::from_value;
use taffy::{AvailableSpace, Dimension, Size, Style};
use super::{image_resource_url, measure_image_node};
use crate::{
GlobalContext,
layout::{
Viewport,
node::{ImageData, ImageSourceInput},
},
rendering::RenderContext,
resources::image::ImageSource,
};
#[test]
fn deserialize_image_src_from_string() -> std::result::Result<(), serde_json::Error> {
let image: ImageData = from_value(serde_json::json!({
"src": "https://example.com/image.png"
}))?;
assert_matches!(image.src, ImageSourceInput::Url(_));
let src = match image.src {
ImageSourceInput::Url(src) => src,
_ => return Ok(()),
};
assert_eq!(src.as_ref(), "https://example.com/image.png");
assert_eq!(
image_resource_url(&ImageData {
src: ImageSourceInput::Url(src),
width: None,
height: None
}),
Some("https://example.com/image.png")
);
Ok(())
}
#[test]
fn deserialize_image_src_from_buffer_source() -> std::result::Result<(), serde_json::Error> {
let image: ImageData = from_value(serde_json::json!({
"src": [137, 80, 78, 71]
}))?;
assert_matches!(image.src, ImageSourceInput::Buffer(_));
let data = match image.src {
ImageSourceInput::Buffer(data) => data,
_ => return Ok(()),
};
assert_eq!(&data[..], [137, 80, 78, 71]);
assert_eq!(
image_resource_url(&ImageData {
src: ImageSourceInput::Buffer(data),
width: None,
height: None
}),
None
);
Ok(())
}
#[test]
fn deserialize_image_src_from_bytes_value() -> std::result::Result<(), serde::de::value::Error> {
use serde::de::{Deserializer, Visitor, value::Error};
use serde::{Deserialize, forward_to_deserialize_any};
struct BytesValue<'a>(&'a [u8]);
impl<'de> Deserializer<'de> for BytesValue<'_> {
type Error = Error;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
visitor.visit_bytes(self.0)
}
forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map struct enum identifier ignored_any
}
}
let src = ImageSourceInput::deserialize(BytesValue(&[0x89, 0x50, 0x4e, 0x47]))?;
assert_matches!(src, ImageSourceInput::Buffer(_));
let data = match src {
ImageSourceInput::Buffer(data) => data,
_ => return Ok(()),
};
assert_eq!(&data[..], [0x89, 0x50, 0x4e, 0x47]);
Ok(())
}
#[test]
fn from_pixmap_creates_loaded_image_source_input() {
let bitmap = RgbaImage::new(2, 2);
let image = ImageData::from(bitmap);
assert_matches!(image.src, ImageSourceInput::Loaded(ImageSource::Bitmap(_)));
}
#[test]
fn fixed_style_size_uses_declared_lengths_instead_of_available_space() {
let global = GlobalContext::default();
let context = RenderContext::new_test(&global, Viewport::new((1200, 630)));
let image = ImageData::from(ImageSource::from(RgbaImage::new(10, 10)));
let style = Style {
size: Size {
width: Dimension::length(42.0),
height: Dimension::length(28.0),
},
..Style::default()
};
let measured = measure_image_node(
&image,
&context,
Size {
width: AvailableSpace::Definite(480.0),
height: AvailableSpace::Definite(320.0),
},
Size::NONE,
&style,
);
assert_eq!(
measured,
Size {
width: 42.0,
height: 28.0,
}
);
}
}