mirror of
https://github.com/servo/servo.git
synced 2025-07-16 03:43:38 +01:00
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:
parent
ff050b71fa
commit
f88dd2a12c
21 changed files with 151 additions and 74 deletions
|
@ -11,9 +11,14 @@ use std::time::Duration;
|
|||
use std::{cmp, fmt, vec};
|
||||
|
||||
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::{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 log::debug;
|
||||
use malloc_size_of_derive::MallocSizeOf;
|
||||
|
@ -352,35 +357,39 @@ pub fn load_from_memory(buffer: &[u8], cors_status: CorsStatus) -> Option<Raster
|
|||
debug!("{}", msg);
|
||||
None
|
||||
},
|
||||
Ok(format) => match format {
|
||||
ImageFormat::Gif => decode_gif(buffer, cors_status),
|
||||
_ => match image::load_from_memory(buffer) {
|
||||
Ok(image) => {
|
||||
let mut rgba = 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,
|
||||
})
|
||||
Ok(format) => {
|
||||
let Ok(image_decoder) = make_decoder(format, buffer) else {
|
||||
return None;
|
||||
};
|
||||
match image_decoder {
|
||||
GenericImageDecoder::Png(png_decoder) => {
|
||||
if png_decoder.is_apng() {
|
||||
let apng_decoder = png_decoder.apng();
|
||||
decode_animated_image(cors_status, apng_decoder)
|
||||
} else {
|
||||
decode_static_image(cors_status, *png_decoder)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
debug!("Image decoding error: {:?}", e);
|
||||
None
|
||||
GenericImageDecoder::Gif(animation_decoder) => {
|
||||
decode_animated_image(cors_status, *animation_decoder)
|
||||
},
|
||||
},
|
||||
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"
|
||||
}
|
||||
|
||||
fn decode_gif(buffer: &[u8], cors_status: CorsStatus) -> Option<RasterImage> {
|
||||
let Ok(decoded_gif) = GifDecoder::new(Cursor::new(buffer)) else {
|
||||
enum GenericImageDecoder<R: std::io::BufRead + std::io::Seek> {
|
||||
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;
|
||||
};
|
||||
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 height = 0;
|
||||
|
||||
|
@ -540,34 +613,34 @@ fn decode_gif(buffer: &[u8], cors_status: CorsStatus) -> Option<RasterImage> {
|
|||
// <https://github.com/image-rs/image/issues/2442>.
|
||||
let mut frame_data = vec![];
|
||||
let mut total_number_of_bytes = 0;
|
||||
let frames: Vec<ImageFrame> = decoded_gif
|
||||
let frames: Vec<ImageFrame> = animated_image_decoder
|
||||
.into_frames()
|
||||
.map_while(|decoded_frame| {
|
||||
let mut gif_frame = match decoded_frame {
|
||||
let mut animated_frame = match decoded_frame {
|
||||
Ok(decoded_frame) => decoded_frame,
|
||||
Err(error) => {
|
||||
debug!("decode GIF frame error: {error}");
|
||||
debug!("decode Animated frame error: {error}");
|
||||
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;
|
||||
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.
|
||||
let frame_width = gif_frame.buffer().width();
|
||||
let frame_height = gif_frame.buffer().height();
|
||||
let frame_width = animated_frame.buffer().width();
|
||||
let frame_height = animated_frame.buffer().height();
|
||||
width = cmp::max(width, frame_width);
|
||||
height = cmp::max(height, frame_height);
|
||||
|
||||
let frame = ImageFrame {
|
||||
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,
|
||||
height: frame_height,
|
||||
};
|
||||
|
||||
frame_data.push(gif_frame);
|
||||
frame_data.push(animated_frame);
|
||||
|
||||
Some(frame)
|
||||
})
|
||||
|
|
17
tests/wpt/meta/MANIFEST.json
vendored
17
tests/wpt/meta/MANIFEST.json
vendored
|
@ -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": [
|
||||
"779ff978689e4f5565b8d45d383efa75ac78b8b2",
|
||||
[
|
||||
|
@ -485994,6 +486007,10 @@
|
|||
"7694add55e0c98ec3ee5d9110e5fb16b4d819137",
|
||||
[]
|
||||
],
|
||||
"animated.webp": [
|
||||
"ebe15f88496fd03725df2445034ef0f400af652c",
|
||||
[]
|
||||
],
|
||||
"blue-10.png": [
|
||||
"62949e08d87dfcdc0987eaef67692c7a1c16aa50",
|
||||
[]
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-acTL-ordering.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-blend-over-repeatedly.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-blend-over-solid.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-blend-source-solid.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-blend-source-transparent.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-dispose-background-final.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-dispose-before-region-background.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-dispose-in-region-previous.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-dispose-previous-final.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-dispose-previous-first.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fcTL-dispose-previous.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fdAT-1bit-PLTE.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fdAT-2bit-PLTE-tRNS.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fdAT-8bit-gray.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fdAT-split-basic.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[fdAT-split-zero-length.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[first-frame-not-IDAT.html]
|
||||
expected: FAIL
|
|
@ -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>
|
BIN
tests/wpt/tests/html/semantics/embedded-content/the-img-element/resources/animated.webp
vendored
Normal file
BIN
tests/wpt/tests/html/semantics/embedded-content/the-img-element/resources/animated.webp
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 152 B |
Loading…
Add table
Add a link
Reference in a new issue