diff --git a/docs/api-channel.md b/docs/api-channel.md index 011934b2..9326126e 100644 --- a/docs/api-channel.md +++ b/docs/api-channel.md @@ -1,5 +1,21 @@ +## removeAlpha + +Remove alpha channel, if any. This is a no-op if the image does not have an alpha channel. + +### Examples + +```javascript +sharp('rgba.png') + .removeAlpha() + .toFile('rgb.png', function(err, info) { + // rgb.png is a 3 channel image without an alpha channel + }); +``` + +Returns **Sharp** + ## extractChannel Extract a single channel from a multi-channel image. diff --git a/docs/api-input.md b/docs/api-input.md index a51a5ae7..9a6dd4d1 100644 --- a/docs/api-input.md +++ b/docs/api-input.md @@ -79,6 +79,7 @@ A Promise is returned when `callback` is not provided. - `maxX` (x-coordinate of one of the pixel where the maximum lies) - `maxY` (y-coordinate of one of the pixel where the maximum lies) - `isOpaque`: Value to identify if the image is opaque or transparent, based on the presence and use of alpha channel +- `entropy`: Histogram-based estimation of greyscale entropy, discarding alpha channel if any (experimental) ### Parameters diff --git a/docs/changelog.md b/docs/changelog.md index 8bc85cd9..0a9988a2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,9 @@ Requires libvips v8.6.1. #### v0.20.6 - TBD +* Add removeAlpha operation to remove alpha channel, if any. + [#1248](https://github.com/lovell/sharp/issues/1248) + * Expose mozjpeg quant_table flag. [#1285](https://github.com/lovell/sharp/pull/1285) [@rexxars](https://github.com/rexxars) diff --git a/lib/channel.js b/lib/channel.js index 8aee9b1a..f7626fab 100644 --- a/lib/channel.js +++ b/lib/channel.js @@ -12,6 +12,23 @@ const bool = { eor: 'eor' }; +/** + * Remove alpha channel, if any. This is a no-op if the image does not have an alpha channel. + * + * @example + * sharp('rgba.png') + * .removeAlpha() + * .toFile('rgb.png', function(err, info) { + * // rgb.png is a 3 channel image without an alpha channel + * }); + * + * @returns {Sharp} + */ +function removeAlpha () { + this.options.removeAlpha = true; + return this; +} + /** * Extract a single channel from a multi-channel image. * @@ -102,6 +119,7 @@ function bandbool (boolOp) { module.exports = function (Sharp) { // Public instance functions [ + removeAlpha, extractChannel, joinChannel, bandbool diff --git a/lib/constructor.js b/lib/constructor.js index 8d27d761..20afa476 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -171,6 +171,7 @@ const Sharp = function (input, options) { booleanFileIn: '', joinChannelIn: [], extractChannel: -1, + removeAlpha: false, colourspace: 'srgb', // overlay overlayGravity: 0, diff --git a/src/operations.cc b/src/operations.cc index 819bbbf0..b783a1c2 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -28,6 +28,16 @@ using vips::VError; namespace sharp { + /* + Removes alpha channel, if any. + */ + VImage RemoveAlpha(VImage image) { + if (HasAlpha(image)) { + image = image.extract_band(0, VImage::option()->set("n", image.bands() - 1)); + } + return image; + } + /* Composite overlayImage over image at given position Assumes alpha channels are already premultiplied and will be unpremultiplied after @@ -223,10 +233,8 @@ namespace sharp { VImage Gamma(VImage image, double const exponent) { if (HasAlpha(image)) { // Separate alpha channel - VImage imageWithoutAlpha = image.extract_band(0, - VImage::option()->set("n", image.bands() - 1)); VImage alpha = image[image.bands() - 1]; - return imageWithoutAlpha.gamma(VImage::option()->set("exponent", exponent)).bandjoin(alpha); + return RemoveAlpha(image).gamma(VImage::option()->set("exponent", exponent)).bandjoin(alpha); } else { return image.gamma(VImage::option()->set("exponent", exponent)); } @@ -374,10 +382,8 @@ namespace sharp { VImage Linear(VImage image, double const a, double const b) { if (HasAlpha(image)) { // Separate alpha channel - VImage imageWithoutAlpha = image.extract_band(0, - VImage::option()->set("n", image.bands() - 1)); VImage alpha = image[image.bands() - 1]; - return imageWithoutAlpha.linear(a, b).bandjoin(alpha); + return RemoveAlpha(image).linear(a, b).bandjoin(alpha); } else { return image.linear(a, b); } diff --git a/src/operations.h b/src/operations.h index 22e13bed..c49ddc0b 100644 --- a/src/operations.h +++ b/src/operations.h @@ -25,6 +25,11 @@ using vips::VImage; namespace sharp { + /* + Removes alpha channel, if any. + */ + VImage RemoveAlpha(VImage image); + /* Alpha composite src over dst with given gravity. Assumes alpha channels are already premultiplied and will be unpremultiplied after. diff --git a/src/pipeline.cc b/src/pipeline.cc index b7084291..1328dd89 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -698,6 +698,12 @@ class PipelineWorker : public Nan::AsyncWorker { .extract_band(baton->extractChannel) .copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_B_W)); } + + // Remove alpha channel, if any + if (baton->removeAlpha) { + image = sharp::RemoveAlpha(image); + } + // Convert image to sRGB, if not already if (sharp::Is16Bit(image.interpretation())) { image = image.cast(VIPS_FORMAT_USHORT); @@ -1235,6 +1241,7 @@ NAN_METHOD(pipeline) { baton->extendLeft = AttrTo(options, "extendLeft"); baton->extendRight = AttrTo(options, "extendRight"); baton->extractChannel = AttrTo(options, "extractChannel"); + baton->removeAlpha = AttrTo(options, "removeAlpha"); if (HasAttr(options, "boolean")) { baton->boolean = CreateInputDescriptor(AttrAs(options, "boolean"), buffersToPersist); baton->booleanOp = sharp::GetBooleanOperation(AttrAsStr(options, "booleanOp")); diff --git a/src/pipeline.h b/src/pipeline.h index 0b5c8baf..9ed89fd2 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -131,6 +131,7 @@ struct PipelineBaton { VipsOperationBoolean booleanOp; VipsOperationBoolean bandBoolOp; int extractChannel; + bool removeAlpha; VipsInterpretation colourspace; int tileSize; int tileOverlap; @@ -213,6 +214,7 @@ struct PipelineBaton { booleanOp(VIPS_OPERATION_BOOLEAN_LAST), bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST), extractChannel(-1), + removeAlpha(false), colourspace(VIPS_INTERPRETATION_LAST), tileSize(256), tileOverlap(0), diff --git a/test/unit/alpha.js b/test/unit/alpha.js index 750f51ed..03315db3 100644 --- a/test/unit/alpha.js +++ b/test/unit/alpha.js @@ -81,35 +81,45 @@ describe('Alpha transparency', function () { }); }); - it('Enlargement with non-nearest neighbor interpolation shouldn’t cause dark edges', function (done) { + it('Enlargement with non-nearest neighbor interpolation shouldn’t cause dark edges', function () { const base = 'alpha-premultiply-enlargement-2048x1536-paper.png'; const actual = fixtures.path('output.' + base); const expected = fixtures.expected(base); - sharp(fixtures.inputPngAlphaPremultiplicationSmall) + return sharp(fixtures.inputPngAlphaPremultiplicationSmall) .resize(2048, 1536) - .toFile(actual, function (err) { - if (err) { - done(err); - } else { - fixtures.assertMaxColourDistance(actual, expected, 102); - done(); - } + .toFile(actual) + .then(function () { + fixtures.assertMaxColourDistance(actual, expected, 102); }); }); - it('Reduction with non-nearest neighbor interpolation shouldn’t cause dark edges', function (done) { + it('Reduction with non-nearest neighbor interpolation shouldn’t cause dark edges', function () { const base = 'alpha-premultiply-reduction-1024x768-paper.png'; const actual = fixtures.path('output.' + base); const expected = fixtures.expected(base); - sharp(fixtures.inputPngAlphaPremultiplicationLarge) + return sharp(fixtures.inputPngAlphaPremultiplicationLarge) .resize(1024, 768) - .toFile(actual, function (err) { - if (err) { - done(err); - } else { - fixtures.assertMaxColourDistance(actual, expected, 102); - done(); - } + .toFile(actual) + .then(function () { + fixtures.assertMaxColourDistance(actual, expected, 102); }); }); + + it('Removes alpha from fixtures with transparency, ignores those without', function () { + return Promise.all([ + fixtures.inputPngWithTransparency, + fixtures.inputPngWithTransparency16bit, + fixtures.inputWebPWithTransparency, + fixtures.inputJpg, + fixtures.inputPng, + fixtures.inputWebP + ].map(function (input) { + return sharp(input) + .removeAlpha() + .toBuffer({ resolveWithObject: true }) + .then(function (result) { + assert.strictEqual(3, result.info.channels); + }); + })); + }); });