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:
Nazar Gargol 2018-08-30 18:30:36 +02:00 committed by Hannah Wolfe
parent de9e5cecd4
commit eb203de714
2 changed files with 147 additions and 0 deletions

View 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;

View 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');
});
});
});
});