mirror of
https://github.com/servo/servo.git
synced 2025-07-16 11:53:39 +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 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)
|
||||||
})
|
})
|
||||||
|
|
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": [
|
"available-images.html": [
|
||||||
"779ff978689e4f5565b8d45d383efa75ac78b8b2",
|
"779ff978689e4f5565b8d45d383efa75ac78b8b2",
|
||||||
[
|
[
|
||||||
|
@ -485994,6 +486007,10 @@
|
||||||
"7694add55e0c98ec3ee5d9110e5fb16b4d819137",
|
"7694add55e0c98ec3ee5d9110e5fb16b4d819137",
|
||||||
[]
|
[]
|
||||||
],
|
],
|
||||||
|
"animated.webp": [
|
||||||
|
"ebe15f88496fd03725df2445034ef0f400af652c",
|
||||||
|
[]
|
||||||
|
],
|
||||||
"blue-10.png": [
|
"blue-10.png": [
|
||||||
"62949e08d87dfcdc0987eaef67692c7a1c16aa50",
|
"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