241 lines
9.2 KiB
JavaScript
241 lines
9.2 KiB
JavaScript
|
|
/* -------------------------------------------- */
|
||
|
|
/* Constants */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Some texture formats
|
||
|
|
* @enum {number}
|
||
|
|
*/
|
||
|
|
const FORMATS = {
|
||
|
|
RED: 6403,
|
||
|
|
RGBA: 6408
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Image Compressor Worker Functions */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Process the image compression.
|
||
|
|
* @param {object} options
|
||
|
|
* @param {Uint8ClampedArray} options.buffer Buffer used to create the image data.
|
||
|
|
* @param {number} options.width Buffered image width.
|
||
|
|
* @param {number} options.height Buffered image height.
|
||
|
|
* @param {string} [options.type="image/png"] The required image type.
|
||
|
|
* @param {number} [options.quality=1] The required image quality.
|
||
|
|
* @param {boolean} [options.debug] The debug option.
|
||
|
|
* @param {string} [hash] Hash to test.
|
||
|
|
* @returns {[result: object, transfer: object[]]}
|
||
|
|
*/
|
||
|
|
async function processBufferToBase64({buffer, width, height, type = "image/png", quality=1, debug, hash, readFormat}={}) {
|
||
|
|
if ( debug ) console.debug(`Compression | The pixel buffer is processed [size: ${(buffer.length / (1e+6)).toFixed(2)} mB]`);
|
||
|
|
|
||
|
|
// Control identical hashes
|
||
|
|
const hashTest = controlHashes(buffer, hash);
|
||
|
|
if ( hashTest.same ) {
|
||
|
|
if ( debug ) console.debug("Compression | Skipped. Texture buffer has not changed.");
|
||
|
|
return [{base64img: undefined, buffer, hash: hashTest.hash}, [buffer.buffer]];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Expand buffer from single R channel to RGBA channels (necessary for offscreen canvas)
|
||
|
|
const rgbaBuffer = (readFormat === FORMATS.RED) ? expandBuffer(buffer, width, height, {debug}) : buffer;
|
||
|
|
// Convert buffer to offscreen canvas image
|
||
|
|
const offscreen = pixelsToOffscreenCanvas(rgbaBuffer, width, height, {debug});
|
||
|
|
// Convert the RGBA buffer to a base64 string image
|
||
|
|
const base64img = await offscreenToBase64(offscreen, type, quality, {debug});
|
||
|
|
|
||
|
|
// Send the result
|
||
|
|
if ( debug ) console.debug("Compression | base64 string sent to caller.");
|
||
|
|
return [{base64img, buffer, hash: hashTest.hash ?? hash}, [buffer.buffer]];
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Expand a single RED channel buffer into a RGBA buffer and returns it to the main thread.
|
||
|
|
* The created RGBA buffer is transfered.
|
||
|
|
* @param {object} options
|
||
|
|
* @param {Uint8ClampedArray} options.buffer Buffer to expand.
|
||
|
|
* @param {number} options.width Width of the image.
|
||
|
|
* @param {number} options.height Height of the image.
|
||
|
|
* @param {boolean} [options.debug] Debug option.
|
||
|
|
* @param {string} [hash] Hash to test.
|
||
|
|
* @returns {[result: object, transfer: object[]]}
|
||
|
|
*/
|
||
|
|
async function processBufferRedToBufferRGBA({buffer, width, height, debug, hash}={}) {
|
||
|
|
|
||
|
|
// Control identical hashes
|
||
|
|
const hashTest = controlHashes(buffer, hash);
|
||
|
|
if ( hashTest.same ) {
|
||
|
|
if ( debug ) console.debug("Compression | Skipped. Texture buffer has not changed.");
|
||
|
|
return [{rgbaBuffer: undefined, buffer, hash: hashTest.hash}, [buffer.buffer]];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Expanding the buffer from single channel to RGBA channel
|
||
|
|
const rgbaBuffer = expandBuffer(buffer, width, height, {debug});
|
||
|
|
if ( debug ) console.debug("Compression | RGBA buffer sent to caller.");
|
||
|
|
return [{rgbaBuffer, buffer, hash: hashTest.hash ?? hash}, [rgbaBuffer.buffer, buffer.buffer]];
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reduce a RGBA buffer into a single RED buffer and returns it to the main thread.
|
||
|
|
* The created RGBA buffer is transfered.
|
||
|
|
* @param {object} options
|
||
|
|
* @param {Uint8ClampedArray} options.buffer Buffer to expand.
|
||
|
|
* @param {number} options.width Width of the image.
|
||
|
|
* @param {number} options.height Height of the image.
|
||
|
|
* @param {boolean} [options.debug] Debug option.
|
||
|
|
* @param {string} [hash] Hash to test.
|
||
|
|
* @returns {[result: object, transfer: object[]]}
|
||
|
|
*/
|
||
|
|
async function processBufferRGBAToBufferRED({buffer, width, height, debug, hash}={}) {
|
||
|
|
|
||
|
|
// Control identical hashes
|
||
|
|
const hashTest = controlHashes(buffer, hash);
|
||
|
|
if ( hashTest.same ) {
|
||
|
|
if ( debug ) console.debug("Compression | Skipped. Texture buffer has not changed.");
|
||
|
|
return [{redBuffer: undefined, buffer, hash: hashTest.hash}, [buffer.buffer]];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Expanding the buffer from single channel to RGBA channel
|
||
|
|
const redBuffer = reduceBuffer(buffer, width, height, {debug});
|
||
|
|
if ( debug ) console.debug("Compression | RED buffer sent to caller.");
|
||
|
|
return [{redBuffer, buffer, hash: hashTest.hash ?? hash}, [buffer.buffer]];
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Control the hash of a provided buffer.
|
||
|
|
* @param {Uint8ClampedArray} buffer Buffer to control.
|
||
|
|
* @param {string} [hash] Hash to test.
|
||
|
|
* @returns {{} | {same: boolean, hash: string}} Returns an empty object if not control is made
|
||
|
|
* else returns {same: <boolean to know if the hashes are the same>,
|
||
|
|
* hash: <the previous or the new hash>}
|
||
|
|
*/
|
||
|
|
function controlHashes(buffer, hash) {
|
||
|
|
if ( hash === undefined ) return {};
|
||
|
|
const textureHash = SparkMD5.ArrayBuffer.hash(buffer);
|
||
|
|
const same = (hash === textureHash);
|
||
|
|
hash = textureHash;
|
||
|
|
return {same, hash};
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create an offscreen canvas element containing the pixel data.
|
||
|
|
* @param {Uint8ClampedArray} buffer Buffer used to create the image data.
|
||
|
|
* @param {number} width Buffered image width.
|
||
|
|
* @param {number} height Buffered image height.
|
||
|
|
* @param {object} [options]
|
||
|
|
* @returns {OffscreenCanvas}
|
||
|
|
*/
|
||
|
|
function pixelsToOffscreenCanvas(buffer, width, height, options={}) {
|
||
|
|
if ( options.debug ) console.debug("Compression | Converting rgba buffer to image data");
|
||
|
|
|
||
|
|
// Create offscreen canvas with provided dimensions
|
||
|
|
const offscreen = new OffscreenCanvas(width, height);
|
||
|
|
|
||
|
|
// Get the context and create a new image data with the buffer
|
||
|
|
const context = offscreen.getContext("2d");
|
||
|
|
const imageData = new ImageData(buffer, width, height);
|
||
|
|
context.putImageData(imageData, 0, 0);
|
||
|
|
|
||
|
|
// Return the offscreen canvas
|
||
|
|
return offscreen;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Asynchronously convert a canvas element to base64.
|
||
|
|
* @param {OffscreenCanvas} offscreen
|
||
|
|
* @param {string} [type="image/png"]
|
||
|
|
* @param {number} [quality]
|
||
|
|
* @param {object} [options]
|
||
|
|
* @returns {Promise<string>} The base64 string of the canvas.
|
||
|
|
*/
|
||
|
|
async function offscreenToBase64(offscreen, type, quality, options={}) {
|
||
|
|
if ( options.debug ) {
|
||
|
|
console.debug(`Compression | Compressing image data to ${type} with quality ${quality.toFixed(1)}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Convert image to base64
|
||
|
|
const base64img = await blobToBase64(await offscreen.convertToBlob({type, quality}));
|
||
|
|
|
||
|
|
if ( options.debug ) {
|
||
|
|
const m = (base64img.length * 2) / (1e+6);
|
||
|
|
console.debug(`Compression | Image converted to a base64 string [size: ${m.toFixed(2)} mB]`);
|
||
|
|
}
|
||
|
|
return base64img;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert a blob to a base64 string.
|
||
|
|
* @param {Blob} blob
|
||
|
|
* @returns {Promise}
|
||
|
|
*/
|
||
|
|
async function blobToBase64(blob) {
|
||
|
|
return new Promise((resolve, _) => {
|
||
|
|
const reader = new FileReader();
|
||
|
|
reader.onloadend = () => resolve(reader.result);
|
||
|
|
reader.readAsDataURL(blob);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Expand a single RED channel buffer into a RGBA buffer.
|
||
|
|
* @param {Uint8ClampedArray} buffer
|
||
|
|
* @param {number} width
|
||
|
|
* @param {number} height
|
||
|
|
* @param {object} [options]
|
||
|
|
* @returns {Uint8ClampedArray}
|
||
|
|
*/
|
||
|
|
function expandBuffer(buffer, width, height, options={}) {
|
||
|
|
// Creating the new buffer with the required size to handle 4 channels
|
||
|
|
const rgbaBuffer = new Uint8ClampedArray(width * height * 4);
|
||
|
|
|
||
|
|
// Converting the single channel buffer to RGBA buffer
|
||
|
|
for ( let i = 0; i < buffer.length; i++ ) {
|
||
|
|
rgbaBuffer[(i * 4)] = buffer[i];
|
||
|
|
rgbaBuffer[(i * 4) + 3] = 0xFF;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( options.debug ) {
|
||
|
|
console.debug(`Compression | Single channel buffer converted to rgba buffer [size: ${(rgbaBuffer.length / (1e+6)).toFixed(2)} mB]`);
|
||
|
|
}
|
||
|
|
return rgbaBuffer;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reduce a RGBA channel buffer into a RED buffer (in-place).
|
||
|
|
* @param {Uint8ClampedArray} buffer
|
||
|
|
* @param {number} width
|
||
|
|
* @param {number} height
|
||
|
|
* @param {object} [options]
|
||
|
|
* @returns {Uint8ClampedArray}
|
||
|
|
*/
|
||
|
|
function reduceBuffer(buffer, width, height, options={}) {
|
||
|
|
// Creating the new buffer with only a single channel
|
||
|
|
const redBuffer = new Uint8ClampedArray(buffer.buffer, 0, width * height);
|
||
|
|
|
||
|
|
// Converting the RGBA buffer to single channel RED buffer
|
||
|
|
for ( let i = 0; i < buffer.length; i+=4 ) {
|
||
|
|
redBuffer[(i / 4)] = buffer[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( options.debug ) {
|
||
|
|
console.debug(`Compression | RGBA channel buffer converted to RED buffer [size: ${(redBuffer.length / (1e+6)).toFixed(2)} mB]`);
|
||
|
|
}
|
||
|
|
return redBuffer;
|
||
|
|
}
|