mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-19 08:31:43 +03:00
6cc92fac9a
no issue - This adds the possibility to format AVIF files in Ghost if requested. - This format is supported in Sharp - Provides smaller file sizes than webp
155 lines
5.6 KiB
JavaScript
155 lines
5.6 KiB
JavaScript
const Promise = require('bluebird');
|
|
const errors = require('@tryghost/errors');
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* Check if this tool can handle any file transformations as Sharp is an optional dependency
|
|
*/
|
|
const canTransformFiles = () => {
|
|
try {
|
|
require('sharp');
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if this tool can handle a particular extension
|
|
* @param {String} ext the extension to check, including the leading dot
|
|
*/
|
|
const canTransformFileExtension = ext => !['.ico'].includes(ext);
|
|
|
|
/**
|
|
* Check if this tool can handle a particular extension, only to resize (= not convert format)
|
|
* - In this case we don't want to resize SVG's (doesn't save file size)
|
|
* - We don't want to resize GIF's (because we would lose the animation)
|
|
* So this is a 'should' instead of a 'could'. Because Sharp can handle them, but animations are lost.
|
|
* This is 'resize' instead of 'transform', because for the transform we might want to convert a SVG to a PNG, which is perfectly possible.
|
|
* @param {String} ext the extension to check, including the leading dot
|
|
*/
|
|
const shouldResizeFileExtension = ext => !['.ico', '.svg', '.svgz'].includes(ext);
|
|
|
|
/**
|
|
* Can we output animation (prevents outputting animated JPGs that are just all the pages listed under each other)
|
|
* Sharp doesn't support AVIF image sequences yet (animation)
|
|
* @param {keyof import('sharp').FormatEnum} format the extension to check, EXCLUDING the leading dot
|
|
*/
|
|
const doesFormatSupportAnimation = format => ['webp', 'gif'].includes(format);
|
|
|
|
/**
|
|
* Check if this tool can convert to a particular format (used in the format option of ResizeFromBuffer)
|
|
* @param {String} format the format to check, EXCLUDING the leading dot
|
|
* @returns {ext is keyof import('sharp').FormatEnum}
|
|
*/
|
|
const canTransformToFormat = format => [
|
|
'gif',
|
|
'jpeg',
|
|
'jpg',
|
|
'png',
|
|
'webp',
|
|
'avif'
|
|
].includes(format);
|
|
|
|
/**
|
|
* @NOTE: Sharp cannot operate on the same image path, that's why we have to use in & out paths.
|
|
*
|
|
* We currently can't enable compression or having more config options, because of
|
|
* https://github.com/lovell/sharp/issues/1360.
|
|
*
|
|
* Resize an image referenced by the `in` path and write it to the `out` path
|
|
* @param {{in, out, width}} options
|
|
*/
|
|
const unsafeResizeFromPath = (options = {}) => {
|
|
return fs.readFile(options.in)
|
|
.then((data) => {
|
|
return unsafeResizeFromBuffer(data, {
|
|
width: options.width
|
|
});
|
|
})
|
|
.then((data) => {
|
|
return fs.writeFile(options.out, data);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Resize an image
|
|
*
|
|
* @param {Buffer} originalBuffer image to resize
|
|
* @param {{width?: number, height?: number, format?: keyof import('sharp').FormatEnum, animated?: boolean, withoutEnlargement?: boolean}} [options]
|
|
* options.animated defaults to true for file formats where animation is supported (will always maintain animation if possible)
|
|
* @returns {Promise<Buffer>} the resizedBuffer
|
|
*/
|
|
const unsafeResizeFromBuffer = async (originalBuffer, options = {}) => {
|
|
const sharp = require('sharp');
|
|
|
|
// Disable the internal libvips cache - https://sharp.pixelplumbing.com/api-utility#cache
|
|
sharp.cache(false);
|
|
|
|
// It is safe to set animated to true for all formats, because if the input image doesn't contain animation
|
|
// nothing will change.
|
|
let animated = options.animated ?? true;
|
|
|
|
if (options.format) {
|
|
// Only set animated to true if the output format supports animation
|
|
// Else we end up with multiple images stacked on top of each other (from the source image)
|
|
animated = doesFormatSupportAnimation(options.format);
|
|
}
|
|
|
|
let s = sharp(originalBuffer, {animated})
|
|
.resize(options.width, options.height, {
|
|
// CASE: dont make the image bigger than it was
|
|
withoutEnlargement: options.withoutEnlargement ?? true
|
|
})
|
|
// CASE: Automatically remove metadata and rotate based on the orientation.
|
|
.rotate();
|
|
|
|
if (options.format) {
|
|
s = s.toFormat(options.format);
|
|
}
|
|
|
|
const resizedBuffer = await s.toBuffer();
|
|
return options.format || resizedBuffer.length < originalBuffer.length ? resizedBuffer : originalBuffer;
|
|
};
|
|
|
|
/**
|
|
* Internal utility to wrap all transform functions in error handling
|
|
* Allows us to keep Sharp as an optional dependency
|
|
*
|
|
* @param {T} fn
|
|
* @return {T}
|
|
* @template {Function} T
|
|
*/
|
|
const makeSafe = fn => (...args) => {
|
|
try {
|
|
require('sharp');
|
|
} catch (err) {
|
|
return Promise.reject(new errors.InternalServerError({
|
|
message: 'Sharp wasn\'t installed',
|
|
code: 'SHARP_INSTALLATION',
|
|
err: err
|
|
}));
|
|
}
|
|
return fn(...args).catch((err) => {
|
|
throw new errors.InternalServerError({
|
|
message: 'Unable to manipulate image.',
|
|
err: err,
|
|
code: 'IMAGE_PROCESSING'
|
|
});
|
|
});
|
|
};
|
|
|
|
const generateOriginalImageName = (originalPath) => {
|
|
const parsedFileName = path.parse(originalPath);
|
|
return path.join(parsedFileName.dir, `${parsedFileName.name}_o${parsedFileName.ext}`);
|
|
};
|
|
|
|
module.exports.canTransformFiles = canTransformFiles;
|
|
module.exports.canTransformFileExtension = canTransformFileExtension;
|
|
module.exports.shouldResizeFileExtension = shouldResizeFileExtension;
|
|
module.exports.canTransformToFormat = canTransformToFormat;
|
|
module.exports.generateOriginalImageName = generateOriginalImageName;
|
|
module.exports.resizeFromPath = makeSafe(unsafeResizeFromPath);
|
|
module.exports.resizeFromBuffer = makeSafe(unsafeResizeFromBuffer);
|