mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
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";
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`, {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user