ghost
Version:
The professional publishing platform
298 lines (267 loc) • 11.4 kB
JavaScript
const debug = require('ghost-ignition').debug('utils:image-size');
const sizeOf = require('image-size');
const probeSizeOf = require('probe-image-size');
const url = require('url');
const Promise = require('bluebird');
const _ = require('lodash');
const errors = require('@tryghost/errors');
// these are formats supported by image-size but not probe-image-size
const FETCH_ONLY_FORMATS = [
'cur', 'icns', 'ico', 'dds'
];
class ImageSize {
constructor({config, i18n, storage, storageUtils, validator, urlUtils, request}) {
this.config = config;
this.i18n = i18n;
this.storage = storage;
this.storageUtils = storageUtils;
this.validator = validator;
this.urlUtils = urlUtils;
this.request = request;
this.REQUEST_OPTIONS = {
// we need the user-agent, otherwise some https request may fail (e.g. cloudfare)
headers: {
'User-Agent': 'Mozilla/5.0 Safari/537.36'
},
timeout: this.config.get('times:getImageSizeTimeoutInMS') || 10000,
retry: 0, // for `got`, used with image-size
encoding: null
};
}
// processes the Buffer result of an image file using image-size
// returns promise which resolves dimensions
_imageSizeFromBuffer(buffer) {
return new Promise((resolve, reject) => {
try {
const dimensions = sizeOf(buffer);
// CASE: `.ico` files might have multiple images and therefore multiple sizes.
// We return the largest size found (image-size default is the first size found)
if (dimensions.images) {
dimensions.width = _.maxBy(dimensions.images, img => img.width).width;
dimensions.height = _.maxBy(dimensions.images, img => img.height).height;
}
return resolve(dimensions);
} catch (err) {
return reject(err);
}
});
}
// use probe-image-size to download enough of an image to get it's dimensions
// returns promise which resolves dimensions
_probeImageSizeFromUrl(imageUrl) {
// probe-image-size uses `request` npm module which doesn't have our `got`
// override with custom URL validation so it needs duplicating here
if (_.isEmpty(imageUrl) || !this.validator.isURL(imageUrl)) {
return Promise.reject(new errors.InternalServerError({
message: 'URL empty or invalid.',
code: 'URL_MISSING_INVALID',
context: imageUrl
}));
}
return probeSizeOf(imageUrl, this.REQUEST_OPTIONS);
}
// download full image then use image-size to get it's dimensions
// returns promise which resolves dimensions
_fetchImageSizeFromUrl(imageUrl) {
return this.request(imageUrl, this.REQUEST_OPTIONS).then((response) => {
return this._imageSizeFromBuffer(response.body);
});
}
// wrapper for appropriate probe/fetch method for getting image dimensions from a URL
// returns promise which resolves dimensions
_imageSizeFromUrl(imageUrl) {
return new Promise((resolve, reject) => {
let parsedUrl;
try {
parsedUrl = url.parse(imageUrl);
} catch (err) {
reject(err);
}
// check if we got an url without any protocol
if (!parsedUrl.protocol) {
// CASE: our gravatar URLs start with '//' and we need to add 'http:'
// to make the request work
imageUrl = 'http:' + imageUrl;
}
const extensionMatch = imageUrl.match(/(?:\.)([a-zA-Z]{3,4})(\?|$)/) || [];
const extension = (extensionMatch[1] || '').toLowerCase();
if (FETCH_ONLY_FORMATS.includes(extension)) {
return resolve(this._fetchImageSizeFromUrl(imageUrl));
} else {
return resolve(this._probeImageSizeFromUrl(imageUrl));
}
});
}
// Supported formats of https://github.com/image-size/image-size:
// BMP, GIF, JPEG, PNG, PSD, TIFF, WebP, SVG, ICO
// ***
// Takes the url of the image and an optional timeout
// getImageSizeFromUrl returns an Object like this
// {
// height: 50,
// url: 'http://myblog.com/images/cat.jpg',
// width: 50
// };
// if the dimensions can be fetched, and rejects with error, if not.
// ***
// In case we get a locally stored image, which is checked withing the `isLocalImage`
// function we switch to read the image from the local file storage with `getImageSizeFromStoragePath`.
// In case the image is not stored locally and is missing the protocol (like //www.gravatar.com/andsoon),
// we add the protocol and use urlFor() to get the absolute URL.
// If the request fails or image-size is not able to read the file, we reject with error.
/**
* @description read image dimensions from URL
* @param {string} imagePath as URL
* @returns {Promise<Object>} imageObject or error
*/
getImageSizeFromUrl(imagePath) {
if (this.storageUtils.isLocalImage(imagePath)) {
// don't make a request for a locally stored image
return this.getImageSizeFromStoragePath(imagePath);
}
// CASE: pre 1.0 users were able to use an asset path for their blog logo
if (imagePath.match(/^\/assets/)) {
imagePath = this.urlUtils.urlJoin(this.urlUtils.urlFor('home', true), this.urlUtils.getSubdir(), '/', imagePath);
}
debug('requested imagePath:', imagePath);
return this._imageSizeFromUrl(imagePath).then((dimensions) => {
debug('Image fetched (URL):', imagePath);
return {
url: imagePath,
width: dimensions.width,
height: dimensions.height
};
}).catch({code: 'URL_MISSING_INVALID'}, (err) => {
return Promise.reject(new errors.InternalServerError({
message: err.message,
code: 'IMAGE_SIZE_URL',
statusCode: err.statusCode,
context: err.url || imagePath
}));
}).catch({code: 'ETIMEDOUT'}, {code: 'ESOCKETTIMEDOUT'}, {statusCode: 408}, (err) => {
return Promise.reject(new errors.InternalServerError({
message: 'Request timed out.',
code: 'IMAGE_SIZE_URL',
statusCode: err.statusCode,
context: err.url || imagePath
}));
}).catch({code: 'ENOENT'}, {statusCode: 404}, (err) => {
return Promise.reject(new errors.NotFoundError({
message: 'Image not found.',
code: 'IMAGE_SIZE_URL',
statusCode: err.statusCode,
context: err.url || imagePath
}));
}).catch(function (err) {
if (errors.utils.isIgnitionError(err)) {
return Promise.reject(err);
}
return Promise.reject(new errors.InternalServerError({
message: 'Unknown Request error.',
code: 'IMAGE_SIZE_URL',
statusCode: err.statusCode,
context: err.url || imagePath,
err: err
}));
});
}
// Supported formats of https://github.com/image-size/image-size:
// BMP, GIF, JPEG, PNG, PSD, TIFF, WebP, SVG, ICO
// ***
// Takes the url or filepath of the image and reads it form the local
// file storage.
// getImageSizeFromStoragePath returns an Object like this
// {
// height: 50,
// url: 'http://myblog.com/images/cat.jpg',
// width: 50
// };
// if the image is found and dimensions can be fetched, and rejects with error, if not.
/**
* @description read image dimensions from local file storage
* @param {string} imagePath
* @returns {object} imageObject or error
*/
getImageSizeFromStoragePath(imagePath) {
let filePath;
imagePath = this.urlUtils.urlFor('image', {image: imagePath}, true);
// get the storage readable filePath
filePath = this.storageUtils.getLocalFileStoragePath(imagePath);
return this.storage.getStorage()
.read({path: filePath})
.then((buf) => {
debug('Image fetched (storage):', filePath);
return this._imageSizeFromBuffer(buf);
})
.then((dimensions) => {
return {
url: imagePath,
width: dimensions.width,
height: dimensions.height
};
})
.catch({code: 'ENOENT'}, (err) => {
return Promise.reject(new errors.NotFoundError({
message: err.message,
code: 'IMAGE_SIZE_STORAGE',
err: err,
context: filePath,
errorDetails: {
originalPath: imagePath,
reqFilePath: filePath
}
}));
}).catch((err) => {
if (errors.utils.isIgnitionError(err)) {
return Promise.reject(err);
}
return Promise.reject(new errors.InternalServerError({
message: err.message,
code: 'IMAGE_SIZE_STORAGE',
err: err,
context: filePath,
errorDetails: {
originalPath: imagePath,
reqFilePath: filePath
}
}));
});
}
/**
* Supported formats of https://github.com/image-size/image-size:
* BMP, GIF, JPEG, PNG, PSD, TIFF, WebP, SVG, ICO
* Get dimensions for a file from its real file storage path
* Always returns {object} getImageDimensions
* @param {string} path
* @returns {Promise<Object>} getImageDimensions
* @description Takes a file path and returns width and height.
*/
getImageSizeFromPath(path) {
return new Promise(function getSize(resolve, reject) {
let dimensions;
try {
dimensions = sizeOf(path);
if (dimensions.images) {
dimensions.width = _.maxBy(dimensions.images, (w) => {
return w.width;
}).width;
dimensions.height = _.maxBy(dimensions.images, (h) => {
return h.height;
}).height;
}
return resolve({
width: dimensions.width,
height: dimensions.height
});
} catch (err) {
return reject(new errors.ValidationError({
message: this.i18n.t('errors.utils.images.invalidDimensions', {
file: path,
error: err.message
})
}));
}
});
}
}
module.exports = ImageSize;