mirror of
https://github.com/denoland/deno.git
synced 2025-03-09 13:49:37 -04: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,
|
outputHeight,
|
||||||
resizeQuality: options.resizeQuality,
|
resizeQuality: options.resizeQuality,
|
||||||
flipY: options.imageOrientation === "flipY",
|
flipY: options.imageOrientation === "flipY",
|
||||||
premultiply: options.premultiplyAlpha === "default"
|
premultiplyAlpha: options.premultiplyAlpha,
|
||||||
? null
|
|
||||||
: (options.premultiplyAlpha === "premultiply"),
|
|
||||||
imageBitmapSource: options.imageBitmapSource,
|
imageBitmapSource: options.imageBitmapSource,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@ use deno_core::op2;
|
||||||
use deno_core::ToJsBuffer;
|
use deno_core::ToJsBuffer;
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
use image::AnimationDecoder;
|
use image::AnimationDecoder;
|
||||||
|
use image::GenericImage;
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
use image::Pixel;
|
use image::Pixel;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -26,13 +27,21 @@ enum ImageResizeQuality {
|
||||||
High,
|
High,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, PartialEq)]
|
||||||
// Follow the cases defined in the spec
|
// Follow the cases defined in the spec
|
||||||
enum ImageBitmapSource {
|
enum ImageBitmapSource {
|
||||||
Blob,
|
Blob,
|
||||||
ImageData,
|
ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum PremultiplyAlpha {
|
||||||
|
Default,
|
||||||
|
Premultiply,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ImageProcessArgs {
|
struct ImageProcessArgs {
|
||||||
|
@ -46,7 +55,7 @@ struct ImageProcessArgs {
|
||||||
output_height: u32,
|
output_height: u32,
|
||||||
resize_quality: ImageResizeQuality,
|
resize_quality: ImageResizeQuality,
|
||||||
flip_y: bool,
|
flip_y: bool,
|
||||||
premultiply: Option<bool>,
|
premultiply_alpha: PremultiplyAlpha,
|
||||||
image_bitmap_source: ImageBitmapSource,
|
image_bitmap_source: ImageBitmapSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,27 +115,56 @@ fn op_image_process(
|
||||||
|
|
||||||
// ignore 9.
|
// ignore 9.
|
||||||
|
|
||||||
|
// 10.
|
||||||
if color.has_alpha() {
|
if color.has_alpha() {
|
||||||
if let Some(premultiply) = args.premultiply {
|
match args.premultiply_alpha {
|
||||||
let is_not_premultiplied = image_out.pixels().any(|(_, _, pixel)| {
|
// 1.
|
||||||
(pixel[0].max(pixel[1]).max(pixel[2])) > (255 * pixel[3])
|
PremultiplyAlpha::Default => { /* noop */ }
|
||||||
});
|
|
||||||
|
|
||||||
if premultiply {
|
// https://html.spec.whatwg.org/multipage/canvas.html#convert-from-premultiplied
|
||||||
if is_not_premultiplied {
|
|
||||||
for (_, _, mut pixel) in &mut image_out.pixels() {
|
// 2.
|
||||||
let alpha = pixel[3];
|
PremultiplyAlpha::Premultiply => {
|
||||||
pixel.apply_without_alpha(|channel| {
|
for (x, y, mut pixel) in image_out.clone().pixels() {
|
||||||
(channel as f32 * (alpha as f32 / 255.0)) as u8
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !is_not_premultiplied {
|
|
||||||
for (_, _, mut pixel) in &mut image_out.pixels() {
|
|
||||||
let alpha = pixel[3];
|
let alpha = pixel[3];
|
||||||
pixel.apply_without_alpha(|channel| {
|
let normalized_alpha = alpha as f64 / u8::MAX as f64;
|
||||||
(channel as f32 / (alpha as f32 / 255.0)) as u8
|
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";
|
import { assertEquals, assertRejects } from "./test_util.ts";
|
||||||
|
|
||||||
|
const prefix = "tests/testdata/image";
|
||||||
|
|
||||||
function generateNumberedData(n: number): Uint8ClampedArray {
|
function generateNumberedData(n: number): Uint8ClampedArray {
|
||||||
return new Uint8ClampedArray(
|
return new Uint8ClampedArray(
|
||||||
Array.from({ length: n }, (_, i) => [i + 1, 0, 0, 1]).flat(),
|
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() {
|
Deno.test(async function imageBitmapFromBlob() {
|
||||||
const prefix = "tests/testdata/image";
|
|
||||||
{
|
{
|
||||||
const imageData = new Blob(
|
const imageData = new Blob(
|
||||||
[await Deno.readFile(`${prefix}/1x1-red8.png`)],
|
[await Deno.readFile(`${prefix}/1x1-red8.png`)],
|
||||||
|
|
Loading…
Add table
Reference in a new issue