diff --git a/components/pixels/lib.rs b/components/pixels/lib.rs index 395b32252f8..6f49b7bad52 100644 --- a/components/pixels/lib.rs +++ b/components/pixels/lib.rs @@ -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 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 { - let Ok(decoded_gif) = GifDecoder::new(Cursor::new(buffer)) else { +enum GenericImageDecoder { + Png(Box>), + Gif(Box>), + Webp(Box>), + Jpeg(Box>), + Bmp(Box>), + Ico(Box>), +} + +fn make_decoder( + format: ImageFormat, + buffer: &[u8], +) -> ImageResult>> { + 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 { + 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 +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 { // . let mut frame_data = vec![]; let mut total_number_of_bytes = 0; - let frames: Vec = decoded_gif + let frames: Vec = 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) }) diff --git a/tests/wpt/meta/MANIFEST.json b/tests/wpt/meta/MANIFEST.json index 8f8e30b8f41..38bbb5c06bc 100644 --- a/tests/wpt/meta/MANIFEST.json +++ b/tests/wpt/meta/MANIFEST.json @@ -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", [] diff --git a/tests/wpt/meta/png/apng/fcTL-acTL-ordering.html.ini b/tests/wpt/meta/png/apng/fcTL-acTL-ordering.html.ini deleted file mode 100644 index ce1f18676be..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-acTL-ordering.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-acTL-ordering.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-blend-over-repeatedly.html.ini b/tests/wpt/meta/png/apng/fcTL-blend-over-repeatedly.html.ini deleted file mode 100644 index 0a78ec8a1f6..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-blend-over-repeatedly.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-blend-over-repeatedly.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-blend-over-solid.html.ini b/tests/wpt/meta/png/apng/fcTL-blend-over-solid.html.ini deleted file mode 100644 index 60baf16c0e6..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-blend-over-solid.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-blend-over-solid.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-blend-source-solid.html.ini b/tests/wpt/meta/png/apng/fcTL-blend-source-solid.html.ini deleted file mode 100644 index 09a4db8843e..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-blend-source-solid.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-blend-source-solid.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-blend-source-transparent.html.ini b/tests/wpt/meta/png/apng/fcTL-blend-source-transparent.html.ini deleted file mode 100644 index 617b4d394d6..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-blend-source-transparent.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-blend-source-transparent.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-dispose-background-final.html.ini b/tests/wpt/meta/png/apng/fcTL-dispose-background-final.html.ini deleted file mode 100644 index 1f43155d865..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-dispose-background-final.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-dispose-background-final.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-dispose-before-region-background.html.ini b/tests/wpt/meta/png/apng/fcTL-dispose-before-region-background.html.ini deleted file mode 100644 index b6bf751063a..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-dispose-before-region-background.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-dispose-before-region-background.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-dispose-in-region-previous.html.ini b/tests/wpt/meta/png/apng/fcTL-dispose-in-region-previous.html.ini deleted file mode 100644 index 78faf0b3d95..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-dispose-in-region-previous.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-dispose-in-region-previous.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-dispose-previous-final.html.ini b/tests/wpt/meta/png/apng/fcTL-dispose-previous-final.html.ini deleted file mode 100644 index feb5b773165..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-dispose-previous-final.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-dispose-previous-final.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-dispose-previous-first.html.ini b/tests/wpt/meta/png/apng/fcTL-dispose-previous-first.html.ini deleted file mode 100644 index 9fcf8c059f2..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-dispose-previous-first.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-dispose-previous-first.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fcTL-dispose-previous.html.ini b/tests/wpt/meta/png/apng/fcTL-dispose-previous.html.ini deleted file mode 100644 index 7cfea0bb9f9..00000000000 --- a/tests/wpt/meta/png/apng/fcTL-dispose-previous.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fcTL-dispose-previous.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fdAT-1bit-PLTE.html.ini b/tests/wpt/meta/png/apng/fdAT-1bit-PLTE.html.ini deleted file mode 100644 index ef7e7278ea9..00000000000 --- a/tests/wpt/meta/png/apng/fdAT-1bit-PLTE.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fdAT-1bit-PLTE.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fdAT-2bit-PLTE-tRNS.html.ini b/tests/wpt/meta/png/apng/fdAT-2bit-PLTE-tRNS.html.ini deleted file mode 100644 index 6a13abde944..00000000000 --- a/tests/wpt/meta/png/apng/fdAT-2bit-PLTE-tRNS.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fdAT-2bit-PLTE-tRNS.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fdAT-8bit-gray.html.ini b/tests/wpt/meta/png/apng/fdAT-8bit-gray.html.ini deleted file mode 100644 index 2cbf3d50e06..00000000000 --- a/tests/wpt/meta/png/apng/fdAT-8bit-gray.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fdAT-8bit-gray.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fdAT-split-basic.html.ini b/tests/wpt/meta/png/apng/fdAT-split-basic.html.ini deleted file mode 100644 index 454d7b5a91a..00000000000 --- a/tests/wpt/meta/png/apng/fdAT-split-basic.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fdAT-split-basic.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/fdAT-split-zero-length.html.ini b/tests/wpt/meta/png/apng/fdAT-split-zero-length.html.ini deleted file mode 100644 index e60f49f6564..00000000000 --- a/tests/wpt/meta/png/apng/fdAT-split-zero-length.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[fdAT-split-zero-length.html] - expected: FAIL diff --git a/tests/wpt/meta/png/apng/first-frame-not-IDAT.html.ini b/tests/wpt/meta/png/apng/first-frame-not-IDAT.html.ini deleted file mode 100644 index cc776779f30..00000000000 --- a/tests/wpt/meta/png/apng/first-frame-not-IDAT.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[first-frame-not-IDAT.html] - expected: FAIL diff --git a/tests/wpt/tests/html/semantics/embedded-content/the-img-element/animated-webp-update.tentative.html b/tests/wpt/tests/html/semantics/embedded-content/the-img-element/animated-webp-update.tentative.html new file mode 100644 index 00000000000..d82d8302052 --- /dev/null +++ b/tests/wpt/tests/html/semantics/embedded-content/the-img-element/animated-webp-update.tentative.html @@ -0,0 +1,21 @@ + + + + Animated image active frame update when passing delay time + + + + + + + + + + diff --git a/tests/wpt/tests/html/semantics/embedded-content/the-img-element/resources/animated.webp b/tests/wpt/tests/html/semantics/embedded-content/the-img-element/resources/animated.webp new file mode 100644 index 00000000000..ebe15f88496 Binary files /dev/null and b/tests/wpt/tests/html/semantics/embedded-content/the-img-element/resources/animated.webp differ