Ensure background is premultiplied when compositing #2858

This commit is contained in:
Lovell Fuller 2021-08-29 16:40:40 +01:00
parent 2031d7d112
commit 60adc110f5
5 changed files with 51 additions and 17 deletions

View File

@ -9,6 +9,9 @@ Requires libvips v8.11.3
* Ensure correct PNG bitdepth is set based on number of colours. * Ensure correct PNG bitdepth is set based on number of colours.
[#2855](https://github.com/lovell/sharp/issues/2855) [#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 ### v0.29.0 - 17th August 2021
* Drop support for Node.js 10, now requires Node.js >= 12.13.0. * Drop support for Node.js 10, now requires Node.js >= 12.13.0.

View File

@ -759,23 +759,27 @@ namespace sharp {
/* /*
Convert RGBA value to another colourspace Convert RGBA value to another colourspace
*/ */
std::vector<double> GetRgbaAsColourspace(std::vector<double> const rgba, VipsInterpretation const interpretation) { std::vector<double> GetRgbaAsColourspace(std::vector<double> const rgba,
VipsInterpretation const interpretation, bool premultiply) {
int const bands = static_cast<int>(rgba.size()); int const bands = static_cast<int>(rgba.size());
if (bands < 3 || interpretation == VIPS_INTERPRETATION_sRGB || interpretation == VIPS_INTERPRETATION_RGB) { if (bands < 3) {
return rgba; 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 Apply the alpha channel to a given colour
*/ */
std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour) { std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour, bool premultiply) {
// Scale up 8-bit values to match 16-bit input image // Scale up 8-bit values to match 16-bit input image
double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0; double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0;
// Create alphaColour colour // Create alphaColour colour
@ -799,7 +803,7 @@ namespace sharp {
alphaColour.push_back(colour[3] * multiplier); alphaColour.push_back(colour[3] * multiplier);
} }
// Ensure alphaColour colour uses correct colourspace // 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 // Add non-transparent alpha channel, if required
if (colour[3] < 255.0 && !HasAlpha(image)) { if (colour[3] < 255.0 && !HasAlpha(image)) {
image = image.bandjoin( image = image.bandjoin(

View File

@ -288,12 +288,13 @@ namespace sharp {
/* /*
Convert RGBA value to another colourspace Convert RGBA value to another colourspace
*/ */
std::vector<double> GetRgbaAsColourspace(std::vector<double> const rgba, VipsInterpretation const interpretation); std::vector<double> GetRgbaAsColourspace(std::vector<double> const rgba,
VipsInterpretation const interpretation, bool premultiply);
/* /*
Apply the alpha channel to a given colour Apply the alpha channel to a given colour
*/ */
std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour); std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour, bool premultiply);
/* /*
Removes alpha channel, if any. Removes alpha channel, if any.

View File

@ -90,7 +90,7 @@ class PipelineWorker : public Napi::AsyncWorker {
} }
if (baton->rotationAngle != 0.0) { if (baton->rotationAngle != 0.0) {
std::vector<double> background; std::vector<double> 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)); 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 (image.width() != baton->width || image.height() != baton->height) {
if (baton->canvas == Canvas::EMBED) { if (baton->canvas == Canvas::EMBED) {
std::vector<double> background; std::vector<double> background;
std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground); std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
// Embed // Embed
@ -480,7 +480,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Rotate post-extract non-90 angle // Rotate post-extract non-90 angle
if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) { if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
std::vector<double> background; std::vector<double> 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)); image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
} }
@ -493,7 +493,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Affine transform // Affine transform
if (baton->affineMatrix.size() > 0) { if (baton->affineMatrix.size() > 0) {
std::vector<double> background; std::vector<double> 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) image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
->set("idx", baton->affineIdx) ->set("idx", baton->affineIdx)
->set("idy", baton->affineIdy) ->set("idy", baton->affineIdy)
@ -505,7 +505,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Extend edges // Extend edges
if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) { if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
std::vector<double> background; std::vector<double> background;
std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground); std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
// Embed // Embed
baton->width = image.width() + baton->extendLeft + baton->extendRight; baton->width = image.width() + baton->extendLeft + baton->extendRight;

View File

@ -124,4 +124,30 @@ describe('Extend', function () {
fixtures.assertSimilar(fixtures.expected('extend-2channel.png'), data, done); 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);
});
}); });