FEATURE: Optimize images before upload (#13432)

Integrates [mozJPEG](https://github.com/mozilla/mozjpeg) and [Resize](https://github.com/PistonDevelopers/resize) using WebAssembly to optimize user uploads in the composer on the client-side.

NPM libraries are sourced from our [Squoosh fork](https://github.com/discourse/squoosh/tree/discourse), which was needed because we have an older asset pipeline.
This commit is contained in:
Rafael dos Santos Silva
2021-06-23 12:31:12 -03:00
committed by GitHub
parent 18de11f3a6
commit fa4a462517
20 changed files with 784 additions and 5 deletions

View File

@@ -0,0 +1,166 @@
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
);
maybeResized = new ImageData(
resizeResult,
target_dimensions.width,
target_dimensions.height,
).data;
width = target_dimensions.width;
height = target_dimensions.height;
} 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`);
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};
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1,129 @@
let wasm_bindgen;
(function() {
const __exports = {};
let wasm;
let cachegetUint8Memory0 = null;
function getUint8Memory0() {
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory0;
}
let WASM_VECTOR_LEN = 0;
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
let cachegetInt32Memory0 = null;
function getInt32Memory0() {
if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachegetInt32Memory0;
}
let cachegetUint8ClampedMemory0 = null;
function getUint8ClampedMemory0() {
if (cachegetUint8ClampedMemory0 === null || cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer) {
cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer);
}
return cachegetUint8ClampedMemory0;
}
function getClampedArrayU8FromWasm0(ptr, len) {
return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {Uint8Array} input_image
* @param {number} input_width
* @param {number} input_height
* @param {number} output_width
* @param {number} output_height
* @param {number} typ_idx
* @param {boolean} premultiply
* @param {boolean} color_space_conversion
* @returns {Uint8ClampedArray}
*/
__exports.resize = function(input_image, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
var ptr0 = passArray8ToWasm0(input_image, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
wasm.resize(retptr, ptr0, len0, input_width, input_height, output_width, output_height, typ_idx, premultiply, color_space_conversion);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getClampedArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
};
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
async function init(input) {
if (typeof input === 'undefined') {
let src;
if (typeof document === 'undefined') {
src = location.href;
} else {
src = document.currentScript.src;
}
input = src.replace(/\.js$/, '_bg.wasm');
}
const imports = {};
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
input = fetch(input);
}
const { instance, module } = await load(await input, imports);
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
return wasm;
}
wasm_bindgen = Object.assign(init, __exports);
})();

Binary file not shown.