FIX: Handle image decoding failure in composer image optimization (#13555)

There are some hard limits in browser Canvas implementations, that will
throw a runtime exception when crossed. Since those limits are platform
dependent, the best we can do is catch it and back off from trying to
optimize a problematic file.

For example, a 60MB PNG can be processed fine by Chrome but Firefox will
fail trying to extract the ImageData from the CanvasRenderingContext2D
with NS_ERROR_FAILURE.

Also cleans up the media-optimization-utils and add post-resize size logs
This commit is contained in:
Rafael dos Santos Silva 2021-06-28 18:21:39 -03:00 committed by GitHub
parent d03aee4642
commit 99da221034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 40 additions and 25 deletions

View File

@ -1,12 +1,10 @@
import { Promise } from "rsvp";
export async function fileToImageData(file) {
let drawable, err;
// Chrome and Firefox use a native method to do Image -> Bitmap Array (it happens of the main thread!)
// Safari uses the `<img async>` element due to https://bugs.webkit.org/show_bug.cgi?id=182424
async function fileToDrawable(file) {
if ("createImageBitmap" in self) {
drawable = await createImageBitmap(file);
return await createImageBitmap(file);
} else {
const url = URL.createObjectURL(file);
const img = new Image();
@ -26,38 +24,55 @@ export async function fileToImageData(file) {
// Always await loaded, as we may have bailed due to the Safari bug above.
await loaded;
drawable = img;
return img;
}
}
function drawableToimageData(drawable) {
const width = drawable.width,
height = drawable.height,
sx = 0,
sy = 0,
sw = width,
sh = height;
// Make canvas same size as image
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
// Draw image onto canvas
const ctx = canvas.getContext("2d");
if (!ctx) {
err = "Could not create canvas context";
throw "Could not create canvas context";
}
ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
canvas.remove();
return imageData;
}
function isTransparent(type, imageData) {
if (!/(\.|\/)(png|webp)$/i.test(type)) {
return false;
}
// potentially transparent
if (/(\.|\/)(png|webp)$/i.test(file.type)) {
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] < 255) {
err = "Image has transparent pixels, won't convert to JPEG!";
break;
}
return true;
}
}
return { imageData, width, height, err };
return false;
}
export async function fileToImageData(file) {
const drawable = await fileToDrawable(file);
const imageData = drawableToimageData(drawable);
if (isTransparent(file.type, imageData)) {
throw "Image has transparent pixels, won't convert to JPEG!";
}
return imageData;
}

View File

@ -54,10 +54,11 @@ export default class MediaOptimizationWorkerService extends Service {
this.currentComposerUploadData = data;
this.currentPromiseResolver = resolve;
const { imageData, width, height, err } = await fileToImageData(file);
if (err) {
this.logIfDebug(err);
let imageData;
try {
imageData = await fileToImageData(file);
} catch (error) {
this.logIfDebug(error);
return resolve(data);
}
@ -66,8 +67,8 @@ export default class MediaOptimizationWorkerService extends Service {
type: "compress",
file: imageData.data.buffer,
fileName: file.name,
width: width,
height: height,
width: imageData.width,
height: imageData.height,
settings: {
mozjpeg_script: getURLWithCDN(
"/javascripts/squoosh/mozjpeg_enc.js"
@ -102,8 +103,6 @@ export default class MediaOptimizationWorkerService extends Service {
registerMessageHandler() {
this.worker.onmessage = (e) => {
this.logIfDebug("Main: Message received from worker script");
this.logIfDebug(e);
switch (e.data.type) {
case "file":
let optimizedFile = new File([e.data.file], `${e.data.fileName}`, {

View File

@ -81,6 +81,7 @@ async function optimize(imageData, fileName, width, height, settings) {
).data;
width = target_dimensions.width;
height = target_dimensions.height;
logIfDebug(`Worker post resizing file: ${maybeResized.byteLength}`);
} catch (error) {
console.error(`Resize failed: ${error}`);
maybeResized = imageData;