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"; import { Promise } from "rsvp";
export async function fileToImageData(file) { // Chrome and Firefox use a native method to do Image -> Bitmap Array (it happens of the main thread!)
let drawable, err; // Safari uses the `<img async>` element due to https://bugs.webkit.org/show_bug.cgi?id=182424
async function fileToDrawable(file) {
// 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
if ("createImageBitmap" in self) { if ("createImageBitmap" in self) {
drawable = await createImageBitmap(file); return await createImageBitmap(file);
} else { } else {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
const img = new Image(); 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. // Always await loaded, as we may have bailed due to the Safari bug above.
await loaded; await loaded;
return img;
drawable = img;
} }
}
function drawableToimageData(drawable) {
const width = drawable.width, const width = drawable.width,
height = drawable.height, height = drawable.height,
sx = 0, sx = 0,
sy = 0, sy = 0,
sw = width, sw = width,
sh = height; sh = height;
// Make canvas same size as image // Make canvas same size as image
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
// Draw image onto canvas // Draw image onto canvas
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (!ctx) { 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); ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height); const imageData = ctx.getImageData(0, 0, width, height);
canvas.remove(); canvas.remove();
return imageData;
}
// potentially transparent function isTransparent(type, imageData) {
if (/(\.|\/)(png|webp)$/i.test(file.type)) { if (!/(\.|\/)(png|webp)$/i.test(type)) {
for (let i = 0; i < imageData.data.length; i += 4) { return false;
if (imageData.data[i + 3] < 255) { }
err = "Image has transparent pixels, won't convert to JPEG!";
break; for (let i = 0; i < imageData.data.length; i += 4) {
} if (imageData.data[i + 3] < 255) {
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.currentComposerUploadData = data;
this.currentPromiseResolver = resolve; this.currentPromiseResolver = resolve;
const { imageData, width, height, err } = await fileToImageData(file); let imageData;
try {
if (err) { imageData = await fileToImageData(file);
this.logIfDebug(err); } catch (error) {
this.logIfDebug(error);
return resolve(data); return resolve(data);
} }
@ -66,8 +67,8 @@ export default class MediaOptimizationWorkerService extends Service {
type: "compress", type: "compress",
file: imageData.data.buffer, file: imageData.data.buffer,
fileName: file.name, fileName: file.name,
width: width, width: imageData.width,
height: height, height: imageData.height,
settings: { settings: {
mozjpeg_script: getURLWithCDN( mozjpeg_script: getURLWithCDN(
"/javascripts/squoosh/mozjpeg_enc.js" "/javascripts/squoosh/mozjpeg_enc.js"
@ -102,8 +103,6 @@ export default class MediaOptimizationWorkerService extends Service {
registerMessageHandler() { registerMessageHandler() {
this.worker.onmessage = (e) => { this.worker.onmessage = (e) => {
this.logIfDebug("Main: Message received from worker script");
this.logIfDebug(e);
switch (e.data.type) { switch (e.data.type) {
case "file": case "file":
let optimizedFile = new File([e.data.file], `${e.data.fileName}`, { 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; ).data;
width = target_dimensions.width; width = target_dimensions.width;
height = target_dimensions.height; height = target_dimensions.height;
logIfDebug(`Worker post resizing file: ${maybeResized.byteLength}`);
} catch (error) { } catch (error) {
console.error(`Resize failed: ${error}`); console.error(`Resize failed: ${error}`);
maybeResized = imageData; maybeResized = imageData;