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();
+ });
+ });
+});