@ -287,6 +287,41 @@ sharp(input)
|
||||
|
||||
Returns **Sharp**
|
||||
|
||||
## modulate
|
||||
|
||||
Transforms the image using brightness, saturation and hue rotation.
|
||||
|
||||
### Parameters
|
||||
|
||||
- `options` **[Object][2]?**
|
||||
- `options.brightness` **[Number][1]?** Brightness multiplier
|
||||
- `options.saturation` **[Number][1]?** Saturation multiplier
|
||||
- `options.hue` **[Number][1]?** Degrees for hue rotation
|
||||
|
||||
### Examples
|
||||
|
||||
```javascript
|
||||
sharp(input)
|
||||
.modulate({
|
||||
brightness: 2 // increase lightness by a factor of 2
|
||||
});
|
||||
|
||||
sharp(input)
|
||||
.modulate({
|
||||
hue: 180 // hue-rotate by 180 degrees
|
||||
});
|
||||
|
||||
// decreate brightness and saturation while also hue-rotating by 90 degrees
|
||||
sharp(input)
|
||||
.modulate({
|
||||
brightness: 0.5,
|
||||
saturation: 0.5,
|
||||
hue: 90
|
||||
});
|
||||
```
|
||||
|
||||
Returns **Sharp**
|
||||
|
||||
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
|
||||
|
||||
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
|
||||
|
@ -139,6 +139,9 @@ const Sharp = function (input, options) {
|
||||
gammaOut: 0,
|
||||
greyscale: false,
|
||||
normalise: 0,
|
||||
brightness: 1,
|
||||
saturation: 1,
|
||||
hue: 0,
|
||||
booleanBufferIn: null,
|
||||
booleanFileIn: '',
|
||||
joinChannelIn: [],
|
||||
|
@ -415,6 +415,62 @@ function recomb (inputMatrix) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the image using brightness, saturation and hue rotation.
|
||||
*
|
||||
* @example
|
||||
* sharp(input)
|
||||
* .modulate({
|
||||
* brightness: 2 // increase lightness by a factor of 2
|
||||
* });
|
||||
*
|
||||
* sharp(input)
|
||||
* .modulate({
|
||||
* hue: 180 // hue-rotate by 180 degrees
|
||||
* });
|
||||
*
|
||||
* // decreate brightness and saturation while also hue-rotating by 90 degrees
|
||||
* sharp(input)
|
||||
* .modulate({
|
||||
* brightness: 0.5,
|
||||
* saturation: 0.5,
|
||||
* hue: 90
|
||||
* });
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {Number} [options.brightness] Brightness multiplier
|
||||
* @param {Number} [options.saturation] Saturation multiplier
|
||||
* @param {Number} [options.hue] Degrees for hue rotation
|
||||
* @returns {Sharp}
|
||||
*/
|
||||
function modulate (options) {
|
||||
if (!is.plainObject(options)) {
|
||||
throw is.invalidParameterError('options', 'plain object', options);
|
||||
}
|
||||
if ('brightness' in options) {
|
||||
if (is.number(options.brightness) && options.brightness >= 0) {
|
||||
this.options.brightness = options.brightness;
|
||||
} else {
|
||||
throw is.invalidParameterError('brightness', 'number above zero', options.brightness);
|
||||
}
|
||||
}
|
||||
if ('saturation' in options) {
|
||||
if (is.number(options.saturation) && options.saturation >= 0) {
|
||||
this.options.saturation = options.saturation;
|
||||
} else {
|
||||
throw is.invalidParameterError('saturation', 'number above zero', options.saturation);
|
||||
}
|
||||
}
|
||||
if ('hue' in options) {
|
||||
if (is.integer(options.hue)) {
|
||||
this.options.hue = options.hue % 360;
|
||||
} else {
|
||||
throw is.invalidParameterError('hue', 'number', options.hue);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorate the Sharp prototype with operation-related functions.
|
||||
* @private
|
||||
@ -436,6 +492,7 @@ module.exports = function (Sharp) {
|
||||
threshold,
|
||||
boolean,
|
||||
linear,
|
||||
recomb
|
||||
recomb,
|
||||
modulate
|
||||
});
|
||||
};
|
||||
|
@ -185,6 +185,21 @@ namespace sharp {
|
||||
0.0, 0.0, 0.0, 1.0));
|
||||
}
|
||||
|
||||
VImage Modulate(VImage image, double const brightness, double const saturation, int const hue) {
|
||||
if (HasAlpha(image)) {
|
||||
// Separate alpha channel
|
||||
VImage alpha = image[image.bands() - 1];
|
||||
return RemoveAlpha(image)
|
||||
.colourspace(VIPS_INTERPRETATION_LCH)
|
||||
.linear({brightness, saturation, 1}, {0, 0, static_cast<double>(hue)})
|
||||
.bandjoin(alpha);
|
||||
} else {
|
||||
return image
|
||||
.colourspace(VIPS_INTERPRETATION_LCH)
|
||||
.linear({brightness, saturation, 1}, {0, 0, static_cast<double>(hue)});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
|
||||
*/
|
||||
|
@ -97,6 +97,11 @@ namespace sharp {
|
||||
*/
|
||||
VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix);
|
||||
|
||||
/*
|
||||
* Modulate brightness, saturation and hue
|
||||
*/
|
||||
VImage Modulate(VImage image, double const brightness, double const saturation, int const hue);
|
||||
|
||||
} // namespace sharp
|
||||
|
||||
#endif // SRC_OPERATIONS_H_
|
||||
|
@ -349,6 +349,7 @@ class PipelineWorker : public Nan::AsyncWorker {
|
||||
bool const shouldSharpen = baton->sharpenSigma != 0.0;
|
||||
bool const shouldApplyMedian = baton->medianSize > 0;
|
||||
bool const shouldComposite = !baton->composite.empty();
|
||||
bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0;
|
||||
|
||||
if (shouldComposite && !HasAlpha(image)) {
|
||||
image = sharp::EnsureAlpha(image);
|
||||
@ -528,6 +529,10 @@ class PipelineWorker : public Nan::AsyncWorker {
|
||||
image = sharp::Recomb(image, baton->recombMatrix);
|
||||
}
|
||||
|
||||
if (shouldModulate) {
|
||||
image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue);
|
||||
}
|
||||
|
||||
// Sharpen
|
||||
if (shouldSharpen) {
|
||||
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
|
||||
@ -1210,6 +1215,9 @@ NAN_METHOD(pipeline) {
|
||||
baton->flattenBackground = AttrAsRgba(options, "flattenBackground");
|
||||
baton->negate = AttrTo<bool>(options, "negate");
|
||||
baton->blurSigma = AttrTo<double>(options, "blurSigma");
|
||||
baton->brightness = AttrTo<double>(options, "brightness");
|
||||
baton->saturation = AttrTo<double>(options, "saturation");
|
||||
baton->hue = AttrTo<int32_t>(options, "hue");
|
||||
baton->medianSize = AttrTo<uint32_t>(options, "medianSize");
|
||||
baton->sharpenSigma = AttrTo<double>(options, "sharpenSigma");
|
||||
baton->sharpenFlat = AttrTo<double>(options, "sharpenFlat");
|
||||
|
@ -87,6 +87,9 @@ struct PipelineBaton {
|
||||
std::vector<double> flattenBackground;
|
||||
bool negate;
|
||||
double blurSigma;
|
||||
double brightness;
|
||||
double saturation;
|
||||
int hue;
|
||||
int medianSize;
|
||||
double sharpenSigma;
|
||||
double sharpenFlat;
|
||||
@ -189,6 +192,9 @@ struct PipelineBaton {
|
||||
flattenBackground{ 0.0, 0.0, 0.0 },
|
||||
negate(false),
|
||||
blurSigma(0.0),
|
||||
brightness(1.0),
|
||||
saturation(1.0),
|
||||
hue(0.0),
|
||||
medianSize(0),
|
||||
sharpenSigma(0.0),
|
||||
sharpenFlat(1.0),
|
||||
|
BIN
test/fixtures/expected/modulate-all.jpg
vendored
Normal file
After Width: | Height: | Size: 664 KiB |
BIN
test/fixtures/expected/modulate-brightness-0-5.jpg
vendored
Normal file
After Width: | Height: | Size: 426 KiB |
BIN
test/fixtures/expected/modulate-brightness-2.jpg
vendored
Normal file
After Width: | Height: | Size: 692 KiB |
BIN
test/fixtures/expected/modulate-hue-120.jpg
vendored
Normal file
After Width: | Height: | Size: 653 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-120.png
vendored
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-150.png
vendored
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-180.png
vendored
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-210.png
vendored
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-240.png
vendored
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-270.png
vendored
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-30.png
vendored
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-300.png
vendored
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-330.png
vendored
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-360.png
vendored
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-60.png
vendored
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
test/fixtures/expected/modulate-hue-angle-90.png
vendored
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
test/fixtures/expected/modulate-saturation-0.5.jpg
vendored
Normal file
After Width: | Height: | Size: 606 KiB |
BIN
test/fixtures/expected/modulate-saturation-2.jpg
vendored
Normal file
After Width: | Height: | Size: 672 KiB |
2
test/fixtures/index.js
vendored
@ -120,6 +120,8 @@ module.exports = {
|
||||
outputTiff: getPath('output.tiff'),
|
||||
outputZoinks: getPath('output.zoinks'), // an 'unknown' file extension
|
||||
|
||||
testPattern: getPath('test-pattern.png'),
|
||||
|
||||
// Path for tests requiring human inspection
|
||||
path: getPath,
|
||||
|
||||
|
BIN
test/fixtures/test-pattern.png
vendored
Normal file
After Width: | Height: | Size: 79 KiB |
125
test/unit/modulate.js
Normal file
@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const sharp = require('../../');
|
||||
const assert = require('assert');
|
||||
const fixtures = require('../fixtures');
|
||||
|
||||
describe('Modulate', function () {
|
||||
describe('Invalid options', function () {
|
||||
[
|
||||
null,
|
||||
undefined,
|
||||
10,
|
||||
{ brightness: -1 },
|
||||
{ brightness: '50%' },
|
||||
{ brightness: null },
|
||||
{ saturation: -1 },
|
||||
{ saturation: '50%' },
|
||||
{ saturation: null },
|
||||
{ hue: '50deg' },
|
||||
{ hue: 1.5 },
|
||||
{ hue: null }
|
||||
].forEach(function (options) {
|
||||
it('should throw', function () {
|
||||
assert.throws(function () {
|
||||
sharp(fixtures.inputJpg).modulate(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to hue-rotate', function () {
|
||||
const base = 'modulate-hue-120.jpg';
|
||||
const actual = fixtures.path('output.' + base);
|
||||
const expected = fixtures.expected(base);
|
||||
|
||||
return sharp(fixtures.inputJpg)
|
||||
.modulate({ hue: 120 })
|
||||
.toFile(actual)
|
||||
.then(function () {
|
||||
fixtures.assertMaxColourDistance(actual, expected, 25);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to brighten', function () {
|
||||
const base = 'modulate-brightness-2.jpg';
|
||||
const actual = fixtures.path('output.' + base);
|
||||
const expected = fixtures.expected(base);
|
||||
|
||||
return sharp(fixtures.inputJpg)
|
||||
.modulate({ brightness: 2 })
|
||||
.toFile(actual)
|
||||
.then(function () {
|
||||
fixtures.assertMaxColourDistance(actual, expected, 25);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to unbrighten', function () {
|
||||
const base = 'modulate-brightness-0-5.jpg';
|
||||
const actual = fixtures.path('output.' + base);
|
||||
const expected = fixtures.expected(base);
|
||||
|
||||
return sharp(fixtures.inputJpg)
|
||||
.modulate({ brightness: 0.5 })
|
||||
.toFile(actual)
|
||||
.then(function () {
|
||||
fixtures.assertMaxColourDistance(actual, expected, 25);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to saturate', function () {
|
||||
const base = 'modulate-saturation-2.jpg';
|
||||
const actual = fixtures.path('output.' + base);
|
||||
const expected = fixtures.expected(base);
|
||||
|
||||
return sharp(fixtures.inputJpg)
|
||||
.modulate({ saturation: 2 })
|
||||
.toFile(actual)
|
||||
.then(function () {
|
||||
fixtures.assertMaxColourDistance(actual, expected, 30);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to desaturate', function () {
|
||||
const base = 'modulate-saturation-0.5.jpg';
|
||||
const actual = fixtures.path('output.' + base);
|
||||
const expected = fixtures.expected(base);
|
||||
|
||||
return sharp(fixtures.inputJpg)
|
||||
.modulate({ saturation: 0.5 })
|
||||
.toFile(actual)
|
||||
.then(function () {
|
||||
fixtures.assertMaxColourDistance(actual, expected, 25);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to modulate all channels', function () {
|
||||
const base = 'modulate-all.jpg';
|
||||
const actual = fixtures.path('output.' + base);
|
||||
const expected = fixtures.expected(base);
|
||||
|
||||
return sharp(fixtures.inputJpg)
|
||||
.modulate({ brightness: 2, saturation: 0.5, hue: 180 })
|
||||
.toFile(actual)
|
||||
.then(function () {
|
||||
fixtures.assertMaxColourDistance(actual, expected, 25);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hue-rotate', function (done) {
|
||||
[30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, 360].forEach(function (angle) {
|
||||
it('should properly hue rotate by ' + angle + 'deg', function () {
|
||||
const base = 'modulate-hue-angle-' + angle + '.png';
|
||||
const actual = fixtures.path('output.' + base);
|
||||
const expected = fixtures.expected(base);
|
||||
|
||||
return sharp(fixtures.testPattern)
|
||||
.modulate({ hue: angle })
|
||||
.toFile(actual)
|
||||
.then(function () {
|
||||
fixtures.assertMaxColourDistance(actual, expected, 25);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|