diff --git a/README.md b/README.md index 89aa50f1..1b667619 100755 --- a/README.md +++ b/README.md @@ -669,6 +669,10 @@ A [guide for contributors](https://github.com/lovell/sharp/blob/master/CONTRIBUT ### Functional tests +Where possible, the functional tests use gradient-based perceptual hashes +based on [dHash](http://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html) +to compare expected vs actual images. + #### Coverage [![Test Coverage](https://coveralls.io/repos/lovell/sharp/badge.png?branch=master)](https://coveralls.io/r/lovell/sharp?branch=master) diff --git a/test/fixtures/expected/exif-5.jpg b/test/fixtures/expected/exif-5.jpg new file mode 100644 index 00000000..63746c52 Binary files /dev/null and b/test/fixtures/expected/exif-5.jpg differ diff --git a/test/fixtures/expected/exif-8.jpg b/test/fixtures/expected/exif-8.jpg new file mode 100644 index 00000000..2d7705b4 Binary files /dev/null and b/test/fixtures/expected/exif-8.jpg differ diff --git a/test/fixtures/expected/extract-resize-crop-extract.jpg b/test/fixtures/expected/extract-resize-crop-extract.jpg new file mode 100644 index 00000000..11c9a9cc Binary files /dev/null and b/test/fixtures/expected/extract-resize-crop-extract.jpg differ diff --git a/test/fixtures/expected/extract-resize.jpg b/test/fixtures/expected/extract-resize.jpg new file mode 100644 index 00000000..7dd8828a Binary files /dev/null and b/test/fixtures/expected/extract-resize.jpg differ diff --git a/test/fixtures/expected/extract-rotate.jpg b/test/fixtures/expected/extract-rotate.jpg new file mode 100644 index 00000000..9ad6d34b Binary files /dev/null and b/test/fixtures/expected/extract-rotate.jpg differ diff --git a/test/fixtures/expected/extract.jpg b/test/fixtures/expected/extract.jpg new file mode 100644 index 00000000..51b5e468 Binary files /dev/null and b/test/fixtures/expected/extract.jpg differ diff --git a/test/fixtures/expected/extract.png b/test/fixtures/expected/extract.png new file mode 100644 index 00000000..689fa162 Binary files /dev/null and b/test/fixtures/expected/extract.png differ diff --git a/test/fixtures/expected/extract.tiff b/test/fixtures/expected/extract.tiff new file mode 100644 index 00000000..74c2cac3 Binary files /dev/null and b/test/fixtures/expected/extract.tiff differ diff --git a/test/fixtures/expected/extract.webp b/test/fixtures/expected/extract.webp new file mode 100644 index 00000000..9165ab5a Binary files /dev/null and b/test/fixtures/expected/extract.webp differ diff --git a/test/fixtures/expected/flatten-black.jpg b/test/fixtures/expected/flatten-black.jpg new file mode 100644 index 00000000..17505f69 Binary files /dev/null and b/test/fixtures/expected/flatten-black.jpg differ diff --git a/test/fixtures/expected/flatten-orange.jpg b/test/fixtures/expected/flatten-orange.jpg new file mode 100644 index 00000000..18f49bb8 Binary files /dev/null and b/test/fixtures/expected/flatten-orange.jpg differ diff --git a/test/fixtures/expected/flip-and-flop.jpg b/test/fixtures/expected/flip-and-flop.jpg new file mode 100644 index 00000000..ce58eadf Binary files /dev/null and b/test/fixtures/expected/flip-and-flop.jpg differ diff --git a/test/fixtures/expected/flip.jpg b/test/fixtures/expected/flip.jpg new file mode 100644 index 00000000..b56a58e0 Binary files /dev/null and b/test/fixtures/expected/flip.jpg differ diff --git a/test/fixtures/expected/flop.jpg b/test/fixtures/expected/flop.jpg new file mode 100644 index 00000000..ce58eadf Binary files /dev/null and b/test/fixtures/expected/flop.jpg differ diff --git a/test/fixtures/expected/gravity-center.jpg b/test/fixtures/expected/gravity-center.jpg new file mode 100644 index 00000000..592b2131 Binary files /dev/null and b/test/fixtures/expected/gravity-center.jpg differ diff --git a/test/fixtures/expected/gravity-centre.jpg b/test/fixtures/expected/gravity-centre.jpg new file mode 100644 index 00000000..a1efc4a1 Binary files /dev/null and b/test/fixtures/expected/gravity-centre.jpg differ diff --git a/test/fixtures/expected/gravity-east.jpg b/test/fixtures/expected/gravity-east.jpg new file mode 100644 index 00000000..769eef96 Binary files /dev/null and b/test/fixtures/expected/gravity-east.jpg differ diff --git a/test/fixtures/expected/gravity-north.jpg b/test/fixtures/expected/gravity-north.jpg new file mode 100644 index 00000000..0fcf1997 Binary files /dev/null and b/test/fixtures/expected/gravity-north.jpg differ diff --git a/test/fixtures/expected/gravity-south.jpg b/test/fixtures/expected/gravity-south.jpg new file mode 100644 index 00000000..1241abbd Binary files /dev/null and b/test/fixtures/expected/gravity-south.jpg differ diff --git a/test/fixtures/expected/gravity-west.jpg b/test/fixtures/expected/gravity-west.jpg new file mode 100644 index 00000000..c43ccff4 Binary files /dev/null and b/test/fixtures/expected/gravity-west.jpg differ diff --git a/test/fixtures/expected/resize-crop-extract.jpg b/test/fixtures/expected/resize-crop-extract.jpg new file mode 100644 index 00000000..166e5e96 Binary files /dev/null and b/test/fixtures/expected/resize-crop-extract.jpg differ diff --git a/test/fixtures/expected/rotate-extract.jpg b/test/fixtures/expected/rotate-extract.jpg new file mode 100644 index 00000000..ab7bb3d9 Binary files /dev/null and b/test/fixtures/expected/rotate-extract.jpg differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 6a47a5c6..29d704fb 100755 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -1,11 +1,42 @@ 'use strict'; var path = require('path'); +var assert = require('assert'); + +var sharp = require('../../index'); var getPath = function(filename) { return path.join(__dirname, filename); }; +// Generates a 64-bit-as-binary-string image fingerprint +// Based on the dHash gradient method - see http://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html +var fingerprint = function(image, done) { + sharp(image) + .greyscale() + .normalise() + .resize(9, 8) + .ignoreAspectRatio() + .interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline) + .raw() + .toBuffer(function(err, data) { + if (err) { + done(err); + } else { + var fingerprint = ''; + for (var col = 0; col < 8; col++) { + var gradient = 0; + for (var row = 0; row < 8; row++) { + var left = data[row * 8 + col]; + var right = data[row * 8 + col + 1]; + fingerprint = fingerprint + (left < right ? '1' : '0'); + } + } + done(null, fingerprint); + } + }); +}; + module.exports = { inputJpg: getPath('2569067123_aca715a2ee_o.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/ @@ -35,6 +66,30 @@ module.exports = { outputWebP: getPath('output.webp'), outputZoinks: getPath('output.zoinks'), // an 'unknown' file extension - path: getPath // allows tests to write files to fixtures directory (for testing with human eyes) + // Path for tests requiring human inspection + path: getPath, + + // Path for expected output images + expected: function(filename) { + return getPath(path.join('expected', filename)); + }, + + // Verify similarity of expected vs actual images via fingerprint + assertSimilar: function(expectedImage, actualImage, done) { + fingerprint(expectedImage, function(err, expectedFingerprint) { + if (err) throw err; + fingerprint(actualImage, function(err, actualFingerprint) { + if (err) throw err; + var distance = 0; + for (var i = 0; i < 64; i++) { + if (expectedFingerprint[i] !== actualFingerprint[i]) { + distance++; + } + } + assert.strictEqual(true, distance <= 5); // ~7% threshold + done(); + }); + }); + } }; diff --git a/test/unit/alpha.js b/test/unit/alpha.js index d022f762..59169a8d 100755 --- a/test/unit/alpha.js +++ b/test/unit/alpha.js @@ -13,7 +13,12 @@ describe('Alpha transparency', function() { sharp(fixtures.inputPngWithTransparency) .flatten() .resize(400, 300) - .toFile(fixtures.path('output.flatten-black.jpg'), done); + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(400, info.width); + assert.strictEqual(300, info.height); + fixtures.assertSimilar(fixtures.expected('flatten-black.jpg'), data, done); + }); }); it('Flatten to RGB orange', function(done) { @@ -21,7 +26,12 @@ describe('Alpha transparency', function() { .flatten() .background({r: 255, g: 102, b: 0}) .resize(400, 300) - .toFile(fixtures.path('output.flatten-rgb-orange.jpg'), done); + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(400, info.width); + assert.strictEqual(300, info.height); + fixtures.assertSimilar(fixtures.expected('flatten-orange.jpg'), data, done); + }); }); it('Flatten to CSS/hex orange', function(done) { @@ -29,7 +39,12 @@ describe('Alpha transparency', function() { .flatten() .background('#ff6600') .resize(400, 300) - .toFile(fixtures.path('output.flatten-hex-orange.jpg'), done); + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(400, info.width); + assert.strictEqual(300, info.height); + fixtures.assertSimilar(fixtures.expected('flatten-orange.jpg'), data, done); + }); }); it('Do not flatten', function(done) { diff --git a/test/unit/crop.js b/test/unit/crop.js index 644804c8..d40be672 100755 --- a/test/unit/crop.js +++ b/test/unit/crop.js @@ -13,11 +13,11 @@ describe('Crop gravities', function() { sharp(fixtures.inputJpg) .resize(320, 80) .crop(sharp.gravity.north) - .toFile(fixtures.path('output.gravity-north.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(320, info.width); assert.strictEqual(80, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('gravity-north.jpg'), data, done); }); }); @@ -25,11 +25,11 @@ describe('Crop gravities', function() { sharp(fixtures.inputJpg) .resize(80, 320) .crop(sharp.gravity.east) - .toFile(fixtures.path('output.gravity-east.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(80, info.width); assert.strictEqual(320, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('gravity-east.jpg'), data, done); }); }); @@ -37,11 +37,11 @@ describe('Crop gravities', function() { sharp(fixtures.inputJpg) .resize(320, 80) .crop(sharp.gravity.south) - .toFile(fixtures.path('output.gravity-south.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(320, info.width); assert.strictEqual(80, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('gravity-south.jpg'), data, done); }); }); @@ -49,11 +49,11 @@ describe('Crop gravities', function() { sharp(fixtures.inputJpg) .resize(80, 320) .crop(sharp.gravity.west) - .toFile(fixtures.path('output.gravity-west.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(80, info.width); assert.strictEqual(320, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('gravity-west.jpg'), data, done); }); }); @@ -61,11 +61,11 @@ describe('Crop gravities', function() { sharp(fixtures.inputJpg) .resize(320, 80) .crop(sharp.gravity.center) - .toFile(fixtures.path('output.gravity-center.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(320, info.width); assert.strictEqual(80, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('gravity-center.jpg'), data, done); }); }); @@ -73,23 +73,18 @@ describe('Crop gravities', function() { sharp(fixtures.inputJpg) .resize(80, 320) .crop(sharp.gravity.centre) - .toFile(fixtures.path('output.gravity-centre.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(80, info.width); assert.strictEqual(320, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('gravity-centre.jpg'), data, done); }); }); - it('Invalid', function(done) { - var isValid = true; - try { + it('Invalid', function() { + assert.throws(function() { sharp(fixtures.inputJpg).crop(5); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + }); }); }); diff --git a/test/unit/extract.js b/test/unit/extract.js index d2fc63f3..9a1f2f79 100755 --- a/test/unit/extract.js +++ b/test/unit/extract.js @@ -12,22 +12,22 @@ describe('Partial image extraction', function() { it('JPEG', function(done) { sharp(fixtures.inputJpg) .extract(2, 2, 20, 20) - .toFile(fixtures.path('output.extract.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(20, info.width); assert.strictEqual(20, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('extract.jpg'), data, done); }); }); it('PNG', function(done) { sharp(fixtures.inputPng) .extract(300, 200, 400, 200) - .toFile(fixtures.path('output.extract.png'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(400, info.width); assert.strictEqual(200, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('extract.png'), data, done); }); }); @@ -35,11 +35,11 @@ describe('Partial image extraction', function() { it('WebP', function(done) { sharp(fixtures.inputWebP) .extract(50, 100, 125, 200) - .toFile(fixtures.path('output.extract.webp'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(125, info.width); assert.strictEqual(200, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('extract.webp'), data, done); }); }); } @@ -47,11 +47,12 @@ describe('Partial image extraction', function() { it('TIFF', function(done) { sharp(fixtures.inputTiff) .extract(63, 34, 341, 529) - .toFile(fixtures.path('output.extract.tiff'), function(err, info) { + .jpeg() + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(341, info.width); assert.strictEqual(529, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('extract.tiff'), data, done); }); }); @@ -59,11 +60,11 @@ describe('Partial image extraction', function() { sharp(fixtures.inputJpg) .extract(10, 10, 10, 500, 500) .resize(100, 100) - .toFile(fixtures.path('output.extract.resize.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(100, info.width); assert.strictEqual(100, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('extract-resize.jpg'), data, done); }); }); @@ -72,11 +73,11 @@ describe('Partial image extraction', function() { .resize(500, 500) .crop(sharp.gravity.north) .extract(10, 10, 100, 100) - .toFile(fixtures.path('output.resize.crop.extract.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(100, info.width); assert.strictEqual(100, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('resize-crop-extract.jpg'), data, done); }); }); @@ -86,11 +87,11 @@ describe('Partial image extraction', function() { .resize(500, 500) .crop(sharp.gravity.north) .extract(10, 10, 100, 100) - .toFile(fixtures.path('output.extract.resize.crop.extract.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(100, info.width); assert.strictEqual(100, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('extract-resize-crop-extract.jpg'), data, done); }); }); @@ -98,11 +99,11 @@ describe('Partial image extraction', function() { sharp(fixtures.inputJpg) .extract(10, 10, 100, 100) .rotate(90) - .toFile(fixtures.path('output.extract.extract-then-rotate.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(100, info.width); assert.strictEqual(100, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('extract-rotate.jpg'), data, done); }); }); @@ -110,69 +111,44 @@ describe('Partial image extraction', function() { sharp(fixtures.inputJpg) .rotate(90) .extract(10, 10, 100, 100) - .toFile(fixtures.path('output.extract.rotate-then-extract.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual(100, info.width); assert.strictEqual(100, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('rotate-extract.jpg'), data, done); }); }); describe('Invalid parameters', function() { - it('Undefined', function(done) { - var isValid = true; - try { + it('Undefined', function() { + assert.throws(function() { sharp(fixtures.inputJpg).extract(); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + }); }); - it('String top', function(done) { - var isValid = true; - try { + it('String top', function() { + assert.throws(function() { sharp(fixtures.inputJpg).extract('spoons', 10, 10, 10); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + }); }); - it('Non-integral left', function(done) { - var isValid = true; - try { + it('Non-integral left', function() { + assert.throws(function() { sharp(fixtures.inputJpg).extract(10, 10.2, 10, 10); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + }); }); - it('Negative width - negative', function(done) { - var isValid = true; - try { + it('Negative width - negative', function() { + assert.throws(function() { sharp(fixtures.inputJpg).extract(10, 10, -10, 10); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + }); }); - it('Null height', function(done) { - var isValid = true; - try { + it('Null height', function() { + assert.throws(function() { sharp(fixtures.inputJpg).extract(10, 10, 10, null); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + }); }); }); diff --git a/test/unit/rotate.js b/test/unit/rotate.js index 58f473a5..d722b3c9 100755 --- a/test/unit/rotate.js +++ b/test/unit/rotate.js @@ -37,12 +37,12 @@ describe('Rotation', function() { sharp(fixtures.inputJpgWithExif) .rotate() .resize(320) - .toFile(fixtures.path('output.exif.8.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(240, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('exif-8.jpg'), data, done); }); }); @@ -50,12 +50,12 @@ describe('Rotation', function() { sharp(fixtures.inputJpgWithExifMirroring) .rotate() .resize(320) - .toFile(fixtures.path('output.exif.5.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(240, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('exif-5.jpg'), data, done); }); }); @@ -85,26 +85,22 @@ describe('Rotation', function() { }); }); - it('Rotate to an invalid angle, should fail', function(done) { - var fail = false; - try { + it('Rotate to an invalid angle, should fail', function() { + assert.throws(function() { sharp(fixtures.inputJpg).rotate(1); - fail = true; - } catch (e) {} - assert(!fail); - done(); + }); }); it('Flip - vertical', function(done) { sharp(fixtures.inputJpg) .resize(320) .flip() - .toFile(fixtures.path('output.flip.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(261, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('flip.jpg'), data, done); }); }); @@ -112,12 +108,12 @@ describe('Rotation', function() { sharp(fixtures.inputJpg) .resize(320) .flop() - .toFile(fixtures.path('output.flop.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(261, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('flop.jpg'), data, done); }); }); @@ -125,12 +121,12 @@ describe('Rotation', function() { sharp(fixtures.inputJpg) .resize(320) .flop() - .toFile(fixtures.path('output.flip.flop.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(261, info.height); - done(); + fixtures.assertSimilar(fixtures.expected('flip-and-flop.jpg'), data, done); }); }); @@ -139,12 +135,12 @@ describe('Rotation', function() { .resize(320) .flip(false) .flop(false) - .toFile(fixtures.path('output.flip.flop.nope.jpg'), function(err, info) { + .toBuffer(function(err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); assert.strictEqual(261, info.height); - done(); + fixtures.assertSimilar(fixtures.inputJpg, data, done); }); });