Skip to content

Commit 3f7313d

Browse files
lovelljcupitt
andauthoredNov 19, 2023
Improve tint luminance with weighting function (#3859)
Co-authored-by: John Cupitt <jcupitt@gmail.com>
1 parent 139e4b9 commit 3f7313d

17 files changed

+70
-55
lines changed
 

‎docs/api-colour.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## tint
2-
> tint(rgb) ⇒ <code>Sharp</code>
2+
> tint(tint) ⇒ <code>Sharp</code>
33
4-
Tint the image using the provided chroma while preserving the image luminance.
4+
Tint the image using the provided colour.
55
An alpha channel may be present and will be unchanged by the operation.
66

77

@@ -12,7 +12,7 @@ An alpha channel may be present and will be unchanged by the operation.
1212

1313
| Param | Type | Description |
1414
| --- | --- | --- |
15-
| rgb | <code>string</code> \| <code>Object</code> | parsed by the [color](https://www.npmjs.org/package/color) module to extract chroma values. |
15+
| tint | <code>String</code> \| <code>Object</code> | Parsed by the [color](https://www.npmjs.org/package/color) module. |
1616

1717
**Example**
1818
```js

‎docs/changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ Requires libvips v8.15.0
2020
* Options for `trim` operation must be an Object, add new `lineArt` option.
2121
[#2363](https://github.com/lovell/sharp/issues/2363)
2222

23+
* Improve luminance of `tint` operation with weighting function.
24+
[#3338](https://github.com/lovell/sharp/issues/3338)
25+
[@jcupitt](https://github.com/jcupitt)
26+
2327
* Ensure all `Error` objects contain a `stack` property.
2428
[#3653](https://github.com/lovell/sharp/issues/3653)
2529

‎docs/search-index.json

+1-1
Large diffs are not rendered by default.

‎lib/colour.js

+4-6
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,20 @@ const colourspace = {
1919
};
2020

2121
/**
22-
* Tint the image using the provided chroma while preserving the image luminance.
22+
* Tint the image using the provided colour.
2323
* An alpha channel may be present and will be unchanged by the operation.
2424
*
2525
* @example
2626
* const output = await sharp(input)
2727
* .tint({ r: 255, g: 240, b: 16 })
2828
* .toBuffer();
2929
*
30-
* @param {string|Object} rgb - parsed by the [color](https://www.npmjs.org/package/color) module to extract chroma values.
30+
* @param {string|Object} tint - Parsed by the [color](https://www.npmjs.org/package/color) module.
3131
* @returns {Sharp}
3232
* @throws {Error} Invalid parameter
3333
*/
34-
function tint (rgb) {
35-
const colour = color(rgb);
36-
this.options.tintA = colour.a();
37-
this.options.tintB = colour.b();
34+
function tint (tint) {
35+
this._setBackgroundColourOption('tint', tint);
3836
return this;
3937
}
4038

‎lib/constructor.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,7 @@ const Sharp = function (input, options) {
212212
kernel: 'lanczos3',
213213
fastShrinkOnLoad: true,
214214
// operations
215-
tintA: 128,
216-
tintB: 128,
215+
tint: [-1, 0, 0, 0],
217216
flatten: false,
218217
flattenBackground: [0, 0, 0],
219218
unflatten: false,

‎lib/index.d.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,12 @@ declare namespace sharp {
239239
//#region Color functions
240240

241241
/**
242-
* Tint the image using the provided chroma while preserving the image luminance.
242+
* Tint the image using the provided colour.
243243
* An alpha channel may be present and will be unchanged by the operation.
244-
* @param rgb Parsed by the color module to extract chroma values.
244+
* @param tint Parsed by the color module.
245245
* @returns A sharp instance that can be used to chain operations
246246
*/
247-
tint(rgb: Color): Sharp;
247+
tint(tint: Color): Sharp;
248248

249249
/**
250250
* Convert to 8-bit greyscale; 256 shades of grey.

‎src/operations.cc

+30-16
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,44 @@ using vips::VError;
1616

1717
namespace sharp {
1818
/*
19-
* Tint an image using the specified chroma, preserving the original image luminance
19+
* Tint an image using the provided RGB.
2020
*/
21-
VImage Tint(VImage image, double const a, double const b) {
22-
// Get original colourspace
21+
VImage Tint(VImage image, std::vector<double> const tint) {
22+
std::vector<double> const tintLab = (VImage::black(1, 1) + tint)
23+
.colourspace(VIPS_INTERPRETATION_LAB, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB))
24+
.getpoint(0, 0);
25+
// LAB identity function
26+
VImage identityLab = VImage::identity(VImage::option()->set("bands", 3))
27+
.colourspace(VIPS_INTERPRETATION_LAB, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB));
28+
// Scale luminance range, 0.0 to 1.0
29+
VImage l = identityLab[0] / 100;
30+
// Weighting functions
31+
VImage weightL = 1.0 - 4.0 * ((l - 0.5) * (l - 0.5));
32+
VImage weightAB = (weightL * tintLab).extract_band(1, VImage::option()->set("n", 2));
33+
identityLab = identityLab[0].bandjoin(weightAB);
34+
// Convert lookup table to sRGB
35+
VImage lut = identityLab.colourspace(VIPS_INTERPRETATION_sRGB,
36+
VImage::option()->set("source_space", VIPS_INTERPRETATION_LAB));
37+
// Original colourspace
2338
VipsInterpretation typeBeforeTint = image.interpretation();
2439
if (typeBeforeTint == VIPS_INTERPRETATION_RGB) {
2540
typeBeforeTint = VIPS_INTERPRETATION_sRGB;
2641
}
27-
// Extract luminance
28-
VImage luminance = image.colourspace(VIPS_INTERPRETATION_LAB)[0];
29-
// Create the tinted version by combining the L from the original and the chroma from the tint
30-
std::vector<double> chroma {a, b};
31-
VImage tinted = luminance
32-
.bandjoin(chroma)
33-
.copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_LAB))
34-
.colourspace(typeBeforeTint);
35-
// Attach original alpha channel, if any
42+
// Apply lookup table
3643
if (HasAlpha(image)) {
37-
// Extract original alpha channel
3844
VImage alpha = image[image.bands() - 1];
39-
// Join alpha channel to normalised image
40-
tinted = tinted.bandjoin(alpha);
45+
image = RemoveAlpha(image)
46+
.colourspace(VIPS_INTERPRETATION_B_W)
47+
.maplut(lut)
48+
.colourspace(typeBeforeTint)
49+
.bandjoin(alpha);
50+
} else {
51+
image = image
52+
.colourspace(VIPS_INTERPRETATION_B_W)
53+
.maplut(lut)
54+
.colourspace(typeBeforeTint);
4155
}
42-
return tinted;
56+
return image;
4357
}
4458

4559
/*

‎src/operations.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ using vips::VImage;
1515
namespace sharp {
1616

1717
/*
18-
* Tint an image using the specified chroma, preserving the original image luminance
18+
* Tint an image using the provided RGB.
1919
*/
20-
VImage Tint(VImage image, double const a, double const b);
20+
VImage Tint(VImage image, std::vector<double> const tint);
2121

2222
/*
2323
* Stretch luminance to cover full dynamic range.

‎src/pipeline.cc

+3-4
Original file line numberDiff line numberDiff line change
@@ -736,8 +736,8 @@ class PipelineWorker : public Napi::AsyncWorker {
736736
}
737737

738738
// Tint the image
739-
if (baton->tintA < 128.0 || baton->tintB < 128.0) {
740-
image = sharp::Tint(image, baton->tintA, baton->tintB);
739+
if (baton->tint[0] >= 0.0) {
740+
image = sharp::Tint(image, baton->tint);
741741
}
742742

743743
// Remove alpha channel, if any
@@ -1527,8 +1527,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
15271527
baton->normalise = sharp::AttrAsBool(options, "normalise");
15281528
baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");
15291529
baton->normaliseUpper = sharp::AttrAsUint32(options, "normaliseUpper");
1530-
baton->tintA = sharp::AttrAsDouble(options, "tintA");
1531-
baton->tintB = sharp::AttrAsDouble(options, "tintB");
1530+
baton->tint = sharp::AttrAsVectorOfDouble(options, "tint");
15321531
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
15331532
baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
15341533
baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");

‎src/pipeline.h

+2-4
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ struct PipelineBaton {
6969
bool premultiplied;
7070
bool tileCentre;
7171
bool fastShrinkOnLoad;
72-
double tintA;
73-
double tintB;
72+
std::vector<double> tint;
7473
bool flatten;
7574
std::vector<double> flattenBackground;
7675
bool unflatten;
@@ -239,8 +238,7 @@ struct PipelineBaton {
239238
attentionX(0),
240239
attentionY(0),
241240
premultiplied(false),
242-
tintA(128.0),
243-
tintB(128.0),
241+
tint{ -1.0, 0.0, 0.0, 0.0 },
244242
flatten(false),
245243
flattenBackground{ 0.0, 0.0, 0.0 },
246244
unflatten(false),

‎test/fixtures/expected/tint-alpha.png

-7.36 KB

Error rendering embedded code

Invalid image source.

‎test/fixtures/expected/tint-blue.jpg

90 Bytes

Error rendering embedded code

Invalid image source.

‎test/fixtures/expected/tint-cmyk.jpg

4.11 KB

Error rendering embedded code

Invalid image source.

‎test/fixtures/expected/tint-green.jpg

1.1 KB

Error rendering embedded code

Invalid image source.

‎test/fixtures/expected/tint-red.jpg

2.41 KB

Error rendering embedded code

Invalid image source.

‎test/fixtures/expected/tint-sepia.jpg

485 Bytes

Error rendering embedded code

Invalid image source.

‎test/unit/tint.js

+17-14
Original file line numberDiff line numberDiff line change
@@ -8,97 +8,100 @@ const assert = require('assert');
88
const sharp = require('../../');
99
const fixtures = require('../fixtures');
1010

11+
// Allow for small rounding differences between platforms
12+
const maxDistance = 6;
13+
1114
describe('Tint', function () {
1215
it('tints rgb image red', function (done) {
1316
const output = fixtures.path('output.tint-red.jpg');
1417
sharp(fixtures.inputJpg)
15-
.resize(320, 240, { fastShrinkOnLoad: false })
18+
.resize(320, 240)
1619
.tint('#FF0000')
1720
.toFile(output, function (err, info) {
1821
if (err) throw err;
1922
assert.strictEqual(true, info.size > 0);
20-
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-red.jpg'), 18);
23+
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-red.jpg'), maxDistance);
2124
done();
2225
});
2326
});
2427

2528
it('tints rgb image green', function (done) {
2629
const output = fixtures.path('output.tint-green.jpg');
2730
sharp(fixtures.inputJpg)
28-
.resize(320, 240, { fastShrinkOnLoad: false })
31+
.resize(320, 240)
2932
.tint('#00FF00')
3033
.toFile(output, function (err, info) {
3134
if (err) throw err;
3235
assert.strictEqual(true, info.size > 0);
33-
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-green.jpg'), 27);
36+
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-green.jpg'), maxDistance);
3437
done();
3538
});
3639
});
3740

3841
it('tints rgb image blue', function (done) {
3942
const output = fixtures.path('output.tint-blue.jpg');
4043
sharp(fixtures.inputJpg)
41-
.resize(320, 240, { fastShrinkOnLoad: false })
44+
.resize(320, 240)
4245
.tint('#0000FF')
4346
.toFile(output, function (err, info) {
4447
if (err) throw err;
4548
assert.strictEqual(true, info.size > 0);
46-
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-blue.jpg'), 14);
49+
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-blue.jpg'), maxDistance);
4750
done();
4851
});
4952
});
5053

5154
it('tints rgb image with sepia tone', function (done) {
5255
const output = fixtures.path('output.tint-sepia-hex.jpg');
5356
sharp(fixtures.inputJpg)
54-
.resize(320, 240, { fastShrinkOnLoad: false })
57+
.resize(320, 240)
5558
.tint('#704214')
5659
.toFile(output, function (err, info) {
5760
if (err) throw err;
5861
assert.strictEqual(320, info.width);
5962
assert.strictEqual(240, info.height);
60-
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), 10);
63+
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), maxDistance);
6164
done();
6265
});
6366
});
6467

6568
it('tints rgb image with sepia tone with rgb colour', function (done) {
6669
const output = fixtures.path('output.tint-sepia-rgb.jpg');
6770
sharp(fixtures.inputJpg)
68-
.resize(320, 240, { fastShrinkOnLoad: false })
71+
.resize(320, 240)
6972
.tint([112, 66, 20])
7073
.toFile(output, function (err, info) {
7174
if (err) throw err;
7275
assert.strictEqual(320, info.width);
7376
assert.strictEqual(240, info.height);
74-
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), 10);
77+
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-sepia.jpg'), maxDistance);
7578
done();
7679
});
7780
});
7881

7982
it('tints rgb image with alpha channel', function (done) {
8083
const output = fixtures.path('output.tint-alpha.png');
8184
sharp(fixtures.inputPngRGBWithAlpha)
82-
.resize(320, 240, { fastShrinkOnLoad: false })
85+
.resize(320, 240)
8386
.tint('#704214')
8487
.toFile(output, function (err, info) {
8588
if (err) throw err;
8689
assert.strictEqual(320, info.width);
8790
assert.strictEqual(240, info.height);
88-
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-alpha.png'), 10);
91+
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-alpha.png'), maxDistance);
8992
done();
9093
});
9194
});
9295

9396
it('tints cmyk image red', function (done) {
9497
const output = fixtures.path('output.tint-cmyk.jpg');
9598
sharp(fixtures.inputJpgWithCmykProfile)
96-
.resize(320, 240, { fastShrinkOnLoad: false })
99+
.resize(320, 240)
97100
.tint('#FF0000')
98101
.toFile(output, function (err, info) {
99102
if (err) throw err;
100103
assert.strictEqual(true, info.size > 0);
101-
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-cmyk.jpg'), 15);
104+
fixtures.assertMaxColourDistance(output, fixtures.expected('tint-cmyk.jpg'), maxDistance);
102105
done();
103106
});
104107
});

0 commit comments

Comments
 (0)
Please sign in to comment.