Add experimental overlayWith API

Composites an overlay image with alpha channel into the input image (which
must have alpha channel) using ‘over’ alpha compositing blend mode. This API
requires both images to have the same dimensions.

References:
- http://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
- https://github.com/jcupitt/ruby-vips/issues/28#issuecomment-9014826

See #97.
This commit is contained in:
Daniel Gasienica
2015-04-24 14:57:48 -07:00
committed by Lovell Fuller
parent c886eaa6b0
commit 64f7f1d662
17 changed files with 312 additions and 8 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

BIN
test/fixtures/alpha-layer-1-fill.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
test/fixtures/alpha-layer-2-ink.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View File

@@ -11,7 +11,7 @@ var getPath = function(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) {
var fingerprint = function(image, callback) {
sharp(image)
.greyscale()
.normalise()
@@ -21,7 +21,7 @@ var fingerprint = function(image, done) {
.raw()
.toBuffer(function(err, data) {
if (err) {
done(err);
callback(err);
} else {
var fingerprint = '';
for (var col = 0; col < 8; col++) {
@@ -32,7 +32,7 @@ var fingerprint = function(image, done) {
fingerprint = fingerprint + (left < right ? '1' : '0');
}
}
done(null, fingerprint);
callback(null, fingerprint);
}
});
};
@@ -52,6 +52,11 @@ module.exports = {
inputPngWithTransparency: getPath('blackbug.png'), // public domain
inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'),
inputPngWithOneColor: getPath('2x2_fdcce6.png'),
inputPngOverlayLayer0: getPath('alpha-layer-0-background.png'),
inputPngOverlayLayer1: getPath('alpha-layer-1-fill.png'),
inputPngOverlayLayer2: getPath('alpha-layer-2-ink.png'),
inputPngOverlayLayer1LowAlpha: getPath('alpha-layer-1-fill-low-alpha.png'),
inputPngOverlayLayer2LowAlpha: getPath('alpha-layer-2-ink-low-alpha.png'),
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
@@ -75,19 +80,46 @@ module.exports = {
},
// Verify similarity of expected vs actual images via fingerprint
assertSimilar: function(expectedImage, actualImage, done) {
// Specify distance threshold using `options={threshold: 42}`, default
// `threshold` is 5;
assertSimilar: function(expectedImage, actualImage, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (typeof options === 'undefined' && options === null) {
options = {};
}
if (options.threshold === null || typeof options.threshold === 'undefined') {
options.threshold = 5; // ~7% threshold
}
if (typeof options.threshold !== 'number') {
throw new TypeError('`options.threshold` must be a number');
}
if (typeof callback !== 'function') {
throw new TypeError('`callback` must be a function');
}
fingerprint(expectedImage, function(err, expectedFingerprint) {
if (err) throw err;
if (err) return callback(err);
fingerprint(actualImage, function(err, actualFingerprint) {
if (err) throw err;
if (err) return callback(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();
if (distance > options.threshold) {
return callback(new Error('Maximum similarity distance: ' + options.threshold + '. Actual: ' + distance));
}
callback();
});
});
}

61
test/unit/overlay.js Normal file
View File

@@ -0,0 +1,61 @@
'use strict';
var fixtures = require('../fixtures');
var fs = require('fs');
var sharp = require('../../index');
sharp.cache(0);
// Main
describe('Overlays', function() {
it('Overlay transparent PNG on solid background', function(done) {
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toBuffer(function (error, data, info) {
if (error) return done(error);
fixtures.assertSimilar(fixtures.expected('alpha-layer-01.png'), data, {threshold: 0}, done);
});
});
it('Composite three transparent PNGs into one', function(done) {
sharp(fixtures.inputPngOverlayLayer0)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toBuffer(function (error, data, info) {
if (error) return done(error);
sharp(data)
.overlayWith(fixtures.inputPngOverlayLayer2)
.toBuffer(function (error, data, info) {
if (error) return done(error);
fixtures.assertSimilar(fixtures.expected('alpha-layer-012.png'), data, {threshold: 0}, done);
});
});
});
// This tests that alpha channel unpremultiplication is correct:
it('Composite three low-alpha transparent PNGs into one', function(done) {
sharp(fixtures.inputPngOverlayLayer1LowAlpha)
.overlayWith(fixtures.inputPngOverlayLayer2LowAlpha)
.toBuffer(function (error, data, info) {
if (error) return done(error);
fixtures.assertSimilar(fixtures.expected('alpha-layer-012-low-alpha.png'), data, {threshold: 0}, done);
});
});
// This tests that alpha channel unpremultiplication is correct:
it('Composite transparent PNG onto JPEG', function(done) {
sharp(fixtures.inputJpg)
.overlayWith(fixtures.inputPngOverlayLayer1)
.toBuffer(function (error, data, info) {
if (error.message !== 'Input image must have an alpha channel') {
return done(new Error('Unexpected error: ' + error.message));
}
done();
});
});
});