diff --git a/docs/api-channel.md b/docs/api-channel.md index 9326126e..7e278111 100644 --- a/docs/api-channel.md +++ b/docs/api-channel.md @@ -16,6 +16,22 @@ sharp('rgba.png') Returns **Sharp** +## ensureAlpha + +Ensure alpha channel, if missing. The added alpha channel will be fully opaque. This is a no-op if the image already has an alpha channel. + +### Examples + +```javascript +sharp('rgb.jpg') + .ensureAlpha() + .toFile('rgba.png', function(err, info) { + // rgba.png is a 4 channel image with a fully opaque alpha channel + }); +``` + +Returns **Sharp** + ## extractChannel Extract a single channel from a multi-channel image. diff --git a/docs/changelog.md b/docs/changelog.md index 3d0d1637..e2cd8e4d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,9 @@ Requires libvips v8.7.0. * Ensure shortest edge is at least one pixel after resizing. [#1003](https://github.com/lovell/sharp/issues/1003) +* Add `ensureAlpha` operation to add an alpha channel, if missing. + [#1153](https://github.com/lovell/sharp/issues/1153) + * Expose `pages` and `pageHeight` metadata for multi-page input images. [#1205](https://github.com/lovell/sharp/issues/1205) diff --git a/lib/channel.js b/lib/channel.js index bb32eb3b..eb5e6413 100644 --- a/lib/channel.js +++ b/lib/channel.js @@ -29,6 +29,23 @@ function removeAlpha () { return this; } +/** + * Ensure alpha channel, if missing. The added alpha channel will be fully opaque. This is a no-op if the image already has an alpha channel. + * + * @example + * sharp('rgb.jpg') + * .ensureAlpha() + * .toFile('rgba.png', function(err, info) { + * // rgba.png is a 4 channel image with a fully opaque alpha channel + * }); + * + * @returns {Sharp} + */ +function ensureAlpha () { + this.options.ensureAlpha = true; + return this; +} + /** * Extract a single channel from a multi-channel image. * @@ -120,6 +137,7 @@ module.exports = function (Sharp) { Object.assign(Sharp.prototype, { // Public instance functions removeAlpha, + ensureAlpha, extractChannel, joinChannel, bandbool diff --git a/lib/constructor.js b/lib/constructor.js index e848fe7a..40d0acd5 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -144,6 +144,7 @@ const Sharp = function (input, options) { joinChannelIn: [], extractChannel: -1, removeAlpha: false, + ensureAlpha: false, colourspace: 'srgb', // overlay overlayGravity: 0, diff --git a/src/operations.cc b/src/operations.cc index 73ecb0b7..f34cae04 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -38,6 +38,18 @@ namespace sharp { return image; } + /* + Ensures alpha channel, if missing. + */ + VImage EnsureAlpha(VImage image) { + if (!HasAlpha(image)) { + std::vector alpha; + alpha.push_back(sharp::MaximumImageAlpha(image.interpretation())); + image = image.bandjoin_const(alpha); + } + return image; + } + /* Composite overlayImage over image at given position Assumes alpha channels are already premultiplied and will be unpremultiplied after diff --git a/src/operations.h b/src/operations.h index 5fa2a53c..dd27f684 100644 --- a/src/operations.h +++ b/src/operations.h @@ -30,6 +30,11 @@ namespace sharp { */ VImage RemoveAlpha(VImage image); + /* + Ensures alpha channel, if missing. + */ + VImage EnsureAlpha(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 3c15330d..a0d3d615 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -677,6 +677,11 @@ class PipelineWorker : public Nan::AsyncWorker { image = sharp::RemoveAlpha(image); } + // Ensure alpha channel, if missing + if (baton->ensureAlpha) { + image = sharp::EnsureAlpha(image); + } + // Convert image to sRGB, if not already if (sharp::Is16Bit(image.interpretation())) { image = image.cast(VIPS_FORMAT_USHORT); @@ -1236,6 +1241,7 @@ NAN_METHOD(pipeline) { baton->extractChannel = AttrTo(options, "extractChannel"); baton->removeAlpha = AttrTo(options, "removeAlpha"); + baton->ensureAlpha = AttrTo(options, "ensureAlpha"); 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 b0303d91..0c05a42e 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -142,6 +142,7 @@ struct PipelineBaton { VipsOperationBoolean bandBoolOp; int extractChannel; bool removeAlpha; + bool ensureAlpha; VipsInterpretation colourspace; int tileSize; int tileOverlap; @@ -237,6 +238,7 @@ struct PipelineBaton { bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST), extractChannel(-1), removeAlpha(false), + ensureAlpha(false), colourspace(VIPS_INTERPRETATION_LAST), tileSize(256), tileOverlap(0), diff --git a/test/unit/alpha.js b/test/unit/alpha.js index 0a02ecea..38c2ad2f 100644 --- a/test/unit/alpha.js +++ b/test/unit/alpha.js @@ -115,6 +115,7 @@ describe('Alpha transparency', function () { fixtures.inputWebP ].map(function (input) { return sharp(input) + .resize(10) .removeAlpha() .toBuffer({ resolveWithObject: true }) .then(function (result) { @@ -122,4 +123,24 @@ describe('Alpha transparency', function () { }); })); }); + + it('Ensures alpha from fixtures without transparency, ignores those with', function () { + return Promise.all([ + fixtures.inputPngWithTransparency, + fixtures.inputPngWithTransparency16bit, + fixtures.inputWebPWithTransparency, + fixtures.inputJpg, + fixtures.inputPng, + fixtures.inputWebP + ].map(function (input) { + return sharp(input) + .resize(10) + .ensureAlpha() + .png() + .toBuffer({ resolveWithObject: true }) + .then(function (result) { + assert.strictEqual(4, result.info.channels); + }); + })); + }); });