use crate::layout::elements::{
Image, ImagePaint, ImageSampling, IntoLayoutNode, LayoutNode, Positioning, ReplacedGeometry,
Svg, SvgPaint,
};
use crate::layout::engine::{ImageFormat, LayoutBorder, PngMetadata, RasterImageAsset};
use crate::layout::flow_metrics::BlockMargins;
use crate::parser::dom::ElementNode;
use crate::parser::png;
use crate::style::computed::ComputedStyle;
use crate::types::Size;
use crate::util::decode_base64;
use super::placement::{ReplacedBoxSize, parse_html_image_dimension};
use super::raster::decode_png_to_rgb_asset;
use super::source::{fetch_remote_url, percent_decode};
use super::svg::{resolve_svg_size, sync_svg_tree_to_layout_box};
pub(crate) fn load_src_bytes(src: &str) -> Option<(Vec<u8>, Option<String>)> {
if let Some(rest) = src.strip_prefix("data:") {
let (header, encoded) = rest.split_once(',')?;
let header_lower = header.to_ascii_lowercase();
let bytes = if header_lower.contains("base64") {
decode_base64(encoded)?
} else {
percent_decode(encoded).into_bytes()
};
let mime = if header_lower.is_empty() {
None
} else {
Some(header_lower)
};
Some((bytes, mime))
} else if src.starts_with("http://") || src.starts_with("https://") {
Some((fetch_remote_url(src)?, None))
} else {
Some((std::fs::read(src).ok()?, None))
}
}
pub(crate) fn looks_like_svg(raw: &[u8]) -> bool {
let prefix = if raw.len() > 512 { &raw[..512] } else { raw };
let text = String::from_utf8_lossy(prefix);
let trimmed = text.trim_start_matches('\u{FEFF}').trim_start();
let trimmed_lower = trimmed.to_ascii_lowercase();
if !(trimmed.starts_with("<svg")
|| trimmed.starts_with("<?xml")
|| trimmed.starts_with("<!--")
|| trimmed_lower.starts_with("<!doctype"))
{
return false;
}
if trimmed.starts_with("<!--") {
return String::from_utf8_lossy(raw).contains("<svg");
}
true
}
pub(crate) fn try_parse_svg_bytes(raw: &[u8]) -> Option<crate::parser::svg::SvgTree> {
if !looks_like_svg(raw) {
return None;
}
let svg_str = String::from_utf8_lossy(raw);
crate::parser::svg::parse_svg_from_string(&svg_str)
}
pub(crate) fn load_image_bytes(raw: Vec<u8>) -> Option<RasterImageAsset> {
if png::is_png(&raw) {
let Some(png_info) = png::parse_png(&raw) else {
return decode_png_to_rgb_asset(&raw);
};
if png_info.channels == 2 || png_info.channels == 4 {
return Some(RasterImageAsset::source(
raw,
png_info.width,
png_info.height,
ImageFormat::PngAlpha,
None,
));
}
let metadata = PngMetadata {
channels: png_info.channels,
bit_depth: png_info.bit_depth,
};
Some(RasterImageAsset::source(
raw,
png_info.width,
png_info.height,
ImageFormat::Png,
Some(metadata),
))
} else if raw.starts_with(&[0xFF, 0xD8]) {
let (source_width, source_height) = crate::parser::jpeg::parse_jpeg_dimensions(&raw)?;
Some(RasterImageAsset::source(
raw,
source_width,
source_height,
ImageFormat::Jpeg,
None,
))
} else {
None
}
}
pub(crate) fn load_image_from_element(
el: &ElementNode,
available_width: f32,
available_height: f32,
style: &ComputedStyle,
_filter_dpi: f32,
) -> Option<LayoutNode> {
let src = el.attributes.get("src")?;
let (raw, mime) = load_src_bytes(src)?;
let skip_svg = mime
.as_deref()
.is_some_and(|m| !m.is_empty() && !m.contains("svg") && !m.contains("xml"));
if !skip_svg && let Some(mut tree) = try_parse_svg_bytes(&raw) {
let intrinsic = resolve_svg_size(&tree, available_width, available_height, false, false);
let html_attr_width = style
.width
.or_else(|| parse_html_image_dimension(el.attributes.get("width")));
let html_attr_height = style
.height
.or_else(|| parse_html_image_dimension(el.attributes.get("height")));
let (width, height) = match (html_attr_width, html_attr_height) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) if intrinsic.0 > 0.0 => (w, intrinsic.1 * (w / intrinsic.0)),
(Some(w), None) => (w, intrinsic.1),
(None, Some(h)) if intrinsic.1 > 0.0 => (intrinsic.0 * (h / intrinsic.1), h),
(None, Some(h)) => (intrinsic.0, h),
(None, None) => intrinsic,
};
let (width, height) = ReplacedBoxSize::new(
width,
height,
html_attr_width.is_none(),
html_attr_height.is_none(),
)
.constrain(available_width, style.max_width, style.max_height)
.dimensions();
let border = LayoutBorder::from_computed(&style.border, style.color);
let content_width = (width - border.horizontal_width()).max(0.0);
let content_height = (height - border.vertical_width()).max(0.0);
sync_svg_tree_to_layout_box(&mut tree, content_width, content_height);
return Some(
Svg {
tree,
geometry: ReplacedGeometry::new(
Size::new(width, height),
BlockMargins::new(style.margin.top, style.margin.bottom),
border,
),
positioning: Positioning::from_style(style),
paint: SvgPaint {
background_color: style.background_color,
border_image: style.border_image.paint(),
border_radii: style.resolve_corner_radii(width, height),
group: crate::layout::elements::PaintGroup::from_style(style),
},
replaced: crate::layout::engine::ReplacedContent {
object_fit: style.object_fit,
object_position: style.object_position,
..Default::default()
},
}
.boxed(),
);
}
let image = load_image_bytes(raw)?;
let attr_width = style
.width
.or_else(|| parse_html_image_dimension(el.attributes.get("width")));
let attr_height = style
.height
.or_else(|| parse_html_image_dimension(el.attributes.get("height")));
let src_w = image.source_width as f32;
let src_h = image.source_height as f32;
let natural_w = src_w * 0.75;
let natural_h = src_h * 0.75;
let (width, height) = match (attr_width, attr_height) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) if src_w > 0.0 => (w, w * (src_h / src_w)),
(Some(w), None) => (w, w), (None, Some(h)) if src_h > 0.0 => (h * (src_w / src_h), h),
(None, Some(h)) => (h, h), (None, None) if natural_w > 0.0 && natural_h > 0.0 => (natural_w, natural_h),
(None, None) => (available_width.min(200.0), 150.0),
};
let (width, height) =
ReplacedBoxSize::new(width, height, attr_width.is_none(), attr_height.is_none())
.constrain(available_width, style.max_width, style.max_height)
.dimensions();
Some(
Image {
source: image,
geometry: ReplacedGeometry::new(
Size::new(width, height),
BlockMargins::new(style.margin.top, style.margin.bottom),
LayoutBorder::from_computed(&style.border, style.color),
),
positioning: Positioning::from_style(style),
sampling: ImageSampling {
replaced: crate::layout::engine::ReplacedContent {
object_fit: style.object_fit,
object_position: style.object_position,
..Default::default()
},
rendering: style.image_rendering,
},
paint: ImagePaint {
background_color: style.background_color,
border_image: style.border_image.paint(),
border_radii: style.resolve_corner_radii(width, height),
filter_effect: None,
group: crate::layout::elements::PaintGroup::from_style(style),
..Default::default()
},
}
.boxed(),
)
}