var debug = require('ghost-ignition').debug('utils:image-size'), sizeOf = require('image-size'), url = require('url'), Promise = require('bluebird'), request = require('../utils/request'), utils = require('../utils'), errors = require('../errors'), config = require('../config'), storage = require('../adapters/storage'), _ = require('lodash'), storageUtils = require('../adapters/storage/utils'), getImageSizeFromUrl, getImageSizeFromFilePath; /** * @description processes the Buffer result of an image file * @param {Object} options * @returns {Object} dimensions */ function fetchDimensionsFromBuffer(options) { var buffer = options.buffer, imagePath = options.imagePath, imageObject = {}, dimensions; imageObject.url = imagePath; try { // Using the Buffer rather than an URL requires to use sizeOf synchronously. // See https://github.com/image-size/image-size#asynchronous 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, function (w) {return w.width;}).width; dimensions.height = _.maxBy(dimensions.images, function (h) {return h.height;}).height; } imageObject.width = dimensions.width; imageObject.height = dimensions.height; return Promise.resolve(imageObject); } catch (err) { return Promise.reject(new errors.InternalServerError({ code: 'IMAGE_SIZE', err: err, context: imagePath })); } } // 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 `getImageSizeFromFilePath`. // 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} imageObject or error */ getImageSizeFromUrl = function getImageSizeFromUrl(imagePath) { var requestOptions, parsedUrl, timeout = config.get('times:getImageSizeTimeoutInMS') || 10000; if (storageUtils.isLocalImage(imagePath)) { // don't make a request for a locally stored image return getImageSizeFromFilePath(imagePath); } // CASE: pre 1.0 users were able to use an asset path for their blog logo if (imagePath.match(/^\/assets/)) { imagePath = utils.url.urlJoin(utils.url.urlFor('home', true), utils.url.getSubdir(), '/', imagePath); } parsedUrl = url.parse(imagePath); // 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 imagePath = 'http:' + imagePath; } debug('requested imagePath:', imagePath); requestOptions = { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: timeout, encoding: null }; return request( imagePath, requestOptions ).then(function (response) { debug('Image fetched (URL):', imagePath); return fetchDimensionsFromBuffer({ buffer: response.body, // we need to return the URL that's accessible for network requests as this imagePath // value will be used as the URL for structured data imagePath: parsedUrl.href }); }).catch({code: 'URL_MISSING_INVALID'}, function (err) { return Promise.reject(new errors.InternalServerError({ message: err.message, code: 'IMAGE_SIZE_URL', statusCode: err.statusCode, context: err.url || imagePath })); }).catch({code: 'ETIMEDOUT'}, {statusCode: 408}, function (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}, function (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. // getImageSizeFromFilePath 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 */ getImageSizeFromFilePath = function getImageSizeFromFilePath(imagePath) { var filePath; imagePath = utils.url.urlFor('image', {image: imagePath}, true); // get the storage readable filePath filePath = storageUtils.getLocalFileStoragePath(imagePath); return storage.getStorage() .read({path: filePath}) .then(function readFile(buf) { debug('Image fetched (storage):', filePath); return fetchDimensionsFromBuffer({ buffer: buf, // we need to return the URL that's accessible for network requests as this imagePath // value will be used as the URL for structured data imagePath: imagePath }); }).catch({code: 'ENOENT'}, function (err) { return Promise.reject(new errors.NotFoundError({ message: err.message, code: 'IMAGE_SIZE_STORAGE', err: err, context: filePath, errorDetails: { originalPath: imagePath, reqFilePath: filePath } })); }).catch(function (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 } })); }); }; module.exports.getImageSizeFromUrl = getImageSizeFromUrl; module.exports.getImageSizeFromFilePath = getImageSizeFromFilePath;