diff --git a/README.md b/README.md index ac3a0414..a9c4b766 100755 --- a/README.md +++ b/README.md @@ -417,10 +417,6 @@ If the background contains an alpha value then WebP and PNG format output images Merge alpha transparency channel, if any, with `background`. -#### Experimental: overlayWith(filename) - -**Experimental:** Composite image with transparent overlay. Both input and overlay image must be RGBA and their dimensions must match. - #### rotate([angle]) Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag. @@ -498,6 +494,14 @@ The output image will still be web-friendly sRGB and contain three (identical) c Enhance output image contrast by stretching its luminance to cover the full dynamic range. This typically reduces performance by 30%. +#### overlayWith(filename) + +_Experimental_ + +Alpha composite `filename` over the processed (resized, extracted) image. The dimensions of the two images must match. + +* `filename` is a String containing the filename of an image with an alpha channel. + ### Output options #### jpeg() diff --git a/src/common.h b/src/common.h index f27e36df..33f63a32 100755 --- a/src/common.h +++ b/src/common.h @@ -1,6 +1,8 @@ #ifndef SRC_COMMON_H_ #define SRC_COMMON_H_ +#include + namespace sharp { enum class ImageType { diff --git a/src/operations.cc b/src/operations.cc index 808c30f3..9338ff9a 100755 --- a/src/operations.cc +++ b/src/operations.cc @@ -1,38 +1,55 @@ #include +#include "common.h" #include "operations.h" namespace sharp { /* - Composite images `src` and `dst` with premultiplied alpha channel and output - image with premultiplied alpha. + Alpha composite src over dst + Assumes alpha channels are already premultiplied and will be unpremultiplied after */ - int Composite(VipsObject *context, VipsImage *srcPremultiplied, VipsImage *dstPremultiplied, VipsImage **outPremultiplied) { - if (srcPremultiplied->Bands != 4 || dstPremultiplied->Bands != 4) - return -1; + int Composite(VipsObject *context, VipsImage *src, VipsImage *dst, VipsImage **out) { + using sharp::HasAlpha; - // Extract RGB bands: - VipsImage *srcRGBPremultiplied; - if (vips_extract_band(srcPremultiplied, &srcRGBPremultiplied, 0, "n", srcPremultiplied->Bands - 1, NULL)) + // Split src into non-alpha and alpha + VipsImage *srcWithoutAlpha; + if (vips_extract_band(src, &srcWithoutAlpha, 0, "n", src->Bands - 1, NULL)) return -1; - vips_object_local(context, srcRGBPremultiplied); - - VipsImage *dstRGBPremultiplied; - if (vips_extract_band(dstPremultiplied, &dstRGBPremultiplied, 0, "n", dstPremultiplied->Bands - 1, NULL)) - return -1; - vips_object_local(context, dstRGBPremultiplied); - - // Extract alpha bands: + vips_object_local(context, srcWithoutAlpha); VipsImage *srcAlpha; - if (vips_extract_band(srcPremultiplied, &srcAlpha, srcPremultiplied->Bands - 1, "n", 1, NULL)) + if (vips_extract_band(src, &srcAlpha, src->Bands - 1, "n", 1, NULL)) return -1; vips_object_local(context, srcAlpha); + // Split dst into non-alpha and alpha channels + VipsImage *dstWithoutAlpha; VipsImage *dstAlpha; - if (vips_extract_band(dstPremultiplied, &dstAlpha, dstPremultiplied->Bands - 1, "n", 1, NULL)) - return -1; - vips_object_local(context, dstAlpha); + if (HasAlpha(dst)) { + // Non-alpha: extract all-but-last channel + if (vips_extract_band(dst, &dstWithoutAlpha, 0, "n", dst->Bands - 1, NULL)) { + return -1; + } + vips_object_local(context, dstWithoutAlpha); + // Alpha: Extract last channel + if (vips_extract_band(dst, &dstAlpha, dst->Bands - 1, "n", 1, NULL)) { + return -1; + } + vips_object_local(context, dstAlpha); + } else { + // Non-alpha: Copy reference + dstWithoutAlpha = dst; + // Alpha: Use blank, opaque (0xFF) image + VipsImage *black; + if (vips_black(&black, dst->Xsize, dst->Ysize, NULL)) { + return -1; + } + vips_object_local(context, black); + if (vips_invert(black, &dstAlpha, NULL)) { + return -1; + } + vips_object_local(context, dstAlpha); + } // Compute normalized input alpha channels: VipsImage *srcAlphaNormalized; @@ -85,12 +102,12 @@ namespace sharp { // externally. // VipsImage *t2; - if (vips_multiply(dstRGBPremultiplied, t0, &t2, NULL)) + if (vips_multiply(dstWithoutAlpha, t0, &t2, NULL)) return -1; vips_object_local(context, t2); VipsImage *outRGBPremultiplied; - if (vips_add(srcRGBPremultiplied, t2, &outRGBPremultiplied, NULL)) + if (vips_add(srcWithoutAlpha, t2, &outRGBPremultiplied, NULL)) return -1; vips_object_local(context, outRGBPremultiplied); @@ -101,24 +118,15 @@ namespace sharp { vips_object_local(context, outAlpha); // Combine RGB and alpha channel into output image: - VipsImage *joined; - if (vips_bandjoin2(outRGBPremultiplied, outAlpha, &joined, NULL)) - return -1; - - // Return a reference to the composited output image - *outPremultiplied = joined; - return 0; + return vips_bandjoin2(outRGBPremultiplied, outAlpha, out, NULL); } /* * Premultiply alpha channel of `image`. */ int Premultiply(VipsObject *context, VipsImage *image, VipsImage **out) { - VipsImage *imagePremultiplied; - #if (VIPS_MAJOR_VERSION >= 9 || (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 1)) - if (vips_premultiply(image, &imagePremultiplied, NULL)) - return -1; + return vips_premultiply(image, out, NULL); #else VipsImage *imageRGB; if (vips_extract_band(image, &imageRGB, 0, "n", image->Bands - 1, NULL)) @@ -140,24 +148,16 @@ namespace sharp { return -1; vips_object_local(context, imageRGBPremultiplied); - if (vips_bandjoin2(imageRGBPremultiplied, imageAlpha, &imagePremultiplied, NULL)) - return -1; + return vips_bandjoin2(imageRGBPremultiplied, imageAlpha, out, NULL); #endif - - // Return a reference to the premultiplied output image - *out = imagePremultiplied; - return 0; } /* * Unpremultiply alpha channel of `image`. */ int Unpremultiply(VipsObject *context, VipsImage *image, VipsImage **out) { - VipsImage *imageUnpremultiplied; - #if (VIPS_MAJOR_VERSION >= 9 || (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 1)) - if (vips_unpremultiply(image, &imageUnpremultiplied, NULL)) - return -1; + return vips_unpremultiply(image, out, NULL); #else VipsImage *imageRGBPremultipliedTransformed; if (vips_extract_band(image, &imageRGBPremultipliedTransformed, 0, "n", image->Bands - 1, NULL)) @@ -179,13 +179,8 @@ namespace sharp { return -1; vips_object_local(context, imageRGBUnpremultipliedTransformed); - if (vips_bandjoin2(imageRGBUnpremultipliedTransformed, imageAlphaTransformed, &imageUnpremultiplied, NULL)) - return -1; + return vips_bandjoin2(imageRGBUnpremultipliedTransformed, imageAlphaTransformed, out, NULL); #endif - - // Return a reference to the unpremultiplied output image - *out = imageUnpremultiplied; - return 0; } } // namespace sharp diff --git a/src/pipeline.cc b/src/pipeline.cc index c23d06fa..5a6dbb35 100755 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -484,9 +484,6 @@ class PipelineWorker : public NanAsyncWorker { } } - // Premultiply image alpha channel before all transformations to avoid - // dark fringing around bright pixels - // See: http://entropymine.com/imageworsener/resizealpha/ bool shouldAffineTransform = xresidual != 0.0 || yresidual != 0.0; bool shouldBlur = baton->blurSigma != 0.0; bool shouldSharpen = baton->sharpenRadius != 0; @@ -494,13 +491,15 @@ class PipelineWorker : public NanAsyncWorker { bool hasOverlay = !baton->overlayPath.empty(); bool shouldPremultiplyAlpha = HasAlpha(image) && image->Bands == 4 && (shouldTransform || hasOverlay); + // Premultiply image alpha channel before all transformations to avoid + // dark fringing around bright pixels + // See: http://entropymine.com/imageworsener/resizealpha/ if (shouldPremultiplyAlpha) { VipsImage *imagePremultiplied; if (Premultiply(hook, image, &imagePremultiplied)) { (baton->err).append("Failed to premultiply alpha channel."); return Error(); } - vips_object_local(hook, imagePremultiplied); image = imagePremultiplied; } @@ -820,41 +819,34 @@ class PipelineWorker : public NanAsyncWorker { if (overlayImageType != ImageType::UNKNOWN) { overlayImage = InitImage(baton->overlayPath.c_str(), baton->accessMethod); if (overlayImage == NULL) { - (baton->err).append("Overlay input file has corrupt header"); - overlayImageType = ImageType::UNKNOWN; + (baton->err).append("Overlay image has corrupt header"); + return Error(); } else { vips_object_local(hook, overlayImage); } } else { - (baton->err).append("Overlay input file is of an unsupported image format"); - } - - if (overlayImage == NULL || overlayImageType == ImageType::UNKNOWN) { + (baton->err).append("Overlay image is of an unsupported image format"); return Error(); } - if (!HasAlpha(overlayImage)) { - (baton->err).append("Overlay input must have an alpha channel"); + (baton->err).append("Overlay image must have an alpha channel"); + return Error(); + } + if (overlayImage->Xsize != image->Xsize && overlayImage->Ysize != image->Ysize) { + (baton->err).append("Overlay image must have same dimensions as resized image"); return Error(); } - if (!HasAlpha(image)) { - (baton->err).append("Input image must have an alpha channel"); - return Error(); - } - - if (overlayImage->Bands != 4) { - (baton->err).append("Overlay input image must have 4 channels"); - return Error(); - } - - if (image->Bands != 4) { - (baton->err).append("Input image must have 4 channels"); + // Ensure overlay is sRGB + VipsImage *overlayImageRGB; + if (vips_colourspace(overlayImage, &overlayImageRGB, VIPS_INTERPRETATION_sRGB, NULL)) { return Error(); } + vips_object_local(hook, overlayImageRGB); + // Premultiply overlay VipsImage *overlayImagePremultiplied; - if (Premultiply(hook, overlayImage, &overlayImagePremultiplied)) { + if (Premultiply(hook, overlayImageRGB, &overlayImagePremultiplied)) { (baton->err).append("Failed to premultiply alpha channel of overlay image."); return Error(); } @@ -883,14 +875,14 @@ class PipelineWorker : public NanAsyncWorker { // Convert image to sRGB, if not already if (image->Type != VIPS_INTERPRETATION_sRGB) { - // Switch intrepretation to sRGB + // Switch interpretation to sRGB VipsImage *rgb; if (vips_colourspace(image, &rgb, VIPS_INTERPRETATION_sRGB, NULL)) { return Error(); } vips_object_local(hook, rgb); image = rgb; - // Tranform colours from embedded profile to sRGB profile + // Transform colours from embedded profile to sRGB profile if (baton->withMetadata && HasProfile(image)) { VipsImage *profiled; if (vips_icc_transform(image, &profiled, srgbProfile.c_str(), "embedded", TRUE, NULL)) { diff --git a/test/fixtures/5_webp_a.webp b/test/fixtures/5_webp_a.webp new file mode 100644 index 00000000..cfffec5b Binary files /dev/null and b/test/fixtures/5_webp_a.webp differ diff --git a/test/fixtures/expected/overlay-jpeg-with-greyscale.jpg b/test/fixtures/expected/overlay-jpeg-with-greyscale.jpg new file mode 100644 index 00000000..c53ed6ba Binary files /dev/null and b/test/fixtures/expected/overlay-jpeg-with-greyscale.jpg differ diff --git a/test/fixtures/expected/overlay-jpeg-with-rgb.jpg b/test/fixtures/expected/overlay-jpeg-with-rgb.jpg new file mode 100644 index 00000000..fbbb2172 Binary files /dev/null and b/test/fixtures/expected/overlay-jpeg-with-rgb.jpg differ diff --git a/test/fixtures/expected/overlay-jpeg-with-webp.jpg b/test/fixtures/expected/overlay-jpeg-with-webp.jpg new file mode 100644 index 00000000..691eabff Binary files /dev/null and b/test/fixtures/expected/overlay-jpeg-with-webp.jpg differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index e2c892c0..5d2cb4e0 100755 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -62,6 +62,7 @@ module.exports = { inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'), inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp + inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm inputGif: getPath('Crash_test.gif'), // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif inputSvg: getPath('Wikimedia-logo.svg'), // http://commons.wikimedia.org/wiki/File:Wikimedia-logo.svg diff --git a/test/unit/overlay.js b/test/unit/overlay.js index 66d81a81..8a3e4057 100644 --- a/test/unit/overlay.js +++ b/test/unit/overlay.js @@ -48,7 +48,7 @@ describe('Overlays', function() { sharp(fixtures.inputPngOverlayLayer0) .overlayWith(fixtures.inputPngOverlayLayer1) - .toBuffer(function (error, data, info) { + .toBuffer(function (error, data) { if (error) return done(error); sharp(data) .overlayWith(fixtures.inputPngOverlayLayer2) @@ -65,7 +65,7 @@ describe('Overlays', function() { sharp(fixtures.inputPngOverlayLayer1) .overlayWith(fixtures.inputPngOverlayLayer2) - .toFile(paths.actual, function (error, data, info) { + .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected); done(); @@ -77,7 +77,7 @@ describe('Overlays', function() { sharp(fixtures.inputPngOverlayLayer1LowAlpha) .overlayWith(fixtures.inputPngOverlayLayer2LowAlpha) - .toFile(paths.actual, function (error, data, info) { + .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected, 2); done(); @@ -89,12 +89,12 @@ describe('Overlays', function() { sharp(fixtures.inputPngOverlayLayer0) .overlayWith(fixtures.inputPngOverlayLayer1LowAlpha) - .toBuffer(function (error, data, info) { + .toBuffer(function (error, data) { if (error) return done(error); sharp(data) .overlayWith(fixtures.inputPngOverlayLayer2LowAlpha) - .toFile(paths.actual, function (error, data, info) { + .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected); done(); @@ -102,14 +102,63 @@ describe('Overlays', function() { }); }); - it('Composite transparent PNG onto JPEG', function(done) { + it('Composite rgb+alpha PNG onto JPEG', function(done) { + var paths = getPaths('overlay-jpeg-with-rgb', 'jpg'); + sharp(fixtures.inputJpg) + .resize(2048, 1536) .overlayWith(fixtures.inputPngOverlayLayer1) - .toBuffer(function (error) { + .toFile(paths.actual, function(error, info) { + if (error) return done(error); + fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102); + done(); + }); + }); + + it('Composite greyscale+alpha PNG onto JPEG', function(done) { + var paths = getPaths('overlay-jpeg-with-greyscale', 'jpg'); + + sharp(fixtures.inputJpg) + .resize(400, 300) + .overlayWith(fixtures.inputPngWithGreyAlpha) + .toFile(paths.actual, function(error, info) { + if (error) return done(error); + fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102); + done(); + }); + }); + + if (sharp.format.webp.input.file) { + it('Composite WebP onto JPEG', function(done) { + var paths = getPaths('overlay-jpeg-with-webp', 'jpg'); + + sharp(fixtures.inputJpg) + .resize(300, 300) + .overlayWith(fixtures.inputWebPWithTransparency) + .toFile(paths.actual, function(error, info) { + if (error) return done(error); + fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102); + done(); + }); + }); + } + + it('Fail when compositing images with different dimensions', function(done) { + sharp(fixtures.inputJpg) + .overlayWith(fixtures.inputPngWithGreyAlpha) + .toBuffer(function(error) { + console.dir(error); + assert.strictEqual(true, error instanceof Error); + done(); + }); + }); + + it('Fail when compositing non-PNG image', function(done) { + sharp(fixtures.inputPngOverlayLayer1) + .overlayWith(fixtures.inputJpg) + .toBuffer(function(error) { + console.dir(error); assert.strictEqual(true, error instanceof Error); - if (error.message !== 'Input image must have an alpha channel') { - return done(new Error('Unexpected error: ' + error.message)); - } done(); }); });