diff --git a/ext/canvas/01_image.js b/ext/canvas/01_image.js index cbc4a6f889..22ad44687d 100644 --- a/ext/canvas/01_image.js +++ b/ext/canvas/01_image.js @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { internals, primordials } from "ext:core/mod.js"; -import { op_image_process } from "ext:core/ops"; +import { op_create_image_bitmap } from "ext:core/ops"; import * as webidl from "ext:deno_webidl/00_webidl.js"; import { DOMException } from "ext:deno_web/01_dom_exception.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; @@ -11,7 +11,6 @@ const { ObjectPrototypeIsPrototypeOf, Symbol, SymbolFor, - TypeError, TypedArrayPrototypeGetBuffer, Uint8Array, PromiseReject, @@ -189,6 +188,7 @@ function createImageBitmap( "Argument 6", ); + // 1. if (sw === 0) { return PromiseReject(new RangeError("sw has to be greater than 0")); } @@ -198,6 +198,7 @@ function createImageBitmap( } } + // 2. if (options.resizeWidth === 0) { return PromiseReject( new DOMException( @@ -217,70 +218,69 @@ function createImageBitmap( const imageBitmap = webidl.createBranded(ImageBitmap); - // 6. Switch on image + // 3. const isBlob = ObjectPrototypeIsPrototypeOf(BlobPrototype, image); const isImageData = ObjectPrototypeIsPrototypeOf(ImageDataPrototype, image); - if ( - isImageData || - isBlob - ) { - return (async () => { - let width = 0; - let height = 0; - let mimeType = ""; - let imageBitmapSource, buf, predefinedColorSpace; - if (isBlob) { - imageBitmapSource = imageBitmapSources[0]; - buf = new Uint8Array(await image.arrayBuffer()); - mimeType = sniffImage(image.type); - } - if (isImageData) { - width = image[_width]; - height = image[_height]; - imageBitmapSource = imageBitmapSources[1]; - buf = new Uint8Array(TypedArrayPrototypeGetBuffer(image[_data])); - predefinedColorSpace = image[_colorSpace]; - } - - let sx; - if (typeof sxOrOptions === "number") { - sx = sxOrOptions; - } - - const processedImage = op_image_process( - buf, - { - width, - height, - sx, - sy, - sw, - sh, - imageOrientation: options.imageOrientation ?? "from-image", - premultiplyAlpha: options.premultiplyAlpha ?? "default", - predefinedColorSpace: predefinedColorSpace ?? "srgb", - colorSpaceConversion: options.colorSpaceConversion ?? "default", - resizeWidth: options.resizeWidth, - resizeHeight: options.resizeHeight, - resizeQuality: options.resizeQuality ?? "low", - imageBitmapSource, - mimeType, - }, - ); - imageBitmap[_bitmapData] = processedImage.data; - imageBitmap[_width] = processedImage.width; - imageBitmap[_height] = processedImage.height; - return imageBitmap; - })(); - } else { + if (!isBlob && !isImageData) { return PromiseReject( - new TypeError( - `${prefix}: The provided value is not of type '(${ + new DOMException( + `${prefix}: The provided value for 'image' is not of type '(${ ArrayPrototypeJoin(imageBitmapSources, " or ") })'.`, + "InvalidStateError", ), ); } + + // 4. + return (async () => { + let width = 0; + let height = 0; + let mimeType = ""; + let imageBitmapSource, buf, predefinedColorSpace; + if (isBlob) { + imageBitmapSource = imageBitmapSources[0]; + buf = new Uint8Array(await image.arrayBuffer()); + mimeType = sniffImage(image.type); + } + if (isImageData) { + width = image[_width]; + height = image[_height]; + imageBitmapSource = imageBitmapSources[1]; + buf = new Uint8Array(TypedArrayPrototypeGetBuffer(image[_data])); + predefinedColorSpace = image[_colorSpace]; + } + + let sx; + if (typeof sxOrOptions === "number") { + sx = sxOrOptions; + } + // TODO(Hajime-san): this should be real async + const processedImage = op_create_image_bitmap( + buf, + { + width, + height, + sx, + sy, + sw, + sh, + imageOrientation: options.imageOrientation ?? "from-image", + premultiplyAlpha: options.premultiplyAlpha ?? "default", + predefinedColorSpace: predefinedColorSpace ?? "srgb", + colorSpaceConversion: options.colorSpaceConversion ?? "default", + resizeWidth: options.resizeWidth, + resizeHeight: options.resizeHeight, + resizeQuality: options.resizeQuality ?? "low", + imageBitmapSource, + mimeType, + }, + ); + imageBitmap[_bitmapData] = processedImage.data; + imageBitmap[_width] = processedImage.width; + imageBitmap[_height] = processedImage.height; + return imageBitmap; + })(); } function getBitmapData(imageBitmap) { diff --git a/ext/canvas/Cargo.toml b/ext/canvas/Cargo.toml index 1bf7323d7b..52b145f2ac 100644 --- a/ext/canvas/Cargo.toml +++ b/ext/canvas/Cargo.toml @@ -19,7 +19,7 @@ deno_core.workspace = true deno_terminal.workspace = true deno_webgpu.workspace = true image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "bmp", "ico", "webp", "gif"] } -# NOTE: The qcms is a color space conversion crate which parses ICC profiles that used in Gecko, +# NOTE: The qcms is a color space conversion crate which parses ICC profiles that used in Gecko, # however it supports only 8-bit color depth currently. # https://searchfox.org/mozilla-central/rev/f09e3f9603a08b5b51bf504846091579bc2ff531/gfx/qcms/src/transform.rs#130-137 lcms2 = "6.1.0" diff --git a/ext/canvas/error.rs b/ext/canvas/error.rs index bd08f9e8d6..e2d1605345 100644 --- a/ext/canvas/error.rs +++ b/ext/canvas/error.rs @@ -1,6 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use deno_core::error::AnyError; +use std::borrow::Cow; use std::fmt; #[derive(Debug)] @@ -28,3 +29,16 @@ pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> { e.downcast_ref::() .map(|_| "DOMExceptionInvalidStateError") } + +/// Returns a string that represents the error message for the image. +pub(crate) fn image_error_message<'a, T: Into>>( + opreation: T, + reason: T, +) -> String { + format!( + "An error has occurred while {}. +reason: {}", + opreation.into(), + reason.into(), + ) +} diff --git a/ext/canvas/idl.rs b/ext/canvas/idl.rs new file mode 100644 index 0000000000..23f33641c9 --- /dev/null +++ b/ext/canvas/idl.rs @@ -0,0 +1,11 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum PredefinedColorSpace { + Srgb, + #[serde(rename = "display-p3")] + DisplayP3, +} diff --git a/ext/canvas/image_decoder.rs b/ext/canvas/image_decoder.rs new file mode 100644 index 0000000000..4b9e4fa53a --- /dev/null +++ b/ext/canvas/image_decoder.rs @@ -0,0 +1,89 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::io::BufRead; +use std::io::BufReader; +use std::io::Cursor; +use std::io::Seek; + +use deno_core::error::AnyError; +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::DynamicImage; +use image::ImageDecoder; +use image::ImageError; + +use crate::error::image_error_message; +use crate::error::DOMExceptionInvalidStateError; + +// +// 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 +// + +pub(crate) trait ImageDecoderFromReader<'a, R: BufRead + Seek> { + fn to_decoder(reader: R) -> Result + where + Self: Sized; + fn to_intermediate_image(self) -> Result; + fn get_icc_profile(&mut self) -> Option>; +} + +pub(crate) type ImageDecoderFromReaderType<'a> = BufReader>; + +pub(crate) fn image_decoding_error( + error: ImageError, +) -> DOMExceptionInvalidStateError { + DOMExceptionInvalidStateError::new(&image_error_message( + "decoding", + &error.to_string(), + )) +} + +macro_rules! impl_image_decoder_from_reader { + ($decoder:ty, $reader:ty) => { + impl<'a, R: BufRead + Seek> ImageDecoderFromReader<'a, R> for $decoder { + fn to_decoder(reader: R) -> Result + where + Self: Sized, + { + match <$decoder>::new(reader) { + Ok(decoder) => Ok(decoder), + Err(err) => return Err(image_decoding_error(err).into()), + } + } + fn to_intermediate_image(self) -> Result { + match DynamicImage::from_decoder(self) { + Ok(image) => Ok(image), + Err(err) => Err(image_decoding_error(err).into()), + } + } + fn get_icc_profile(&mut self) -> Option> { + match self.icc_profile() { + Ok(profile) => profile, + Err(_) => None, + } + } + } + }; +} + +// If PngDecoder decodes an animated image, it returns the default image if one is set, or the first frame if not. +impl_image_decoder_from_reader!(PngDecoder, ImageDecoderFromReaderType); +impl_image_decoder_from_reader!(JpegDecoder, ImageDecoderFromReaderType); +// The GifDecoder decodes the first frame. +impl_image_decoder_from_reader!(GifDecoder, ImageDecoderFromReaderType); +impl_image_decoder_from_reader!(BmpDecoder, ImageDecoderFromReaderType); +impl_image_decoder_from_reader!(IcoDecoder, ImageDecoderFromReaderType); +// The WebPDecoder decodes the first frame. +impl_image_decoder_from_reader!(WebPDecoder, ImageDecoderFromReaderType); diff --git a/ext/canvas/image_ops.rs b/ext/canvas/image_ops.rs new file mode 100644 index 0000000000..bbd6fcc55c --- /dev/null +++ b/ext/canvas/image_ops.rs @@ -0,0 +1,597 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use bytemuck::cast_slice; +use deno_core::error::AnyError; +use image::ColorType; +use image::DynamicImage; +use image::GenericImageView; +use image::ImageBuffer; +use image::Luma; +use image::LumaA; +use image::Pixel; +use image::Primitive; +use image::Rgb; +use image::Rgba; +use lcms2::PixelFormat; +use lcms2::Pod; +use lcms2::Profile; +use lcms2::Transform; +use num_traits::NumCast; +use num_traits::SaturatingMul; + +pub(crate) trait PremultiplyAlpha { + fn premultiply_alpha(&self) -> Self; +} + +impl PremultiplyAlpha for LumaA { + fn premultiply_alpha(&self) -> Self { + let max_t = T::DEFAULT_MAX_VALUE; + + let mut pixel = [self.0[0], self.0[1]]; + let alpha_index = pixel.len() - 1; + let alpha = pixel[alpha_index]; + let normalized_alpha = alpha.to_f32().unwrap() / max_t.to_f32().unwrap(); + + if normalized_alpha == 0.0 { + return LumaA::([pixel[0], pixel[alpha_index]]); + } + + for rgb in pixel.iter_mut().take(alpha_index) { + *rgb = NumCast::from((rgb.to_f32().unwrap() * normalized_alpha).round()) + .unwrap() + } + + LumaA::([pixel[0], pixel[alpha_index]]) + } +} + +impl PremultiplyAlpha for Rgba { + fn premultiply_alpha(&self) -> Self { + let max_t = T::DEFAULT_MAX_VALUE; + + let mut pixel = [self.0[0], self.0[1], self.0[2], self.0[3]]; + let alpha_index = pixel.len() - 1; + let alpha = pixel[alpha_index]; + let normalized_alpha = alpha.to_f32().unwrap() / max_t.to_f32().unwrap(); + + if normalized_alpha == 0.0 { + return Rgba::([pixel[0], pixel[1], pixel[2], pixel[alpha_index]]); + } + + for rgb in pixel.iter_mut().take(alpha_index) { + *rgb = NumCast::from((rgb.to_f32().unwrap() * normalized_alpha).round()) + .unwrap() + } + + Rgba::([pixel[0], pixel[1], pixel[2], pixel[alpha_index]]) + } +} + +// make public if needed +fn process_premultiply_alpha(image: &I) -> ImageBuffer> +where + I: GenericImageView, + P: Pixel + PremultiplyAlpha + 'static, + S: Primitive + 'static, +{ + let (width, height) = image.dimensions(); + let mut out = ImageBuffer::new(width, height); + + for (x, y, pixel) in image.pixels() { + let pixel = pixel.premultiply_alpha(); + + out.put_pixel(x, y, pixel); + } + + out +} + +/// Premultiply the alpha channel of the image. +pub(crate) fn premultiply_alpha( + image: DynamicImage, + unmatch: Option Result>, +) -> Result { + let color = image.color(); + match color { + ColorType::La8 => Ok(DynamicImage::ImageLumaA8(process_premultiply_alpha( + &image.to_luma_alpha8(), + ))), + ColorType::Rgba8 => Ok(DynamicImage::ImageRgba8( + process_premultiply_alpha(&image.to_rgba8()), + )), + ColorType::La16 => Ok(DynamicImage::ImageLumaA16( + process_premultiply_alpha(&image.to_luma_alpha16()), + )), + ColorType::Rgba16 => Ok(DynamicImage::ImageRgba16( + process_premultiply_alpha(&image.to_rgba16()), + )), + x => match unmatch { + Some(unmatch) => unmatch(x), + None => Ok(image), + }, + } +} + +pub(crate) trait UnpremultiplyAlpha { + /// To determine if the image is premultiplied alpha, + /// checking premultiplied RGBA value is one where any of the R/G/B channel values exceeds the alpha channel value.\ + /// https://www.w3.org/TR/webgpu/#color-spaces + fn is_premultiplied_alpha(&self) -> bool; + fn unpremultiply_alpha(&self) -> Self; +} + +impl UnpremultiplyAlpha for Rgba { + fn is_premultiplied_alpha(&self) -> bool { + let max_t = T::DEFAULT_MAX_VALUE; + + let pixel = [self.0[0], self.0[1], self.0[2]]; + let alpha_index = self.0.len() - 1; + let alpha = self.0[alpha_index]; + + match pixel.iter().max() { + Some(rgb_max) => rgb_max < &max_t.saturating_mul(&alpha), + // usually doesn't reach here + None => false, + } + } + + fn unpremultiply_alpha(&self) -> Self { + let max_t = T::DEFAULT_MAX_VALUE; + + let mut pixel = [self.0[0], self.0[1], self.0[2], self.0[3]]; + let alpha_index = pixel.len() - 1; + let alpha = pixel[alpha_index]; + + for rgb in pixel.iter_mut().take(alpha_index) { + *rgb = NumCast::from( + (rgb.to_f32().unwrap() + / (alpha.to_f32().unwrap() / max_t.to_f32().unwrap())) + .round(), + ) + .unwrap(); + } + + Rgba::([pixel[0], pixel[1], pixel[2], pixel[alpha_index]]) + } +} + +impl UnpremultiplyAlpha for LumaA { + fn is_premultiplied_alpha(&self) -> bool { + let max_t = T::DEFAULT_MAX_VALUE; + + let pixel = [self.0[0]]; + let alpha_index = self.0.len() - 1; + let alpha = self.0[alpha_index]; + + pixel[0] < max_t.saturating_mul(&alpha) + } + + fn unpremultiply_alpha(&self) -> Self { + let max_t = T::DEFAULT_MAX_VALUE; + + let mut pixel = [self.0[0], self.0[1]]; + let alpha_index = pixel.len() - 1; + let alpha = pixel[alpha_index]; + + for rgb in pixel.iter_mut().take(alpha_index) { + *rgb = NumCast::from( + (rgb.to_f32().unwrap() + / (alpha.to_f32().unwrap() / max_t.to_f32().unwrap())) + .round(), + ) + .unwrap(); + } + + LumaA::([pixel[0], pixel[alpha_index]]) + } +} + +// make public if needed +fn process_unpremultiply_alpha(image: &I) -> ImageBuffer> +where + I: GenericImageView, + P: Pixel + UnpremultiplyAlpha + 'static, + S: Primitive + 'static, +{ + let (width, height) = image.dimensions(); + let mut out = ImageBuffer::new(width, height); + + let is_premultiplied_alpha = image + .pixels() + .any(|(_, _, pixel)| pixel.is_premultiplied_alpha()); + + for (x, y, pixel) in image.pixels() { + let pixel = if is_premultiplied_alpha { + pixel.unpremultiply_alpha() + } else { + // return the original + pixel + }; + + out.put_pixel(x, y, pixel); + } + + out +} + +/// Invert the premultiplied alpha channel of the image. +pub(crate) fn unpremultiply_alpha( + image: DynamicImage, + unmatch: Option Result>, +) -> Result { + match image.color() { + ColorType::La8 => Ok(DynamicImage::ImageLumaA8( + process_unpremultiply_alpha(&image.to_luma_alpha8()), + )), + ColorType::Rgba8 => Ok(DynamicImage::ImageRgba8( + process_unpremultiply_alpha(&image.to_rgba8()), + )), + ColorType::La16 => Ok(DynamicImage::ImageLumaA16( + process_unpremultiply_alpha(&image.to_luma_alpha16()), + )), + ColorType::Rgba16 => Ok(DynamicImage::ImageRgba16( + process_unpremultiply_alpha(&image.to_rgba16()), + )), + x => match unmatch { + Some(unmatch) => unmatch(x), + None => Ok(image), + }, + } +} + +// reference +// https://www.w3.org/TR/css-color-4/#color-conversion-code +fn srgb_to_linear(value: T) -> f32 { + if value.to_f32().unwrap() <= 0.04045 { + value.to_f32().unwrap() / 12.92 + } else { + ((value.to_f32().unwrap() + 0.055) / 1.055).powf(2.4) + } +} + +// reference +// https://www.w3.org/TR/css-color-4/#color-conversion-code +fn linear_to_display_p3(value: T) -> f32 { + if value.to_f32().unwrap() <= 0.0031308 { + value.to_f32().unwrap() * 12.92 + } else { + 1.055 * value.to_f32().unwrap().powf(1.0 / 2.4) - 0.055 + } +} + +fn normalize_value_to_0_1(value: T) -> f32 { + value.to_f32().unwrap() / T::DEFAULT_MAX_VALUE.to_f32().unwrap() +} + +fn unnormalize_value_from_0_1(value: f32) -> T { + NumCast::from( + (value.clamp(0.0, 1.0) * T::DEFAULT_MAX_VALUE.to_f32().unwrap()).round(), + ) + .unwrap() +} + +fn apply_conversion_matrix_srgb_to_display_p3( + r: T, + g: T, + b: T, +) -> (T, T, T) { + // normalize the value to 0.0 - 1.0 + let (r, g, b) = ( + normalize_value_to_0_1(r), + normalize_value_to_0_1(g), + normalize_value_to_0_1(b), + ); + + // sRGB -> Linear RGB + let (r, g, b) = (srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)); + + // Display-P3 (RGB) -> Display-P3 (XYZ) + // + // inv[ P3-D65 (D65) to XYZ ] * [ sRGB (D65) to XYZ ] + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + // https://fujiwaratko.sakura.ne.jp/infosci/colorspace/colorspace2_e.html + + // [ sRGB (D65) to XYZ ] + #[rustfmt::skip] + let (m1x, m1y, m1z) = ( + [0.4124564, 0.3575761, 0.1804375], + [0.2126729, 0.7151522, 0.0721750], + [0.0193339, 0.119_192, 0.9503041], + ); + + let (r, g, b) = ( + r * m1x[0] + g * m1x[1] + b * m1x[2], + r * m1y[0] + g * m1y[1] + b * m1y[2], + r * m1z[0] + g * m1z[1] + b * m1z[2], + ); + + // inv[ P3-D65 (D65) to XYZ ] + #[rustfmt::skip] + let (m2x, m2y, m2z) = ( + [ 2.493_497, -0.931_383_6, -0.402_710_8 ], + [ -0.829_489, 1.762_664_1, 0.023_624_687 ], + [ 0.035_845_83, -0.076_172_39, 0.956_884_5 ], + ); + + let (r, g, b) = ( + r * m2x[0] + g * m2x[1] + b * m2x[2], + r * m2y[0] + g * m2y[1] + b * m2y[2], + r * m2z[0] + g * m2z[1] + b * m2z[2], + ); + + // This calculation is similar as above that it is a little faster, but less accurate. + // let r = 0.8225 * r + 0.1774 * g + 0.0000 * b; + // let g = 0.0332 * r + 0.9669 * g + 0.0000 * b; + // let b = 0.0171 * r + 0.0724 * g + 0.9108 * b; + + // Display-P3 (Linear) -> Display-P3 + let (r, g, b) = ( + linear_to_display_p3(r), + linear_to_display_p3(g), + linear_to_display_p3(b), + ); + + // unnormalize the value from 0.0 - 1.0 + ( + unnormalize_value_from_0_1(r), + unnormalize_value_from_0_1(g), + unnormalize_value_from_0_1(b), + ) +} + +pub(crate) trait ColorSpaceConversion { + /// Display P3 Color Encoding (v 1.0) + /// https://www.color.org/chardata/rgb/DisplayP3.xalter + fn srgb_to_display_p3(&self) -> Self; +} + +impl ColorSpaceConversion for Rgb { + fn srgb_to_display_p3(&self) -> Self { + let (r, g, b) = (self.0[0], self.0[1], self.0[2]); + + let (r, g, b) = apply_conversion_matrix_srgb_to_display_p3(r, g, b); + + Rgb::([r, g, b]) + } +} + +impl ColorSpaceConversion for Rgba { + fn srgb_to_display_p3(&self) -> Self { + let (r, g, b, a) = (self.0[0], self.0[1], self.0[2], self.0[3]); + + let (r, g, b) = apply_conversion_matrix_srgb_to_display_p3(r, g, b); + + Rgba::([r, g, b, a]) + } +} + +// make public if needed +fn process_srgb_to_display_p3(image: &I) -> ImageBuffer> +where + I: GenericImageView, + P: Pixel + ColorSpaceConversion + 'static, + S: Primitive + 'static, +{ + let (width, height) = image.dimensions(); + let mut out = ImageBuffer::new(width, height); + + for (x, y, pixel) in image.pixels() { + let pixel = pixel.srgb_to_display_p3(); + + out.put_pixel(x, y, pixel); + } + + out +} + +/// Convert the color space of the image from sRGB to Display-P3. +pub(crate) fn srgb_to_display_p3( + image: DynamicImage, + unmatch: Option Result>, +) -> Result { + match image.color() { + // The conversion of the lumincance color types to the display-p3 color space is meaningless. + ColorType::L8 => Ok(DynamicImage::ImageLuma8(image.to_luma8())), + ColorType::L16 => Ok(DynamicImage::ImageLuma16(image.to_luma16())), + ColorType::La8 => Ok(DynamicImage::ImageLumaA8(image.to_luma_alpha8())), + ColorType::La16 => Ok(DynamicImage::ImageLumaA16(image.to_luma_alpha16())), + ColorType::Rgb8 => Ok(DynamicImage::ImageRgb8(process_srgb_to_display_p3( + &image.to_rgb8(), + ))), + ColorType::Rgb16 => Ok(DynamicImage::ImageRgb16( + process_srgb_to_display_p3(&image.to_rgb16()), + )), + ColorType::Rgba8 => Ok(DynamicImage::ImageRgba8( + process_srgb_to_display_p3(&image.to_rgba8()), + )), + ColorType::Rgba16 => Ok(DynamicImage::ImageRgba16( + process_srgb_to_display_p3(&image.to_rgba16()), + )), + x => match unmatch { + Some(unmatch) => unmatch(x), + None => Ok(image), + }, + } +} + +pub(crate) trait SliceToPixel { + fn slice_to_pixel(pixel: &[u8]) -> Self; +} + +impl SliceToPixel for Luma { + fn slice_to_pixel(pixel: &[u8]) -> Self { + let pixel: &[T] = cast_slice(pixel); + let pixel = [pixel[0]]; + + Luma::(pixel) + } +} + +impl SliceToPixel for LumaA { + fn slice_to_pixel(pixel: &[u8]) -> Self { + let pixel: &[T] = cast_slice(pixel); + let pixel = [pixel[0], pixel[1]]; + + LumaA::(pixel) + } +} + +impl SliceToPixel for Rgb { + fn slice_to_pixel(pixel: &[u8]) -> Self { + let pixel: &[T] = cast_slice(pixel); + let pixel = [pixel[0], pixel[1], pixel[2]]; + + Rgb::(pixel) + } +} + +impl SliceToPixel for Rgba { + fn slice_to_pixel(pixel: &[u8]) -> Self { + let pixel: &[T] = cast_slice(pixel); + let pixel = [pixel[0], pixel[1], pixel[2], pixel[3]]; + + Rgba::(pixel) + } +} + +/// Convert the pixel slice to an array to avoid the copy to Vec. +/// I implemented this trait because of I couldn't find a way to effectively combine +/// the `Transform` struct of `lcms2` and `Pixel` trait of `image`. +/// If there is an implementation that is safer and can withstand changes, I would like to adopt it. +pub(crate) trait SliceToArray { + fn slice_to_array(pixel: &[u8]) -> [u8; N]; +} + +macro_rules! impl_slice_to_array { + ($type:ty, $n:expr) => { + impl SliceToArray<$n> for $type { + fn slice_to_array(pixel: &[u8]) -> [u8; $n] { + let mut dst = [0_u8; $n]; + dst.copy_from_slice(&pixel[..$n]); + + dst + } + } + }; +} + +impl_slice_to_array!(Luma, 1); +impl_slice_to_array!(Luma, 2); +impl_slice_to_array!(LumaA, 2); +impl_slice_to_array!(LumaA, 4); +impl_slice_to_array!(Rgb, 3); +impl_slice_to_array!(Rgb, 6); +impl_slice_to_array!(Rgba, 4); +impl_slice_to_array!(Rgba, 8); + +// make public if needed +fn process_icc_profile_conversion( + image: &DynamicImage, + input_icc_profile: Profile, + output_icc_profile: Profile, +) -> ImageBuffer> +where + P: Pixel + SliceToPixel + SliceToArray + 'static, + S: Primitive + 'static, +{ + let (width, height) = image.dimensions(); + let mut out = ImageBuffer::new(width, height); + let chunk_size = image.color().bytes_per_pixel() as usize; + let pixel_iter = image + .as_bytes() + .chunks_exact(chunk_size) + .zip(image.pixels()); + let pixel_format = match image.color() { + ColorType::L8 => PixelFormat::GRAY_8, + ColorType::L16 => PixelFormat::GRAY_16, + ColorType::La8 => PixelFormat::GRAYA_8, + ColorType::La16 => PixelFormat::GRAYA_16, + ColorType::Rgb8 => PixelFormat::RGB_8, + ColorType::Rgb16 => PixelFormat::RGB_16, + ColorType::Rgba8 => PixelFormat::RGBA_8, + ColorType::Rgba16 => PixelFormat::RGBA_16, + // This arm usually doesn't reach, but it should be handled with returning the original image. + _ => { + return { + for (pixel, (x, y, _)) in pixel_iter { + out.put_pixel(x, y, P::slice_to_pixel(pixel)); + } + out + } + } + }; + let transformer = Transform::new( + &input_icc_profile, + pixel_format, + &output_icc_profile, + pixel_format, + output_icc_profile.header_rendering_intent(), + ); + + for (pixel, (x, y, _)) in pixel_iter { + let pixel = match transformer { + Ok(ref transformer) => { + let mut dst = P::slice_to_array(pixel); + transformer.transform_in_place(&mut dst); + + dst + } + // This arm will reach when the ffi call fails. + Err(_) => P::slice_to_array(pixel), + }; + + out.put_pixel(x, y, P::slice_to_pixel(&pixel)); + } + + out +} + +#[rustfmt::skip] +/// Convert the color space of the image from the ICC profile to sRGB. +pub(crate) fn to_srgb_from_icc_profile( + image: DynamicImage, + icc_profile: Option>, + unmatch: Option Result>, +) -> Result { + match icc_profile { + // If there is no color profile information, return the image as is. + None => Ok(image), + Some(icc_profile) => match Profile::new_icc(&icc_profile) { + // If the color profile information is invalid, return the image as is. + Err(_) => Ok(image), + Ok(icc_profile) => { + let srgb_icc_profile = Profile::new_srgb(); + match image.color() { + ColorType::L8 => { + Ok(DynamicImage::ImageLuma8(process_icc_profile_conversion::<_,_,1>(&image,icc_profile,srgb_icc_profile))) + } + ColorType::L16 => { + Ok(DynamicImage::ImageLuma16(process_icc_profile_conversion::<_,_,2>(&image,icc_profile,srgb_icc_profile))) + } + ColorType::La8 => { + Ok(DynamicImage::ImageLumaA8(process_icc_profile_conversion::<_,_,2>(&image,icc_profile,srgb_icc_profile))) + } + ColorType::La16 => { + Ok(DynamicImage::ImageLumaA16(process_icc_profile_conversion::<_, _, 4>(&image,icc_profile,srgb_icc_profile))) + }, + ColorType::Rgb8 => { + Ok(DynamicImage::ImageRgb8(process_icc_profile_conversion::<_,_,3>(&image,icc_profile,srgb_icc_profile))) + } + ColorType::Rgb16 => { + Ok(DynamicImage::ImageRgb16(process_icc_profile_conversion::<_,_,6>(&image,icc_profile,srgb_icc_profile))) + } + ColorType::Rgba8 => { + Ok(DynamicImage::ImageRgba8(process_icc_profile_conversion::<_,_,4>(&image,icc_profile,srgb_icc_profile))) + } + ColorType::Rgba16 => { + Ok(DynamicImage::ImageRgba16(process_icc_profile_conversion::<_,_,8>(&image,icc_profile,srgb_icc_profile))) + } + x => match unmatch { + Some(unmatch) => unmatch(x), + None => Ok(image), + }, + } + } + }, + } +} diff --git a/ext/canvas/lib.rs b/ext/canvas/lib.rs index 80898d4788..a5d7f32c38 100644 --- a/ext/canvas/lib.rs +++ b/ext/canvas/lib.rs @@ -1,1138 +1,18 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use bytemuck::cast_slice; -use deno_core::error::type_error; -use deno_core::error::AnyError; -use deno_core::op2; -use deno_core::JsBuffer; -use deno_core::ToJsBuffer; -use deno_terminal::colors::cyan; -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::ColorType; -use image::DynamicImage; -use image::GenericImageView; -use image::ImageBuffer; -use image::ImageDecoder; -use image::ImageError; -use image::Luma; -use image::LumaA; -use image::Pixel; -use image::Primitive; -use image::Rgb; -use image::Rgba; -use image::RgbaImage; -use lcms2::PixelFormat; -use lcms2::Pod; -use lcms2::Profile; -use lcms2::Transform; -use num_traits::NumCast; -use num_traits::SaturatingMul; -use serde::Deserialize; -use serde::Serialize; -use std::borrow::Cow; -use std::io::BufRead; -use std::io::BufReader; -use std::io::Cursor; -use std::io::Seek; use std::path::PathBuf; pub mod error; -use error::DOMExceptionInvalidStateError; - -fn to_js_buffer(image: &DynamicImage) -> ToJsBuffer { - image.as_bytes().to_vec().into() -} - -fn image_error_message<'a, T: Into>>( - opreation: T, - reason: T, -) -> String { - format!( - "An error has occurred while {}. -reason: {}", - opreation.into(), - reason.into(), - ) -} - -// reference -// https://github.com/image-rs/image/blob/6d19ffa72756c1b00e7979a90f8794a0ef847b88/src/color.rs#L739 -trait ProcessPremultiplyAlpha { - fn premultiply_alpha(&self) -> Self; -} - -impl ProcessPremultiplyAlpha for LumaA { - fn premultiply_alpha(&self) -> Self { - let max_t = T::DEFAULT_MAX_VALUE; - - let mut pixel = [self.0[0], self.0[1]]; - let alpha_index = pixel.len() - 1; - let alpha = pixel[alpha_index]; - let normalized_alpha = alpha.to_f32().unwrap() / max_t.to_f32().unwrap(); - - if normalized_alpha == 0.0 { - return LumaA::([pixel[0], pixel[alpha_index]]); - } - - for rgb in pixel.iter_mut().take(alpha_index) { - *rgb = NumCast::from((rgb.to_f32().unwrap() * normalized_alpha).round()) - .unwrap() - } - - LumaA::([pixel[0], pixel[alpha_index]]) - } -} - -impl ProcessPremultiplyAlpha for Rgba { - fn premultiply_alpha(&self) -> Self { - let max_t = T::DEFAULT_MAX_VALUE; - - let mut pixel = [self.0[0], self.0[1], self.0[2], self.0[3]]; - let alpha_index = pixel.len() - 1; - let alpha = pixel[alpha_index]; - let normalized_alpha = alpha.to_f32().unwrap() / max_t.to_f32().unwrap(); - - if normalized_alpha == 0.0 { - return Rgba::([pixel[0], pixel[1], pixel[2], pixel[alpha_index]]); - } - - for rgb in pixel.iter_mut().take(alpha_index) { - *rgb = NumCast::from((rgb.to_f32().unwrap() * normalized_alpha).round()) - .unwrap() - } - - Rgba::([pixel[0], pixel[1], pixel[2], pixel[alpha_index]]) - } -} - -fn process_premultiply_alpha(image: &I) -> ImageBuffer> -where - I: GenericImageView, - P: Pixel + ProcessPremultiplyAlpha + 'static, - S: Primitive + 'static, -{ - let (width, height) = image.dimensions(); - let mut out = ImageBuffer::new(width, height); - - for (x, y, pixel) in image.pixels() { - let pixel = pixel.premultiply_alpha(); - - out.put_pixel(x, y, pixel); - } - - out -} - -fn apply_premultiply_alpha( - image: &DynamicImage, -) -> Result { - match image.color() { - ColorType::La8 => Ok(DynamicImage::ImageLumaA8(process_premultiply_alpha( - &image.to_luma_alpha8(), - ))), - ColorType::Rgba8 => Ok(DynamicImage::ImageRgba8( - process_premultiply_alpha(&image.to_rgba8()), - )), - ColorType::La16 => Ok(DynamicImage::ImageLumaA16( - process_premultiply_alpha(&image.to_luma_alpha16()), - )), - ColorType::Rgba16 => Ok(DynamicImage::ImageRgba16( - process_premultiply_alpha(&image.to_rgba16()), - )), - _ => Err(type_error(image_error_message( - "apply premultiplyAlpha: premultiply", - "The color type is not supported.", - ))), - } -} - -trait ProcessUnpremultiplyAlpha { - /// To determine if the image is premultiplied alpha, - /// checking premultiplied RGBA value is one where any of the R/G/B channel values exceeds the alpha channel value.\ - /// https://www.w3.org/TR/webgpu/#color-spaces - fn is_premultiplied_alpha(&self) -> bool; - fn unpremultiply_alpha(&self) -> Self; -} - -impl ProcessUnpremultiplyAlpha for Rgba { - fn is_premultiplied_alpha(&self) -> bool { - let max_t = T::DEFAULT_MAX_VALUE; - - let pixel = [self.0[0], self.0[1], self.0[2]]; - let alpha_index = self.0.len() - 1; - let alpha = self.0[alpha_index]; - - match pixel.iter().max() { - Some(rgb_max) => rgb_max < &max_t.saturating_mul(&alpha), - // usually doesn't reach here - None => false, - } - } - - fn unpremultiply_alpha(&self) -> Self { - let max_t = T::DEFAULT_MAX_VALUE; - - let mut pixel = [self.0[0], self.0[1], self.0[2], self.0[3]]; - let alpha_index = pixel.len() - 1; - let alpha = pixel[alpha_index]; - - for rgb in pixel.iter_mut().take(alpha_index) { - *rgb = NumCast::from( - (rgb.to_f32().unwrap() - / (alpha.to_f32().unwrap() / max_t.to_f32().unwrap())) - .round(), - ) - .unwrap(); - } - - Rgba::([pixel[0], pixel[1], pixel[2], pixel[alpha_index]]) - } -} - -impl ProcessUnpremultiplyAlpha - for LumaA -{ - fn is_premultiplied_alpha(&self) -> bool { - let max_t = T::DEFAULT_MAX_VALUE; - - let pixel = [self.0[0]]; - let alpha_index = self.0.len() - 1; - let alpha = self.0[alpha_index]; - - pixel[0] < max_t.saturating_mul(&alpha) - } - - fn unpremultiply_alpha(&self) -> Self { - let max_t = T::DEFAULT_MAX_VALUE; - - let mut pixel = [self.0[0], self.0[1]]; - let alpha_index = pixel.len() - 1; - let alpha = pixel[alpha_index]; - - for rgb in pixel.iter_mut().take(alpha_index) { - *rgb = NumCast::from( - (rgb.to_f32().unwrap() - / (alpha.to_f32().unwrap() / max_t.to_f32().unwrap())) - .round(), - ) - .unwrap(); - } - - LumaA::([pixel[0], pixel[alpha_index]]) - } -} - -fn process_unpremultiply_alpha(image: &I) -> ImageBuffer> -where - I: GenericImageView, - P: Pixel + ProcessUnpremultiplyAlpha + 'static, - S: Primitive + 'static, -{ - let (width, height) = image.dimensions(); - let mut out = ImageBuffer::new(width, height); - - let is_premultiplied_alpha = image - .pixels() - .any(|(_, _, pixel)| pixel.is_premultiplied_alpha()); - - for (x, y, pixel) in image.pixels() { - let pixel = if is_premultiplied_alpha { - pixel.unpremultiply_alpha() - } else { - // return the original - pixel - }; - - out.put_pixel(x, y, pixel); - } - - out -} - -fn apply_unpremultiply_alpha( - image: &DynamicImage, -) -> Result { - match image.color() { - ColorType::La8 => Ok(DynamicImage::ImageLumaA8( - process_unpremultiply_alpha(&image.to_luma_alpha8()), - )), - ColorType::Rgba8 => Ok(DynamicImage::ImageRgba8( - process_unpremultiply_alpha(&image.to_rgba8()), - )), - ColorType::La16 => Ok(DynamicImage::ImageLumaA16( - process_unpremultiply_alpha(&image.to_luma_alpha16()), - )), - ColorType::Rgba16 => Ok(DynamicImage::ImageRgba16( - process_unpremultiply_alpha(&image.to_rgba16()), - )), - _ => Err(type_error(image_error_message( - "apply premultiplyAlpha: none", - "The color type is not supported.", - ))), - } -} - -// reference -// https://www.w3.org/TR/css-color-4/#color-conversion-code -fn srgb_to_linear(value: T) -> f32 { - if value.to_f32().unwrap() <= 0.04045 { - value.to_f32().unwrap() / 12.92 - } else { - ((value.to_f32().unwrap() + 0.055) / 1.055).powf(2.4) - } -} - -// reference -// https://www.w3.org/TR/css-color-4/#color-conversion-code -fn linear_to_display_p3(value: T) -> f32 { - if value.to_f32().unwrap() <= 0.0031308 { - value.to_f32().unwrap() * 12.92 - } else { - 1.055 * value.to_f32().unwrap().powf(1.0 / 2.4) - 0.055 - } -} - -fn normalize_value_to_0_1(value: T) -> f32 { - value.to_f32().unwrap() / T::DEFAULT_MAX_VALUE.to_f32().unwrap() -} - -fn unnormalize_value_from_0_1(value: f32) -> T { - NumCast::from( - (value.clamp(0.0, 1.0) * T::DEFAULT_MAX_VALUE.to_f32().unwrap()).round(), - ) - .unwrap() -} - -fn srgb_to_display_p3(r: T, g: T, b: T) -> (T, T, T) { - // normalize the value to 0.0 - 1.0 - let (r, g, b) = ( - normalize_value_to_0_1(r), - normalize_value_to_0_1(g), - normalize_value_to_0_1(b), - ); - - // sRGB -> Linear RGB - let (r, g, b) = (srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)); - - // Display-P3 (RGB) -> Display-P3 (XYZ) - // - // inv[ P3-D65 (D65) to XYZ ] * [ sRGB (D65) to XYZ ] - // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html - // https://fujiwaratko.sakura.ne.jp/infosci/colorspace/colorspace2_e.html - - // [ sRGB (D65) to XYZ ] - #[rustfmt::skip] - let (m1x, m1y, m1z) = ( - [0.4124564, 0.3575761, 0.1804375], - [0.2126729, 0.7151522, 0.0721750], - [0.0193339, 0.1191920, 0.9503041], - ); - - let (r, g, b) = ( - r * m1x[0] + g * m1x[1] + b * m1x[2], - r * m1y[0] + g * m1y[1] + b * m1y[2], - r * m1z[0] + g * m1z[1] + b * m1z[2], - ); - - // inv[ P3-D65 (D65) to XYZ ] - #[rustfmt::skip] - let (m2x, m2y, m2z) = ( - [ 2.493496911941425, -0.9313836179191239, -0.40271078445071684 ], - [ -0.8294889695615747, 1.7626640603183463, 0.023624685841943577 ], - [ 0.03584583024378447,-0.07617238926804182, 0.9568845240076872 ], - ); - - let (r, g, b) = ( - r * m2x[0] + g * m2x[1] + b * m2x[2], - r * m2y[0] + g * m2y[1] + b * m2y[2], - r * m2z[0] + g * m2z[1] + b * m2z[2], - ); - - // This calculation is similar as above that it is a little faster, but less accurate. - // let r = 0.8225 * r + 0.1774 * g + 0.0000 * b; - // let g = 0.0332 * r + 0.9669 * g + 0.0000 * b; - // let b = 0.0171 * r + 0.0724 * g + 0.9108 * b; - - // Display-P3 (Linear) -> Display-P3 - let (r, g, b) = ( - linear_to_display_p3(r), - linear_to_display_p3(g), - linear_to_display_p3(b), - ); - - // unnormalize the value from 0.0 - 1.0 - ( - unnormalize_value_from_0_1(r), - unnormalize_value_from_0_1(g), - unnormalize_value_from_0_1(b), - ) -} - -trait ProcessColorSpaceConversion { - /// Display P3 Color Encoding (v 1.0) - /// https://www.color.org/chardata/rgb/DisplayP3.xalter - fn process_srgb_to_display_p3(&self) -> Self; -} - -impl ProcessColorSpaceConversion for Rgb { - fn process_srgb_to_display_p3(&self) -> Self { - let (r, g, b) = (self.0[0], self.0[1], self.0[2]); - - let (r, g, b) = srgb_to_display_p3(r, g, b); - - Rgb::([r, g, b]) - } -} - -impl ProcessColorSpaceConversion for Rgba { - fn process_srgb_to_display_p3(&self) -> Self { - let (r, g, b, a) = (self.0[0], self.0[1], self.0[2], self.0[3]); - - let (r, g, b) = srgb_to_display_p3(r, g, b); - - Rgba::([r, g, b, a]) - } -} - -fn process_srgb_to_display_p3(image: &I) -> ImageBuffer> -where - I: GenericImageView, - P: Pixel + ProcessColorSpaceConversion + 'static, - S: Primitive + 'static, -{ - let (width, height) = image.dimensions(); - let mut out = ImageBuffer::new(width, height); - - for (x, y, pixel) in image.pixels() { - let pixel = pixel.process_srgb_to_display_p3(); - - out.put_pixel(x, y, pixel); - } - - out -} - -trait SliceToPixel { - fn slice_to_pixel(pixel: &[u8]) -> Self; -} - -impl SliceToPixel for Luma { - fn slice_to_pixel(pixel: &[u8]) -> Self { - let pixel: &[T] = cast_slice(pixel); - let pixel = [pixel[0]]; - - Luma::(pixel) - } -} - -impl SliceToPixel for LumaA { - fn slice_to_pixel(pixel: &[u8]) -> Self { - let pixel: &[T] = cast_slice(pixel); - let pixel = [pixel[0], pixel[1]]; - - LumaA::(pixel) - } -} - -impl SliceToPixel for Rgb { - fn slice_to_pixel(pixel: &[u8]) -> Self { - let pixel: &[T] = cast_slice(pixel); - let pixel = [pixel[0], pixel[1], pixel[2]]; - - Rgb::(pixel) - } -} - -impl SliceToPixel for Rgba { - fn slice_to_pixel(pixel: &[u8]) -> Self { - let pixel: &[T] = cast_slice(pixel); - let pixel = [pixel[0], pixel[1], pixel[2], pixel[3]]; - - Rgba::(pixel) - } -} - -/// Convert the pixel slice to an array to avoid the copy to Vec. -/// I implemented this trait because of I couldn't find a way to effectively combine -/// the `Transform` struct of `lcms2` and `Pixel` trait of `image`. -/// If there is an implementation that is safer and can withstand changes, I would like to adopt it. -trait SliceToArray { - fn slice_to_array(pixel: &[u8]) -> [u8; N]; -} - -macro_rules! impl_slice_to_array { - ($type:ty, $n:expr) => { - impl SliceToArray<$n> for $type { - fn slice_to_array(pixel: &[u8]) -> [u8; $n] { - let mut dst = [0_u8; $n]; - dst.copy_from_slice(&pixel[..$n]); - - dst - } - } - }; -} - -impl_slice_to_array!(Luma, 1); -impl_slice_to_array!(Luma, 2); -impl_slice_to_array!(LumaA, 2); -impl_slice_to_array!(LumaA, 4); -impl_slice_to_array!(Rgb, 3); -impl_slice_to_array!(Rgb, 6); -impl_slice_to_array!(Rgba, 4); -impl_slice_to_array!(Rgba, 8); - -fn process_color_space_from_icc_profile_to_srgb( - image: &DynamicImage, - icc_profile: Profile, -) -> ImageBuffer> -where - P: Pixel + SliceToPixel + SliceToArray + 'static, - S: Primitive + 'static, -{ - let (width, height) = image.dimensions(); - let mut out = ImageBuffer::new(width, height); - let chunk_size = image.color().bytes_per_pixel() as usize; - let pixel_iter = image - .as_bytes() - .chunks_exact(chunk_size) - .zip(image.pixels()); - let pixel_format = match image.color() { - ColorType::L8 => PixelFormat::GRAY_8, - ColorType::L16 => PixelFormat::GRAY_16, - ColorType::La8 => PixelFormat::GRAYA_8, - ColorType::La16 => PixelFormat::GRAYA_16, - ColorType::Rgb8 => PixelFormat::RGB_8, - ColorType::Rgb16 => PixelFormat::RGB_16, - ColorType::Rgba8 => PixelFormat::RGBA_8, - ColorType::Rgba16 => PixelFormat::RGBA_16, - // This arm usually doesn't reach, but it should be handled with returning the original image. - _ => { - return { - for (pixel, (x, y, _)) in pixel_iter { - out.put_pixel(x, y, P::slice_to_pixel(&pixel)); - } - out - } - } - }; - let srgb_icc_profile = Profile::new_srgb(); - let transformer = Transform::new( - &icc_profile, - pixel_format, - &srgb_icc_profile, - pixel_format, - srgb_icc_profile.header_rendering_intent(), - ); - - for (pixel, (x, y, _)) in pixel_iter { - let pixel = match transformer { - Ok(ref transformer) => { - let mut dst = P::slice_to_array(pixel); - transformer.transform_in_place(&mut dst); - - dst - } - // This arm will reach when the ffi call fails. - Err(_) => P::slice_to_array(pixel), - }; - - out.put_pixel(x, y, P::slice_to_pixel(&pixel)); - } - - out -} - -/// 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>, - image_bitmap_source: &ImageBitmapSource, - color_space_conversion: &ColorSpaceConversion, - predefined_color_space: &PredefinedColorSpace, -) -> Result { - match color_space_conversion { - // return the decoded image as is. - ColorSpaceConversion::None => Ok(image), - ColorSpaceConversion::Default => { - match image_bitmap_source { - ImageBitmapSource::Blob => match icc_profile { - // If there is no color profile information, return the image as is. - None => Ok(image), - Some(icc_profile) => match Profile::new_icc(&icc_profile) { - // If the color profile information is invalid, return the image as is. - Err(_) => Ok(image), - Ok(icc_profile) => match image.color() { - ColorType::L8 => Ok(DynamicImage::ImageLuma8( - process_color_space_from_icc_profile_to_srgb::<_, _, 1>( - &image, - icc_profile, - ), - )), - ColorType::L16 => Ok(DynamicImage::ImageLuma16( - process_color_space_from_icc_profile_to_srgb::<_, _, 2>( - &image, - icc_profile, - ), - )), - ColorType::La8 => Ok(DynamicImage::ImageLumaA8( - process_color_space_from_icc_profile_to_srgb::<_, _, 2>( - &image, - icc_profile, - ), - )), - ColorType::La16 => Ok(DynamicImage::ImageLumaA16( - process_color_space_from_icc_profile_to_srgb::<_, _, 4>( - &image, - icc_profile, - ), - )), - ColorType::Rgb8 => Ok(DynamicImage::ImageRgb8( - process_color_space_from_icc_profile_to_srgb::<_, _, 3>( - &image, - icc_profile, - ), - )), - ColorType::Rgb16 => Ok(DynamicImage::ImageRgb16( - process_color_space_from_icc_profile_to_srgb::<_, _, 6>( - &image, - icc_profile, - ), - )), - ColorType::Rgba8 => Ok(DynamicImage::ImageRgba8( - process_color_space_from_icc_profile_to_srgb::<_, _, 4>( - &image, - icc_profile, - ), - )), - ColorType::Rgba16 => Ok(DynamicImage::ImageRgba16( - process_color_space_from_icc_profile_to_srgb::<_, _, 8>( - &image, - icc_profile, - ), - )), - _ => Err(type_error(image_error_message( - "apply colorspaceConversion: default", - "The color type is not supported.", - ))), - }, - }, - }, - ImageBitmapSource::ImageData => match predefined_color_space { - // If the color space is sRGB, return the image as is. - PredefinedColorSpace::Srgb => Ok(image), - PredefinedColorSpace::DisplayP3 => { - match image.color() { - // The conversion of the lumincance color types to the display-p3 color space is meaningless. - ColorType::L8 => Ok(DynamicImage::ImageLuma8(image.to_luma8())), - ColorType::L16 => { - Ok(DynamicImage::ImageLuma16(image.to_luma16())) - } - ColorType::La8 => { - Ok(DynamicImage::ImageLumaA8(image.to_luma_alpha8())) - } - ColorType::La16 => { - Ok(DynamicImage::ImageLumaA16(image.to_luma_alpha16())) - } - ColorType::Rgb8 => Ok(DynamicImage::ImageRgb8( - process_srgb_to_display_p3(&image.to_rgb8()), - )), - ColorType::Rgb16 => Ok(DynamicImage::ImageRgb16( - process_srgb_to_display_p3(&image.to_rgb16()), - )), - ColorType::Rgba8 => Ok(DynamicImage::ImageRgba8( - process_srgb_to_display_p3(&image.to_rgba8()), - )), - ColorType::Rgba16 => Ok(DynamicImage::ImageRgba16( - process_srgb_to_display_p3(&image.to_rgba16()), - )), - _ => Err(type_error(image_error_message( - "apply colorspace: display-p3", - "The color type is not supported.", - ))), - } - } - }, - } - } - } -} - -#[derive(Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -enum ImageResizeQuality { - Pixelated, - Low, - Medium, - High, -} - -#[derive(Debug, Deserialize, PartialEq)] -// Follow the cases defined in the spec -enum ImageBitmapSource { - Blob, - ImageData, -} - -#[derive(Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -enum PremultiplyAlpha { - Default, - Premultiply, - None, -} - -// https://github.com/gfx-rs/wgpu/blob/04618b36a89721c23dc46f5844c71c0e10fc7844/wgpu-types/src/lib.rs#L6948C10-L6948C30 -#[derive(Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -enum PredefinedColorSpace { - Srgb, - #[serde(rename = "display-p3")] - DisplayP3, -} - -#[derive(Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -enum ColorSpaceConversion { - Default, - None, -} - -#[derive(Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -enum ImageOrientation { - FlipY, - #[serde(rename = "from-image")] - FromImage, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ImageProcessArgs { - width: u32, - height: u32, - sx: Option, - sy: Option, - sw: Option, - sh: Option, - image_orientation: ImageOrientation, - premultiply_alpha: PremultiplyAlpha, - predefined_color_space: PredefinedColorSpace, - color_space_conversion: ColorSpaceConversion, - resize_width: Option, - resize_height: Option, - resize_quality: ImageResizeQuality, - image_bitmap_source: ImageBitmapSource, - mime_type: String, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct ImageProcessResult { - data: ToJsBuffer, - width: u32, - height: u32, -} - -// -// 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 -// - -trait ImageDecoderFromReader<'a, R: BufRead + Seek> { - fn to_decoder(reader: R) -> Result - where - Self: Sized; - fn to_intermediate_image(self) -> Result; - fn get_icc_profile(&mut self) -> Option>; -} - -type ImageDecoderFromReaderType<'a> = BufReader>; - -fn image_decoding_error(error: ImageError) -> DOMExceptionInvalidStateError { - DOMExceptionInvalidStateError::new(&image_error_message( - "decoding", - &error.to_string(), - )) -} - -macro_rules! impl_image_decoder_from_reader { - ($decoder:ty, $reader:ty) => { - impl<'a, R: BufRead + Seek> ImageDecoderFromReader<'a, R> for $decoder { - fn to_decoder(reader: R) -> Result - where - Self: Sized, - { - match <$decoder>::new(reader) { - Ok(decoder) => Ok(decoder), - Err(err) => return Err(image_decoding_error(err).into()), - } - } - fn to_intermediate_image(self) -> Result { - match DynamicImage::from_decoder(self) { - Ok(image) => Ok(image), - Err(err) => Err(image_decoding_error(err).into()), - } - } - fn get_icc_profile(&mut self) -> Option> { - match self.icc_profile() { - Ok(profile) => profile, - Err(_) => None, - } - } - } - }; -} - -// If PngDecoder decodes an animated image, it returns the default image if one is set, or the first frame if not. -impl_image_decoder_from_reader!(PngDecoder, ImageDecoderFromReaderType); -impl_image_decoder_from_reader!(JpegDecoder, ImageDecoderFromReaderType); -// The GifDecoder decodes the first frame. -impl_image_decoder_from_reader!(GifDecoder, ImageDecoderFromReaderType); -impl_image_decoder_from_reader!(BmpDecoder, ImageDecoderFromReaderType); -impl_image_decoder_from_reader!(IcoDecoder, ImageDecoderFromReaderType); -// The WebPDecoder decodes the first frame. -impl_image_decoder_from_reader!(WebPDecoder, ImageDecoderFromReaderType); - -type DecodeBitmapDataReturn = (DynamicImage, u32, u32, Option>); - -fn decode_bitmap_data( - buf: &[u8], - width: u32, - height: u32, - image_bitmap_source: &ImageBitmapSource, - mime_type: String, -) -> Result { - let (view, width, height, icc_profile) = match image_bitmap_source { - ImageBitmapSource::Blob => { - let (image, icc_profile) = match &*mime_type { - // Should we support the "image/apng" MIME type here? - "image/png" => { - let mut decoder: PngDecoder = - ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( - buf, - )))?; - let icc_profile = decoder.get_icc_profile(); - (decoder.to_intermediate_image()?, icc_profile) - } - "image/jpeg" => { - let mut decoder: JpegDecoder = - ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( - buf, - )))?; - let icc_profile = decoder.get_icc_profile(); - (decoder.to_intermediate_image()?, icc_profile) - } - "image/gif" => { - let mut decoder: GifDecoder = - ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( - buf, - )))?; - let icc_profile = decoder.get_icc_profile(); - (decoder.to_intermediate_image()?, icc_profile) - } - "image/bmp" => { - let mut decoder: BmpDecoder = - ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( - buf, - )))?; - let icc_profile = decoder.get_icc_profile(); - (decoder.to_intermediate_image()?, icc_profile) - } - "image/x-icon" => { - let mut decoder: IcoDecoder = - ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( - buf, - )))?; - let icc_profile = decoder.get_icc_profile(); - (decoder.to_intermediate_image()?, icc_profile) - } - "image/webp" => { - let mut decoder: WebPDecoder = - ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( - buf, - )))?; - let icc_profile = decoder.get_icc_profile(); - (decoder.to_intermediate_image()?, icc_profile) - } - "" => { - return Err( - DOMExceptionInvalidStateError::new( - &format!("The MIME type of source image is not specified. -INFO: The behavior of the Blob constructor in browsers is different from the spec. -It needs to specify the MIME type like {} that works well between Deno and browsers. -See: https://developer.mozilla.org/en-US/docs/Web/API/Blob/type\n", - cyan("new Blob([blobParts], { type: 'image/png' })") - )).into(), - ) - } - // return an error if the MIME type is not supported in the variable list of ImageTypePatternTable below - // ext/web/01_mimesniff.js - // - // NOTE: Chromium supports AVIF - // https://source.chromium.org/chromium/chromium/src/+/ef3f4e4ed97079dc57861d1195fb2389483bc195:third_party/blink/renderer/platform/image-decoders/image_decoder.cc;l=311 - x => { - return Err( - DOMExceptionInvalidStateError::new( - &format!("The the MIME type {} of source image is not a supported format. -INFO: The following MIME types are supported: -See: https://mimesniff.spec.whatwg.org/#image-type-pattern-matching-algorithm\n", - x - )).into() - ) - } - }; - - let width = image.width(); - let height = image.height(); - - (image, width, height, 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(type_error(image_error_message( - "decoding", - "The Chunk Data is not big enough with the specified width and height.", - ))) - } - }; - - (image, width, height, None) - } - }; - - Ok((view, width, height, icc_profile)) -} - -#[op2] -#[serde] -fn op_image_process( - #[buffer] zero_copy: JsBuffer, - #[serde] args: ImageProcessArgs, -) -> Result { - let buf = &*zero_copy; - let ImageProcessArgs { - width, - height, - sh, - sw, - sx, - sy, - image_orientation, - premultiply_alpha, - predefined_color_space, - color_space_conversion, - resize_width, - resize_height, - resize_quality, - image_bitmap_source, - mime_type, - } = ImageProcessArgs { - width: args.width, - height: args.height, - sx: args.sx, - sy: args.sy, - sw: args.sw, - sh: args.sh, - image_orientation: args.image_orientation, - premultiply_alpha: args.premultiply_alpha, - predefined_color_space: args.predefined_color_space, - color_space_conversion: args.color_space_conversion, - resize_width: args.resize_width, - resize_height: args.resize_height, - resize_quality: args.resize_quality, - image_bitmap_source: args.image_bitmap_source, - mime_type: args.mime_type, - }; - - let (view, width, height, icc_profile) = - decode_bitmap_data(buf, width, height, &image_bitmap_source, mime_type)?; - - #[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; - - 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 - }; - - 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 - }; - - let color = view.color(); - - let surface = if !(width == surface_width - && height == surface_height - && input_x == 0 - && input_y == 0) - { - let mut surface = DynamicImage::new(surface_width, surface_height, color); - overlay(&mut surface, &view, input_x, input_y); - - surface - } else { - view - }; - - let filter_type = match resize_quality { - ImageResizeQuality::Pixelated => FilterType::Nearest, - ImageResizeQuality::Low => FilterType::Triangle, - ImageResizeQuality::Medium => FilterType::CatmullRom, - ImageResizeQuality::High => FilterType::Lanczos3, - }; - - // should use resize_exact - // https://github.com/image-rs/image/issues/1220#issuecomment-632060015 - let image_out = - surface.resize_exact(output_width, output_height, filter_type); - - // - // FIXME: It also need to fix about orientation when the spec is updated. - // - // > Multiple browser vendors discussed this a while back and (99% sure, from recollection) - // > agreed to change createImageBitmap's behavior. - // > The HTML spec should be updated to say: - // > first EXIF orientation is applied, and then if imageOrientation is flipY, the image is flipped vertically - // https://github.com/whatwg/html/issues/8085#issuecomment-2204696312 - let image_out = if image_orientation == ImageOrientation::FlipY { - image_out.flipv() - } else { - image_out - }; - - // 9. - let image_out = apply_color_space_conversion( - image_out, - icc_profile, - &image_bitmap_source, - &color_space_conversion, - &predefined_color_space, - )?; - - // 10. - if color.has_alpha() { - match premultiply_alpha { - // 1. - PremultiplyAlpha::Default => { /* noop */ } - - // https://html.spec.whatwg.org/multipage/canvas.html#convert-from-premultiplied - - // 2. - PremultiplyAlpha::Premultiply => { - let result = apply_premultiply_alpha(&image_out)?; - let data = to_js_buffer(&result); - return Ok(ImageProcessResult { - data, - width: output_width, - height: output_height, - }); - } - // 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(ImageProcessResult { - data: image_out.clone().into_bytes().into(), - width: output_width, - height: output_height, - }); - } - - let result = apply_unpremultiply_alpha(&image_out)?; - let data = to_js_buffer(&result); - return Ok(ImageProcessResult { - data, - width: output_width, - height: output_height, - }); - } - } - } - - Ok(ImageProcessResult { - data: image_out.clone().into_bytes().into(), - width: output_width, - height: output_height, - }) -} +pub mod idl; +mod image_decoder; +mod image_ops; +mod op_create_image_bitmap; +use op_create_image_bitmap::op_create_image_bitmap; deno_core::extension!( deno_canvas, deps = [deno_webidl, deno_web, deno_webgpu], - ops = [op_image_process], + ops = [op_create_image_bitmap], lazy_loaded_esm = ["01_image.js"], ); diff --git a/ext/canvas/op_create_image_bitmap.rs b/ext/canvas/op_create_image_bitmap.rs new file mode 100644 index 0000000000..91cf68a08c --- /dev/null +++ b/ext/canvas/op_create_image_bitmap.rs @@ -0,0 +1,453 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::io::BufReader; +use std::io::Cursor; + +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::op2; +use deno_core::JsBuffer; +use deno_core::ToJsBuffer; +use deno_terminal::colors::cyan; +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::ColorType; +use image::DynamicImage; +use image::RgbaImage; +use serde::Deserialize; +use serde::Serialize; + +use crate::error::image_error_message; +use crate::error::DOMExceptionInvalidStateError; +use crate::idl::PredefinedColorSpace; +use crate::image_decoder::ImageDecoderFromReader; +use crate::image_decoder::ImageDecoderFromReaderType; +use crate::image_ops::premultiply_alpha as process_premultiply_alpha; +use crate::image_ops::srgb_to_display_p3; +use crate::image_ops::to_srgb_from_icc_profile; +use crate::image_ops::unpremultiply_alpha; + +#[derive(Debug, Deserialize, PartialEq)] +// Follow the cases defined in the spec +enum ImageBitmapSource { + Blob, + ImageData, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +enum ImageOrientation { + FlipY, + #[serde(rename = "from-image")] + FromImage, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +enum PremultiplyAlpha { + Default, + Premultiply, + None, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +enum ColorSpaceConversion { + Default, + None, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +enum ResizeQuality { + Pixelated, + Low, + Medium, + High, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct OpCreateImageBitmapArgs { + width: u32, + height: u32, + sx: Option, + sy: Option, + sw: Option, + sh: Option, + image_orientation: ImageOrientation, + premultiply_alpha: PremultiplyAlpha, + predefined_color_space: PredefinedColorSpace, + color_space_conversion: ColorSpaceConversion, + resize_width: Option, + resize_height: Option, + resize_quality: ResizeQuality, + image_bitmap_source: ImageBitmapSource, + mime_type: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct OpCreateImageBitmapReturn { + data: ToJsBuffer, + width: u32, + height: u32, +} + +type DecodeBitmapDataReturn = (DynamicImage, u32, u32, Option>); + +fn decode_bitmap_data( + buf: &[u8], + width: u32, + height: u32, + image_bitmap_source: &ImageBitmapSource, + mime_type: String, +) -> Result { + let (image, width, height, icc_profile) = match image_bitmap_source { + ImageBitmapSource::Blob => { + let (image, icc_profile) = match &*mime_type { + // Should we support the "image/apng" MIME type here? + "image/png" => { + let mut decoder: PngDecoder = + ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( + buf, + )))?; + let icc_profile = decoder.get_icc_profile(); + (decoder.to_intermediate_image()?, icc_profile) + } + "image/jpeg" => { + let mut decoder: JpegDecoder = + ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( + buf, + )))?; + let icc_profile = decoder.get_icc_profile(); + (decoder.to_intermediate_image()?, icc_profile) + } + "image/gif" => { + let mut decoder: GifDecoder = + ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( + buf, + )))?; + let icc_profile = decoder.get_icc_profile(); + (decoder.to_intermediate_image()?, icc_profile) + } + "image/bmp" => { + let mut decoder: BmpDecoder = + ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( + buf, + )))?; + let icc_profile = decoder.get_icc_profile(); + (decoder.to_intermediate_image()?, icc_profile) + } + "image/x-icon" => { + let mut decoder: IcoDecoder = + ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( + buf, + )))?; + let icc_profile = decoder.get_icc_profile(); + (decoder.to_intermediate_image()?, icc_profile) + } + "image/webp" => { + let mut decoder: WebPDecoder = + ImageDecoderFromReader::to_decoder(BufReader::new(Cursor::new( + buf, + )))?; + let icc_profile = decoder.get_icc_profile(); + (decoder.to_intermediate_image()?, icc_profile) + } + "" => { + return Err( + DOMExceptionInvalidStateError::new( + &format!("The MIME type of source image is not specified. +INFO: The behavior of the Blob constructor in browsers is different from the spec. +It needs to specify the MIME type like {} that works well between Deno and browsers. +See: https://developer.mozilla.org/en-US/docs/Web/API/Blob/type\n", + cyan("new Blob([blobParts], { type: 'image/png' })") + )).into(), + ) + } + // return an error if the MIME type is not supported in the variable list of ImageTypePatternTable below + // ext/web/01_mimesniff.js + // + // NOTE: Chromium supports AVIF + // https://source.chromium.org/chromium/chromium/src/+/ef3f4e4ed97079dc57861d1195fb2389483bc195:third_party/blink/renderer/platform/image-decoders/image_decoder.cc;l=311 + x => { + return Err( + DOMExceptionInvalidStateError::new( + &format!("The the MIME type {} of source image is not a supported format. +INFO: The following MIME types are supported: +See: https://mimesniff.spec.whatwg.org/#image-type-pattern-matching-algorithm\n", + x + )).into() + ) + } + }; + + let width = image.width(); + let height = image.height(); + + (image, width, height, 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(type_error(image_error_message( + "decoding", + "The Chunk Data is not big enough with the specified width and height.", + ))) + } + }; + + (image, width, height, None) + } + }; + + Ok((image, width, height, 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>, + image_bitmap_source: &ImageBitmapSource, + color_space_conversion: &ColorSpaceConversion, + predefined_color_space: &PredefinedColorSpace, +) -> Result { + match color_space_conversion { + // return the decoded image as is. + ColorSpaceConversion::None => Ok(image), + ColorSpaceConversion::Default => { + match image_bitmap_source { + ImageBitmapSource::Blob => { + fn color_unmatch(x: ColorType) -> Result { + Err(type_error(image_error_message( + "apply colorspaceConversion: default", + &format!("The color type {:?} is not supported.", x), + ))) + } + to_srgb_from_icc_profile(image, icc_profile, Some(color_unmatch)) + } + ImageBitmapSource::ImageData => match predefined_color_space { + // If the color space is sRGB, return the image as is. + PredefinedColorSpace::Srgb => Ok(image), + PredefinedColorSpace::DisplayP3 => { + fn unmatch(x: ColorType) -> Result { + Err(type_error(image_error_message( + "apply colorspace: display-p3", + &format!("The color type {:?} is not supported.", x), + ))) + } + srgb_to_display_p3(image, Some(unmatch)) + } + }, + } + } + } +} + +fn apply_premultiply_alpha( + image: DynamicImage, + image_bitmap_source: &ImageBitmapSource, + premultiply_alpha: &PremultiplyAlpha, +) -> Result { + let color = image.color(); + if !color.has_alpha() { + Ok(image) + } else { + 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, None), + // 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, None) + } + } + } +} + +#[op2] +#[serde] +pub(super) fn op_create_image_bitmap( + #[buffer] zero_copy: JsBuffer, + #[serde] args: OpCreateImageBitmapArgs, +) -> Result { + let buf = &*zero_copy; + let OpCreateImageBitmapArgs { + width, + height, + sh, + sw, + sx, + sy, + image_orientation, + premultiply_alpha, + predefined_color_space, + color_space_conversion, + resize_width, + resize_height, + resize_quality, + image_bitmap_source, + mime_type, + } = OpCreateImageBitmapArgs { + width: args.width, + height: args.height, + sx: args.sx, + sy: args.sy, + sw: args.sw, + sh: args.sh, + image_orientation: args.image_orientation, + premultiply_alpha: args.premultiply_alpha, + predefined_color_space: args.predefined_color_space, + color_space_conversion: args.color_space_conversion, + resize_width: args.resize_width, + resize_height: args.resize_height, + resize_quality: args.resize_quality, + image_bitmap_source: args.image_bitmap_source, + mime_type: args.mime_type, + }; + + // 6. Switch on image: + let (image, width, height, 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 image = image.resize_exact(output_width, output_height, filter_type); + + // 8. + let image = if image_orientation == ImageOrientation::FlipY { + image.flipv() + } else { + image + }; + + // 9. + let image = apply_color_space_conversion( + image, + icc_profile, + &image_bitmap_source, + &color_space_conversion, + &predefined_color_space, + )?; + + // 10. + let image = + apply_premultiply_alpha(image, &image_bitmap_source, &premultiply_alpha)?; + + Ok(OpCreateImageBitmapReturn { + data: image.into_bytes().into(), + width: output_width, + height: output_height, + }) +}