add support for apng and webp animated image decoding (#37637)

Add support for APNG animated image decoding. Rework `load_from_memory`
image decoding api, to handle all the image format that currently
supported.

Testing: This change should allow `apng` and `webp` format image start
moving, and should make some WPT test related to APNG pass.

Partially address: https://github.com/servo/servo/issues/37493

[wpt try
run](https://github.com/rayguo17/servo/actions/runs/15840339570)

cc @xiaochengh

Signed-off-by: rayguo17 <rayguo17@gmail.com>
This commit is contained in:
TIN TUN AUNG 2025-07-10 16:54:16 +08:00 committed by GitHub
parent ff050b71fa
commit f88dd2a12c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 151 additions and 74 deletions

View file

@ -11,9 +11,14 @@ use std::time::Duration;
use std::{cmp, fmt, vec}; use std::{cmp, fmt, vec};
use euclid::default::{Point2D, Rect, Size2D}; use euclid::default::{Point2D, Rect, Size2D};
use image::codecs::gif::GifDecoder; use image::codecs::{bmp, gif, ico, jpeg, png, webp};
use image::error::ImageFormatHint;
use image::imageops::{self, FilterType}; use image::imageops::{self, FilterType};
use image::{AnimationDecoder as _, ImageBuffer, ImageFormat, Rgba}; use image::io::Limits;
use image::{
AnimationDecoder, DynamicImage, ImageBuffer, ImageDecoder, ImageError, ImageFormat,
ImageResult, Rgba,
};
use ipc_channel::ipc::IpcSharedMemory; use ipc_channel::ipc::IpcSharedMemory;
use log::debug; use log::debug;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
@ -352,35 +357,39 @@ pub fn load_from_memory(buffer: &[u8], cors_status: CorsStatus) -> Option<Raster
debug!("{}", msg); debug!("{}", msg);
None None
}, },
Ok(format) => match format { Ok(format) => {
ImageFormat::Gif => decode_gif(buffer, cors_status), let Ok(image_decoder) = make_decoder(format, buffer) else {
_ => match image::load_from_memory(buffer) { return None;
Ok(image) => { };
let mut rgba = image.into_rgba8(); match image_decoder {
rgba8_byte_swap_colors_inplace(&mut rgba); GenericImageDecoder::Png(png_decoder) => {
let frame = ImageFrame { if png_decoder.is_apng() {
delay: None, let apng_decoder = png_decoder.apng();
byte_range: 0..rgba.len(), decode_animated_image(cors_status, apng_decoder)
width: rgba.width(), } else {
height: rgba.height(), decode_static_image(cors_status, *png_decoder)
}; }
Some(RasterImage {
metadata: ImageMetadata {
width: rgba.width(),
height: rgba.height(),
},
format: PixelFormat::BGRA8,
frames: vec![frame],
bytes: IpcSharedMemory::from_bytes(&rgba),
id: None,
cors_status,
})
}, },
Err(e) => { GenericImageDecoder::Gif(animation_decoder) => {
debug!("Image decoding error: {:?}", e); decode_animated_image(cors_status, *animation_decoder)
None
}, },
}, GenericImageDecoder::Webp(webp_decoder) => {
if webp_decoder.has_animation() {
decode_animated_image(cors_status, *webp_decoder)
} else {
decode_static_image(cors_status, *webp_decoder)
}
},
GenericImageDecoder::Bmp(image_decoder) => {
decode_static_image(cors_status, *image_decoder)
},
GenericImageDecoder::Jpeg(image_decoder) => {
decode_static_image(cors_status, *image_decoder)
},
GenericImageDecoder::Ico(image_decoder) => {
decode_static_image(cors_status, *image_decoder)
},
}
}, },
} }
} }
@ -528,10 +537,74 @@ fn is_webp(buffer: &[u8]) -> bool {
buffer[8..].len() >= len && &buffer[8..12] == b"WEBP" buffer[8..].len() >= len && &buffer[8..12] == b"WEBP"
} }
fn decode_gif(buffer: &[u8], cors_status: CorsStatus) -> Option<RasterImage> { enum GenericImageDecoder<R: std::io::BufRead + std::io::Seek> {
let Ok(decoded_gif) = GifDecoder::new(Cursor::new(buffer)) else { Png(Box<png::PngDecoder<R>>),
Gif(Box<gif::GifDecoder<R>>),
Webp(Box<webp::WebPDecoder<R>>),
Jpeg(Box<jpeg::JpegDecoder<R>>),
Bmp(Box<bmp::BmpDecoder<R>>),
Ico(Box<ico::IcoDecoder<R>>),
}
fn make_decoder(
format: ImageFormat,
buffer: &[u8],
) -> ImageResult<GenericImageDecoder<Cursor<&[u8]>>> {
let limits = Limits::default();
let reader = Cursor::new(buffer);
Ok(match format {
ImageFormat::Png => {
GenericImageDecoder::Png(Box::new(png::PngDecoder::with_limits(reader, limits)?))
},
ImageFormat::Gif => GenericImageDecoder::Gif(Box::new(gif::GifDecoder::new(reader)?)),
ImageFormat::WebP => GenericImageDecoder::Webp(Box::new(webp::WebPDecoder::new(reader)?)),
ImageFormat::Jpeg => GenericImageDecoder::Jpeg(Box::new(jpeg::JpegDecoder::new(reader)?)),
ImageFormat::Bmp => GenericImageDecoder::Bmp(Box::new(bmp::BmpDecoder::new(reader)?)),
ImageFormat::Ico => GenericImageDecoder::Ico(Box::new(ico::IcoDecoder::new(reader)?)),
_ => {
return Err(ImageError::Unsupported(
ImageFormatHint::Exact(format).into(),
));
},
})
}
fn decode_static_image<'a>(
cors_status: CorsStatus,
image_decoder: impl ImageDecoder<'a>,
) -> Option<RasterImage> {
let Ok(dynamic_image) = DynamicImage::from_decoder(image_decoder) else {
debug!("Image decoding error");
return None; return None;
}; };
let mut rgba = dynamic_image.into_rgba8();
rgba8_byte_swap_colors_inplace(&mut rgba);
let frame = ImageFrame {
delay: None,
byte_range: 0..rgba.len(),
width: rgba.width(),
height: rgba.height(),
};
Some(RasterImage {
metadata: ImageMetadata {
width: rgba.width(),
height: rgba.height(),
},
format: PixelFormat::BGRA8,
frames: vec![frame],
bytes: IpcSharedMemory::from_bytes(&rgba),
id: None,
cors_status,
})
}
fn decode_animated_image<'a, T>(
cors_status: CorsStatus,
animated_image_decoder: T,
) -> Option<RasterImage>
where
T: AnimationDecoder<'a>,
{
let mut width = 0; let mut width = 0;
let mut height = 0; let mut height = 0;
@ -540,34 +613,34 @@ fn decode_gif(buffer: &[u8], cors_status: CorsStatus) -> Option<RasterImage> {
// <https://github.com/image-rs/image/issues/2442>. // <https://github.com/image-rs/image/issues/2442>.
let mut frame_data = vec![]; let mut frame_data = vec![];
let mut total_number_of_bytes = 0; let mut total_number_of_bytes = 0;
let frames: Vec<ImageFrame> = decoded_gif let frames: Vec<ImageFrame> = animated_image_decoder
.into_frames() .into_frames()
.map_while(|decoded_frame| { .map_while(|decoded_frame| {
let mut gif_frame = match decoded_frame { let mut animated_frame = match decoded_frame {
Ok(decoded_frame) => decoded_frame, Ok(decoded_frame) => decoded_frame,
Err(error) => { Err(error) => {
debug!("decode GIF frame error: {error}"); debug!("decode Animated frame error: {error}");
return None; return None;
}, },
}; };
rgba8_byte_swap_colors_inplace(gif_frame.buffer_mut()); rgba8_byte_swap_colors_inplace(animated_frame.buffer_mut());
let frame_start = total_number_of_bytes; let frame_start = total_number_of_bytes;
total_number_of_bytes += gif_frame.buffer().len(); total_number_of_bytes += animated_frame.buffer().len();
// The image size should be at least as large as the largest frame. // The image size should be at least as large as the largest frame.
let frame_width = gif_frame.buffer().width(); let frame_width = animated_frame.buffer().width();
let frame_height = gif_frame.buffer().height(); let frame_height = animated_frame.buffer().height();
width = cmp::max(width, frame_width); width = cmp::max(width, frame_width);
height = cmp::max(height, frame_height); height = cmp::max(height, frame_height);
let frame = ImageFrame { let frame = ImageFrame {
byte_range: frame_start..total_number_of_bytes, byte_range: frame_start..total_number_of_bytes,
delay: Some(Duration::from(gif_frame.delay())), delay: Some(Duration::from(animated_frame.delay())),
width: frame_width, width: frame_width,
height: frame_height, height: frame_height,
}; };
frame_data.push(gif_frame); frame_data.push(animated_frame);
Some(frame) Some(frame)
}) })

View file

@ -357193,6 +357193,19 @@
{} {}
] ]
], ],
"animated-webp-update.tentative.html": [
"d82d830205270d4bf32de3f01f3fe69afb44fad3",
[
"html/semantics/embedded-content/the-img-element/animated-webp-update.tentative.html",
[
[
"/html/semantics/embedded-content/the-img-element/animated-image-update-ref.tentative.html",
"=="
]
],
{}
]
],
"available-images.html": [ "available-images.html": [
"779ff978689e4f5565b8d45d383efa75ac78b8b2", "779ff978689e4f5565b8d45d383efa75ac78b8b2",
[ [
@ -485994,6 +486007,10 @@
"7694add55e0c98ec3ee5d9110e5fb16b4d819137", "7694add55e0c98ec3ee5d9110e5fb16b4d819137",
[] []
], ],
"animated.webp": [
"ebe15f88496fd03725df2445034ef0f400af652c",
[]
],
"blue-10.png": [ "blue-10.png": [
"62949e08d87dfcdc0987eaef67692c7a1c16aa50", "62949e08d87dfcdc0987eaef67692c7a1c16aa50",
[] []

View file

@ -1,2 +0,0 @@
[fcTL-acTL-ordering.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-blend-over-repeatedly.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-blend-over-solid.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-blend-source-solid.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-blend-source-transparent.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-dispose-background-final.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-dispose-before-region-background.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-dispose-in-region-previous.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-dispose-previous-final.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-dispose-previous-first.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fcTL-dispose-previous.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fdAT-1bit-PLTE.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fdAT-2bit-PLTE-tRNS.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fdAT-8bit-gray.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fdAT-split-basic.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fdAT-split-zero-length.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[first-frame-not-IDAT.html]
expected: FAIL

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html class="reftest-wait">
<head>
<title>Animated image active frame update when passing delay time</title>
<link rel="match" href="animated-image-update-ref.tentative.html" />
<link rel="author" title="Tin Tun Aung" href="mailto:rayguo17@gmail.com" />
<script src="/common/reftest-wait.js"></script>
</head>
<body>
<img id="image" src="resources/animated.webp" />
<script>
const image = document.getElementById("image");
requestAnimationFrame(() => {
setTimeout(() => {
takeScreenshot();
}, 150);
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B