@ -287,6 +287,41 @@ sharp(input)
|
|||||||
|
|
||||||
Returns **Sharp**
|
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
|
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
|
||||||
|
|
||||||
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
|
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
|
||||||
|
@ -139,6 +139,9 @@ const Sharp = function (input, options) {
|
|||||||
gammaOut: 0,
|
gammaOut: 0,
|
||||||
greyscale: false,
|
greyscale: false,
|
||||||
normalise: 0,
|
normalise: 0,
|
||||||
|
brightness: 1,
|
||||||
|
saturation: 1,
|
||||||
|
hue: 0,
|
||||||
booleanBufferIn: null,
|
booleanBufferIn: null,
|
||||||
booleanFileIn: '',
|
booleanFileIn: '',
|
||||||
joinChannelIn: [],
|
joinChannelIn: [],
|
||||||
|
@ -415,6 +415,62 @@ function recomb (inputMatrix) {
|
|||||||
return this;
|
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.
|
* Decorate the Sharp prototype with operation-related functions.
|
||||||
* @private
|
* @private
|
||||||
@ -436,6 +492,7 @@ module.exports = function (Sharp) {
|
|||||||
threshold,
|
threshold,
|
||||||
boolean,
|
boolean,
|
||||||
linear,
|
linear,
|
||||||
recomb
|
recomb,
|
||||||
|
modulate
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -185,6 +185,21 @@ namespace sharp {
|
|||||||
0.0, 0.0, 0.0, 1.0));
|
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.
|
* 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);
|
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
|
} // namespace sharp
|
||||||
|
|
||||||
#endif // SRC_OPERATIONS_H_
|
#endif // SRC_OPERATIONS_H_
|
||||||
|
@ -349,6 +349,7 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
bool const shouldSharpen = baton->sharpenSigma != 0.0;
|
bool const shouldSharpen = baton->sharpenSigma != 0.0;
|
||||||
bool const shouldApplyMedian = baton->medianSize > 0;
|
bool const shouldApplyMedian = baton->medianSize > 0;
|
||||||
bool const shouldComposite = !baton->composite.empty();
|
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)) {
|
if (shouldComposite && !HasAlpha(image)) {
|
||||||
image = sharp::EnsureAlpha(image);
|
image = sharp::EnsureAlpha(image);
|
||||||
@ -528,6 +529,10 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
image = sharp::Recomb(image, baton->recombMatrix);
|
image = sharp::Recomb(image, baton->recombMatrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldModulate) {
|
||||||
|
image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue);
|
||||||
|
}
|
||||||
|
|
||||||
// Sharpen
|
// Sharpen
|
||||||
if (shouldSharpen) {
|
if (shouldSharpen) {
|
||||||
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
|
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
|
||||||
@ -1210,6 +1215,9 @@ NAN_METHOD(pipeline) {
|
|||||||
baton->flattenBackground = AttrAsRgba(options, "flattenBackground");
|
baton->flattenBackground = AttrAsRgba(options, "flattenBackground");
|
||||||
baton->negate = AttrTo<bool>(options, "negate");
|
baton->negate = AttrTo<bool>(options, "negate");
|
||||||
baton->blurSigma = AttrTo<double>(options, "blurSigma");
|
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->medianSize = AttrTo<uint32_t>(options, "medianSize");
|
||||||
baton->sharpenSigma = AttrTo<double>(options, "sharpenSigma");
|
baton->sharpenSigma = AttrTo<double>(options, "sharpenSigma");
|
||||||
baton->sharpenFlat = AttrTo<double>(options, "sharpenFlat");
|
baton->sharpenFlat = AttrTo<double>(options, "sharpenFlat");
|
||||||
|
@ -87,6 +87,9 @@ struct PipelineBaton {
|
|||||||
std::vector<double> flattenBackground;
|
std::vector<double> flattenBackground;
|
||||||
bool negate;
|
bool negate;
|
||||||
double blurSigma;
|
double blurSigma;
|
||||||
|
double brightness;
|
||||||
|
double saturation;
|
||||||
|
int hue;
|
||||||
int medianSize;
|
int medianSize;
|
||||||
double sharpenSigma;
|
double sharpenSigma;
|
||||||
double sharpenFlat;
|
double sharpenFlat;
|
||||||
@ -189,6 +192,9 @@ struct PipelineBaton {
|
|||||||
flattenBackground{ 0.0, 0.0, 0.0 },
|
flattenBackground{ 0.0, 0.0, 0.0 },
|
||||||
negate(false),
|
negate(false),
|
||||||
blurSigma(0.0),
|
blurSigma(0.0),
|
||||||
|
brightness(1.0),
|
||||||
|
saturation(1.0),
|
||||||
|
hue(0.0),
|
||||||
medianSize(0),
|
medianSize(0),
|
||||||
sharpenSigma(0.0),
|
sharpenSigma(0.0),
|
||||||
sharpenFlat(1.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'),
|
outputTiff: getPath('output.tiff'),
|
||||||
outputZoinks: getPath('output.zoinks'), // an 'unknown' file extension
|
outputZoinks: getPath('output.zoinks'), // an 'unknown' file extension
|
||||||
|
|
||||||
|
testPattern: getPath('test-pattern.png'),
|
||||||
|
|
||||||
// Path for tests requiring human inspection
|
// Path for tests requiring human inspection
|
||||||
path: getPath,
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|