render: clip image rasters to their visible bound, browser-like alt fallback
paint_image clipped an object-fit:cover overflow to the element box but ignored the ancestor overflow:hidden clip entirely, so a partially visible image (a half-scrolled carousel slide, a hero cropped by its section) painted its full raster past the viewport edge. Pass the already clip-intersected visible rect down and mask against it whenever the destination exceeds it on any side. Broken images now fall back the way browsers do: a non-empty alt renders as text in place of the image with no placeholder box, alt="" renders nothing (declared decorative), and only a missing alt keeps the neutral grey placeholder. Verified with a repro covering the half-clipped cover slide and all three alt variants; no movement on the image-heavy spot set; obstacle course 33/33.
This commit is contained in:
1 parent
c29c64fc57
commit
1d96c316ca
1 file changed
+34
-17
@@ -176,7 +176,9 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
|||||||
if let Some(img_rect) = img_rect {
|
if let Some(img_rect) = img_rect {
|
||||||
// object-fit does not apply to background images (that is
|
// object-fit does not apply to background images (that is
|
||||||
// background-size, handled above); paint the layer as-is.
|
// background-size, handled above); paint the layer as-is.
|
||||||
paint_image(bg_url, base_url, &img_rect, crate::ObjectFit::Fill, &mut pixmap, &mut image_cache);
|
// img_rect is already clip-intersected, so it doubles as the
|
||||||
|
// visible bound.
|
||||||
|
paint_image(bg_url, base_url, &img_rect, &img_rect, crate::ObjectFit::Fill, &mut pixmap, &mut image_cache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,22 +233,32 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
|
|||||||
|
|
||||||
if name.local.as_ref() == "img" {
|
if name.local.as_ref() == "img" {
|
||||||
if let Some((src, _density)) = resolve_img_url(tree, nid) {
|
if let Some((src, _density)) = resolve_img_url(tree, nid) {
|
||||||
let painted = paint_image(&src, base_url, &rect, style.object_fit, &mut pixmap, &mut image_cache);
|
// `visible_rect` is the border box already intersected with the
|
||||||
// Fall back to alt text only when the image itself did not paint.
|
// ancestor overflow clip: the raster must not paint past it (a
|
||||||
|
// half-scrolled carousel slide's image otherwise bleeds over
|
||||||
|
// the viewport edge).
|
||||||
|
let painted =
|
||||||
|
paint_image(&src, base_url, &rect, &visible_rect, style.object_fit, &mut pixmap, &mut image_cache);
|
||||||
|
// Fall back when the image itself did not paint, following
|
||||||
|
// what browsers show for a broken image: a non-empty alt
|
||||||
|
// renders as text in place of the image (no placeholder box),
|
||||||
|
// alt="" renders nothing at all (the author declared the
|
||||||
|
// image decorative), and only a MISSING alt keeps the neutral
|
||||||
|
// grey placeholder. box_rect/visible_rect are already
|
||||||
|
// clip-intersected, so none of this paints outside an
|
||||||
|
// overflow:hidden clip.
|
||||||
if !painted {
|
if !painted {
|
||||||
// A fetch/decode failure paints a neutral grey box (like a
|
match node.get_attribute("alt") {
|
||||||
// browser's lazy/broken-image placeholder) so a missing
|
Some(alt) if !alt.trim().is_empty() => {
|
||||||
// image reads as "not loaded", not as a broken render.
|
draw_text(&mut pixmap, &alt, rect.x, rect.y, [0, 0, 0, 255], 12.0, false, clip);
|
||||||
// box_rect/visible_rect are already clip-intersected, so
|
}
|
||||||
// this never paints outside an overflow:hidden clip.
|
Some(_) => {}
|
||||||
|
None => {
|
||||||
if visible_rect.width >= 4.0 && visible_rect.height >= 4.0 {
|
if visible_rect.width >= 4.0 && visible_rect.height >= 4.0 {
|
||||||
let mut ph = Paint::default();
|
let mut ph = Paint::default();
|
||||||
ph.set_color(Color::from_rgba8(0xE9, 0xEA, 0xEC, 0xFF));
|
ph.set_color(Color::from_rgba8(0xE9, 0xEA, 0xEC, 0xFF));
|
||||||
pixmap.fill_rect(box_rect, &ph, Transform::identity(), None);
|
pixmap.fill_rect(box_rect, &ph, Transform::identity(), None);
|
||||||
}
|
}
|
||||||
if let Some(alt) = node.get_attribute("alt") {
|
|
||||||
if !alt.is_empty() {
|
|
||||||
draw_text(&mut pixmap, alt, rect.x, rect.y, [0, 0, 0, 255], 12.0, false, clip);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1138,6 +1150,7 @@ fn paint_image(
|
|||||||
src: &str,
|
src: &str,
|
||||||
base_url: Option<&str>,
|
base_url: Option<&str>,
|
||||||
rect: &crate::Rect,
|
rect: &crate::Rect,
|
||||||
|
visible_rect: &crate::Rect,
|
||||||
object_fit: crate::ObjectFit,
|
object_fit: crate::ObjectFit,
|
||||||
pixmap: &mut Pixmap,
|
pixmap: &mut Pixmap,
|
||||||
cache: &mut std::collections::HashMap<String, Option<Vec<u8>>>,
|
cache: &mut std::collections::HashMap<String, Option<Vec<u8>>>,
|
||||||
@@ -1174,12 +1187,16 @@ fn paint_image(
|
|||||||
};
|
};
|
||||||
let Some(content) = content else { return false };
|
let Some(content) = content else { return false };
|
||||||
|
|
||||||
// `Cover`/`None` can size the image past the box; clip it to the box so it
|
// The raster may not paint past `visible_rect` (the border box already
|
||||||
// does not paint over neighboring content (CSS clips object-fit to the
|
// intersected with the ancestor overflow clip): `Cover`/`None` can size
|
||||||
// content box). `Fill`/`Contain`/`scale-down` always fit inside the box, so
|
// the image past the box, and an ancestor clip can cut into the box
|
||||||
// they take the faster unclipped path.
|
// itself. Only the fully-inside case takes the unmasked fast path.
|
||||||
let clip = if dest.width > rect.width + 0.5 || dest.height > rect.height + 0.5 {
|
let clip = if dest.width > visible_rect.width + 0.5
|
||||||
box_clip_mask(pixmap.width(), pixmap.height(), rect)
|
|| dest.height > visible_rect.height + 0.5
|
||||||
|
|| dest.x < visible_rect.x - 0.5
|
||||||
|
|| dest.y < visible_rect.y - 0.5
|
||||||
|
{
|
||||||
|
box_clip_mask(pixmap.width(), pixmap.height(), visible_rect)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user