diff --git a/docs/changelog.md b/docs/changelog.md index 0eb87f9a..d8e94791 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,10 @@ Requires libvips v8.10.5 * 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. [#2399](https://github.com/lovell/sharp/issues/2399) diff --git a/lib/composite.js b/lib/composite.js index 8f45f690..88ab83cb 100644 --- a/lib/composite.js +++ b/lib/composite.js @@ -105,8 +105,9 @@ function composite (images) { input: this._createInputDescriptor(image.input, inputOptions, { allowStream: false }), blend: 'over', tile: false, - left: -1, - top: -1, + left: 0, + top: 0, + hasOffset: false, gravity: 0, premultiplied: false }; @@ -125,21 +126,23 @@ function composite (images) { } } if (is.defined(image.left)) { - if (is.integer(image.left) && image.left >= 0) { + if (is.integer(image.left)) { composite.left = image.left; } else { - throw is.invalidParameterError('left', 'positive integer', image.left); + throw is.invalidParameterError('left', 'integer', image.left); } } if (is.defined(image.top)) { - if (is.integer(image.top) && image.top >= 0) { + if (is.integer(image.top)) { composite.top = image.top; } 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'); + } else { + composite.hasOffset = is.integer(image.top) && is.integer(image.left); } if (is.defined(image.gravity)) { if (is.integer(image.gravity) && is.inRange(image.gravity, 0, 8)) { diff --git a/package.json b/package.json index f213f683..6c3d3bda 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,9 @@ "Roman Malieiev ", "Tomas Szabo ", "Robert O'Rourke ", - "Guillermo Alfonso Varela ChouciƱo " + "Guillermo Alfonso Varela ChouciƱo ", + "Christian Flintrup ", + "Manan Jadhav " ], "scripts": { "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", diff --git a/src/common.cc b/src/common.cc index 5da9d400..9091a257 100644 --- a/src/common.cc +++ b/src/common.cc @@ -658,26 +658,18 @@ namespace sharp { int top = 0; // assign only if valid - if (x >= 0 && x < (inWidth - outWidth)) { + if (x < (inWidth - outWidth)) { left = x; } else if (x >= (inWidth - outWidth)) { left = inWidth - outWidth; } - if (y >= 0 && y < (inHeight - outHeight)) { + if (y < (inHeight - outHeight)) { top = y; } else if (y >= (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); } diff --git a/src/pipeline.cc b/src/pipeline.cc index 55af804c..fdb4856b 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -570,7 +570,7 @@ class PipelineWorker : public Napi::AsyncWorker { int left; int top; compositeImage = compositeImage.replicate(across, down); - if (composite->left >= 0 && composite->top >= 0) { + if (composite->hasOffset) { std::tie(left, top) = sharp::CalculateCrop( compositeImage.width(), compositeImage.height(), image.width(), image.height(), composite->left, composite->top); @@ -592,7 +592,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Calculate position int left; int top; - if (composite->left >= 0 && composite->top >= 0) { + if (composite->hasOffset) { // Composite image at given offsets std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(), 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->left = sharp::AttrAsInt32(compositeObject, "left"); composite->top = sharp::AttrAsInt32(compositeObject, "top"); + composite->hasOffset = sharp::AttrAsBool(compositeObject, "hasOffset"); composite->tile = sharp::AttrAsBool(compositeObject, "tile"); composite->premultiplied = sharp::AttrAsBool(compositeObject, "premultiplied"); baton->composite.push_back(composite); diff --git a/src/pipeline.h b/src/pipeline.h index 1ef5b05e..135552df 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -40,6 +40,7 @@ struct Composite { int gravity; int left; int top; + bool hasOffset; bool tile; bool premultiplied; @@ -47,8 +48,9 @@ struct Composite { input(nullptr), mode(VIPS_BLEND_MODE_OVER), gravity(0), - left(-1), - top(-1), + left(0), + top(0), + hasOffset(false), tile(false), premultiplied(false) {} }; diff --git a/test/fixtures/expected/overlay-negative-offset-with-gravity.jpg b/test/fixtures/expected/overlay-negative-offset-with-gravity.jpg new file mode 100644 index 00000000..06e585e2 Binary files /dev/null and b/test/fixtures/expected/overlay-negative-offset-with-gravity.jpg differ diff --git a/test/unit/composite.js b/test/unit/composite.js index d7e50e56..959ef4aa 100644 --- a/test/unit/composite.js +++ b/test/unit/composite.js @@ -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 => { sharp(fixtures.inputJpg) .resize(80) @@ -333,13 +351,25 @@ describe('composite', () => { it('invalid left', () => { assert.throws(() => { 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', () => { assert.throws(() => { - sharp().composite([{ input: 'test', top: -1 }]); - }, /Expected positive integer for top but received -1 of type number/); + sharp().composite([{ input: 'test', top: 0.5 }]); + }, /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', () => {