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: '',
|
overlayFileIn: '',
|
||||||
overlayBufferIn: null,
|
overlayBufferIn: null,
|
||||||
overlayGravity: 0,
|
overlayGravity: 0,
|
||||||
|
overlayTile: false,
|
||||||
// output options
|
// output options
|
||||||
formatOut: 'input',
|
formatOut: 'input',
|
||||||
fileOut: '',
|
fileOut: '',
|
||||||
@ -354,6 +355,15 @@ Sharp.prototype.overlayWith = function(overlay, options) {
|
|||||||
throw new Error('Unsupported overlay ' + typeof overlay);
|
throw new Error('Unsupported overlay ' + typeof overlay);
|
||||||
}
|
}
|
||||||
if (isObject(options)) {
|
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)) {
|
if (isInteger(options.gravity) && inRange(options.gravity, 0, 8)) {
|
||||||
this.options.overlayGravity = options.gravity;
|
this.options.overlayGravity = options.gravity;
|
||||||
} else if (isString(options.gravity) && isInteger(module.exports.gravity[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) {
|
if (overlayImageType == ImageType::UNKNOWN) {
|
||||||
return Error();
|
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
|
// Ensure overlay is premultiplied sRGB
|
||||||
overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply();
|
overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply();
|
||||||
// Composite images with given gravity
|
// Composite images with given gravity
|
||||||
@ -1050,6 +1079,7 @@ NAN_METHOD(pipeline) {
|
|||||||
baton->overlayBufferIn = node::Buffer::Data(overlayBufferIn);
|
baton->overlayBufferIn = node::Buffer::Data(overlayBufferIn);
|
||||||
}
|
}
|
||||||
baton->overlayGravity = attrAs<int32_t>(options, "overlayGravity");
|
baton->overlayGravity = attrAs<int32_t>(options, "overlayGravity");
|
||||||
|
baton->overlayTile = attrAs<bool>(options, "overlayTile");
|
||||||
// Resize options
|
// Resize options
|
||||||
baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement");
|
baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement");
|
||||||
baton->crop = attrAs<int32_t>(options, "crop");
|
baton->crop = attrAs<int32_t>(options, "crop");
|
||||||
|
@ -33,6 +33,7 @@ struct PipelineBaton {
|
|||||||
char *overlayBufferIn;
|
char *overlayBufferIn;
|
||||||
size_t overlayBufferInLength;
|
size_t overlayBufferInLength;
|
||||||
int overlayGravity;
|
int overlayGravity;
|
||||||
|
bool overlayTile;
|
||||||
int topOffsetPre;
|
int topOffsetPre;
|
||||||
int leftOffsetPre;
|
int leftOffsetPre;
|
||||||
int widthPre;
|
int widthPre;
|
||||||
@ -97,6 +98,7 @@ struct PipelineBaton {
|
|||||||
bufferOutLength(0),
|
bufferOutLength(0),
|
||||||
overlayBufferInLength(0),
|
overlayBufferInLength(0),
|
||||||
overlayGravity(0),
|
overlayGravity(0),
|
||||||
|
overlayTile(false),
|
||||||
topOffsetPre(-1),
|
topOffsetPre(-1),
|
||||||
topOffsetPost(-1),
|
topOffsetPost(-1),
|
||||||
channels(0),
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|