From 1169afbe90b645e4772c583ed1d6070d935d3ecb Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Sat, 25 Mar 2017 16:52:09 +0000 Subject: [PATCH] Avoid (un)premultiplication for overlay image without alpha channel Add 'premultiplied' boolean attribute to output info, helps test --- docs/api-composite.md | 2 + docs/api-output.md | 6 +- docs/changelog.md | 4 + lib/composite.js | 2 + lib/output.js | 6 +- src/operations.cc | 71 +++++------------- src/operations.h | 14 +--- src/pipeline.cc | 71 +++++++++++------- src/pipeline.h | 2 + .../expected/overlay-jpeg-with-jpeg.jpg | Bin 0 -> 17404 bytes test/unit/overlay.js | 35 +++++++-- 11 files changed, 109 insertions(+), 104 deletions(-) create mode 100644 test/fixtures/expected/overlay-jpeg-with-jpeg.jpg diff --git a/docs/api-composite.md b/docs/api-composite.md index f5caccc8..a4c84619 100644 --- a/docs/api-composite.md +++ b/docs/api-composite.md @@ -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. 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** - `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. diff --git a/docs/api-output.md b/docs/api-output.md index 2eb6feba..39534f54 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -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. - `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 @@ -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. - `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. **Parameters** diff --git a/docs/changelog.md b/docs/changelog.md index c35218b9..842045c7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,10 @@ Requires libvips v8.5.2. #### 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. [#607](https://github.com/lovell/sharp/issues/607) [@puzrin](https://github.com/puzrin) diff --git a/lib/composite.js b/lib/composite.js index 6d2353e1..e795ab0d 100644 --- a/lib/composite.js +++ b/lib/composite.js @@ -8,6 +8,8 @@ const is = require('./is'); * 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 the overlay image contains an alpha channel then composition with premultiplication will occur. + * * @example * sharp('input.png') * .rotate(180) diff --git a/lib/output.js b/lib/output.js index f6b8afbe..a3d375ba 100644 --- a/lib/output.js +++ b/lib/output.js @@ -15,7 +15,8 @@ const sharp = require('../build/Release/sharp.node'); * * @param {String} fileOut - the path to write the image data to. * @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} - when no callback is provided * @throws {Error} Invalid parameters */ @@ -51,7 +52,8 @@ function toFile (fileOut, callback) { * `callback`, if present, gets three arguments `(err, data, info)` where: * - `err` is an error, if any. * - `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. * * @param {Object} [options] diff --git a/src/operations.cc b/src/operations.cc index 6e4bebae..1cfdb02b 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -29,67 +29,32 @@ using vips::VError; namespace sharp { /* - Alpha composite src over dst with given gravity. - Assumes alpha channels are already premultiplied and will be unpremultiplied after. + 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 gravity) { - 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(), gravity); - // Embed onto transparent background - std::vector background { 0.0, 0.0, 0.0, 0.0 }; - src = src.embed(left, top, dst.width(), dst.height(), VImage::option() + VImage Composite(VImage image, VImage overlayImage, int const left, int const top) { + if (HasAlpha(overlayImage)) { + // Alpha composite + if (overlayImage.width() < image.width() || overlayImage.height() < image.height()) { + // Enlarge overlay + std::vector const background { 0.0, 0.0, 0.0, 0.0 }; + overlayImage = overlayImage.embed(left, top, image.width(), image.height(), VImage::option() ->set("extend", VIPS_EXTEND_BACKGROUND) ->set("background", background)); } - return CompositeImage(src, dst); - } - // If the input was not valid for composition the return the input image itself - return dst; - } - - VImage Composite(VImage src, VImage dst, const int x, const int y) { - 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 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 AlphaComposite(image, overlayImage); + } else { + if (HasAlpha(image)) { + // Add alpha channel to overlayImage so channels match + 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)); } - 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) { - 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) { + VImage AlphaComposite(VImage dst, VImage src) { // Split src into non-alpha and alpha channels VImage srcWithoutAlpha = src.extract_band(0, VImage::option()->set("n", src.bands() - 1)); VImage srcAlpha = src[src.bands() - 1] * (1.0 / 255.0); diff --git a/src/operations.h b/src/operations.h index e079e995..66836e77 100644 --- a/src/operations.h +++ b/src/operations.h @@ -32,20 +32,14 @@ namespace sharp { VImage Composite(VImage src, VImage dst, const int gravity); /* - Alpha composite src over dst with given x and y offsets. - Assumes alpha channels are already premultiplied and will be unpremultiplied after. + Composite overlayImage over image at given position */ - 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); - - /* - Given a valid src and dst, returns the composite of the two images - */ - VImage CompositeImage(VImage src, VImage dst); + VImage AlphaComposite(VImage image, VImage overlayImage); /* Cutout src over dst with given gravity. diff --git a/src/pipeline.cc b/src/pipeline.cc index fe16d537..3224bdd1 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -333,12 +333,20 @@ class PipelineWorker : public Nan::AsyncWorker { image = image.colourspace(VIPS_INTERPRETATION_B_W); } - // Ensure image has an alpha channel when there is an overlay - bool hasOverlay = baton->overlay != nullptr; - if (hasOverlay && !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)); + // Ensure image has an alpha channel when there is an overlay with an alpha channel + VImage overlayImage; + ImageType overlayImageType = ImageType::UNKNOWN; + bool shouldOverlayWithAlpha = FALSE; + if (baton->overlay != nullptr) { + 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; @@ -346,9 +354,8 @@ class PipelineWorker : public Nan::AsyncWorker { bool const shouldBlur = baton->blurSigma != 0.0; bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0; bool const shouldSharpen = baton->sharpenSigma != 0.0; - bool const shouldCutout = baton->overlayCutout; 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 // dark fringing around bright pixels @@ -584,10 +591,11 @@ class PipelineWorker : public Nan::AsyncWorker { } // Composite with overlay, if present - if (hasOverlay) { - VImage overlayImage; - ImageType overlayImageType = ImageType::UNKNOWN; - std::tie(overlayImage, overlayImageType) = OpenInput(baton->overlay, baton->accessMethod); + if (baton->overlay != nullptr) { + // Verify overlay image is within current dimensions + if (overlayImage.width() > image.width() || overlayImage.height() > image.height()) { + throw vips::VError("Overlay image must have same dimensions or smaller"); + } // Check if overlay is tiled if (baton->overlayTile) { 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 baton->overlayGravity = 0; } - if (shouldCutout) { + if (baton->overlayCutout) { // 'cut out' the image, premultiplication is not required image = sharp::Cutout(overlayImage, image, baton->overlayGravity); } else { - // 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)); + // Ensure overlay is sRGB + overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB); + // Ensure overlay matches premultiplication state + if (shouldPremultiplyAlpha) { + // 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 - 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)); - } - // Ensure overlay is premultiplied sRGB - overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply(); + int left; + int top; if (baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) { - // Composite images with given offsets - image = sharp::Composite(overlayImage, image, baton->overlayXOffset, baton->overlayYOffset); + // Composite images at given offsets + std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(), + overlayImage.width(), overlayImage.height(), baton->overlayXOffset, baton->overlayYOffset); } else { // 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); } } + baton->premultiplied = shouldPremultiplyAlpha; // Gamma decoding (brighten) if (baton->gamma >= 1 && baton->gamma <= 3) { @@ -942,6 +954,7 @@ class PipelineWorker : public Nan::AsyncWorker { Set(info, New("width").ToLocalChecked(), New(static_cast(width))); Set(info, New("height").ToLocalChecked(), New(static_cast(height))); Set(info, New("channels").ToLocalChecked(), New(static_cast(baton->channels))); + Set(info, New("premultiplied").ToLocalChecked(), New(baton->premultiplied)); if (baton->cropCalcLeft != -1 && baton->cropCalcLeft != -1) { Set(info, New("cropCalcLeft").ToLocalChecked(), New(static_cast(baton->cropCalcLeft))); Set(info, New("cropCalcTop").ToLocalChecked(), New(static_cast(baton->cropCalcTop))); diff --git a/src/pipeline.h b/src/pipeline.h index a7469395..da3be3d9 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -64,6 +64,7 @@ struct PipelineBaton { int crop; int cropCalcLeft; int cropCalcTop; + bool premultiplied; std::string kernel; std::string interpolator; bool centreSampling; @@ -143,6 +144,7 @@ struct PipelineBaton { crop(0), cropCalcLeft(-1), cropCalcTop(-1), + premultiplied(false), centreSampling(false), flatten(false), negate(false), diff --git a/test/fixtures/expected/overlay-jpeg-with-jpeg.jpg b/test/fixtures/expected/overlay-jpeg-with-jpeg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..718552e04f85e3e9f84bfbcae66ac6fc3d177e77 GIT binary patch literal 17404 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 literal 0 HcmV?d00001 diff --git a/test/unit/overlay.js b/test/unit/overlay.js index 9e33c3d7..7e60c232 100644 --- a/test/unit/overlay.js +++ b/test/unit/overlay.js @@ -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) .overlayWith(fixtures.inputJpgWithLandscapeExif1) - .toBuffer(function (error) { - if (error) return done(error); + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(false, info.premultiplied); done(); }); }); - it('Composite opaque JPEG onto JPEG', function (done) { + it('Composite opaque JPEG onto JPEG, no premultiply', function (done) { sharp(fixtures.inputJpg) .overlayWith(fixtures.inputJpgWithLandscapeExif1) - .toBuffer(function (error) { - if (error) return done(error); + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(false, info.premultiplied); done(); }); }); @@ -561,14 +563,15 @@ describe('Overlays', function () { sharp(fixtures.inputJpg) .resize(2048, 1536) .overlayWith(data, { raw: info }) - .toBuffer(function (err, data) { + .toBuffer(function (err, data, info) { if (err) throw err; + assert.strictEqual(true, info.premultiplied); 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) .overlayWith('notfound.png') .toBuffer(function (err) { @@ -576,4 +579,20 @@ describe('Overlays', function () { 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); + }); + }); });