diff --git a/docs/api.md b/docs/api.md index 52c6c456..a88e33a5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -272,6 +272,10 @@ The default background is `{r: 0, g: 0, b: 0, a: 1}`, black without transparency Merge alpha transparency channel, if any, with `background`. +#### negate() + +Produces the "negative" of the image. White => Black, Black => White, Blue => Yellow, etc. + #### rotate([angle]) Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag. diff --git a/index.js b/index.js index c2585f3b..009890ad 100644 --- a/index.js +++ b/index.js @@ -58,6 +58,7 @@ var Sharp = function(input) { // operations background: [0, 0, 0, 255], flatten: false, + negate: false, blurSigma: 0, sharpenRadius: 0, sharpenFlat: 1, @@ -215,6 +216,11 @@ Sharp.prototype.flatten = function(flatten) { return this; }; +Sharp.prototype.negate = function(negate) { + this.options.negate = (typeof negate === 'boolean') ? negate : true; + return this; +}; + Sharp.prototype.overlayWith = function(overlayPath) { if (typeof overlayPath !== 'string') { throw new Error('The overlay path must be a string'); diff --git a/src/pipeline.cc b/src/pipeline.cc index d9629c32..5b53da3f 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -100,6 +100,7 @@ struct PipelineBaton { std::string interpolator; double background[4]; bool flatten; + bool negate; double blurSigma; int sharpenRadius; double sharpenFlat; @@ -138,6 +139,7 @@ struct PipelineBaton { canvas(Canvas::CROP), gravity(0), flatten(false), + negate(false), blurSigma(0.0), sharpenRadius(0), sharpenFlat(1.0), @@ -451,6 +453,16 @@ class PipelineWorker : public AsyncWorker { image = flattened; } + // Negate the colors in the image. + if (baton->negate) { + VipsImage *negated; + if (vips_invert(image, &negated, nullptr)) { + return Error(); + } + vips_object_local(hook, negated); + image = negated; + } + // Gamma encoding (darken) if (baton->gamma >= 1 && baton->gamma <= 3 && !HasAlpha(image)) { VipsImage *gammaEncoded; @@ -1212,6 +1224,7 @@ NAN_METHOD(pipeline) { baton->interpolator = *Utf8String(Get(options, New("interpolator").ToLocalChecked()).ToLocalChecked()); // Operators baton->flatten = To(Get(options, New("flatten").ToLocalChecked()).ToLocalChecked()).FromJust(); + baton->negate = To(Get(options, New("negate").ToLocalChecked()).ToLocalChecked()).FromJust(); baton->blurSigma = To(Get(options, New("blurSigma").ToLocalChecked()).ToLocalChecked()).FromJust(); baton->sharpenRadius = To(Get(options, New("sharpenRadius").ToLocalChecked()).ToLocalChecked()).FromJust(); baton->sharpenFlat = To(Get(options, New("sharpenFlat").ToLocalChecked()).ToLocalChecked()).FromJust(); diff --git a/test/fixtures/expected/negate-alpha.png b/test/fixtures/expected/negate-alpha.png new file mode 100644 index 00000000..dabea054 Binary files /dev/null and b/test/fixtures/expected/negate-alpha.png differ diff --git a/test/fixtures/expected/negate-trans.png b/test/fixtures/expected/negate-trans.png new file mode 100644 index 00000000..52e79e5f Binary files /dev/null and b/test/fixtures/expected/negate-trans.png differ diff --git a/test/fixtures/expected/negate-trans.webp b/test/fixtures/expected/negate-trans.webp new file mode 100644 index 00000000..9e9d0efe Binary files /dev/null and b/test/fixtures/expected/negate-trans.webp differ diff --git a/test/fixtures/expected/negate.jpg b/test/fixtures/expected/negate.jpg new file mode 100644 index 00000000..e2c57b72 Binary files /dev/null and b/test/fixtures/expected/negate.jpg differ diff --git a/test/fixtures/expected/negate.png b/test/fixtures/expected/negate.png new file mode 100644 index 00000000..d77d513c Binary files /dev/null and b/test/fixtures/expected/negate.png differ diff --git a/test/fixtures/expected/negate.webp b/test/fixtures/expected/negate.webp new file mode 100644 index 00000000..dec914bf Binary files /dev/null and b/test/fixtures/expected/negate.webp differ diff --git a/test/unit/negate.js b/test/unit/negate.js new file mode 100644 index 00000000..c8936934 --- /dev/null +++ b/test/unit/negate.js @@ -0,0 +1,107 @@ +'use strict'; + +var assert = require('assert'); + +var sharp = require('../../index'); +var fixtures = require('../fixtures'); + +sharp.cache(0); + +describe('Negate', function() { + it('negate (jpeg)', function(done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .negate() + .toBuffer(function(err, data, info) { + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('negate.jpg'), data, done); + }); + }); + + it('negate (png)', function(done) { + sharp(fixtures.inputPng) + .resize(320, 240) + .negate() + .toBuffer(function(err, data, info) { + assert.strictEqual('png', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('negate.png'), data, done); + }); + }); + + it('negate (png, trans)', function(done) { + sharp(fixtures.inputPngWithTransparency) + .resize(320, 240) + .negate() + .toBuffer(function(err, data, info) { + assert.strictEqual('png', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('negate-trans.png'), data, done); + }); + }); + + it('negate (png, alpha)', function(done) { + sharp(fixtures.inputPngWithGreyAlpha) + .resize(320, 240) + .negate() + .toBuffer(function(err, data, info) { + assert.strictEqual('png', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('negate-alpha.png'), data, done); + }); + }); + + if (sharp.format.webp.output.file) { + it('negate (webp)', function(done) { + sharp(fixtures.inputWebP) + .resize(320, 240) + .negate() + .toBuffer(function(err, data, info) { + assert.strictEqual('webp', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('negate.webp'), data, done); + }); + }); + + it('negate (webp, trans)', function(done) { + sharp(fixtures.inputWebPWithTransparency) + .resize(320, 240) + .negate() + .toBuffer(function(err, data, info) { + assert.strictEqual('webp', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('negate-trans.webp'), data, done); + }); + }); + } + + it('negate (true)', function(done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .negate(true) + .toBuffer(function(err, data, info) { + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('negate.jpg'), data, done); + }); + }); + + it('negate (false)', function(done) { + var output = fixtures.path('output.unmodified-by-negate.png'); + sharp(fixtures.inputJpgWithLowContrast) + .negate(false) + .toFile(output, function(err, info) { + if (err) done(err); + fixtures.assertMaxColourDistance(output, fixtures.inputJpgWithLowContrast, 0); + done(); + }); + }); +});