mirror of
https://github.com/discourse/discourse.git
synced 2024-11-27 00:13:37 +08:00
9b51b9bf4e
On iOS 15 beta, if you select the camera app when uploading an image and try to upload a freshly taken picture, from the second picture onwards the resize WASM operation will return an array filled with zeroes. Since every 4th byte is alpha, and at this step we are only dealing with non-transparent images this a O(1) way to detect that the bug was hit. (On normal images, all 4th bytes are 255 at this point) Also adds a "catch-all" when the original image became too small to try to accomodate other bugs of the same type. By default we only trigger this whole operation on images over 1MB, so if the end result is <20KB something weird did happen. Throwing here will let the upload continue using the original file, so nothing is lost and the user can continue.
174 lines
4.5 KiB
JavaScript
174 lines
4.5 KiB
JavaScript
function resizeWithAspect(
|
|
input_width,
|
|
input_height,
|
|
target_width,
|
|
target_height,
|
|
) {
|
|
if (!target_width && !target_height) {
|
|
throw Error('Need to specify at least width or height when resizing');
|
|
}
|
|
|
|
if (target_width && target_height) {
|
|
return { width: target_width, height: target_height };
|
|
}
|
|
|
|
if (!target_width) {
|
|
return {
|
|
width: Math.round((input_width / input_height) * target_height),
|
|
height: target_height,
|
|
};
|
|
}
|
|
|
|
return {
|
|
width: target_width,
|
|
height: Math.round((input_height / input_width) * target_width),
|
|
};
|
|
}
|
|
|
|
function logIfDebug(message) {
|
|
if (DedicatedWorkerGlobalScope.debugMode) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(message);
|
|
}
|
|
}
|
|
|
|
async function optimize(imageData, fileName, width, height, settings) {
|
|
|
|
await loadLibs(settings);
|
|
|
|
const mozJpegDefaultOptions = {
|
|
quality: settings.encode_quality,
|
|
baseline: false,
|
|
arithmetic: false,
|
|
progressive: true,
|
|
optimize_coding: true,
|
|
smoothing: 0,
|
|
color_space: 3 /*YCbCr*/,
|
|
quant_table: 3,
|
|
trellis_multipass: false,
|
|
trellis_opt_zero: false,
|
|
trellis_opt_table: false,
|
|
trellis_loops: 1,
|
|
auto_subsample: true,
|
|
chroma_subsample: 2,
|
|
separate_chroma_quality: false,
|
|
chroma_quality: 75,
|
|
};
|
|
|
|
const initialSize = imageData.byteLength;
|
|
logIfDebug(`Worker received imageData: ${initialSize}`);
|
|
|
|
let maybeResized;
|
|
|
|
// resize
|
|
if (width > settings.resize_threshold) {
|
|
try {
|
|
const target_dimensions = resizeWithAspect(width, height, settings.resize_target);
|
|
const resizeResult = self.codecs.resize(
|
|
new Uint8ClampedArray(imageData),
|
|
width, //in
|
|
height, //in
|
|
target_dimensions.width, //out
|
|
target_dimensions.height, //out
|
|
3, // 3 is lanczos
|
|
settings.resize_pre_multiply,
|
|
settings.resize_linear_rgb
|
|
);
|
|
if (resizeResult[3] !== 255) {
|
|
throw "Image corrupted during resize. Falling back to the original for encode"
|
|
}
|
|
maybeResized = new ImageData(
|
|
resizeResult,
|
|
target_dimensions.width,
|
|
target_dimensions.height,
|
|
).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;
|
|
}
|
|
} else {
|
|
logIfDebug(`Skipped resize: ${width} < ${settings.resize_threshold}`);
|
|
maybeResized = imageData;
|
|
}
|
|
|
|
// mozJPEG re-encode
|
|
const result = self.codecs.mozjpeg_enc.encode(
|
|
maybeResized,
|
|
width,
|
|
height,
|
|
mozJpegDefaultOptions
|
|
);
|
|
|
|
const finalSize = result.byteLength
|
|
logIfDebug(`Worker post reencode file: ${finalSize}`);
|
|
logIfDebug(`Reduction: ${(initialSize / finalSize).toFixed(1)}x speedup`);
|
|
|
|
if (finalSize < 20000) {
|
|
throw "Final size suspciously small, discarding optimizations"
|
|
}
|
|
|
|
let transferrable = Uint8Array.from(result).buffer; // decoded was allocated inside WASM so it **cannot** be transfered to another context, need to copy by value
|
|
|
|
return transferrable;
|
|
}
|
|
|
|
onmessage = async function (e) {
|
|
switch (e.data.type) {
|
|
case "compress":
|
|
try {
|
|
DedicatedWorkerGlobalScope.debugMode = e.data.settings.debug_mode;
|
|
let optimized = await optimize(
|
|
e.data.file,
|
|
e.data.fileName,
|
|
e.data.width,
|
|
e.data.height,
|
|
e.data.settings
|
|
);
|
|
postMessage(
|
|
{
|
|
type: "file",
|
|
file: optimized,
|
|
fileName: e.data.fileName
|
|
},
|
|
[optimized]
|
|
);
|
|
} catch (error) {
|
|
console.error(error);
|
|
postMessage({
|
|
type: "error",
|
|
});
|
|
}
|
|
break;
|
|
default:
|
|
logIfDebug(`Sorry, we are out of ${e}.`);
|
|
}
|
|
};
|
|
|
|
async function loadLibs(settings){
|
|
|
|
if (self.codecs) return;
|
|
|
|
importScripts(settings.mozjpeg_script);
|
|
importScripts(settings.resize_script);
|
|
|
|
let encoderModuleOverrides = {
|
|
locateFile: function(path, prefix) {
|
|
// if it's a mem init file, use a custom dir
|
|
if (path.endsWith(".wasm")) return settings.mozjpeg_wasm;
|
|
// otherwise, use the default, the prefix (JS file's dir) + the path
|
|
return prefix + path;
|
|
},
|
|
onRuntimeInitialized: function () {
|
|
return this;
|
|
},
|
|
};
|
|
const mozjpeg_enc_module = await mozjpeg_enc(encoderModuleOverrides);
|
|
|
|
const { resize } = wasm_bindgen;
|
|
await wasm_bindgen(settings.resize_wasm);
|
|
|
|
self.codecs = {mozjpeg_enc: mozjpeg_enc_module, resize: resize};
|
|
} |