diff --git a/docs/changelog.md b/docs/changelog.md index f4ad65571..6bdfb3c40 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,11 @@ Requires libvips v8.12.2 +### v0.30.2 - TBD + +* Improve performance and accuracy when compositing multiple images. + [#2286](https://github.com/lovell/sharp/issues/2286) + ### v0.30.1 - 9th February 2022 * Allow use of `toBuffer` and `toFile` on the same instance. diff --git a/lib/composite.js b/lib/composite.js index f3b736a6a..38d177936 100644 --- a/lib/composite.js +++ b/lib/composite.js @@ -162,7 +162,6 @@ function composite (images) { throw is.invalidParameterError('premultiplied', 'boolean', image.premultiplied); } } - return composite; }); return this; diff --git a/src/pipeline.cc b/src/pipeline.cc index a426dfbb3..b2c2b3a7b 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -581,6 +581,8 @@ class PipelineWorker : public Napi::AsyncWorker { // Composite if (shouldComposite) { + std::vector images = { image }; + std::vector modes, xs, ys; for (Composite *composite : baton->composite) { VImage compositeImage; sharp::ImageType compositeImageType = sharp::ImageType::UNKNOWN; @@ -626,12 +628,12 @@ class PipelineWorker : public Napi::AsyncWorker { // gravity was used for extract_area, set it back to its default value of 0 composite->gravity = 0; } - // Ensure image to composite is sRGB with premultiplied alpha + // Ensure image to composite is sRGB with unpremultiplied alpha compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB); if (!sharp::HasAlpha(compositeImage)) { compositeImage = sharp::EnsureAlpha(compositeImage, 1); } - if (!composite->premultiplied) compositeImage = compositeImage.premultiply(); + if (composite->premultiplied) compositeImage = compositeImage.unpremultiply(); // Calculate position int left; int top; @@ -649,12 +651,12 @@ class PipelineWorker : public Napi::AsyncWorker { std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(), compositeImage.width(), compositeImage.height(), composite->gravity); } - // Composite - image = image.composite2(compositeImage, composite->mode, VImage::option() - ->set("premultiplied", TRUE) - ->set("x", left) - ->set("y", top)); + images.push_back(compositeImage); + modes.push_back(composite->mode); + xs.push_back(left); + ys.push_back(top); } + image = image.composite(images, modes, VImage::option()->set("x", xs)->set("y", ys)); } // Reverse premultiplication after all transformations: diff --git a/test/fixtures/expected/composite-multiple.png b/test/fixtures/expected/composite-multiple.png index d72d1eda3..2aef0fbde 100644 Binary files a/test/fixtures/expected/composite-multiple.png and b/test/fixtures/expected/composite-multiple.png differ diff --git a/test/fixtures/expected/composite.blend.dest-over.png b/test/fixtures/expected/composite.blend.dest-over.png index bda1c9ddf..a230c331a 100644 Binary files a/test/fixtures/expected/composite.blend.dest-over.png and b/test/fixtures/expected/composite.blend.dest-over.png differ diff --git a/test/fixtures/expected/composite.blend.over.png b/test/fixtures/expected/composite.blend.over.png index ce5e0bb4e..142e75ecb 100644 Binary files a/test/fixtures/expected/composite.blend.over.png and b/test/fixtures/expected/composite.blend.over.png differ diff --git a/test/fixtures/expected/composite.blend.saturate.png b/test/fixtures/expected/composite.blend.saturate.png index ce22bf256..86090025a 100644 Binary files a/test/fixtures/expected/composite.blend.saturate.png and b/test/fixtures/expected/composite.blend.saturate.png differ diff --git a/test/fixtures/expected/composite.blend.xor.png b/test/fixtures/expected/composite.blend.xor.png index 01160d953..755440458 100644 Binary files a/test/fixtures/expected/composite.blend.xor.png and b/test/fixtures/expected/composite.blend.xor.png differ diff --git a/test/unit/composite.js b/test/unit/composite.js index 4ab9974a3..773a6ffb5 100644 --- a/test/unit/composite.js +++ b/test/unit/composite.js @@ -45,22 +45,20 @@ const blends = [ // Test describe('composite', () => { - it('blend', () => Promise.all( - blends.map(blend => { + blends.forEach(blend => { + it(`blend ${blend}`, async () => { const filename = `composite.blend.${blend}.png`; const actual = fixtures.path(`output.${filename}`); const expected = fixtures.expected(filename); - return sharp(redRect) + await sharp(redRect) .composite([{ input: blueRect, blend }]) - .toFile(actual) - .then(() => { - fixtures.assertMaxColourDistance(actual, expected); - }); - }) - )); + .toFile(actual); + fixtures.assertMaxColourDistance(actual, expected); + }); + }); it('premultiplied true', () => { const filename = 'composite.premultiplied.png'; @@ -121,11 +119,11 @@ describe('composite', () => { }); }); - it('multiple', () => { + it('multiple', async () => { const filename = 'composite-multiple.png'; const actual = fixtures.path(`output.${filename}`); const expected = fixtures.expected(filename); - return sharp(redRect) + await sharp(redRect) .composite([{ input: blueRect, gravity: 'northeast' @@ -133,10 +131,8 @@ describe('composite', () => { input: greenRect, gravity: 'southwest' }]) - .toFile(actual) - .then(() => { - fixtures.assertMaxColourDistance(actual, expected); - }); + .toFile(actual); + fixtures.assertMaxColourDistance(actual, expected); }); it('zero offset', done => { diff --git a/test/unit/extend.js b/test/unit/extend.js index 9e49ce517..3d81e3120 100644 --- a/test/unit/extend.js +++ b/test/unit/extend.js @@ -140,7 +140,7 @@ describe('Extend', function () { }); it('Premultiply background when compositing', async () => { - const background = '#bf1942cc'; + const background = { r: 191, g: 25, b: 66, alpha: 0.8 }; const data = await sharp({ create: { width: 1, height: 1, channels: 4, background: '#fff0' @@ -158,10 +158,6 @@ describe('Extend', function () { }) .raw() .toBuffer(); - const [r1, g1, b1, a1, r2, g2, b2, a2] = data; - assert.strictEqual(true, Math.abs(r2 - r1) < 2); - assert.strictEqual(true, Math.abs(g2 - g1) < 2); - assert.strictEqual(true, Math.abs(b2 - b1) < 2); - assert.strictEqual(true, Math.abs(a2 - a1) < 2); + assert.deepStrictEqual(Array.from(data), [191, 25, 65, 204, 238, 31, 82, 204]); }); });