diff --git a/lib/constructor.js b/lib/constructor.js index 62921f2a..43d7d3f1 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -217,6 +217,7 @@ const Sharp = function (input, options) { tintB: 128, flatten: false, flattenBackground: [0, 0, 0], + unflatten: false, negate: false, negateAlpha: true, medianSize: 0, diff --git a/lib/index.d.ts b/lib/index.d.ts index d5dbb0be..b0642a7b 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -427,6 +427,13 @@ declare namespace sharp { */ flatten(flatten?: boolean | FlattenOptions): Sharp; + /** + * Unflatten - add an alpha channel to the image if required, and make white pixels fully transparent. Alpha for non-white pixels will be unchanged/opaque. + * @param unflatten true to enable and false to disable (defaults to true) + * @returns A sharp instance that can be used to chain operations + */ + unflatten(unflatten?: boolean): Sharp; + /** * Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of 1/gamma then increasing the encoding (brighten) post-resize at a factor of gamma. * This can improve the perceived brightness of a resized image in non-linear colour spaces. diff --git a/lib/operation.js b/lib/operation.js index b58583ad..6ea9f739 100644 --- a/lib/operation.js +++ b/lib/operation.js @@ -405,6 +405,25 @@ function flatten (options) { return this; } +/** + * Unflatten - add an alpha channel to the image if required, and make white pixels fully transparent. Alpha for non-white pixels will be unchanged/opaque. + * + * @example + * await sharp(rgbInput) + * .unflatten() + * .toBuffer(); + * + * @example + * await sharp(rgbInput) + * .threshold(128, { grayscale: false }) // converter bright pixels to white + * .unflatten() + * .toBuffer(); + */ +function unflatten (options) { + this.options.unflatten = is.bool(options) ? options : true; + return this; +} + /** * Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of `1/gamma` * then increasing the encoding (brighten) post-resize at a factor of `gamma`. @@ -875,6 +894,7 @@ module.exports = function (Sharp) { median, blur, flatten, + unflatten, gamma, negate, normalise, diff --git a/src/operations.cc b/src/operations.cc index 026a0ffd..5f421940 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -342,6 +342,19 @@ namespace sharp { } } + /* + * Unflatten + */ + VImage Unflatten(VImage image) { + if (HasAlpha(image)) { + VImage alpha = image[image.bands() - 1]; + VImage noAlpha = RemoveAlpha(image); + return noAlpha.bandjoin(alpha & (noAlpha.colourspace(VIPS_INTERPRETATION_B_W) < 255)); + } else { + return image.bandjoin(image.colourspace(VIPS_INTERPRETATION_B_W) < 255); + } + } + /* * Ensure the image is in a given colourspace */ diff --git a/src/operations.h b/src/operations.h index df4d1eaf..10a0c6ce 100644 --- a/src/operations.h +++ b/src/operations.h @@ -86,6 +86,11 @@ namespace sharp { */ VImage Linear(VImage image, std::vector const a, std::vector const b); + /* + * Unflatten + */ + VImage Unflatten(VImage image); + /* * Recomb with a Matrix of the given bands/channel size. * Eg. RGB will be a 3x3 matrix. diff --git a/src/pipeline.cc b/src/pipeline.cc index 4aad4cbc..ef33dccc 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -550,7 +550,9 @@ class PipelineWorker : public Napi::AsyncWorker { if (baton->medianSize > 0) { image = image.median(baton->medianSize); } + // Threshold - must happen before blurring, due to the utility of blurring after thresholding + // Threshold - must happen before unflatten to enable non-white unflattening if (baton->threshold != 0) { image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale); } @@ -560,6 +562,11 @@ class PipelineWorker : public Napi::AsyncWorker { image = sharp::Blur(image, baton->blurSigma); } + // Unflatten the image + if (baton->unflatten) { + image = sharp::Unflatten(image); + } + // Convolve if (shouldConv) { image = sharp::Convolve(image, @@ -1460,6 +1467,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { // Operators baton->flatten = sharp::AttrAsBool(options, "flatten"); baton->flattenBackground = sharp::AttrAsVectorOfDouble(options, "flattenBackground"); + baton->unflatten = sharp::AttrAsBool(options, "unflatten"); baton->negate = sharp::AttrAsBool(options, "negate"); baton->negateAlpha = sharp::AttrAsBool(options, "negateAlpha"); baton->blurSigma = sharp::AttrAsDouble(options, "blurSigma"); diff --git a/src/pipeline.h b/src/pipeline.h index aa37090b..19d9bde9 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -73,6 +73,7 @@ struct PipelineBaton { double tintB; bool flatten; std::vector flattenBackground; + bool unflatten; bool negate; bool negateAlpha; double blurSigma; @@ -239,6 +240,7 @@ struct PipelineBaton { tintB(128.0), flatten(false), flattenBackground{ 0.0, 0.0, 0.0 }, + unflatten(false), negate(false), negateAlpha(true), blurSigma(0.0), diff --git a/test/fixtures/expected/unflatten-flag-white-transparent.png b/test/fixtures/expected/unflatten-flag-white-transparent.png new file mode 100644 index 00000000..afed7213 Binary files /dev/null and b/test/fixtures/expected/unflatten-flag-white-transparent.png differ diff --git a/test/fixtures/expected/unflatten-swiss.png b/test/fixtures/expected/unflatten-swiss.png new file mode 100644 index 00000000..89c8a867 Binary files /dev/null and b/test/fixtures/expected/unflatten-swiss.png differ diff --git a/test/fixtures/expected/unflatten-white-transparent.png b/test/fixtures/expected/unflatten-white-transparent.png new file mode 100644 index 00000000..abf3ed48 Binary files /dev/null and b/test/fixtures/expected/unflatten-white-transparent.png differ diff --git a/test/unit/unflatten.js b/test/unit/unflatten.js new file mode 100644 index 00000000..1c6fb483 --- /dev/null +++ b/test/unit/unflatten.js @@ -0,0 +1,37 @@ +'use strict'; + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +// const assert = require('assert'); + +describe('Unflatten', function () { + it('unflatten white background', function (done) { + sharp(fixtures.inputPng).unflatten() + .toBuffer(function (err, data) { + if (err) throw err; + fixtures.assertSimilar(fixtures.expected('unflatten-white-transparent.png'), data, { threshold: 0 }, done); + }); + }); + it('unflatten transparent image', function (done) { + sharp(fixtures.inputPngTrimSpecificColourIncludeAlpha).unflatten() + .toBuffer(function (err, data) { + if (err) throw err; + fixtures.assertSimilar(fixtures.expected('unflatten-flag-white-transparent.png'), data, { threshold: 0 }, done); + }); + }); + it('unflatten using threshold', function (done) { + sharp(fixtures.inputPngPalette).unflatten(true).threshold(128, { grayscale: false }) + .toBuffer(function (err, data) { + if (err) throw err; + fixtures.assertSimilar(fixtures.expected('unflatten-swiss.png'), data, { threshold: 1 }, done); + }); + }); + it('no unflatten', function (done) { + sharp(fixtures.inputPng).unflatten(false) + .toBuffer(function (err, data) { + if (err) throw err; + fixtures.assertSimilar(fixtures.inputPng, data, { threshold: 0 }, done); + }); + }); +});