0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-02-21 21:03:16 -05:00
denoland-deno/ext/canvas/op_create_image_bitmap.rs

563 lines
17 KiB
Rust

// Copyright 2018-2025 the Deno authors. MIT license.
use std::io::BufReader;
use std::io::Cursor;
use deno_core::op2;
use deno_core::JsBuffer;
use deno_core::ToJsBuffer;
use image::codecs::bmp::BmpDecoder;
// use image::codecs::gif::GifDecoder;
use image::codecs::ico::IcoDecoder;
use image::codecs::jpeg::JpegDecoder;
use image::codecs::png::PngDecoder;
// use image::codecs::webp::WebPDecoder;
use image::imageops::overlay;
use image::imageops::FilterType;
use image::metadata::Orientation;
use image::DynamicImage;
use image::ImageDecoder;
use image::RgbaImage;
use crate::image_ops::create_image_from_raw_bytes;
use crate::image_ops::premultiply_alpha as process_premultiply_alpha;
use crate::image_ops::to_srgb_from_icc_profile;
use crate::image_ops::unpremultiply_alpha;
use crate::CanvasError;
#[derive(Debug, PartialEq)]
enum ImageBitmapSource {
Blob,
ImageData,
ImageBitmap,
}
#[derive(Debug, PartialEq)]
enum ImageOrientation {
FlipY,
FromImage,
}
#[derive(Debug, PartialEq)]
enum PremultiplyAlpha {
Default,
Premultiply,
None,
}
#[derive(Debug, PartialEq)]
enum ColorSpaceConversion {
Default,
None,
}
#[derive(Debug, PartialEq)]
enum ResizeQuality {
Pixelated,
Low,
Medium,
High,
}
#[derive(Debug, PartialEq)]
enum MimeType {
NoMatch,
Png,
Jpeg,
Gif,
Bmp,
Ico,
Webp,
}
type DecodeBitmapDataReturn =
(DynamicImage, u32, u32, Option<Orientation>, Option<Vec<u8>>);
fn decode_bitmap_data(
buf: &[u8],
width: u32,
height: u32,
image_bitmap_source: &ImageBitmapSource,
mime_type: MimeType,
) -> Result<DecodeBitmapDataReturn, CanvasError> {
let (image, width, height, orientation, icc_profile) =
match image_bitmap_source {
ImageBitmapSource::Blob => {
//
// About the animated image
// > Blob .4
// > ... If this is an animated image, imageBitmap's bitmap data must only be taken from
// > the default image of the animation (the one that the format defines is to be used when animation is
// > not supported or is disabled), or, if there is no such image, the first frame of the animation.
// https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html
//
// see also browser implementations: (The implementation of Gecko and WebKit is hard to read.)
// https://source.chromium.org/chromium/chromium/src/+/bdbc054a6cabbef991904b5df9066259505cc686:third_party/blink/renderer/platform/image-decoders/image_decoder.h;l=175-189
//
let (image, orientation, icc_profile) = match mime_type {
MimeType::Png => {
// If PngDecoder decodes an animated image, it returns the default image if one is set, or the first frame if not.
let mut decoder = PngDecoder::new(BufReader::new(Cursor::new(buf)))
.map_err(CanvasError::image_error_to_invalid_image)?;
let orientation = decoder.orientation()?;
let icc_profile = decoder.icc_profile()?;
(
DynamicImage::from_decoder(decoder)
.map_err(CanvasError::image_error_to_invalid_image)?,
orientation,
icc_profile,
)
}
MimeType::Jpeg => {
let mut decoder =
JpegDecoder::new(BufReader::new(Cursor::new(buf)))
.map_err(CanvasError::image_error_to_invalid_image)?;
let orientation = decoder.orientation()?;
let icc_profile = decoder.icc_profile()?;
(
DynamicImage::from_decoder(decoder)
.map_err(CanvasError::image_error_to_invalid_image)?,
orientation,
icc_profile,
)
}
MimeType::Gif => {
// NOTE: Temporarily not supported due to build size concerns
// https://github.com/denoland/deno/pull/25517#issuecomment-2626044644
unimplemented!();
// The GifDecoder decodes the first frame.
// let mut decoder = GifDecoder::new(BufReader::new(Cursor::new(buf)))
// .map_err(CanvasError::image_error_to_invalid_image)?;
// let orientation = decoder.orientation()?;
// let icc_profile = decoder.icc_profile()?;
// (
// DynamicImage::from_decoder(decoder)
// .map_err(CanvasError::image_error_to_invalid_image)?,
// orientation,
// icc_profile,
// )
}
MimeType::Bmp => {
let mut decoder = BmpDecoder::new(BufReader::new(Cursor::new(buf)))
.map_err(CanvasError::image_error_to_invalid_image)?;
let orientation = decoder.orientation()?;
let icc_profile = decoder.icc_profile()?;
(
DynamicImage::from_decoder(decoder)
.map_err(CanvasError::image_error_to_invalid_image)?,
orientation,
icc_profile,
)
}
MimeType::Ico => {
let mut decoder = IcoDecoder::new(BufReader::new(Cursor::new(buf)))
.map_err(CanvasError::image_error_to_invalid_image)?;
let orientation = decoder.orientation()?;
let icc_profile = decoder.icc_profile()?;
(
DynamicImage::from_decoder(decoder)
.map_err(CanvasError::image_error_to_invalid_image)?,
orientation,
icc_profile,
)
}
MimeType::Webp => {
// NOTE: Temporarily not supported due to build size concerns
// https://github.com/denoland/deno/pull/25517#issuecomment-2626044644
unimplemented!();
// The WebPDecoder decodes the first frame.
// let mut decoder =
// WebPDecoder::new(BufReader::new(Cursor::new(buf)))
// .map_err(CanvasError::image_error_to_invalid_image)?;
// let orientation = decoder.orientation()?;
// let icc_profile = decoder.icc_profile()?;
// (
// DynamicImage::from_decoder(decoder)
// .map_err(CanvasError::image_error_to_invalid_image)?,
// orientation,
// icc_profile,
// )
}
// This pattern is unreachable due to current block is already checked by the ImageBitmapSource above.
MimeType::NoMatch => unreachable!(),
};
let width = image.width();
let height = image.height();
(image, width, height, Some(orientation), icc_profile)
}
ImageBitmapSource::ImageData => {
// > 4.12.5.1.15 Pixel manipulation
// > imagedata.data
// > Returns the one-dimensional array containing the data in RGBA order, as integers in the range 0 to 255.
// https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation
let image = match RgbaImage::from_raw(width, height, buf.into()) {
Some(image) => image.into(),
None => {
return Err(CanvasError::NotBigEnoughChunk(width, height));
}
};
(image, width, height, None, None)
}
ImageBitmapSource::ImageBitmap => {
let image = create_image_from_raw_bytes(width, height, buf)?;
(image, width, height, None, None)
}
};
Ok((image, width, height, orientation, icc_profile))
}
/// According to the spec, it's not clear how to handle the color space conversion.
///
/// Therefore, if you interpret the specification description from the implementation and wpt results, it will be as follows.
///
/// Let val be the value of the colorSpaceConversion member of options, and then run these substeps:
/// 1. If val is "default", to convert to the sRGB color space.
/// 2. If val is "none", to use the decoded image data as is.
///
/// related issue in whatwg
/// https://github.com/whatwg/html/issues/10578
///
/// reference in wpt
/// https://github.com/web-platform-tests/wpt/blob/d575dc75ede770df322fbc5da3112dcf81f192ec/html/canvas/element/manual/imagebitmap/createImageBitmap-colorSpaceConversion.html#L18
/// https://wpt.live/html/canvas/element/manual/imagebitmap/createImageBitmap-colorSpaceConversion.html
fn apply_color_space_conversion(
image: DynamicImage,
icc_profile: Option<Vec<u8>>,
color_space_conversion: &ColorSpaceConversion,
) -> Result<DynamicImage, CanvasError> {
match color_space_conversion {
// return the decoded image as is.
ColorSpaceConversion::None => Ok(image),
ColorSpaceConversion::Default => {
to_srgb_from_icc_profile(image, icc_profile)
}
}
}
fn apply_premultiply_alpha(
image: DynamicImage,
image_bitmap_source: &ImageBitmapSource,
premultiply_alpha: &PremultiplyAlpha,
) -> Result<DynamicImage, CanvasError> {
match premultiply_alpha {
// 1.
PremultiplyAlpha::Default => Ok(image),
// https://html.spec.whatwg.org/multipage/canvas.html#convert-from-premultiplied
// 2.
PremultiplyAlpha::Premultiply => process_premultiply_alpha(image),
// 3.
PremultiplyAlpha::None => {
// NOTE: It's not clear how to handle the case of ImageData.
// https://issues.chromium.org/issues/339759426
// https://github.com/whatwg/html/issues/5365
if *image_bitmap_source == ImageBitmapSource::ImageData {
return Ok(image);
}
unpremultiply_alpha(image)
}
}
}
#[derive(Debug, PartialEq)]
struct ParsedArgs {
resize_width: Option<u32>,
resize_height: Option<u32>,
sx: Option<i32>,
sy: Option<i32>,
sw: Option<i32>,
sh: Option<i32>,
image_orientation: ImageOrientation,
premultiply_alpha: PremultiplyAlpha,
color_space_conversion: ColorSpaceConversion,
resize_quality: ResizeQuality,
image_bitmap_source: ImageBitmapSource,
mime_type: MimeType,
}
#[allow(clippy::too_many_arguments)]
fn parse_args(
sx: i32,
sy: i32,
sw: i32,
sh: i32,
image_orientation: u8,
premultiply_alpha: u8,
color_space_conversion: u8,
resize_width: u32,
resize_height: u32,
resize_quality: u8,
image_bitmap_source: u8,
mime_type: u8,
) -> ParsedArgs {
let resize_width = if resize_width == 0 {
None
} else {
Some(resize_width)
};
let resize_height = if resize_height == 0 {
None
} else {
Some(resize_height)
};
let sx = if sx == 0 { None } else { Some(sx) };
let sy = if sy == 0 { None } else { Some(sy) };
let sw = if sw == 0 { None } else { Some(sw) };
let sh = if sh == 0 { None } else { Some(sh) };
// Their unreachable wildcard patterns are validated in JavaScript-side.
let image_orientation = match image_orientation {
0 => ImageOrientation::FromImage,
1 => ImageOrientation::FlipY,
_ => unreachable!(),
};
let premultiply_alpha = match premultiply_alpha {
0 => PremultiplyAlpha::Default,
1 => PremultiplyAlpha::Premultiply,
2 => PremultiplyAlpha::None,
_ => unreachable!(),
};
let color_space_conversion = match color_space_conversion {
0 => ColorSpaceConversion::Default,
1 => ColorSpaceConversion::None,
_ => unreachable!(),
};
let resize_quality = match resize_quality {
0 => ResizeQuality::Low,
1 => ResizeQuality::Pixelated,
2 => ResizeQuality::Medium,
3 => ResizeQuality::High,
_ => unreachable!(),
};
let image_bitmap_source = match image_bitmap_source {
0 => ImageBitmapSource::Blob,
1 => ImageBitmapSource::ImageData,
2 => ImageBitmapSource::ImageBitmap,
_ => unreachable!(),
};
let mime_type = match mime_type {
0 => MimeType::NoMatch,
1 => MimeType::Png,
2 => MimeType::Jpeg,
3 => MimeType::Gif,
4 => MimeType::Bmp,
5 => MimeType::Ico,
6 => MimeType::Webp,
_ => unreachable!(),
};
ParsedArgs {
resize_width,
resize_height,
sx,
sy,
sw,
sh,
image_orientation,
premultiply_alpha,
color_space_conversion,
resize_quality,
image_bitmap_source,
mime_type,
}
}
#[op2]
#[serde]
#[allow(clippy::too_many_arguments)]
pub(super) fn op_create_image_bitmap(
#[buffer] buf: JsBuffer,
width: u32,
height: u32,
sx: i32,
sy: i32,
sw: i32,
sh: i32,
image_orientation: u8,
premultiply_alpha: u8,
color_space_conversion: u8,
resize_width: u32,
resize_height: u32,
resize_quality: u8,
image_bitmap_source: u8,
mime_type: u8,
) -> Result<(ToJsBuffer, u32, u32), CanvasError> {
let ParsedArgs {
resize_width,
resize_height,
sx,
sy,
sw,
sh,
image_orientation,
premultiply_alpha,
color_space_conversion,
resize_quality,
image_bitmap_source,
mime_type,
} = parse_args(
sx,
sy,
sw,
sh,
image_orientation,
premultiply_alpha,
color_space_conversion,
resize_width,
resize_height,
resize_quality,
image_bitmap_source,
mime_type,
);
// 6. Switch on image:
let (image, width, height, orientation, icc_profile) =
decode_bitmap_data(&buf, width, height, &image_bitmap_source, mime_type)?;
// crop bitmap data
// 2.
#[rustfmt::skip]
let source_rectangle: [[i32; 2]; 4] =
if let (Some(sx), Some(sy), Some(sw), Some(sh)) = (sx, sy, sw, sh) {
[
[sx, sy],
[sx + sw, sy],
[sx + sw, sy + sh],
[sx, sy + sh]
]
} else {
[
[0, 0],
[width as i32, 0],
[width as i32, height as i32],
[0, height as i32],
]
};
/*
* The cropping works differently than the spec specifies:
* The spec states to create an infinite surface and place the top-left corner
* of the image a 0,0 and crop based on sourceRectangle.
*
* We instead create a surface the size of sourceRectangle, and position
* the image at the correct location, which is the inverse of the x & y of
* sourceRectangle's top-left corner.
*/
let input_x = -(source_rectangle[0][0] as i64);
let input_y = -(source_rectangle[0][1] as i64);
let surface_width = (source_rectangle[1][0] - source_rectangle[0][0]) as u32;
let surface_height = (source_rectangle[3][1] - source_rectangle[0][1]) as u32;
// 3.
let output_width = if let Some(resize_width) = resize_width {
resize_width
} else if let Some(resize_height) = resize_height {
(surface_width * resize_height).div_ceil(surface_height)
} else {
surface_width
};
// 4.
let output_height = if let Some(resize_height) = resize_height {
resize_height
} else if let Some(resize_width) = resize_width {
(surface_height * resize_width).div_ceil(surface_width)
} else {
surface_height
};
// 5.
let image = if !(width == surface_width
&& height == surface_height
&& input_x == 0
&& input_y == 0)
{
let mut surface =
DynamicImage::new(surface_width, surface_height, image.color());
overlay(&mut surface, &image, input_x, input_y);
surface
} else {
image
};
// 7.
let filter_type = match resize_quality {
ResizeQuality::Pixelated => FilterType::Nearest,
ResizeQuality::Low => FilterType::Triangle,
ResizeQuality::Medium => FilterType::CatmullRom,
ResizeQuality::High => FilterType::Lanczos3,
};
// should use resize_exact
// https://github.com/image-rs/image/issues/1220#issuecomment-632060015
let mut image = image.resize_exact(output_width, output_height, filter_type);
// 8.
let image = match image_bitmap_source {
ImageBitmapSource::Blob => {
// Note: According to browser behavior and wpt results, if Exif contains image orientation,
// it applies the rotation from it before following the value of imageOrientation.
// This is not stated in the spec but in MDN currently.
// https://github.com/mdn/content/pull/34366
// SAFETY: The orientation is always Some if the image is from a Blob.
let orientation = orientation.unwrap();
DynamicImage::apply_orientation(&mut image, orientation);
match image_orientation {
ImageOrientation::FlipY => image.flipv(),
ImageOrientation::FromImage => image,
}
}
ImageBitmapSource::ImageData | ImageBitmapSource::ImageBitmap => {
match image_orientation {
ImageOrientation::FlipY => image.flipv(),
ImageOrientation::FromImage => image,
}
}
};
// 9.
let image =
apply_color_space_conversion(image, icc_profile, &color_space_conversion)?;
// 10.
let image =
apply_premultiply_alpha(image, &image_bitmap_source, &premultiply_alpha)?;
Ok((image.into_bytes().into(), output_width, output_height))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_args() {
let parsed_args = parse_args(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
assert_eq!(
parsed_args,
ParsedArgs {
resize_width: None,
resize_height: None,
sx: None,
sy: None,
sw: None,
sh: None,
image_orientation: ImageOrientation::FromImage,
premultiply_alpha: PremultiplyAlpha::Default,
color_space_conversion: ColorSpaceConversion::Default,
resize_quality: ResizeQuality::Low,
image_bitmap_source: ImageBitmapSource::Blob,
mime_type: MimeType::NoMatch,
}
);
}
}