diff --git a/docs/api-colour.md b/docs/api-colour.md index 431c0c0a..378be15b 100644 --- a/docs/api-colour.md +++ b/docs/api-colour.md @@ -3,10 +3,11 @@ ### Table of Contents - [background][1] -- [greyscale][2] -- [grayscale][3] -- [toColourspace][4] -- [toColorspace][5] +- [tint][2] +- [greyscale][3] +- [grayscale][4] +- [toColourspace][5] +- [toColorspace][6] ## background @@ -19,10 +20,24 @@ The alpha value is a float between `0` (transparent) and `1` (opaque). **Parameters** -- `rgba` **([String][6] \| [Object][7])** parsed by the [color][8] module to extract values for red, green, blue and alpha. +- `rgba` **([String][7] \| [Object][8])** parsed by the [color][9] module to extract values for red, green, blue and alpha. -- Throws **[Error][9]** Invalid parameter +- Throws **[Error][10]** Invalid parameter + +Returns **Sharp** + +## tint + +Tint the image using the provided chroma while preserving the image luminance. +An alpha channel may be present and will be unchanged by the operation. + +**Parameters** + +- `rgb` **([String][7] \| [Object][8])** parsed by the [color][9] module to extract chroma values. + + +- Throws **[Error][10]** Invalid parameter Returns **Sharp** @@ -37,7 +52,7 @@ An alpha channel may be present, and will be unchanged by the operation. **Parameters** -- `greyscale` **[Boolean][10]** (optional, default `true`) +- `greyscale` **[Boolean][11]** (optional, default `true`) Returns **Sharp** @@ -47,7 +62,7 @@ Alternative spelling of `greyscale`. **Parameters** -- `grayscale` **[Boolean][10]** (optional, default `true`) +- `grayscale` **[Boolean][11]** (optional, default `true`) Returns **Sharp** @@ -58,10 +73,10 @@ By default output image will be web-friendly sRGB, with additional channels inte **Parameters** -- `colourspace` **[String][6]?** output colourspace e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...][11] +- `colourspace` **[String][7]?** output colourspace e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...][12] -- Throws **[Error][9]** Invalid parameters +- Throws **[Error][10]** Invalid parameters Returns **Sharp** @@ -71,31 +86,33 @@ Alternative spelling of `toColourspace`. **Parameters** -- `colorspace` **[String][6]?** output colorspace. +- `colorspace` **[String][7]?** output colorspace. -- Throws **[Error][9]** Invalid parameters +- Throws **[Error][10]** Invalid parameters Returns **Sharp** [1]: #background -[2]: #greyscale +[2]: #tint -[3]: #grayscale +[3]: #greyscale -[4]: #tocolourspace +[4]: #grayscale -[5]: #tocolorspace +[5]: #tocolourspace -[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[6]: #tocolorspace -[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[8]: https://www.npmjs.org/package/color +[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error +[9]: https://www.npmjs.org/package/color -[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error -[11]: https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L568 +[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean + +[12]: https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L568 diff --git a/docs/changelog.md b/docs/changelog.md index 6adec105..735aa410 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,12 @@ Requires libvips v8.6.1. +#### v0.20.2 - TBD + +* Add tint operation to set image chroma. + [#825](https://github.com/lovell/sharp/pull/825) + [@rikh42](https://github.com/rikh42) + #### v0.20.1 - 17th March 2018 * Improve installation experience when a globally-installed libvips below the minimum required version is found. diff --git a/lib/colour.js b/lib/colour.js index e115946c..d36cbd06 100644 --- a/lib/colour.js +++ b/lib/colour.js @@ -38,6 +38,21 @@ function background (rgba) { return this; } +/** + * Tint the image using the provided chroma while preserving the image luminance. + * An alpha channel may be present and will be unchanged by the operation. + * + * @param {String|Object} rgb - parsed by the [color](https://www.npmjs.org/package/color) module to extract chroma values. + * @returns {Sharp} + * @throws {Error} Invalid parameter + */ +function tint (rgb) { + const colour = color(rgb); + this.options.tintA = colour.a(); + this.options.tintB = colour.b(); + return this; +} + /** * Convert to 8-bit greyscale; 256 shades of grey. * This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use `gamma()` with `greyscale()` for the best results. @@ -95,6 +110,7 @@ module.exports = function (Sharp) { // Public instance functions [ background, + tint, greyscale, grayscale, toColourspace, diff --git a/lib/constructor.js b/lib/constructor.js index d83b685f..5776c69f 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -151,6 +151,8 @@ const Sharp = function (input, options) { fastShrinkOnLoad: true, // operations background: [0, 0, 0, 255], + tintA: 0, + tintB: 0, flatten: false, negate: false, medianSize: 0, diff --git a/package.json b/package.json index 86b35f4d..c707801d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "Kenric D'Souza ", "Oleh Aleinyk ", "Marcel Bretschneider ", - "Andrea Bianco " + "Andrea Bianco ", + "Rik Heywood " ], "scripts": { "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", diff --git a/src/operations.cc b/src/operations.cc index 7c3cda74..da5f56d7 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -152,6 +152,32 @@ namespace sharp { return dst.bandjoin(mask.cast(dst.format())); } + /* + * Tint an image using the specified chroma, preserving the original image luminance + */ + VImage Tint(VImage image, double const a, double const b) { + // Get original colourspace + VipsInterpretation typeBeforeTint = image.interpretation(); + if (typeBeforeTint == VIPS_INTERPRETATION_RGB) { + typeBeforeTint = VIPS_INTERPRETATION_sRGB; + } + // Create 2 band image with every pixel set to the tint chroma + std::vector chromaPixel {a, b}; + VImage chroma = image.new_from_image(chromaPixel); + // Extract luminance + VImage luminance = image.colourspace(VIPS_INTERPRETATION_LAB)[0]; + // Create the tinted version by combining the L from the original and the chroma from the tint + VImage tinted = luminance.bandjoin(chroma).colourspace(typeBeforeTint); + // Attach original alpha channel, if any + if (HasAlpha(image)) { + // Extract original alpha channel + VImage alpha = image[image.bands() - 1]; + // Join alpha channel to normalised image + tinted = tinted.bandjoin(alpha); + } + return tinted; + } + /* * Stretch luminance to cover full dynamic range. */ diff --git a/src/operations.h b/src/operations.h index aae0f027..22e13bed 100644 --- a/src/operations.h +++ b/src/operations.h @@ -46,6 +46,11 @@ namespace sharp { */ VImage Cutout(VImage src, VImage dst, const int gravity); + /* + * Tint an image using the specified chroma, preserving the original image luminance + */ + VImage Tint(VImage image, double const a, double const b); + /* * Stretch luminance to cover full dynamic range. */ diff --git a/src/pipeline.cc b/src/pipeline.cc index a920c08f..abd5ab1d 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -682,6 +682,11 @@ class PipelineWorker : public Nan::AsyncWorker { image = sharp::Bandbool(image, baton->bandBoolOp); } + // Tint the image + if (baton->tintA > 0 || baton->tintB > 0) { + image = sharp::Tint(image, baton->tintA, baton->tintB); + } + // Extract an image channel (aka vips band) if (baton->extractChannel > -1) { if (baton->extractChannel >= image.bands()) { @@ -1167,6 +1172,9 @@ NAN_METHOD(pipeline) { for (unsigned int i = 0; i < 4; i++) { baton->background[i] = AttrTo(background, i); } + // Tint chroma + baton->tintA = AttrTo(options, "tintA"); + baton->tintB = AttrTo(options, "tintB"); // Overlay options if (HasAttr(options, "overlay")) { baton->overlay = CreateInputDescriptor(AttrAs(options, "overlay"), buffersToPersist); diff --git a/src/pipeline.h b/src/pipeline.h index f8367270..0ae147dc 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -70,6 +70,8 @@ struct PipelineBaton { std::string kernel; bool fastShrinkOnLoad; double background[4]; + double tintA; + double tintB; bool flatten; bool negate; double blurSigma; @@ -155,6 +157,8 @@ struct PipelineBaton { cropOffsetLeft(0), cropOffsetTop(0), premultiplied(false), + tintA(0.0), + tintB(0.0), flatten(false), negate(false), blurSigma(0.0), diff --git a/test/fixtures/2569067123_aca715a2ee_o.png b/test/fixtures/2569067123_aca715a2ee_o.png new file mode 100644 index 00000000..1262ec6e Binary files /dev/null and b/test/fixtures/2569067123_aca715a2ee_o.png differ diff --git a/test/fixtures/expected/tint-alpha.png b/test/fixtures/expected/tint-alpha.png new file mode 100644 index 00000000..01620a96 Binary files /dev/null and b/test/fixtures/expected/tint-alpha.png differ diff --git a/test/fixtures/expected/tint-red.jpg b/test/fixtures/expected/tint-red.jpg new file mode 100644 index 00000000..f4167bea Binary files /dev/null and b/test/fixtures/expected/tint-red.jpg differ diff --git a/test/fixtures/expected/tint-sepia.jpg b/test/fixtures/expected/tint-sepia.jpg new file mode 100644 index 00000000..e6c5dade Binary files /dev/null and b/test/fixtures/expected/tint-sepia.jpg differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 3ccc7167..efbbb916 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -89,6 +89,7 @@ module.exports = { inputPngTestJoinChannel: getPath('testJoinChannel.png'), inputPngTruncated: getPath('truncated.png'), // gm convert 2569067123_aca715a2ee_o.jpg -resize 320x240 saw.png ; head -c 10000 saw.png > truncated.png inputPngEmbed: getPath('embedgravitybird.png'), // Released to sharp under a CC BY 4.0 + inputPngRGBWithAlpha: getPath('2569067123_aca715a2ee_o.png'), // http://www.flickr.com/photos/grizdave/2569067123/ (same as inputJpg) 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 diff --git a/test/unit/tint.js b/test/unit/tint.js new file mode 100644 index 00000000..7e18b920 --- /dev/null +++ b/test/unit/tint.js @@ -0,0 +1,63 @@ +'use strict'; + +const assert = require('assert'); + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +describe('Tint', function () { + it('tints rgb image red', function (done) { + const output = fixtures.path('output.tint-red.jpg'); + sharp(fixtures.inputJpg) + .resize(320, 240) + .tint('#FF0000') + .toFile(output, function (err, info) { + if (err) throw err; + assert.strictEqual(true, info.size > 0); + fixtures.assertMaxColourDistance(output, fixtures.expected('tint-red.jpg'), 10); + done(); + }); + }); + + it('tints rgb image with sepia tone', function (done) { + const output = fixtures.path('output.tint-sepia.jpg'); + sharp(fixtures.inputJpg) + .resize(320, 240) + .tint('#704214') + .toFile(output, function (err, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), 10); + done(); + }); + }); + + it('tints rgb image with sepia tone with rgb colour', function (done) { + const output = fixtures.path('output.tint-sepia.jpg'); + sharp(fixtures.inputJpg) + .resize(320, 240) + .tint([112, 66, 20]) + .toFile(output, function (err, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), 10); + done(); + }); + }); + + it('tints rgb image with alpha channel', function (done) { + const output = fixtures.path('output.tint-alpha.png'); + sharp(fixtures.inputPngRGBWithAlpha) + .resize(320, 240) + .tint('#704214') + .toFile(output, function (err, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertMaxColourDistance(output, fixtures.expected('tint-alpha.png'), 10); + done(); + }); + }); +});