mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 21:33:24 +03:00
✨Added ability to resize and compress images on upload (#9837)
refs #4453 * On by default * Added config to disable resizing * Added basic image optimization processing * Added dep: sharp (optional dep) * Added resize middleware * Take care of rotation based on EXIF information * Removed all meta data from optimised image * Added handling if sharp could not get installed * Do not read ext twice - optimisation * Do not call sharp if config is disabled * Do not remove the original image which was uploaded (store 2 images) * Support of `req.files` for internal logic * Disabled cache to enable file removal on Windows
This commit is contained in:
parent
de9e5cecd4
commit
eb203de714
50
ghost/image-transform/lib/transform.js
Normal file
50
ghost/image-transform/lib/transform.js
Normal file
@ -0,0 +1,50 @@
|
||||
const Promise = require('bluebird');
|
||||
const common = require('../common');
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
const process = (options = {}) => {
|
||||
let sharp, img;
|
||||
|
||||
try {
|
||||
sharp = require('sharp');
|
||||
|
||||
// @NOTE: workaround for Windows as libvips keeps a reference to the input file
|
||||
// which makes it impossible to fs.unlink() it on cleanup stage
|
||||
sharp.cache(false);
|
||||
img = sharp(options.in);
|
||||
} catch (err) {
|
||||
return Promise.reject(new common.errors.InternalServerError({
|
||||
message: 'Sharp wasn\'t installed',
|
||||
code: 'SHARP_INSTALLATION',
|
||||
err: err
|
||||
}));
|
||||
}
|
||||
|
||||
return img.metadata()
|
||||
.then((metadata) => {
|
||||
if (metadata.width > options.width) {
|
||||
img.resize(options.width);
|
||||
}
|
||||
|
||||
// CASE: if you call `rotate` it will automatically remove the orientation (and all other meta data) and rotates
|
||||
// based on the orientation. It does not rotate if no orientation is set.
|
||||
img.rotate();
|
||||
})
|
||||
.then(() => {
|
||||
return img.toFile(options.out);
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new common.errors.InternalServerError({
|
||||
message: 'Unable to manipulate image.',
|
||||
err: err,
|
||||
code: 'IMAGE_PROCESSING'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.process = process;
|
97
ghost/image-transform/test/transform.test.js
Normal file
97
ghost/image-transform/test/transform.test.js
Normal file
@ -0,0 +1,97 @@
|
||||
const should = require('should');
|
||||
const sinon = require('sinon');
|
||||
const common = require('../../../../server/lib/common');
|
||||
const manipulator = require('../../../../server/lib/image/manipulator');
|
||||
const testUtils = require('../../../utils');
|
||||
const sandbox = sinon.sandbox.create();
|
||||
|
||||
describe('lib/image: manipulator', function () {
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
testUtils.unmockNotExistingModule();
|
||||
});
|
||||
|
||||
describe('cases', function () {
|
||||
let sharp, sharpInstance;
|
||||
|
||||
beforeEach(function () {
|
||||
sharpInstance = {
|
||||
metadata: sandbox.stub(),
|
||||
resize: sandbox.stub(),
|
||||
rotate: sandbox.stub(),
|
||||
toFile: sandbox.stub()
|
||||
};
|
||||
|
||||
sharp = sandbox.stub().callsFake(() => {
|
||||
return sharpInstance;
|
||||
});
|
||||
|
||||
sharp.cache = sandbox.stub();
|
||||
testUtils.mockNotExistingModule('sharp', sharp);
|
||||
});
|
||||
|
||||
it('resize image', function () {
|
||||
sharpInstance.metadata.resolves({width: 2000, height: 2000});
|
||||
sharpInstance.toFile.resolves();
|
||||
|
||||
return manipulator.process({width: 1000})
|
||||
.then(() => {
|
||||
sharp.cache.calledOnce.should.be.true();
|
||||
sharpInstance.metadata.calledOnce.should.be.true();
|
||||
sharpInstance.toFile.calledOnce.should.be.true();
|
||||
sharpInstance.resize.calledOnce.should.be.true();
|
||||
sharpInstance.rotate.calledOnce.should.be.true();
|
||||
});
|
||||
});
|
||||
|
||||
it('skip resizing if image is too small', function () {
|
||||
sharpInstance.metadata.resolves({width: 50, height: 50});
|
||||
sharpInstance.toFile.resolves();
|
||||
|
||||
return manipulator.process({width: 1000})
|
||||
.then(() => {
|
||||
sharp.cache.calledOnce.should.be.true();
|
||||
sharpInstance.metadata.calledOnce.should.be.true();
|
||||
sharpInstance.toFile.calledOnce.should.be.true();
|
||||
sharpInstance.resize.calledOnce.should.be.false();
|
||||
sharpInstance.rotate.calledOnce.should.be.true();
|
||||
});
|
||||
});
|
||||
|
||||
it('sharp throws error during processing', function () {
|
||||
sharpInstance.metadata.resolves({width: 500, height: 500});
|
||||
sharpInstance.toFile.rejects(new Error('whoops'));
|
||||
|
||||
return manipulator.process({width: 2000})
|
||||
.then(() => {
|
||||
'1'.should.eql(1, 'Expected to fail');
|
||||
})
|
||||
.catch((err) => {
|
||||
(err instanceof common.errors.InternalServerError).should.be.true;
|
||||
err.code.should.eql('IMAGE_PROCESSING');
|
||||
sharp.cache.calledOnce.should.be.true;
|
||||
sharpInstance.metadata.calledOnce.should.be.true();
|
||||
sharpInstance.toFile.calledOnce.should.be.true();
|
||||
sharpInstance.resize.calledOnce.should.be.false();
|
||||
sharpInstance.rotate.calledOnce.should.be.true();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('installation', function () {
|
||||
beforeEach(function () {
|
||||
testUtils.mockNotExistingModule('sharp', new Error(), true);
|
||||
});
|
||||
|
||||
it('sharp was not installed', function () {
|
||||
return manipulator.process()
|
||||
.then(() => {
|
||||
'1'.should.eql(1, 'Expected to fail');
|
||||
})
|
||||
.catch((err) => {
|
||||
(err instanceof common.errors.InternalServerError).should.be.true();
|
||||
err.code.should.eql('SHARP_INSTALLATION');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user