pixels: Ensure expected formats when accesing bytes of snapshot (#37767)

I introduced snapshot in #36119 to pack raw bytes and metadata together,
now we take the next step and require for user to always specify what
kind of byte data they want when calling `as_bytes` or `to_vec`
(basically joining transform and data). There are also valid usages when
one might require just one property of bytes (textures can generally
handle both RGBA and BGRA). There are also valid usages of using just
raw bytes (when cropping). This PR tries to make such usages more
obvious.

This will make it easier to fix stuff around 2d canvas (we do not want
to assume any bytes properties in abstraction).

Testing: Code is covered by WPT tests.

---------

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
sagudev 2025-07-03 17:02:41 +02:00 committed by GitHub
parent e3baec4807
commit a631b42e60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 142 additions and 94 deletions

View file

@ -17,7 +17,7 @@ use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER; use ipc_channel::router::ROUTER;
use log::warn; use log::warn;
use net_traits::ResourceThreads; use net_traits::ResourceThreads;
use pixels::Snapshot; use pixels::{Snapshot, SnapshotPixelFormat};
use style::color::AbsoluteColor; use style::color::AbsoluteColor;
use style::properties::style_structs::Font as FontStyleStruct; use style::properties::style_structs::Font as FontStyleStruct;
use webrender_api::ImageKey; use webrender_api::ImageKey;
@ -175,14 +175,17 @@ impl<'a> CanvasPaintThread<'a> {
.canvas(canvas_id) .canvas(canvas_id)
.is_point_in_path_(&path[..], x, y, fill_rule, chan), .is_point_in_path_(&path[..], x, y, fill_rule, chan),
Canvas2dMsg::DrawImage(snapshot, dest_rect, source_rect, smoothing_enabled) => { Canvas2dMsg::DrawImage(snapshot, dest_rect, source_rect, smoothing_enabled) => {
let snapshot = snapshot.to_owned(); let mut snapshot = snapshot.to_owned();
let size = snapshot.size();
let (data, alpha_mode, _) =
snapshot.as_bytes(None, Some(SnapshotPixelFormat::BGRA));
self.canvas(canvas_id).draw_image( self.canvas(canvas_id).draw_image(
snapshot.data(), data,
snapshot.size(), size,
dest_rect, dest_rect,
source_rect, source_rect,
smoothing_enabled, smoothing_enabled,
!snapshot.alpha_mode().is_premultiplied(), alpha_mode.alpha().needs_alpha_multiplication(),
) )
}, },
Canvas2dMsg::DrawEmptyImage(image_size, dest_rect, source_rect) => { Canvas2dMsg::DrawEmptyImage(image_size, dest_rect, source_rect) => {
@ -202,16 +205,18 @@ impl<'a> CanvasPaintThread<'a> {
source_rect, source_rect,
smoothing, smoothing,
) => { ) => {
let image_data = self let mut snapshot = self
.canvas(canvas_id) .canvas(canvas_id)
.read_pixels(Some(source_rect.to_u32()), Some(image_size)); .read_pixels(Some(source_rect.to_u32()), Some(image_size));
let (data, alpha_mode, _) =
snapshot.as_bytes(None, Some(SnapshotPixelFormat::BGRA));
self.canvas(other_canvas_id).draw_image( self.canvas(other_canvas_id).draw_image(
image_data.data(), data,
source_rect.size.to_u32(), source_rect.size.to_u32(),
dest_rect, dest_rect,
source_rect, source_rect,
smoothing, smoothing,
false, alpha_mode.alpha().needs_alpha_multiplication(),
); );
}, },
Canvas2dMsg::MoveTo(ref point) => self.canvas(canvas_id).move_to(point), Canvas2dMsg::MoveTo(ref point) => self.canvas(canvas_id).move_to(point),
@ -403,7 +408,7 @@ impl Canvas<'_> {
dest_rect: Rect<f64>, dest_rect: Rect<f64>,
source_rect: Rect<f64>, source_rect: Rect<f64>,
smoothing_enabled: bool, smoothing_enabled: bool,
is_premultiplied: bool, premultiply: bool,
) { ) {
match self { match self {
Canvas::Raqote(canvas_data) => canvas_data.draw_image( Canvas::Raqote(canvas_data) => canvas_data.draw_image(
@ -412,7 +417,7 @@ impl Canvas<'_> {
dest_rect, dest_rect,
source_rect, source_rect,
smoothing_enabled, smoothing_enabled,
is_premultiplied, premultiply,
), ),
} }
} }

View file

@ -18,6 +18,33 @@ pub enum SnapshotPixelFormat {
BGRA, BGRA,
} }
#[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
pub enum Alpha {
Premultiplied,
NotPremultiplied,
/// This is used for opaque textures for which the presence of alpha in the
/// output data format does not matter.
DontCare,
}
impl Alpha {
pub const fn from_premultiplied(is_premultiplied: bool) -> Self {
if is_premultiplied {
Self::Premultiplied
} else {
Self::NotPremultiplied
}
}
pub const fn needs_alpha_multiplication(&self) -> bool {
match self {
Alpha::Premultiplied => false,
Alpha::NotPremultiplied => true,
Alpha::DontCare => false,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
pub enum SnapshotAlphaMode { pub enum SnapshotAlphaMode {
/// Internal data is opaque (alpha is cleared to 1) /// Internal data is opaque (alpha is cleared to 1)
@ -37,20 +64,17 @@ impl Default for SnapshotAlphaMode {
} }
impl SnapshotAlphaMode { impl SnapshotAlphaMode {
pub const fn is_premultiplied(&self) -> bool { pub const fn alpha(&self) -> Alpha {
match self { match self {
SnapshotAlphaMode::Opaque => true, SnapshotAlphaMode::Opaque => Alpha::DontCare,
SnapshotAlphaMode::AsOpaque { premultiplied } => *premultiplied, SnapshotAlphaMode::AsOpaque { premultiplied } => {
SnapshotAlphaMode::Transparent { premultiplied } => *premultiplied, Alpha::from_premultiplied(*premultiplied)
},
SnapshotAlphaMode::Transparent { premultiplied } => {
Alpha::from_premultiplied(*premultiplied)
},
} }
} }
pub const fn is_opaque(&self) -> bool {
matches!(
self,
SnapshotAlphaMode::Opaque | SnapshotAlphaMode::AsOpaque { .. }
)
}
} }
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] #[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
@ -112,14 +136,6 @@ impl<T> Snapshot<T> {
pub const fn alpha_mode(&self) -> SnapshotAlphaMode { pub const fn alpha_mode(&self) -> SnapshotAlphaMode {
self.alpha_mode self.alpha_mode
} }
pub const fn is_premultiplied(&self) -> bool {
self.alpha_mode().is_premultiplied()
}
pub const fn is_opaque(&self) -> bool {
self.alpha_mode().is_opaque()
}
} }
impl Snapshot<SnapshotData> { impl Snapshot<SnapshotData> {
@ -181,14 +197,6 @@ impl Snapshot<SnapshotData> {
} }
*/ */
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn data_mut(&mut self) -> &mut [u8] {
&mut self.data
}
/// Convert inner data of snapshot to target format and alpha mode. /// Convert inner data of snapshot to target format and alpha mode.
/// If data is already in target format and alpha mode no work will be done. /// If data is already in target format and alpha mode no work will be done.
pub fn transform( pub fn transform(
@ -200,7 +208,7 @@ impl Snapshot<SnapshotData> {
let multiply = match (self.alpha_mode, target_alpha_mode) { let multiply = match (self.alpha_mode, target_alpha_mode) {
(SnapshotAlphaMode::Opaque, _) => Multiply::None, (SnapshotAlphaMode::Opaque, _) => Multiply::None,
(alpha_mode, SnapshotAlphaMode::Opaque) => { (alpha_mode, SnapshotAlphaMode::Opaque) => {
if alpha_mode.is_premultiplied() { if alpha_mode.alpha() == Alpha::Premultiplied {
Multiply::UnMultiply Multiply::UnMultiply
} else { } else {
Multiply::None Multiply::None
@ -232,6 +240,37 @@ impl Snapshot<SnapshotData> {
self.format = target_format; self.format = target_format;
} }
pub fn as_raw_bytes(&self) -> &[u8] {
&self.data
}
pub fn as_raw_bytes_mut(&mut self) -> &mut [u8] {
&mut self.data
}
pub fn as_bytes(
&mut self,
target_alpha_mode: Option<SnapshotAlphaMode>,
target_format: Option<SnapshotPixelFormat>,
) -> (&mut [u8], SnapshotAlphaMode, SnapshotPixelFormat) {
let target_alpha_mode = target_alpha_mode.unwrap_or(self.alpha_mode);
let target_format = target_format.unwrap_or(self.format);
self.transform(target_alpha_mode, target_format);
(&mut self.data, target_alpha_mode, target_format)
}
pub fn to_vec(
mut self,
target_alpha_mode: Option<SnapshotAlphaMode>,
target_format: Option<SnapshotPixelFormat>,
) -> (Vec<u8>, SnapshotAlphaMode, SnapshotPixelFormat) {
let target_alpha_mode = target_alpha_mode.unwrap_or(self.alpha_mode);
let target_format = target_format.unwrap_or(self.format);
self.transform(target_alpha_mode, target_format);
let SnapshotData::Owned(data) = self.data;
(data, target_alpha_mode, target_format)
}
pub fn as_ipc(self) -> Snapshot<IpcSharedMemory> { pub fn as_ipc(self) -> Snapshot<IpcSharedMemory> {
let Snapshot { let Snapshot {
size, size,
@ -250,12 +289,6 @@ impl Snapshot<SnapshotData> {
alpha_mode, alpha_mode,
} }
} }
pub fn to_vec(self) -> Vec<u8> {
match self.data {
SnapshotData::Owned(data) => data,
}
}
} }
impl Snapshot<IpcSharedMemory> { impl Snapshot<IpcSharedMemory> {

View file

@ -394,14 +394,15 @@ impl CanvasState {
let (sender, receiver) = ipc::channel().unwrap(); let (sender, receiver) = ipc::channel().unwrap();
self.send_canvas_2d_msg(Canvas2dMsg::GetImageData(rect, canvas_size, sender)); self.send_canvas_2d_msg(Canvas2dMsg::GetImageData(rect, canvas_size, sender));
let mut snapshot = receiver.recv().unwrap().to_owned(); let snapshot = receiver.recv().unwrap().to_owned();
snapshot.transform( snapshot
SnapshotAlphaMode::Transparent { .to_vec(
Some(SnapshotAlphaMode::Transparent {
premultiplied: false, premultiplied: false,
}, }),
SnapshotPixelFormat::RGBA, Some(SnapshotPixelFormat::RGBA),
); )
snapshot.to_vec() .0
} }
/// ///
@ -1177,7 +1178,14 @@ impl CanvasState {
let size = snapshot.size(); let size = snapshot.size();
Ok(Some(CanvasPattern::new( Ok(Some(CanvasPattern::new(
global, global,
snapshot.to_vec(), snapshot
.to_vec(
Some(SnapshotAlphaMode::Transparent {
premultiplied: true,
}),
Some(SnapshotPixelFormat::BGRA),
)
.0, // TODO: send snapshot
size.cast(), size.cast(),
rep, rep,
self.is_origin_clean(image), self.is_origin_clean(image),

View file

@ -390,16 +390,27 @@ impl HTMLCanvasElement {
&self, &self,
image_type: &EncodedImageType, image_type: &EncodedImageType,
quality: Option<f64>, quality: Option<f64>,
snapshot: &Snapshot, mut snapshot: Snapshot,
encoder: &mut W, encoder: &mut W,
) -> Result<(), ImageError> { ) -> Result<(), ImageError> {
// We can't use self.Width() or self.Height() here, since the size of the canvas // We can't use self.Width() or self.Height() here, since the size of the canvas
// may have changed since the snapshot was created. Truncating the dimensions to a // may have changed since the snapshot was created. Truncating the dimensions to a
// u32 can't panic, since the data comes from a canvas which is always smaller than // u32 can't panic, since the data comes from a canvas which is always smaller than
// u32::MAX. // u32::MAX.
let canvas_data = snapshot.data();
let width = snapshot.size().width; let width = snapshot.size().width;
let height = snapshot.size().height; let height = snapshot.size().height;
let (canvas_data, _, _) = snapshot.as_bytes(
if *image_type == EncodedImageType::Jpeg {
Some(SnapshotAlphaMode::AsOpaque {
premultiplied: true,
})
} else {
Some(SnapshotAlphaMode::Transparent {
premultiplied: false,
})
},
Some(SnapshotPixelFormat::RGBA),
);
match image_type { match image_type {
EncodedImageType::Png => { EncodedImageType::Png => {
@ -537,23 +548,12 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
// Step 3: Let file be a serialization of this canvas element's bitmap as a file, // Step 3: Let file be a serialization of this canvas element's bitmap as a file,
// passing type and quality if given. // passing type and quality if given.
let Some(mut snapshot) = self.get_image_data() else { let Some(snapshot) = self.get_image_data() else {
return Ok(USVString("data:,".into())); return Ok(USVString("data:,".into()));
}; };
let image_type = EncodedImageType::from(mime_type); let image_type = EncodedImageType::from(mime_type);
snapshot.transform(
if image_type == EncodedImageType::Jpeg {
SnapshotAlphaMode::AsOpaque {
premultiplied: true,
}
} else {
SnapshotAlphaMode::Transparent {
premultiplied: false,
}
},
SnapshotPixelFormat::RGBA,
);
let mut url = format!("data:{};base64,", image_type.as_mime_type()); let mut url = format!("data:{};base64,", image_type.as_mime_type());
let mut encoder = base64::write::EncoderStringWriter::from_consumer( let mut encoder = base64::write::EncoderStringWriter::from_consumer(
@ -565,7 +565,7 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
.encode_for_mime_type( .encode_for_mime_type(
&image_type, &image_type,
Self::maybe_quality(quality), Self::maybe_quality(quality),
&snapshot, snapshot,
&mut encoder, &mut encoder,
) )
.is_err() .is_err()
@ -622,16 +622,11 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
return error!("Expected blob callback, but found none!"); return error!("Expected blob callback, but found none!");
}; };
let Some(mut snapshot) = result else { let Some(snapshot) = result else {
let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note()); let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note());
return; return;
}; };
snapshot.transform(
SnapshotAlphaMode::Transparent { premultiplied: false },
SnapshotPixelFormat::RGBA
);
// Step 4.1: If result is non-null, then set result to a serialization of // Step 4.1: If result is non-null, then set result to a serialization of
// result as a file with type and quality if given. // result as a file with type and quality if given.
// Step 4.2: Queue an element task on the canvas blob serialization task // Step 4.2: Queue an element task on the canvas blob serialization task
@ -639,7 +634,7 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
let mut encoded: Vec<u8> = vec![]; let mut encoded: Vec<u8> = vec![];
let blob_impl; let blob_impl;
let blob; let blob;
let result = match this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded) { let result = match this.encode_for_mime_type(&image_type, quality, snapshot, &mut encoded) {
Ok(..) => { Ok(..) => {
// Step 4.2.1: If result is non-null, then set result to a new Blob // Step 4.2.1: If result is non-null, then set result to a new Blob
// object, created in the relevant realm of this canvas element, // object, created in the relevant realm of this canvas element,

View file

@ -198,10 +198,10 @@ impl ImageBitmap {
pixels::copy_rgba8_image( pixels::copy_rgba8_image(
input.size(), input.size(),
input_rect_cropped.cast(), input_rect_cropped.cast(),
input.data(), input.as_raw_bytes(),
source.size(), source.size(),
source_rect_cropped.cast(), source_rect_cropped.cast(),
source.data_mut(), source.as_raw_bytes_mut(),
); );
// Step 7. Scale output to the size specified by outputWidth and outputHeight. // Step 7. Scale output to the size specified by outputWidth and outputHeight.
@ -213,9 +213,12 @@ impl ImageBitmap {
ResizeQuality::High => pixels::FilterQuality::High, ResizeQuality::High => pixels::FilterQuality::High,
}; };
let Some(output_data) = let Some(output_data) = pixels::scale_rgba8_image(
pixels::scale_rgba8_image(source.size(), source.data(), output_size, quality) source.size(),
else { source.as_raw_bytes(),
output_size,
quality,
) else {
log::warn!( log::warn!(
"Failed to scale the bitmap of size {:?} to required size {:?}", "Failed to scale the bitmap of size {:?} to required size {:?}",
source.size(), source.size(),
@ -240,7 +243,7 @@ impl ImageBitmap {
// output must be flipped vertically, disregarding any image orientation metadata // output must be flipped vertically, disregarding any image orientation metadata
// of the source (such as EXIF metadata), if any. // of the source (such as EXIF metadata), if any.
if options.imageOrientation == ImageOrientation::FlipY { if options.imageOrientation == ImageOrientation::FlipY {
pixels::flip_y_rgba8_image_inplace(output.size(), output.data_mut()); pixels::flip_y_rgba8_image_inplace(output.size(), output.as_raw_bytes_mut());
} }
// TODO: Step 9. If image is an img element or a Blob object, let val be the value // TODO: Step 9. If image is an img element or a Blob object, let val be the value

View file

@ -21,7 +21,7 @@ use js::jsapi::{JSObject, Type};
use js::jsval::{BooleanValue, DoubleValue, Int32Value, NullValue, ObjectValue, UInt32Value}; use js::jsval::{BooleanValue, DoubleValue, Int32Value, NullValue, ObjectValue, UInt32Value};
use js::rust::{CustomAutoRooterGuard, HandleObject, MutableHandleValue}; use js::rust::{CustomAutoRooterGuard, HandleObject, MutableHandleValue};
use js::typedarray::{ArrayBufferView, CreateWith, Float32, Int32Array, Uint32, Uint32Array}; use js::typedarray::{ArrayBufferView, CreateWith, Float32, Int32Array, Uint32, Uint32Array};
use pixels::Snapshot; use pixels::{Alpha, Snapshot};
use script_bindings::interfaces::WebGL2RenderingContextHelpers; use script_bindings::interfaces::WebGL2RenderingContextHelpers;
use servo_config::pref; use servo_config::pref;
use url::Host; use url::Host;
@ -3257,7 +3257,8 @@ impl WebGL2RenderingContextMethods<crate::DomTypeHolder> for WebGL2RenderingCont
let size = Size2D::new(width, height); let size = Size2D::new(width, height);
let (alpha_treatment, y_axis_treatment) = self.base.get_current_unpack_state(false); let (alpha_treatment, y_axis_treatment) =
self.base.get_current_unpack_state(Alpha::NotPremultiplied);
self.base.tex_image_2d( self.base.tex_image_2d(
&texture, &texture,

View file

@ -29,7 +29,7 @@ use js::typedarray::{
ArrayBufferView, CreateWith, Float32, Float32Array, Int32, Int32Array, TypedArray, ArrayBufferView, CreateWith, Float32, Float32Array, Int32, Int32Array, TypedArray,
TypedArrayElementCreator, Uint32Array, TypedArrayElementCreator, Uint32Array,
}; };
use pixels::{self, PixelFormat, Snapshot, SnapshotPixelFormat}; use pixels::{self, Alpha, PixelFormat, Snapshot, SnapshotPixelFormat};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use servo_config::pref; use servo_config::pref;
use webrender_api::ImageKey; use webrender_api::ImageKey;
@ -507,14 +507,14 @@ impl WebGLRenderingContext {
pub(crate) fn get_current_unpack_state( pub(crate) fn get_current_unpack_state(
&self, &self,
premultiplied: bool, premultiplied: Alpha,
) -> (Option<AlphaTreatment>, YAxisTreatment) { ) -> (Option<AlphaTreatment>, YAxisTreatment) {
let settings = self.texture_unpacking_settings.get(); let settings = self.texture_unpacking_settings.get();
let dest_premultiplied = settings.contains(TextureUnpacking::PREMULTIPLY_ALPHA); let dest_premultiplied = settings.contains(TextureUnpacking::PREMULTIPLY_ALPHA);
let alpha_treatment = match (premultiplied, dest_premultiplied) { let alpha_treatment = match (premultiplied, dest_premultiplied) {
(true, false) => Some(AlphaTreatment::Unmultiply), (Alpha::Premultiplied, false) => Some(AlphaTreatment::Unmultiply),
(false, true) => Some(AlphaTreatment::Premultiply), (Alpha::NotPremultiplied, true) => Some(AlphaTreatment::Premultiply),
_ => None, _ => None,
}; };
@ -626,7 +626,8 @@ impl WebGLRenderingContext {
) )
}, },
TexImageSource::ImageData(image_data) => { TexImageSource::ImageData(image_data) => {
let (alpha_treatment, y_axis_treatment) = self.get_current_unpack_state(false); let (alpha_treatment, y_axis_treatment) =
self.get_current_unpack_state(Alpha::NotPremultiplied);
TexPixels::new( TexPixels::new(
image_data.to_shared_memory(), image_data.to_shared_memory(),
@ -665,7 +666,7 @@ impl WebGLRenderingContext {
}; };
let (alpha_treatment, y_axis_treatment) = let (alpha_treatment, y_axis_treatment) =
self.get_current_unpack_state(snapshot.alpha_mode().is_premultiplied()); self.get_current_unpack_state(Alpha::NotPremultiplied);
TexPixels::new( TexPixels::new(
snapshot.to_ipc_shared_memory(), snapshot.to_ipc_shared_memory(),
@ -695,7 +696,7 @@ impl WebGLRenderingContext {
}; };
let (alpha_treatment, y_axis_treatment) = let (alpha_treatment, y_axis_treatment) =
self.get_current_unpack_state(snapshot.alpha_mode().is_premultiplied()); self.get_current_unpack_state(snapshot.alpha_mode().alpha());
TexPixels::new( TexPixels::new(
snapshot.to_ipc_shared_memory(), snapshot.to_ipc_shared_memory(),
@ -722,7 +723,7 @@ impl WebGLRenderingContext {
}; };
let (alpha_treatment, y_axis_treatment) = let (alpha_treatment, y_axis_treatment) =
self.get_current_unpack_state(snapshot.alpha_mode().is_premultiplied()); self.get_current_unpack_state(snapshot.alpha_mode().alpha());
TexPixels::new( TexPixels::new(
snapshot.to_ipc_shared_memory(), snapshot.to_ipc_shared_memory(),
@ -4539,7 +4540,8 @@ impl WebGLRenderingContextMethods<crate::DomTypeHolder> for WebGLRenderingContex
let size = Size2D::new(width, height); let size = Size2D::new(width, height);
let (alpha_treatment, y_axis_treatment) = self.get_current_unpack_state(false); let (alpha_treatment, y_axis_treatment) =
self.get_current_unpack_state(Alpha::NotPremultiplied);
self.tex_image_2d( self.tex_image_2d(
&texture, &texture,
@ -4716,7 +4718,8 @@ impl WebGLRenderingContextMethods<crate::DomTypeHolder> for WebGLRenderingContex
}; };
} }
let (alpha_treatment, y_axis_treatment) = self.get_current_unpack_state(false); let (alpha_treatment, y_axis_treatment) =
self.get_current_unpack_state(Alpha::NotPremultiplied);
self.tex_sub_image_2d( self.tex_sub_image_2d(
texture, texture,