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:
parent
d03aee4642
commit
99da221034
|
@ -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
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
function isTransparent(type, imageData) {
|
||||
if (!/(\.|\/)(png|webp)$/i.test(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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}`, {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue