mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
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:
committed by
GitHub
parent
18de11f3a6
commit
fa4a462517
166
public/javascripts/media-optimization-worker.js
Normal file
166
public/javascripts/media-optimization-worker.js
Normal 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};
|
||||
}
|
||||
21
public/javascripts/squoosh/mozjpeg_enc.js
Normal file
21
public/javascripts/squoosh/mozjpeg_enc.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/javascripts/squoosh/mozjpeg_enc.wasm
Executable file
BIN
public/javascripts/squoosh/mozjpeg_enc.wasm
Executable file
Binary file not shown.
129
public/javascripts/squoosh/squoosh_resize.js
Normal file
129
public/javascripts/squoosh/squoosh_resize.js
Normal 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);
|
||||
|
||||
})();
|
||||
BIN
public/javascripts/squoosh/squoosh_resize_bg.wasm
Normal file
BIN
public/javascripts/squoosh/squoosh_resize_bg.wasm
Normal file
Binary file not shown.
Reference in New Issue
Block a user