UNPKG

ghost

Version:

The professional publishing platform

298 lines (267 loc) 11.4 kB
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;