From c62002554b5938c43c14fefafad7353ae4d720cf Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Wed, 16 Feb 2022 18:56:35 +0000 Subject: [PATCH] Improve performance and accuracy of multi-image composite #2286 --- docs/changelog.md | 5 ++++ lib/composite.js | 1 - src/pipeline.cc | 16 ++++++----- test/fixtures/expected/composite-multiple.png | Bin 222 -> 320 bytes .../expected/composite.blend.dest-over.png | Bin 197 -> 291 bytes .../expected/composite.blend.over.png | Bin 197 -> 292 bytes .../expected/composite.blend.saturate.png | Bin 194 -> 288 bytes .../fixtures/expected/composite.blend.xor.png | Bin 192 -> 286 bytes test/unit/composite.js | 26 ++++++++---------- test/unit/extend.js | 8 ++---- 10 files changed, 27 insertions(+), 29 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index f4ad6557..6bdfb3c4 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 f3b736a6..38d17793 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 a426dfbb..b2c2b3a7 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 d72d1eda322ae7121f5c233485d8f7064674ebb8..2aef0fbdee9faa6bb1fd6002bb265361800e4461 100644 GIT binary patch delta 272 zcmV+r0q_3a0l)%~Ie+p=L_t(|ob8)Y4MQ;u0}sT&1ji5+O=%d)lS2{Pc1_9SZHsrJtXd3AI_fzhUf_01@EOfy)t&Z{Pt7 zqg)~gu-kEEj6s0it}BBy0z5_c0q2^h90DBOPP0rvfTP=O)>#N}oF)rZ1UNb!_yJla W9Ple{DG~qx00{s|MNUMnLSTZfv2dFJ literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^0YGfS!3HEhs@wJeDb50q$YKTt<`*E$xV1Vc8z?xz z)5S5Q;?~=nhJprY+p%qgLlJpwsloqYCQ~>x1DrnVO*9w z#h8up*^*)pT{i?7~Pm(F8?vB#g=8q-<%V#y$ljr pETlZ_U4`8mAolv*UJ;*uhIfOuOGwW}wmb$P@O1TaS?83{1OVc0UN`^% delta 148 zcmZ3?bd+&|WqrA)i(^Q|t+y8(MHvJHSP#etDO64o=;iyp!=PQ$AkuTWo)8O{h=xET z6D!w~qz4|e?FBUg7C3;oBBU^COupqp&N#a@QRIgMX}O%j1(zII9+2Dyvp z7;+e>&vlk!bYp(GJmU~hXm*W@K*B9I%VdEBKdW{VrXIFS{vS;_t2qjO-<)uLYQiCo q0+R`KU1~iH%fa@}*%r9}+zjc1Zc&#OR!-W(00f?{elF{r5}E+n>szY; delta 148 zcmZ3&bd+&|WqrA)i(^Q|t+y8(MHvJHSP#etDO64o=;iyp!=PQ$AkuTWo)8O{h=xET z6D!w~q=eHm>;*Lf7C3;oBBU^COu(uG*f{| zhBxKCkU=7gh13alp?wxVFS$0jH%w=nSJkD~!*F@qNp}{;Ww}#~*%;FZmZMD*uv)JX(CgEYFdCs-Tp^=GIDPVy^ zBM)0x*nbxwgNc<(L<1y3G~>fn2Ns|n0gw)t2d2_$Zn2An4?j<5VBlh4@O1TaS?83{ F1OUmdDenLP diff --git a/test/fixtures/expected/composite.blend.xor.png b/test/fixtures/expected/composite.blend.xor.png index 01160d9530b37353d84897a632921881287d80d5..755440458d809c50bd8acea0983146be516dbd0e 100644 GIT binary patch literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^0YGfS!3HEhs@wJeDb50q$YKTt<`*E$xV1Vc8z^|e z)5S5QV$R#y8+n->c@FQ`xo4-b^CYKy8*Y7v2^OZu4^{j*!|?FqbO$DvhRbU_m<1nX zD3nic;8Z?v(IIZahYgjN*D{1REN+bUzt|#}aLepxhr)p^y+y|q4#Zfsn=tjTU81JM zM?=nPj)K2ACmwqlB(hjYdDy!OyERFVdQ I&MBb@0IctB$N&HU literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^0YGfS!3HEhs@wJeDb50q$YKTt<`*E$xV1Vc8z@-h z>EaktaqI1cje-mU3@jV&|H(ba*ViTzsW9_oLc9uxh(>?{6Dya { - 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 9e49ce51..3d81e312 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]); }); });