var debug = require('debug')('ylt:imageReformater'); var sharp = require('sharp'); // Disable sharp cache to reduce the "disk is full" error on Amazon Lambda sharp.cache(false); var ImageOptimizer = function() { // https://www.industrialempathy.com/posts/avif-webp-quality-settings const WEBP_QUALITY = 82; const AVIF_QUALITY = 64; async function reformatImage(entry) { if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) { // No valid file available return entry; } var fileSize = entry.weightCheck.uncompressedSize; debug('Let\'s try to convert %s to other image formats', entry.url); debug('Current file size is %d', fileSize); var animated = await isAnimated(entry); debug('Check if the file is animated: %s', animated); if (isJPEG(entry) || isPNG(entry)) { debug('File is %s, let\'s try to convert it to WebP', entry.contentType); try { const webpFile = await convertToWebp(entry.weightCheck.bodyBuffer, animated); if (webpFile) { var webpFileSize = webpFile.length; debug('WebP transformation complete for %s', entry.url); debug('WebP size is %d bytes', webpFileSize); if (webpFile.length > 0 && gainIsEnough(fileSize, webpFileSize)) { entry.weightCheck.webpSize = webpFileSize; debug('WebP size is %d bytes smaller (-%d%)', fileSize - webpFileSize, Math.round((fileSize - webpFileSize) * 100 / fileSize)); } } else { debug('Convertion to WebP didn\'t work'); } } catch(err) { debug('Error while converting to WebP, ignoring'); } } if (!animated && (isJPEG(entry) || isPNG(entry) || isWebP(entry))) { debug('File is %s and is not animated, let\'s try to convert it to AVIF', entry.contentType); try { const avifFile = await convertToAvif(entry.weightCheck.bodyBuffer); if (avifFile) { var avifFileSize = avifFile.length; debug('AVIF transformation complete for %s', entry.url); debug('AVIF size is %d bytes', avifFileSize); if (avifFile.length > 0 && gainIsEnough(fileSize, avifFileSize)) { entry.weightCheck.avifSize = avifFileSize; debug('AVIF size is %d bytes smaller (-%d%)', fileSize - avifFileSize, Math.round((fileSize - avifFileSize) * 100 / fileSize)); } } else { debug('Convertion to AVIF didn\'t work'); } } catch(err) { debug('Error while converting to AVIF, ignoring'); } } return entry; } async function convertToWebp(bodyBuffer, isAnimated) { return sharp(bodyBuffer, {animated: isAnimated}) .webp({quality: WEBP_QUALITY, alphaQuality: WEBP_QUALITY}) .toBuffer(); } async function convertToAvif(bodyBuffer) { return sharp(bodyBuffer) .webp({quality: AVIF_QUALITY}) .toBuffer(); } // The gain is estimated of enough value if it's over 2KB or over 20%, // but it's ignored if is below 100 bytes function gainIsEnough(oldWeight, newWeight) { var gain = oldWeight - newWeight; var ratio = gain / oldWeight; return (gain > 2048 || (ratio > 0.2 && gain > 100)); } function isJPEG(entry) { return entry.isImage && entry.contentType === 'image/jpeg'; } function isPNG(entry) { return entry.isImage && entry.contentType === 'image/png'; } function isWebP(entry) { return entry.isImage && entry.contentType === 'image/webp'; } function entryTypeCanBeReformated(entry) { return isJPEG(entry) || isPNG(entry) || isWebP(entry); } async function isAnimated(entry) { if (isWebP(entry)) { const metadata = await sharp(entry.weightCheck.bodyBuffer).metadata(); return metadata.pages > 1; } return false; } return { reformatImage: reformatImage, convertToWebp: convertToWebp, convertToAvif: convertToAvif, gainIsEnough: gainIsEnough, entryTypeCanBeReformated: entryTypeCanBeReformated, isAnimated: isAnimated }; }; module.exports = new ImageOptimizer();