Avoid (un)premultiplication for overlay image without alpha channel

Add 'premultiplied' boolean attribute to output info, helps test
This commit is contained in:
Lovell Fuller 2017-03-25 16:52:09 +00:00
parent 301bfbd271
commit 1169afbe90
11 changed files with 109 additions and 104 deletions

View File

@ -11,6 +11,8 @@ Overlay (composite) an image over the processed (resized, extracted etc.) image.
The overlay image must be the same size or smaller than the processed image. The overlay image must be the same size or smaller than the processed image.
If both `top` and `left` options are provided, they take precedence over `gravity`. If both `top` and `left` options are provided, they take precedence over `gravity`.
If the overlay image contains an alpha channel then composition with premultiplication will occur.
**Parameters** **Parameters**
- `overlay` **([Buffer](https://nodejs.org/api/buffer.html) \| [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String))** Buffer containing image data or String containing the path to an image file. - `overlay` **([Buffer](https://nodejs.org/api/buffer.html) \| [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String))** Buffer containing image data or String containing the path to an image file.

View File

@ -27,7 +27,8 @@ A Promises/A+ promise is returned when `callback` is not provided.
- `fileOut` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** the path to write the image data to. - `fileOut` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** the path to write the image data to.
- `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** called on completion with two arguments `(err, info)`. - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** called on completion with two arguments `(err, info)`.
`info` contains the output image `format`, `size` (bytes), `width`, `height` and `channels`. `info` contains the output image `format`, `size` (bytes), `width`, `height`,
`channels` and `premultiplied` (indicating if premultiplication was used).
- Throws **[Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)** Invalid parameters - Throws **[Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)** Invalid parameters
@ -44,7 +45,8 @@ By default, the format will match the input image, except GIF and SVG input whic
- `err` is an error, if any. - `err` is an error, if any.
- `data` is the output image data. - `data` is the output image data.
- `info` contains the output image `format`, `size` (bytes), `width`, `height` and `channels`. - `info` contains the output image `format`, `size` (bytes), `width`, `height`,
`channels` and `premultiplied` (indicating if premultiplication was used).
A Promise is returned when `callback` is not provided. A Promise is returned when `callback` is not provided.
**Parameters** **Parameters**

View File

@ -6,6 +6,10 @@ Requires libvips v8.5.2.
#### v0.18.0 - TBD #### v0.18.0 - TBD
* Avoid costly (un)premultiply when using overlayWith without alpha channel.
[#573](https://github.com/lovell/sharp/issues/573)
[@strarsis](https://github.com/strarsis)
* Expose warnings from libvips via NODE_DEBUG=sharp environment variable. * Expose warnings from libvips via NODE_DEBUG=sharp environment variable.
[#607](https://github.com/lovell/sharp/issues/607) [#607](https://github.com/lovell/sharp/issues/607)
[@puzrin](https://github.com/puzrin) [@puzrin](https://github.com/puzrin)

View File

@ -8,6 +8,8 @@ const is = require('./is');
* The overlay image must be the same size or smaller than the processed image. * The overlay image must be the same size or smaller than the processed image.
* If both `top` and `left` options are provided, they take precedence over `gravity`. * If both `top` and `left` options are provided, they take precedence over `gravity`.
* *
* If the overlay image contains an alpha channel then composition with premultiplication will occur.
*
* @example * @example
* sharp('input.png') * sharp('input.png')
* .rotate(180) * .rotate(180)

View File

@ -15,7 +15,8 @@ const sharp = require('../build/Release/sharp.node');
* *
* @param {String} fileOut - the path to write the image data to. * @param {String} fileOut - the path to write the image data to.
* @param {Function} [callback] - called on completion with two arguments `(err, info)`. * @param {Function} [callback] - called on completion with two arguments `(err, info)`.
* `info` contains the output image `format`, `size` (bytes), `width`, `height` and `channels`. * `info` contains the output image `format`, `size` (bytes), `width`, `height`,
* `channels` and `premultiplied` (indicating if premultiplication was used).
* @returns {Promise<Object>} - when no callback is provided * @returns {Promise<Object>} - when no callback is provided
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
@ -51,7 +52,8 @@ function toFile (fileOut, callback) {
* `callback`, if present, gets three arguments `(err, data, info)` where: * `callback`, if present, gets three arguments `(err, data, info)` where:
* - `err` is an error, if any. * - `err` is an error, if any.
* - `data` is the output image data. * - `data` is the output image data.
* - `info` contains the output image `format`, `size` (bytes), `width`, `height` and `channels`. * - `info` contains the output image `format`, `size` (bytes), `width`, `height`,
* `channels` and `premultiplied` (indicating if premultiplication was used).
* A Promise is returned when `callback` is not provided. * A Promise is returned when `callback` is not provided.
* *
* @param {Object} [options] * @param {Object} [options]

View File

@ -29,67 +29,32 @@ using vips::VError;
namespace sharp { namespace sharp {
/* /*
Alpha composite src over dst with given gravity. Composite overlayImage over image at given position
Assumes alpha channels are already premultiplied and will be unpremultiplied after. Assumes alpha channels are already premultiplied and will be unpremultiplied after
*/ */
VImage Composite(VImage src, VImage dst, const int gravity) { VImage Composite(VImage image, VImage overlayImage, int const left, int const top) {
if (IsInputValidForComposition(src, dst)) { if (HasAlpha(overlayImage)) {
// Enlarge overlay src, if required // Alpha composite
if (src.width() < dst.width() || src.height() < dst.height()) { if (overlayImage.width() < image.width() || overlayImage.height() < image.height()) {
// Calculate the (left, top) coordinates of the output image within the input image, applying the given gravity. // Enlarge overlay
int left; std::vector<double> const background { 0.0, 0.0, 0.0, 0.0 };
int top; overlayImage = overlayImage.embed(left, top, image.width(), image.height(), VImage::option()
std::tie(left, top) = CalculateCrop(dst.width(), dst.height(), src.width(), src.height(), gravity);
// Embed onto transparent background
std::vector<double> background { 0.0, 0.0, 0.0, 0.0 };
src = src.embed(left, top, dst.width(), dst.height(), VImage::option()
->set("extend", VIPS_EXTEND_BACKGROUND) ->set("extend", VIPS_EXTEND_BACKGROUND)
->set("background", background)); ->set("background", background));
} }
return CompositeImage(src, dst); return AlphaComposite(image, overlayImage);
} } else {
// If the input was not valid for composition the return the input image itself if (HasAlpha(image)) {
return dst; // Add alpha channel to overlayImage so channels match
} double const multiplier = sharp::Is16Bit(overlayImage.interpretation()) ? 256.0 : 1.0;
overlayImage = overlayImage.bandjoin(
VImage Composite(VImage src, VImage dst, const int x, const int y) { VImage::new_matrix(overlayImage.width(), overlayImage.height()).new_from_image(255 * multiplier));
if (IsInputValidForComposition(src, dst)) {
// Enlarge overlay src, if required
if (src.width() < dst.width() || src.height() < dst.height()) {
// Calculate the (left, top) coordinates of the output image within the input image, applying the given gravity.
int left;
int top;
std::tie(left, top) = CalculateCrop(dst.width(), dst.height(), src.width(), src.height(), x, y);
// Embed onto transparent background
std::vector<double> background { 0.0, 0.0, 0.0, 0.0 };
src = src.embed(left, top, dst.width(), dst.height(), VImage::option()
->set("extend", VIPS_EXTEND_BACKGROUND)
->set("background", background));
} }
return CompositeImage(src, dst); return image.insert(overlayImage, left, top);
} }
// If the input was not valid for composition the return the input image itself
return dst;
} }
bool IsInputValidForComposition(VImage src, VImage dst) { VImage AlphaComposite(VImage dst, VImage src) {
using sharp::CalculateCrop;
using sharp::HasAlpha;
if (!HasAlpha(src)) {
throw VError("Overlay image must have an alpha channel");
}
if (!HasAlpha(dst)) {
throw VError("Image to be overlaid must have an alpha channel");
}
if (src.width() > dst.width() || src.height() > dst.height()) {
throw VError("Overlay image must have same dimensions or smaller");
}
return true;
}
VImage CompositeImage(VImage src, VImage dst) {
// Split src into non-alpha and alpha channels // Split src into non-alpha and alpha channels
VImage srcWithoutAlpha = src.extract_band(0, VImage::option()->set("n", src.bands() - 1)); VImage srcWithoutAlpha = src.extract_band(0, VImage::option()->set("n", src.bands() - 1));
VImage srcAlpha = src[src.bands() - 1] * (1.0 / 255.0); VImage srcAlpha = src[src.bands() - 1] * (1.0 / 255.0);

View File

@ -32,20 +32,14 @@ namespace sharp {
VImage Composite(VImage src, VImage dst, const int gravity); VImage Composite(VImage src, VImage dst, const int gravity);
/* /*
Alpha composite src over dst with given x and y offsets. Composite overlayImage over image at given position
Assumes alpha channels are already premultiplied and will be unpremultiplied after.
*/ */
VImage Composite(VImage src, VImage dst, const int x, const int y); VImage Composite(VImage image, VImage overlayImage, int const x, int const y);
/* /*
Check if the src and dst Images for composition operation are valid Alpha composite overlayImage over image, assumes matching dimensions
*/ */
bool IsInputValidForComposition(VImage src, VImage dst); VImage AlphaComposite(VImage image, VImage overlayImage);
/*
Given a valid src and dst, returns the composite of the two images
*/
VImage CompositeImage(VImage src, VImage dst);
/* /*
Cutout src over dst with given gravity. Cutout src over dst with given gravity.

View File

@ -333,12 +333,20 @@ class PipelineWorker : public Nan::AsyncWorker {
image = image.colourspace(VIPS_INTERPRETATION_B_W); image = image.colourspace(VIPS_INTERPRETATION_B_W);
} }
// Ensure image has an alpha channel when there is an overlay // Ensure image has an alpha channel when there is an overlay with an alpha channel
bool hasOverlay = baton->overlay != nullptr; VImage overlayImage;
if (hasOverlay && !HasAlpha(image)) { ImageType overlayImageType = ImageType::UNKNOWN;
double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0; bool shouldOverlayWithAlpha = FALSE;
image = image.bandjoin( if (baton->overlay != nullptr) {
VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier)); std::tie(overlayImage, overlayImageType) = OpenInput(baton->overlay, baton->accessMethod);
if (HasAlpha(overlayImage)) {
shouldOverlayWithAlpha = !baton->overlayCutout;
if (!HasAlpha(image)) {
double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0;
image = image.bandjoin(
VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier));
}
}
} }
bool const shouldShrink = xshrink > 1 || yshrink > 1; bool const shouldShrink = xshrink > 1 || yshrink > 1;
@ -346,9 +354,8 @@ class PipelineWorker : public Nan::AsyncWorker {
bool const shouldBlur = baton->blurSigma != 0.0; bool const shouldBlur = baton->blurSigma != 0.0;
bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0; bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
bool const shouldSharpen = baton->sharpenSigma != 0.0; bool const shouldSharpen = baton->sharpenSigma != 0.0;
bool const shouldCutout = baton->overlayCutout;
bool const shouldPremultiplyAlpha = HasAlpha(image) && bool const shouldPremultiplyAlpha = HasAlpha(image) &&
(shouldShrink || shouldReduce || shouldBlur || shouldConv || shouldSharpen || (hasOverlay && !shouldCutout)); (shouldShrink || shouldReduce || shouldBlur || shouldConv || shouldSharpen || shouldOverlayWithAlpha);
// Premultiply image alpha channel before all transformations to avoid // Premultiply image alpha channel before all transformations to avoid
// dark fringing around bright pixels // dark fringing around bright pixels
@ -584,10 +591,11 @@ class PipelineWorker : public Nan::AsyncWorker {
} }
// Composite with overlay, if present // Composite with overlay, if present
if (hasOverlay) { if (baton->overlay != nullptr) {
VImage overlayImage; // Verify overlay image is within current dimensions
ImageType overlayImageType = ImageType::UNKNOWN; if (overlayImage.width() > image.width() || overlayImage.height() > image.height()) {
std::tie(overlayImage, overlayImageType) = OpenInput(baton->overlay, baton->accessMethod); throw vips::VError("Overlay image must have same dimensions or smaller");
}
// Check if overlay is tiled // Check if overlay is tiled
if (baton->overlayTile) { if (baton->overlayTile) {
int const overlayImageWidth = overlayImage.width(); int const overlayImageWidth = overlayImage.width();
@ -620,31 +628,34 @@ class PipelineWorker : public Nan::AsyncWorker {
// the overlayGravity was used for extract_area, therefore set it back to its default value of 0 // the overlayGravity was used for extract_area, therefore set it back to its default value of 0
baton->overlayGravity = 0; baton->overlayGravity = 0;
} }
if (shouldCutout) { if (baton->overlayCutout) {
// 'cut out' the image, premultiplication is not required // 'cut out' the image, premultiplication is not required
image = sharp::Cutout(overlayImage, image, baton->overlayGravity); image = sharp::Cutout(overlayImage, image, baton->overlayGravity);
} else { } else {
// Ensure overlay has alpha channel // Ensure overlay is sRGB
if (!HasAlpha(overlayImage)) { overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB);
double const multiplier = sharp::Is16Bit(overlayImage.interpretation()) ? 256.0 : 1.0; // Ensure overlay matches premultiplication state
overlayImage = overlayImage.bandjoin( if (shouldPremultiplyAlpha) {
VImage::new_matrix(overlayImage.width(), overlayImage.height()).new_from_image(255 * multiplier)); // Ensure overlay has alpha channel
if (!HasAlpha(overlayImage)) {
double const multiplier = sharp::Is16Bit(overlayImage.interpretation()) ? 256.0 : 1.0;
overlayImage = overlayImage.bandjoin(
VImage::new_matrix(overlayImage.width(), overlayImage.height()).new_from_image(255 * multiplier));
}
overlayImage = overlayImage.premultiply();
} }
// Ensure image has alpha channel int left;
if (!HasAlpha(image)) { int top;
double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0;
image = image.bandjoin(
VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier));
}
// Ensure overlay is premultiplied sRGB
overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply();
if (baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) { if (baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) {
// Composite images with given offsets // Composite images at given offsets
image = sharp::Composite(overlayImage, image, baton->overlayXOffset, baton->overlayYOffset); std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
overlayImage.width(), overlayImage.height(), baton->overlayXOffset, baton->overlayYOffset);
} else { } else {
// Composite images with given gravity // Composite images with given gravity
image = sharp::Composite(overlayImage, image, baton->overlayGravity); std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
overlayImage.width(), overlayImage.height(), baton->overlayGravity);
} }
image = sharp::Composite(image, overlayImage, left, top);
} }
} }
@ -658,6 +669,7 @@ class PipelineWorker : public Nan::AsyncWorker {
image = image.cast(VIPS_FORMAT_UCHAR); image = image.cast(VIPS_FORMAT_UCHAR);
} }
} }
baton->premultiplied = shouldPremultiplyAlpha;
// Gamma decoding (brighten) // Gamma decoding (brighten)
if (baton->gamma >= 1 && baton->gamma <= 3) { if (baton->gamma >= 1 && baton->gamma <= 3) {
@ -942,6 +954,7 @@ class PipelineWorker : public Nan::AsyncWorker {
Set(info, New("width").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(width))); Set(info, New("width").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(width)));
Set(info, New("height").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(height))); Set(info, New("height").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(height)));
Set(info, New("channels").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(baton->channels))); Set(info, New("channels").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(baton->channels)));
Set(info, New("premultiplied").ToLocalChecked(), New<v8::Boolean>(baton->premultiplied));
if (baton->cropCalcLeft != -1 && baton->cropCalcLeft != -1) { if (baton->cropCalcLeft != -1 && baton->cropCalcLeft != -1) {
Set(info, New("cropCalcLeft").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(baton->cropCalcLeft))); Set(info, New("cropCalcLeft").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(baton->cropCalcLeft)));
Set(info, New("cropCalcTop").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(baton->cropCalcTop))); Set(info, New("cropCalcTop").ToLocalChecked(), New<v8::Uint32>(static_cast<uint32_t>(baton->cropCalcTop)));

View File

@ -64,6 +64,7 @@ struct PipelineBaton {
int crop; int crop;
int cropCalcLeft; int cropCalcLeft;
int cropCalcTop; int cropCalcTop;
bool premultiplied;
std::string kernel; std::string kernel;
std::string interpolator; std::string interpolator;
bool centreSampling; bool centreSampling;
@ -143,6 +144,7 @@ struct PipelineBaton {
crop(0), crop(0),
cropCalcLeft(-1), cropCalcLeft(-1),
cropCalcTop(-1), cropCalcTop(-1),
premultiplied(false),
centreSampling(false), centreSampling(false),
flatten(false), flatten(false),
negate(false), negate(false),

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -155,20 +155,22 @@ describe('Overlays', function () {
}); });
} }
it('Composite JPEG onto PNG', function (done) { it('Composite JPEG onto PNG, no premultiply', function (done) {
sharp(fixtures.inputPngOverlayLayer1) sharp(fixtures.inputPngOverlayLayer1)
.overlayWith(fixtures.inputJpgWithLandscapeExif1) .overlayWith(fixtures.inputJpgWithLandscapeExif1)
.toBuffer(function (error) { .toBuffer(function (err, data, info) {
if (error) return done(error); if (err) throw err;
assert.strictEqual(false, info.premultiplied);
done(); done();
}); });
}); });
it('Composite opaque JPEG onto JPEG', function (done) { it('Composite opaque JPEG onto JPEG, no premultiply', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.overlayWith(fixtures.inputJpgWithLandscapeExif1) .overlayWith(fixtures.inputJpgWithLandscapeExif1)
.toBuffer(function (error) { .toBuffer(function (err, data, info) {
if (error) return done(error); if (err) throw err;
assert.strictEqual(false, info.premultiplied);
done(); done();
}); });
}); });
@ -561,14 +563,15 @@ describe('Overlays', function () {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(2048, 1536) .resize(2048, 1536)
.overlayWith(data, { raw: info }) .overlayWith(data, { raw: info })
.toBuffer(function (err, data) { .toBuffer(function (err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, info.premultiplied);
fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-rgb.jpg'), data, done); fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-rgb.jpg'), data, done);
}); });
}); });
}); });
it('Throws an error when called with an invalid file', function (done) { it('Returns an error when called with an invalid file', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.overlayWith('notfound.png') .overlayWith('notfound.png')
.toBuffer(function (err) { .toBuffer(function (err) {
@ -576,4 +579,20 @@ describe('Overlays', function () {
done(); done();
}); });
}); });
it('Composite JPEG onto JPEG, no premultiply', function (done) {
sharp(fixtures.inputJpg)
.resize(480, 320)
.overlayWith(fixtures.inputJpgBooleanTest)
.png()
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(480, info.width);
assert.strictEqual(320, info.height);
assert.strictEqual(3, info.channels);
assert.strictEqual(false, info.premultiplied);
fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-jpeg.jpg'), data, done);
});
});
}); });