Allow for negative top/left offsets in composite overlays

A top or left offset value of -1 will no longer mean that the
value is not set, but will now be an actual offset of -1.

INT_MIN for left & top will mean that the values are not set.

Co-authored-by: Christian Flintrup <chr@gigahost.dk>
This commit is contained in:
Manan Jadhav 2020-12-20 17:26:11 +00:00 committed by Lovell Fuller
parent 182beaa4a1
commit 02676140e8
8 changed files with 59 additions and 25 deletions

View File

@ -10,6 +10,10 @@ Requires libvips v8.10.5
* Remove experimental status from `heif` output, defaults are now AVIF-centric. * Remove experimental status from `heif` output, defaults are now AVIF-centric.
* Allow negative top/left offsets for composite operation.
[#2391](https://github.com/lovell/sharp/pull/2391)
[@CurosMJ](https://github.com/CurosMJ)
* Ensure all platforms use fontconfig for font rendering. * Ensure all platforms use fontconfig for font rendering.
[#2399](https://github.com/lovell/sharp/issues/2399) [#2399](https://github.com/lovell/sharp/issues/2399)

View File

@ -105,8 +105,9 @@ function composite (images) {
input: this._createInputDescriptor(image.input, inputOptions, { allowStream: false }), input: this._createInputDescriptor(image.input, inputOptions, { allowStream: false }),
blend: 'over', blend: 'over',
tile: false, tile: false,
left: -1, left: 0,
top: -1, top: 0,
hasOffset: false,
gravity: 0, gravity: 0,
premultiplied: false premultiplied: false
}; };
@ -125,21 +126,23 @@ function composite (images) {
} }
} }
if (is.defined(image.left)) { if (is.defined(image.left)) {
if (is.integer(image.left) && image.left >= 0) { if (is.integer(image.left)) {
composite.left = image.left; composite.left = image.left;
} else { } else {
throw is.invalidParameterError('left', 'positive integer', image.left); throw is.invalidParameterError('left', 'integer', image.left);
} }
} }
if (is.defined(image.top)) { if (is.defined(image.top)) {
if (is.integer(image.top) && image.top >= 0) { if (is.integer(image.top)) {
composite.top = image.top; composite.top = image.top;
} else { } else {
throw is.invalidParameterError('top', 'positive integer', image.top); throw is.invalidParameterError('top', 'integer', image.top);
} }
} }
if (composite.left !== composite.top && Math.min(composite.left, composite.top) === -1) { if (is.defined(image.top) !== is.defined(image.left)) {
throw new Error('Expected both left and top to be set'); throw new Error('Expected both left and top to be set');
} else {
composite.hasOffset = is.integer(image.top) && is.integer(image.left);
} }
if (is.defined(image.gravity)) { if (is.defined(image.gravity)) {
if (is.integer(image.gravity) && is.inRange(image.gravity, 0, 8)) { if (is.integer(image.gravity) && is.inRange(image.gravity, 0, 8)) {

View File

@ -70,7 +70,9 @@
"Roman Malieiev <aromaleev@gmail.com>", "Roman Malieiev <aromaleev@gmail.com>",
"Tomas Szabo <tomas.szabo@deftomat.com>", "Tomas Szabo <tomas.szabo@deftomat.com>",
"Robert O'Rourke <robert@o-rourke.org>", "Robert O'Rourke <robert@o-rourke.org>",
"Guillermo Alfonso Varela Chouciño <guillevch@gmail.com>" "Guillermo Alfonso Varela Chouciño <guillevch@gmail.com>",
"Christian Flintrup <chr@gigahost.dk>",
"Manan Jadhav <manan@motionden.com>"
], ],
"scripts": { "scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",

View File

@ -658,26 +658,18 @@ namespace sharp {
int top = 0; int top = 0;
// assign only if valid // assign only if valid
if (x >= 0 && x < (inWidth - outWidth)) { if (x < (inWidth - outWidth)) {
left = x; left = x;
} else if (x >= (inWidth - outWidth)) { } else if (x >= (inWidth - outWidth)) {
left = inWidth - outWidth; left = inWidth - outWidth;
} }
if (y >= 0 && y < (inHeight - outHeight)) { if (y < (inHeight - outHeight)) {
top = y; top = y;
} else if (y >= (inHeight - outHeight)) { } else if (y >= (inHeight - outHeight)) {
top = inHeight - outHeight; top = inHeight - outHeight;
} }
// the resulting left and top could have been outside the image after calculation from bottom/right edges
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
return std::make_tuple(left, top); return std::make_tuple(left, top);
} }

View File

@ -570,7 +570,7 @@ class PipelineWorker : public Napi::AsyncWorker {
int left; int left;
int top; int top;
compositeImage = compositeImage.replicate(across, down); compositeImage = compositeImage.replicate(across, down);
if (composite->left >= 0 && composite->top >= 0) { if (composite->hasOffset) {
std::tie(left, top) = sharp::CalculateCrop( std::tie(left, top) = sharp::CalculateCrop(
compositeImage.width(), compositeImage.height(), image.width(), image.height(), compositeImage.width(), compositeImage.height(), image.width(), image.height(),
composite->left, composite->top); composite->left, composite->top);
@ -592,7 +592,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Calculate position // Calculate position
int left; int left;
int top; int top;
if (composite->left >= 0 && composite->top >= 0) { if (composite->hasOffset) {
// Composite image at given offsets // Composite image at given offsets
std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(), std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
compositeImage.width(), compositeImage.height(), composite->left, composite->top); compositeImage.width(), compositeImage.height(), composite->left, composite->top);
@ -1253,6 +1253,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
composite->gravity = sharp::AttrAsUint32(compositeObject, "gravity"); composite->gravity = sharp::AttrAsUint32(compositeObject, "gravity");
composite->left = sharp::AttrAsInt32(compositeObject, "left"); composite->left = sharp::AttrAsInt32(compositeObject, "left");
composite->top = sharp::AttrAsInt32(compositeObject, "top"); composite->top = sharp::AttrAsInt32(compositeObject, "top");
composite->hasOffset = sharp::AttrAsBool(compositeObject, "hasOffset");
composite->tile = sharp::AttrAsBool(compositeObject, "tile"); composite->tile = sharp::AttrAsBool(compositeObject, "tile");
composite->premultiplied = sharp::AttrAsBool(compositeObject, "premultiplied"); composite->premultiplied = sharp::AttrAsBool(compositeObject, "premultiplied");
baton->composite.push_back(composite); baton->composite.push_back(composite);

View File

@ -40,6 +40,7 @@ struct Composite {
int gravity; int gravity;
int left; int left;
int top; int top;
bool hasOffset;
bool tile; bool tile;
bool premultiplied; bool premultiplied;
@ -47,8 +48,9 @@ struct Composite {
input(nullptr), input(nullptr),
mode(VIPS_BLEND_MODE_OVER), mode(VIPS_BLEND_MODE_OVER),
gravity(0), gravity(0),
left(-1), left(0),
top(-1), top(0),
hasOffset(false),
tile(false), tile(false),
premultiplied(false) {} premultiplied(false) {}
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -172,6 +172,24 @@ describe('composite', () => {
}); });
}); });
it('negative offset and gravity', done => {
sharp(fixtures.inputJpg)
.resize(400)
.composite([{
input: fixtures.inputPngWithTransparency16bit,
left: -10,
top: -10,
gravity: 4
}])
.toBuffer((err, data, info) => {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(
fixtures.expected('overlay-negative-offset-with-gravity.jpg'), data, done);
});
});
it('offset, gravity and tile', done => { it('offset, gravity and tile', done => {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(80) .resize(80)
@ -333,13 +351,25 @@ describe('composite', () => {
it('invalid left', () => { it('invalid left', () => {
assert.throws(() => { assert.throws(() => {
sharp().composite([{ input: 'test', left: 0.5 }]); sharp().composite([{ input: 'test', left: 0.5 }]);
}, /Expected positive integer for left but received 0.5 of type number/); }, /Expected integer for left but received 0.5 of type number/);
assert.throws(() => {
sharp().composite([{ input: 'test', left: 'invalid' }]);
}, /Expected integer for left but received invalid of type string/);
assert.throws(() => {
sharp().composite([{ input: 'test', left: 'invalid', top: 10 }]);
}, /Expected integer for left but received invalid of type string/);
}); });
it('invalid top', () => { it('invalid top', () => {
assert.throws(() => { assert.throws(() => {
sharp().composite([{ input: 'test', top: -1 }]); sharp().composite([{ input: 'test', top: 0.5 }]);
}, /Expected positive integer for top but received -1 of type number/); }, /Expected integer for top but received 0.5 of type number/);
assert.throws(() => {
sharp().composite([{ input: 'test', top: 'invalid' }]);
}, /Expected integer for top but received invalid of type string/);
assert.throws(() => {
sharp().composite([{ input: 'test', top: 'invalid', left: 10 }]);
}, /Expected integer for top but received invalid of type string/);
}); });
it('left but no top', () => { it('left but no top', () => {