imagebitmap: Crop bitmap data with formatting (#37397)

Follow the ImageBitmap specification and make cropping of the bitmap
data to the source rectangle with formatting:
https://html.spec.whatwg.org/multipage/#cropped-to-the-source-rectangle-with-formatting

For now the next functionality not implemented:
- image orientation support (such as EXIF metadata)
- color space conversion (image, blob)

The convertion from ResizeQuality to "image" FilterType:
 - pixelated/low/medium/high -> Nearest/Triangle/CatmullRom/Lanczos3

Other browsers use the following sample filtering:
 - chromium (skia): Nearest/Linear/Linear/CatmullRom
 - firefox (skia): Lanczos3

Testing: Improvements in the following WPT tests
 - html/canvas/element/manual/imagebitmap/*

Fixes (partially): #34112

Signed-off-by: Andrei Volykhin <andrei.volykhin@gmail.com>
This commit is contained in:
Andrei Volykhin 2025-06-16 15:09:04 +03:00 committed by GitHub
parent 0f61361e27
commit bcade589e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 441 additions and 164 deletions

View file

@ -10,13 +10,26 @@ use std::{cmp, fmt, vec};
use euclid::default::{Point2D, Rect, Size2D};
use image::codecs::gif::GifDecoder;
use image::{AnimationDecoder as _, ImageFormat};
use image::imageops::{self, FilterType};
use image::{AnimationDecoder as _, ImageBuffer, ImageFormat, Rgba};
use ipc_channel::ipc::IpcSharedMemory;
use log::debug;
use malloc_size_of_derive::MallocSizeOf;
use serde::{Deserialize, Serialize};
use webrender_api::ImageKey;
#[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
pub enum FilterQuality {
/// No image interpolation (Nearest-neighbor)
None,
/// Low-quality image interpolation (Bilinear)
Low,
/// Medium-quality image interpolation (CatmullRom, Mitchell)
Medium,
/// High-quality image interpolation (Lanczos)
High,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
pub enum PixelFormat {
/// Luminance channel only
@ -31,8 +44,8 @@ pub enum PixelFormat {
BGRA8,
}
// Computes image byte length, returning None if overflow occurred or the total length exceeds
// the maximum image allocation size.
/// Computes image byte length, returning None if overflow occurred or the total length exceeds
/// the maximum image allocation size.
pub fn compute_rgba8_byte_length_if_within_limit(width: usize, height: usize) -> Option<usize> {
// Maximum allowed image allocation size (2^31-1 ~ 2GB).
const MAX_IMAGE_BYTE_LENGTH: usize = 2147483647;
@ -45,6 +58,89 @@ pub fn compute_rgba8_byte_length_if_within_limit(width: usize, height: usize) ->
.filter(|v| *v <= MAX_IMAGE_BYTE_LENGTH)
}
/// Copies the rectangle of the source image to the destination image.
pub fn copy_rgba8_image(
src_size: Size2D<u32>,
src_rect: Rect<u32>,
src_pixels: &[u8],
dest_size: Size2D<u32>,
dest_rect: Rect<u32>,
dest_pixels: &mut [u8],
) {
assert!(!src_rect.is_empty());
assert!(!dest_rect.is_empty());
assert!(Rect::from_size(src_size).contains_rect(&src_rect));
assert!(Rect::from_size(dest_size).contains_rect(&dest_rect));
assert!(src_rect.size == dest_rect.size);
assert_eq!(src_pixels.len() % 4, 0);
assert_eq!(dest_pixels.len() % 4, 0);
if src_size == dest_size && src_rect == dest_rect {
dest_pixels.copy_from_slice(src_pixels);
return;
}
let src_first_column_start = src_rect.origin.x as usize * 4;
let src_row_length = src_size.width as usize * 4;
let src_first_row_start = src_rect.origin.y as usize * src_row_length;
let dest_first_column_start = dest_rect.origin.x as usize * 4;
let dest_row_length = dest_size.width as usize * 4;
let dest_first_row_start = dest_rect.origin.y as usize * dest_row_length;
let (chunk_length, chunk_count) = (
src_rect.size.width as usize * 4,
src_rect.size.height as usize,
);
for i in 0..chunk_count {
let src = &src_pixels[src_first_row_start + i * src_row_length..][src_first_column_start..]
[..chunk_length];
let dest = &mut dest_pixels[dest_first_row_start + i * dest_row_length..]
[dest_first_column_start..][..chunk_length];
dest.copy_from_slice(src);
}
}
/// Scales the source image to the required size, performing sampling filter algorithm.
pub fn scale_rgba8_image(
size: Size2D<u32>,
pixels: &[u8],
required_size: Size2D<u32>,
quality: FilterQuality,
) -> Option<Vec<u8>> {
let filter = match quality {
FilterQuality::None => FilterType::Nearest,
FilterQuality::Low => FilterType::Triangle,
FilterQuality::Medium => FilterType::CatmullRom,
FilterQuality::High => FilterType::Lanczos3,
};
let buffer: ImageBuffer<Rgba<u8>, &[u8]> =
ImageBuffer::from_raw(size.width, size.height, pixels)?;
let scaled_buffer =
imageops::resize(&buffer, required_size.width, required_size.height, filter);
Some(scaled_buffer.into_vec())
}
/// Flips the source image vertically in place.
pub fn flip_y_rgba8_image_inplace(size: Size2D<u32>, pixels: &mut [u8]) {
assert_eq!(pixels.len() % 4, 0);
let row_length = size.width as usize * 4;
let half_height = (size.height / 2) as usize;
let (left, right) = pixels.split_at_mut(pixels.len() - row_length * half_height);
for i in 0..half_height {
let top = &mut left[i * row_length..][..row_length];
let bottom = &mut right[(half_height - i - 1) * row_length..][..row_length];
top.swap_with_slice(bottom);
}
}
pub fn rgba8_get_rect(pixels: &[u8], size: Size2D<u32>, rect: Rect<u32>) -> Cow<[u8]> {
assert!(!rect.is_empty());
assert!(Rect::from_size(size).contains_rect(&rect));