mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Alpha compositing: support grey+alpha src and non-alpha dst
This commit is contained in:
parent
36be0453dd
commit
1091be374e
12
README.md
12
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()
|
||||
|
@ -1,6 +1,8 @@
|
||||
#ifndef SRC_COMMON_H_
|
||||
#define SRC_COMMON_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace sharp {
|
||||
|
||||
enum class ImageType {
|
||||
|
@ -1,38 +1,55 @@
|
||||
#include <vips/vips.h>
|
||||
|
||||
#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))
|
||||
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
|
||||
|
@ -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)) {
|
||||
|
BIN
test/fixtures/5_webp_a.webp
vendored
Normal file
BIN
test/fixtures/5_webp_a.webp
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
BIN
test/fixtures/expected/overlay-jpeg-with-greyscale.jpg
vendored
Normal file
BIN
test/fixtures/expected/overlay-jpeg-with-greyscale.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
test/fixtures/expected/overlay-jpeg-with-rgb.jpg
vendored
Normal file
BIN
test/fixtures/expected/overlay-jpeg-with-rgb.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 230 KiB |
BIN
test/fixtures/expected/overlay-jpeg-with-webp.jpg
vendored
Normal file
BIN
test/fixtures/expected/overlay-jpeg-with-webp.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
1
test/fixtures/index.js
vendored
1
test/fixtures/index.js
vendored
@ -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
|
||||
|
@ -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) {
|
||||
assert.strictEqual(true, error instanceof Error);
|
||||
if (error.message !== 'Input image must have an alpha channel') {
|
||||
return done(new Error('Unexpected error: ' + error.message));
|
||||
.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);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user