mirror of
https://github.com/denoland/deno.git
synced 2025-01-21 04:52:26 -05:00
cleanup premultiplyAlpha
and adding test
This commit is contained in:
parent
22cf2f9bd8
commit
32d71f5bdc
4 changed files with 119 additions and 24 deletions
|
@ -352,9 +352,7 @@ function processImage(input, width, height, sx, sy, sw, sh, options) {
|
|||
outputHeight,
|
||||
resizeQuality: options.resizeQuality,
|
||||
flipY: options.imageOrientation === "flipY",
|
||||
premultiply: options.premultiplyAlpha === "default"
|
||||
? null
|
||||
: (options.premultiplyAlpha === "premultiply"),
|
||||
premultiplyAlpha: options.premultiplyAlpha,
|
||||
imageBitmapSource: options.imageBitmapSource,
|
||||
},
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@ use deno_core::op2;
|
|||
use deno_core::ToJsBuffer;
|
||||
use image::imageops::FilterType;
|
||||
use image::AnimationDecoder;
|
||||
use image::GenericImage;
|
||||
use image::GenericImageView;
|
||||
use image::Pixel;
|
||||
use serde::Deserialize;
|
||||
|
@ -26,13 +27,21 @@ enum ImageResizeQuality {
|
|||
High,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
// Follow the cases defined in the spec
|
||||
enum ImageBitmapSource {
|
||||
Blob,
|
||||
ImageData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum PremultiplyAlpha {
|
||||
Default,
|
||||
Premultiply,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ImageProcessArgs {
|
||||
|
@ -46,7 +55,7 @@ struct ImageProcessArgs {
|
|||
output_height: u32,
|
||||
resize_quality: ImageResizeQuality,
|
||||
flip_y: bool,
|
||||
premultiply: Option<bool>,
|
||||
premultiply_alpha: PremultiplyAlpha,
|
||||
image_bitmap_source: ImageBitmapSource,
|
||||
}
|
||||
|
||||
|
@ -106,27 +115,56 @@ fn op_image_process(
|
|||
|
||||
// ignore 9.
|
||||
|
||||
// 10.
|
||||
if color.has_alpha() {
|
||||
if let Some(premultiply) = args.premultiply {
|
||||
let is_not_premultiplied = image_out.pixels().any(|(_, _, pixel)| {
|
||||
(pixel[0].max(pixel[1]).max(pixel[2])) > (255 * pixel[3])
|
||||
});
|
||||
match args.premultiply_alpha {
|
||||
// 1.
|
||||
PremultiplyAlpha::Default => { /* noop */ }
|
||||
|
||||
if premultiply {
|
||||
if is_not_premultiplied {
|
||||
for (_, _, mut pixel) in &mut image_out.pixels() {
|
||||
let alpha = pixel[3];
|
||||
pixel.apply_without_alpha(|channel| {
|
||||
(channel as f32 * (alpha as f32 / 255.0)) as u8
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if !is_not_premultiplied {
|
||||
for (_, _, mut pixel) in &mut image_out.pixels() {
|
||||
// https://html.spec.whatwg.org/multipage/canvas.html#convert-from-premultiplied
|
||||
|
||||
// 2.
|
||||
PremultiplyAlpha::Premultiply => {
|
||||
for (x, y, mut pixel) in image_out.clone().pixels() {
|
||||
let alpha = pixel[3];
|
||||
pixel.apply_without_alpha(|channel| {
|
||||
(channel as f32 / (alpha as f32 / 255.0)) as u8
|
||||
})
|
||||
let normalized_alpha = alpha as f64 / u8::MAX as f64;
|
||||
pixel.apply_without_alpha(|rgb| {
|
||||
(rgb as f64 * normalized_alpha).round() as u8
|
||||
});
|
||||
// FIXME: Looking at the API, put_pixel doesn't seem to be necessary,
|
||||
// but apply_without_alpha with DynamicImage doesn't seem to work as expected.
|
||||
image_out.put_pixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
// 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 args.image_bitmap_source == ImageBitmapSource::ImageData {
|
||||
return Ok(image_out.into_bytes().into());
|
||||
}
|
||||
|
||||
// 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
|
||||
let is_not_premultiplied = image_out.pixels().any(|(_, _, pixel)| {
|
||||
let [r, g, b] = [pixel[0], pixel[1], pixel[2]];
|
||||
let alpha = pixel[3];
|
||||
(r.max(g).max(b)) > u8::MAX.saturating_mul(alpha)
|
||||
});
|
||||
if is_not_premultiplied {
|
||||
return Ok(image_out.into_bytes().into());
|
||||
}
|
||||
|
||||
for (x, y, mut pixel) in image_out.clone().pixels() {
|
||||
let alpha = pixel[3];
|
||||
pixel.apply_without_alpha(|rgb| {
|
||||
(rgb as f64 / (alpha as f64 / u8::MAX as f64)).round() as u8
|
||||
});
|
||||
// FIXME: Looking at the API, put_pixel doesn't seem to be necessary,
|
||||
// but apply_without_alpha with DynamicImage doesn't seem to work as expected.
|
||||
image_out.put_pixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
BIN
tests/testdata/image/2x2-transparent8.png
vendored
Normal file
BIN
tests/testdata/image/2x2-transparent8.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 86 B |
|
@ -2,6 +2,8 @@
|
|||
|
||||
import { assertEquals, assertRejects } from "./test_util.ts";
|
||||
|
||||
const prefix = "tests/testdata/image";
|
||||
|
||||
function generateNumberedData(n: number): Uint8ClampedArray {
|
||||
return new Uint8ClampedArray(
|
||||
Array.from({ length: n }, (_, i) => [i + 1, 0, 0, 1]).flat(),
|
||||
|
@ -91,8 +93,65 @@ Deno.test(async function imageBitmapFlipY() {
|
|||
]));
|
||||
});
|
||||
|
||||
Deno.test(async function imageBitmapPremultiplyAlpha() {
|
||||
const imageData = new ImageData(
|
||||
new Uint8ClampedArray([
|
||||
255,
|
||||
255,
|
||||
0,
|
||||
153,
|
||||
]),
|
||||
1,
|
||||
1,
|
||||
);
|
||||
{
|
||||
const imageBitmap = await createImageBitmap(imageData, {
|
||||
premultiplyAlpha: "default",
|
||||
});
|
||||
// @ts-ignore: Deno[Deno.internal].core allowed
|
||||
// deno-fmt-ignore
|
||||
assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
|
||||
255, 255, 0, 153,
|
||||
]));
|
||||
}
|
||||
{
|
||||
const imageBitmap = await createImageBitmap(imageData, {
|
||||
premultiplyAlpha: "premultiply",
|
||||
});
|
||||
// @ts-ignore: Deno[Deno.internal].core allowed
|
||||
// deno-fmt-ignore
|
||||
assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
|
||||
153, 153, 0, 153
|
||||
]));
|
||||
}
|
||||
{
|
||||
const imageBitmap = await createImageBitmap(imageData, {
|
||||
premultiplyAlpha: "none",
|
||||
});
|
||||
// @ts-ignore: Deno[Deno.internal].core allowed
|
||||
// deno-fmt-ignore
|
||||
assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
|
||||
255, 255, 0, 153,
|
||||
]));
|
||||
}
|
||||
{
|
||||
const imageData = new Blob(
|
||||
[await Deno.readFile(`${prefix}/2x2-transparent8.png`)],
|
||||
{ type: "image/png" },
|
||||
);
|
||||
const imageBitmap = await createImageBitmap(imageData, {
|
||||
premultiplyAlpha: "none",
|
||||
});
|
||||
// @ts-ignore: Deno[Deno.internal].core allowed
|
||||
// deno-fmt-ignore
|
||||
assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
|
||||
255, 0, 0, 255, 0, 255, 0, 255,
|
||||
0, 0, 255, 255, 255, 0, 0, 127
|
||||
]));
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test(async function imageBitmapFromBlob() {
|
||||
const prefix = "tests/testdata/image";
|
||||
{
|
||||
const imageData = new Blob(
|
||||
[await Deno.readFile(`${prefix}/1x1-red8.png`)],
|
||||
|
|
Loading…
Add table
Reference in a new issue