Add support for repeated/tiled overlay image (#443)
USAGE: overlayWith('overlayimage.png', { tile: true, gravity: northwest} ) When using the tile option, the gravity option is applied to the extracted part of the tiled overlay image.
10
index.js
@ -92,6 +92,7 @@ var Sharp = function(input, options) {
|
||||
overlayFileIn: '',
|
||||
overlayBufferIn: null,
|
||||
overlayGravity: 0,
|
||||
overlayTile: false,
|
||||
// output options
|
||||
formatOut: 'input',
|
||||
fileOut: '',
|
||||
@ -354,6 +355,15 @@ Sharp.prototype.overlayWith = function(overlay, options) {
|
||||
throw new Error('Unsupported overlay ' + typeof overlay);
|
||||
}
|
||||
if (isObject(options)) {
|
||||
if (typeof options.tile === 'undefined') {
|
||||
this.options.overlayTile = false;
|
||||
}
|
||||
else if (isBoolean(options.tile)) {
|
||||
this.options.overlayTile = options.tile;
|
||||
} else {
|
||||
throw new Error(' Invalid Value for tile ' + options.tile + 'Only Boolean Values allowed for overlay.tile.');
|
||||
}
|
||||
|
||||
if (isInteger(options.gravity) && inRange(options.gravity, 0, 8)) {
|
||||
this.options.overlayGravity = options.gravity;
|
||||
} else if (isString(options.gravity) && isInteger(module.exports.gravity[options.gravity])) {
|
||||
|
@ -664,6 +664,35 @@ class PipelineWorker : public AsyncWorker {
|
||||
if (overlayImageType == ImageType::UNKNOWN) {
|
||||
return Error();
|
||||
}
|
||||
// Check if overlay is tiled
|
||||
if (baton->overlayTile) {
|
||||
int overlayImageWidth = overlayImage.width();
|
||||
int overlayImageHeight = overlayImage.height();
|
||||
int across = 0;
|
||||
int down = 0;
|
||||
|
||||
// use gravity in ovelay
|
||||
if(overlayImageWidth <= baton->width) {
|
||||
across = static_cast<int>(ceil(static_cast<double>(baton->width) / overlayImageWidth));
|
||||
}
|
||||
if(overlayImageHeight <= baton->height) {
|
||||
down = static_cast<int>(ceil(static_cast<double>(baton->height) / overlayImageHeight));
|
||||
}
|
||||
if(across != 0 || down != 0) {
|
||||
int left;
|
||||
int top;
|
||||
overlayImage = overlayImage.replicate(across, down);
|
||||
// the overlayGravity will now be used to CalculateCrop for extract_area
|
||||
std::tie(left, top) = CalculateCrop(
|
||||
overlayImage.width(), overlayImage.height(), baton->width, baton->height, baton->overlayGravity
|
||||
);
|
||||
overlayImage = overlayImage.extract_area(
|
||||
left, top, baton->width, baton->height
|
||||
);
|
||||
}
|
||||
// the overlayGravity was used for extract_area, therefore set it back to its default value of 0
|
||||
baton->overlayGravity = 0;
|
||||
}
|
||||
// Ensure overlay is premultiplied sRGB
|
||||
overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply();
|
||||
// Composite images with given gravity
|
||||
@ -1050,6 +1079,7 @@ NAN_METHOD(pipeline) {
|
||||
baton->overlayBufferIn = node::Buffer::Data(overlayBufferIn);
|
||||
}
|
||||
baton->overlayGravity = attrAs<int32_t>(options, "overlayGravity");
|
||||
baton->overlayTile = attrAs<bool>(options, "overlayTile");
|
||||
// Resize options
|
||||
baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement");
|
||||
baton->crop = attrAs<int32_t>(options, "crop");
|
||||
|
@ -33,6 +33,7 @@ struct PipelineBaton {
|
||||
char *overlayBufferIn;
|
||||
size_t overlayBufferInLength;
|
||||
int overlayGravity;
|
||||
bool overlayTile;
|
||||
int topOffsetPre;
|
||||
int leftOffsetPre;
|
||||
int widthPre;
|
||||
@ -97,6 +98,7 @@ struct PipelineBaton {
|
||||
bufferOutLength(0),
|
||||
overlayBufferInLength(0),
|
||||
overlayGravity(0),
|
||||
overlayTile(false),
|
||||
topOffsetPre(-1),
|
||||
topOffsetPost(-1),
|
||||
channels(0),
|
||||
|
BIN
test/fixtures/expected/overlay-tile-gravity-center.jpg
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
test/fixtures/expected/overlay-tile-gravity-centre.jpg
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
test/fixtures/expected/overlay-tile-gravity-east.jpg
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
test/fixtures/expected/overlay-tile-gravity-north.jpg
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
test/fixtures/expected/overlay-tile-gravity-northeast.jpg
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
test/fixtures/expected/overlay-tile-gravity-northwest.jpg
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
test/fixtures/expected/overlay-tile-gravity-south.jpg
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
test/fixtures/expected/overlay-tile-gravity-southeast.jpg
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
test/fixtures/expected/overlay-tile-gravity-southwest.jpg
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
test/fixtures/expected/overlay-tile-gravity-west.jpg
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
test/fixtures/expected/overlay-tile-rotated90-gravity-northwest.jpg
vendored
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
test/fixtures/expected/overlay-tile-rotated90.jpg
vendored
Normal file
After Width: | Height: | Size: 4.4 KiB |
@ -242,4 +242,65 @@ describe('Overlays', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Overlay with tile enabled and gravity', function() {
|
||||
Object.keys(sharp.gravity).forEach(function(gravity) {
|
||||
it(gravity, function(done) {
|
||||
var expected = fixtures.expected('overlay-tile-gravity-' + gravity + '.jpg');
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(80)
|
||||
.overlayWith(fixtures.inputPngWithTransparency16bit, {
|
||||
tile: true,
|
||||
gravity: gravity
|
||||
})
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(80, info.width);
|
||||
assert.strictEqual(65, info.height);
|
||||
assert.strictEqual(3, info.channels);
|
||||
fixtures.assertSimilar(expected, data, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('With tile enabled and image rotated 90 degrees', function(done) {
|
||||
var expected = fixtures.expected('overlay-tile-rotated90.jpg');
|
||||
sharp(fixtures.inputJpg)
|
||||
.rotate(90)
|
||||
.resize(80)
|
||||
.overlayWith(fixtures.inputPngWithTransparency16bit, {
|
||||
tile: true
|
||||
})
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(80, info.width);
|
||||
assert.strictEqual(98, info.height);
|
||||
assert.strictEqual(3, info.channels);
|
||||
fixtures.assertSimilar(expected, data, done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('With tile enabled and image rotated 90 degrees and gravity northwest', function(done) {
|
||||
var expected = fixtures.expected('overlay-tile-rotated90-gravity-northwest.jpg');
|
||||
sharp(fixtures.inputJpg)
|
||||
.rotate(90)
|
||||
.resize(80)
|
||||
.overlayWith(fixtures.inputPngWithTransparency16bit, {
|
||||
tile: true,
|
||||
gravity: 'northwest'
|
||||
})
|
||||
.toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(80, info.width);
|
||||
assert.strictEqual(98, info.height);
|
||||
assert.strictEqual(3, info.channels);
|
||||
fixtures.assertSimilar(expected, data, done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|