diff --git a/docs/changelog.md b/docs/changelog.md index cf448cae..6f31f21b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,9 @@ Requires libvips v8.11.3 * Ensure correct PNG bitdepth is set based on number of colours. [#2855](https://github.com/lovell/sharp/issues/2855) +* Ensure background is always premultiplied when compositing. + [#2858](https://github.com/lovell/sharp/issues/2858) + ### v0.29.0 - 17th August 2021 * Drop support for Node.js 10, now requires Node.js >= 12.13.0. diff --git a/src/common.cc b/src/common.cc index 73ff17fe..da79995e 100644 --- a/src/common.cc +++ b/src/common.cc @@ -759,23 +759,27 @@ namespace sharp { /* Convert RGBA value to another colourspace */ - std::vector GetRgbaAsColourspace(std::vector const rgba, VipsInterpretation const interpretation) { + std::vector GetRgbaAsColourspace(std::vector const rgba, + VipsInterpretation const interpretation, bool premultiply) { int const bands = static_cast(rgba.size()); - if (bands < 3 || interpretation == VIPS_INTERPRETATION_sRGB || interpretation == VIPS_INTERPRETATION_RGB) { + if (bands < 3) { return rgba; - } else { - VImage pixel = VImage::new_matrix(1, 1); - pixel.set("bands", bands); - pixel = pixel.new_from_image(rgba); - pixel = pixel.colourspace(interpretation, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB)); - return pixel(0, 0); } + VImage pixel = VImage::new_matrix(1, 1); + pixel.set("bands", bands); + pixel = pixel + .new_from_image(rgba) + .colourspace(interpretation, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB)); + if (premultiply) { + pixel = pixel.premultiply(); + } + return pixel(0, 0); } /* Apply the alpha channel to a given colour */ - std::tuple> ApplyAlpha(VImage image, std::vector colour) { + std::tuple> ApplyAlpha(VImage image, std::vector colour, bool premultiply) { // Scale up 8-bit values to match 16-bit input image double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0; // Create alphaColour colour @@ -799,7 +803,7 @@ namespace sharp { alphaColour.push_back(colour[3] * multiplier); } // Ensure alphaColour colour uses correct colourspace - alphaColour = sharp::GetRgbaAsColourspace(alphaColour, image.interpretation()); + alphaColour = sharp::GetRgbaAsColourspace(alphaColour, image.interpretation(), premultiply); // Add non-transparent alpha channel, if required if (colour[3] < 255.0 && !HasAlpha(image)) { image = image.bandjoin( diff --git a/src/common.h b/src/common.h index 03ecf67e..a59bcee4 100644 --- a/src/common.h +++ b/src/common.h @@ -288,12 +288,13 @@ namespace sharp { /* Convert RGBA value to another colourspace */ - std::vector GetRgbaAsColourspace(std::vector const rgba, VipsInterpretation const interpretation); + std::vector GetRgbaAsColourspace(std::vector const rgba, + VipsInterpretation const interpretation, bool premultiply); /* Apply the alpha channel to a given colour */ - std::tuple> ApplyAlpha(VImage image, std::vector colour); + std::tuple> ApplyAlpha(VImage image, std::vector colour, bool premultiply); /* Removes alpha channel, if any. diff --git a/src/pipeline.cc b/src/pipeline.cc index e11de97f..7a154d76 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -90,7 +90,7 @@ class PipelineWorker : public Napi::AsyncWorker { } if (baton->rotationAngle != 0.0) { std::vector background; - std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground); + std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, FALSE); image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background)); } } @@ -423,7 +423,7 @@ class PipelineWorker : public Napi::AsyncWorker { if (image.width() != baton->width || image.height() != baton->height) { if (baton->canvas == Canvas::EMBED) { std::vector background; - std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground); + std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha); // Embed @@ -480,7 +480,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Rotate post-extract non-90 angle if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) { std::vector background; - std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground); + std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha); image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background)); } @@ -493,7 +493,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Affine transform if (baton->affineMatrix.size() > 0) { std::vector background; - std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground); + std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha); image = image.affine(baton->affineMatrix, VImage::option()->set("background", background) ->set("idx", baton->affineIdx) ->set("idy", baton->affineIdy) @@ -505,7 +505,7 @@ class PipelineWorker : public Napi::AsyncWorker { // Extend edges if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) { std::vector background; - std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground); + std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha); // Embed baton->width = image.width() + baton->extendLeft + baton->extendRight; diff --git a/test/unit/extend.js b/test/unit/extend.js index 8ab6d907..ea491e12 100644 --- a/test/unit/extend.js +++ b/test/unit/extend.js @@ -124,4 +124,30 @@ describe('Extend', function () { fixtures.assertSimilar(fixtures.expected('extend-2channel.png'), data, done); }); }); + + it('Premultiply background when compositing', async () => { + const background = '#bf1942cc'; + const data = await sharp({ + create: { + width: 1, height: 1, channels: 4, background: '#fff0' + } + }) + .composite([{ + input: { + create: { + width: 1, height: 1, channels: 4, background + } + } + }]) + .extend({ + left: 1, background + }) + .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); + }); });