sharp 0.27.1 → 0.30.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/libvips.js CHANGED
@@ -4,24 +4,29 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const spawnSync = require('child_process').spawnSync;
7
- const semver = require('semver');
7
+ const semverCoerce = require('semver/functions/coerce');
8
+ const semverGreaterThanOrEqualTo = require('semver/functions/gte');
9
+
8
10
  const platform = require('./platform');
11
+ const { config } = require('../package.json');
9
12
 
10
13
  const env = process.env;
11
14
  const minimumLibvipsVersionLabelled = env.npm_package_config_libvips || /* istanbul ignore next */
12
- require('../package.json').config.libvips;
13
- const minimumLibvipsVersion = semver.coerce(minimumLibvipsVersionLabelled).version;
15
+ config.libvips;
16
+ const minimumLibvipsVersion = semverCoerce(minimumLibvipsVersionLabelled).version;
14
17
 
15
18
  const spawnSyncOptions = {
16
19
  encoding: 'utf8',
17
20
  shell: true
18
21
  };
19
22
 
23
+ const vendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platform());
24
+
20
25
  const mkdirSync = function (dirPath) {
21
26
  try {
22
- fs.mkdirSync(dirPath);
27
+ fs.mkdirSync(dirPath, { recursive: true });
23
28
  } catch (err) {
24
- /* istanbul ignore if */
29
+ /* istanbul ignore next */
25
30
  if (err.code !== 'EEXIST') {
26
31
  throw err;
27
32
  }
@@ -37,9 +42,35 @@ const cachePath = function () {
37
42
  return libvipsCachePath;
38
43
  };
39
44
 
45
+ const integrity = function (platformAndArch) {
46
+ return env[`npm_package_config_integrity_${platformAndArch.replace('-', '_')}`] || config.integrity[platformAndArch];
47
+ };
48
+
49
+ const log = function (item) {
50
+ if (item instanceof Error) {
51
+ console.error(`sharp: Installation error: ${item.message}`);
52
+ } else {
53
+ console.log(`sharp: ${item}`);
54
+ }
55
+ };
56
+
57
+ const isRosetta = function () {
58
+ /* istanbul ignore next */
59
+ if (process.platform === 'darwin' && process.arch === 'x64') {
60
+ const translated = spawnSync('sysctl sysctl.proc_translated', spawnSyncOptions).stdout;
61
+ return (translated || '').trim() === 'sysctl.proc_translated: 1';
62
+ }
63
+ return false;
64
+ };
65
+
40
66
  const globalLibvipsVersion = function () {
41
67
  if (process.platform !== 'win32') {
42
- const globalLibvipsVersion = spawnSync(`PKG_CONFIG_PATH="${pkgConfigPath()}" pkg-config --modversion vips-cpp`, spawnSyncOptions).stdout;
68
+ const globalLibvipsVersion = spawnSync('pkg-config --modversion vips-cpp', {
69
+ ...spawnSyncOptions,
70
+ env: {
71
+ PKG_CONFIG_PATH: pkgConfigPath()
72
+ }
73
+ }).stdout;
43
74
  /* istanbul ignore next */
44
75
  return (globalLibvipsVersion || '').trim();
45
76
  } else {
@@ -48,31 +79,29 @@ const globalLibvipsVersion = function () {
48
79
  };
49
80
 
50
81
  const hasVendoredLibvips = function () {
51
- const currentPlatformId = platform();
52
- const vendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion);
53
- let vendorPlatformId;
54
- try {
55
- vendorPlatformId = require(path.join(vendorPath, 'platform.json'));
56
- } catch (err) {}
57
- /* istanbul ignore else */
58
- if (vendorPlatformId) {
59
- /* istanbul ignore else */
60
- if (currentPlatformId === vendorPlatformId) {
61
- return true;
62
- } else {
63
- throw new Error(`'${vendorPlatformId}' binaries cannot be used on the '${currentPlatformId}' platform. Please remove the 'node_modules/sharp' directory and run 'npm install' on the '${currentPlatformId}' platform.`);
64
- }
65
- } else {
66
- return false;
67
- }
82
+ return fs.existsSync(vendorPath);
83
+ };
84
+
85
+ /* istanbul ignore next */
86
+ const removeVendoredLibvips = function () {
87
+ const rm = fs.rmSync ? fs.rmSync : fs.rmdirSync;
88
+ rm(vendorPath, { recursive: true, maxRetries: 3, force: true });
68
89
  };
69
90
 
70
91
  const pkgConfigPath = function () {
71
92
  if (process.platform !== 'win32') {
72
- const brewPkgConfigPath = spawnSync('which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR', spawnSyncOptions).stdout || '';
73
- return [brewPkgConfigPath.trim(), env.PKG_CONFIG_PATH, '/usr/local/lib/pkgconfig', '/usr/lib/pkgconfig']
74
- .filter(function (p) { return !!p; })
75
- .join(':');
93
+ const brewPkgConfigPath = spawnSync(
94
+ 'which brew >/dev/null 2>&1 && brew environment --plain | grep PKG_CONFIG_LIBDIR | cut -d" " -f2',
95
+ spawnSyncOptions
96
+ ).stdout || '';
97
+ return [
98
+ brewPkgConfigPath.trim(),
99
+ env.PKG_CONFIG_PATH,
100
+ '/usr/local/lib/pkgconfig',
101
+ '/usr/lib/pkgconfig',
102
+ '/usr/local/libdata/pkgconfig',
103
+ '/usr/libdata/pkgconfig'
104
+ ].filter(Boolean).join(':');
76
105
  } else {
77
106
  return '';
78
107
  }
@@ -82,18 +111,24 @@ const useGlobalLibvips = function () {
82
111
  if (Boolean(env.SHARP_IGNORE_GLOBAL_LIBVIPS) === true) {
83
112
  return false;
84
113
  }
85
-
114
+ /* istanbul ignore next */
115
+ if (isRosetta()) {
116
+ return false;
117
+ }
86
118
  const globalVipsVersion = globalLibvipsVersion();
87
119
  return !!globalVipsVersion && /* istanbul ignore next */
88
- semver.gte(globalVipsVersion, minimumLibvipsVersion);
120
+ semverGreaterThanOrEqualTo(globalVipsVersion, minimumLibvipsVersion);
89
121
  };
90
122
 
91
123
  module.exports = {
92
124
  minimumLibvipsVersion,
93
125
  minimumLibvipsVersionLabelled,
94
126
  cachePath,
127
+ integrity,
128
+ log,
95
129
  globalLibvipsVersion,
96
130
  hasVendoredLibvips,
131
+ removeVendoredLibvips,
97
132
  pkgConfigPath,
98
133
  useGlobalLibvips,
99
134
  mkdirSync
package/lib/operation.js CHANGED
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const { flatten: flattenArray } = require('array-flatten');
4
3
  const color = require('color');
5
4
  const is = require('./is');
6
5
 
@@ -64,6 +63,10 @@ function rotate (angle, options) {
64
63
  /**
65
64
  * Flip the image about the vertical Y axis. This always occurs after rotation, if any.
66
65
  * The use of `flip` implies the removal of the EXIF `Orientation` tag, if any.
66
+ *
67
+ * @example
68
+ * const output = await sharp(input).flip().toBuffer();
69
+ *
67
70
  * @param {Boolean} [flip=true]
68
71
  * @returns {Sharp}
69
72
  */
@@ -75,6 +78,10 @@ function flip (flip) {
75
78
  /**
76
79
  * Flop the image about the horizontal X axis. This always occurs after rotation, if any.
77
80
  * The use of `flop` implies the removal of the EXIF `Orientation` tag, if any.
81
+ *
82
+ * @example
83
+ * const output = await sharp(input).flop().toBuffer();
84
+ *
78
85
  * @param {Boolean} [flop=true]
79
86
  * @returns {Sharp}
80
87
  */
@@ -127,7 +134,7 @@ function flop (flop) {
127
134
  * @throws {Error} Invalid parameters
128
135
  */
129
136
  function affine (matrix, options) {
130
- const flatMatrix = flattenArray(matrix);
137
+ const flatMatrix = [].concat(...matrix);
131
138
  if (flatMatrix.length === 4 && flatMatrix.every(is.number)) {
132
139
  this.options.affineMatrix = flatMatrix;
133
140
  } else {
@@ -186,40 +193,107 @@ function affine (matrix, options) {
186
193
  * When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space.
187
194
  * Separate control over the level of sharpening in "flat" and "jagged" areas is available.
188
195
  *
189
- * @param {number} [sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
190
- * @param {number} [flat=1.0] - the level of sharpening to apply to "flat" areas.
191
- * @param {number} [jagged=2.0] - the level of sharpening to apply to "jagged" areas.
196
+ * See {@link https://www.libvips.org/API/current/libvips-convolution.html#vips-sharpen|libvips sharpen} operation.
197
+ *
198
+ * @example
199
+ * const data = await sharp(input).sharpen().toBuffer();
200
+ *
201
+ * @example
202
+ * const data = await sharp(input).sharpen({ sigma: 2 }).toBuffer();
203
+ *
204
+ * @example
205
+ * const data = await sharp(input)
206
+ * .sharpen({
207
+ * sigma: 2,
208
+ * m1: 0
209
+ * m2: 3,
210
+ * x1: 3,
211
+ * y2: 15,
212
+ * y3: 15,
213
+ * })
214
+ * .toBuffer();
215
+ *
216
+ * @param {Object|number} [options] - if present, is an Object with attributes or (deprecated) a number for `options.sigma`.
217
+ * @param {number} [options.sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
218
+ * @param {number} [options.m1=1.0] - the level of sharpening to apply to "flat" areas.
219
+ * @param {number} [options.m2=2.0] - the level of sharpening to apply to "jagged" areas.
220
+ * @param {number} [options.x1=2.0] - threshold between "flat" and "jagged"
221
+ * @param {number} [options.y2=10.0] - maximum amount of brightening.
222
+ * @param {number} [options.y3=20.0] - maximum amount of darkening.
223
+ * @param {number} [flat] - (deprecated) see `options.m1`.
224
+ * @param {number} [jagged] - (deprecated) see `options.m2`.
192
225
  * @returns {Sharp}
193
226
  * @throws {Error} Invalid parameters
194
227
  */
195
- function sharpen (sigma, flat, jagged) {
196
- if (!is.defined(sigma)) {
228
+ function sharpen (options, flat, jagged) {
229
+ if (!is.defined(options)) {
197
230
  // No arguments: default to mild sharpen
198
231
  this.options.sharpenSigma = -1;
199
- } else if (is.bool(sigma)) {
200
- // Boolean argument: apply mild sharpen?
201
- this.options.sharpenSigma = sigma ? -1 : 0;
202
- } else if (is.number(sigma) && is.inRange(sigma, 0.01, 10000)) {
203
- // Numeric argument: specific sigma
204
- this.options.sharpenSigma = sigma;
205
- // Control over flat areas
232
+ } else if (is.bool(options)) {
233
+ // Deprecated boolean argument: apply mild sharpen?
234
+ this.options.sharpenSigma = options ? -1 : 0;
235
+ } else if (is.number(options) && is.inRange(options, 0.01, 10000)) {
236
+ // Deprecated numeric argument: specific sigma
237
+ this.options.sharpenSigma = options;
238
+ // Deprecated control over flat areas
206
239
  if (is.defined(flat)) {
207
240
  if (is.number(flat) && is.inRange(flat, 0, 10000)) {
208
- this.options.sharpenFlat = flat;
241
+ this.options.sharpenM1 = flat;
209
242
  } else {
210
243
  throw is.invalidParameterError('flat', 'number between 0 and 10000', flat);
211
244
  }
212
245
  }
213
- // Control over jagged areas
246
+ // Deprecated control over jagged areas
214
247
  if (is.defined(jagged)) {
215
248
  if (is.number(jagged) && is.inRange(jagged, 0, 10000)) {
216
- this.options.sharpenJagged = jagged;
249
+ this.options.sharpenM2 = jagged;
217
250
  } else {
218
251
  throw is.invalidParameterError('jagged', 'number between 0 and 10000', jagged);
219
252
  }
220
253
  }
254
+ } else if (is.plainObject(options)) {
255
+ if (is.number(options.sigma) && is.inRange(options.sigma, 0.01, 10000)) {
256
+ this.options.sharpenSigma = options.sigma;
257
+ } else {
258
+ throw is.invalidParameterError('options.sigma', 'number between 0.01 and 10000', options.sigma);
259
+ }
260
+ if (is.defined(options.m1)) {
261
+ if (is.number(options.m1) && is.inRange(options.m1, 0, 10000)) {
262
+ this.options.sharpenM1 = options.m1;
263
+ } else {
264
+ throw is.invalidParameterError('options.m1', 'number between 0 and 10000', options.m1);
265
+ }
266
+ }
267
+ if (is.defined(options.m2)) {
268
+ if (is.number(options.m2) && is.inRange(options.m2, 0, 10000)) {
269
+ this.options.sharpenM2 = options.m2;
270
+ } else {
271
+ throw is.invalidParameterError('options.m2', 'number between 0 and 10000', options.m2);
272
+ }
273
+ }
274
+ if (is.defined(options.x1)) {
275
+ if (is.number(options.x1) && is.inRange(options.x1, 0, 10000)) {
276
+ this.options.sharpenX1 = options.x1;
277
+ } else {
278
+ throw is.invalidParameterError('options.x1', 'number between 0 and 10000', options.x1);
279
+ }
280
+ }
281
+ if (is.defined(options.y2)) {
282
+ if (is.number(options.y2) && is.inRange(options.y2, 0, 10000)) {
283
+ this.options.sharpenY2 = options.y2;
284
+ } else {
285
+ throw is.invalidParameterError('options.y2', 'number between 0 and 10000', options.y2);
286
+ }
287
+ }
288
+ if (is.defined(options.y3)) {
289
+ if (is.number(options.y3) && is.inRange(options.y3, 0, 10000)) {
290
+ this.options.sharpenY3 = options.y3;
291
+ } else {
292
+ throw is.invalidParameterError('options.y3', 'number between 0 and 10000', options.y3);
293
+ }
294
+ }
221
295
  } else {
222
- throw is.invalidParameterError('sigma', 'number between 0.01 and 10000', sigma);
296
+ throw is.invalidParameterError('sigma', 'number between 0.01 and 10000', options);
223
297
  }
224
298
  return this;
225
299
  }
@@ -227,6 +301,13 @@ function sharpen (sigma, flat, jagged) {
227
301
  /**
228
302
  * Apply median filter.
229
303
  * When used without parameters the default window is 3x3.
304
+ *
305
+ * @example
306
+ * const output = await sharp(input).median().toBuffer();
307
+ *
308
+ * @example
309
+ * const output = await sharp(input).median(5).toBuffer();
310
+ *
230
311
  * @param {number} [size=3] square mask size: size x size
231
312
  * @returns {Sharp}
232
313
  * @throws {Error} Invalid parameters
@@ -246,8 +327,21 @@ function median (size) {
246
327
 
247
328
  /**
248
329
  * Blur the image.
249
- * When used without parameters, performs a fast, mild blur of the output image.
330
+ *
331
+ * When used without parameters, performs a fast 3x3 box blur (equivalent to a box linear filter).
332
+ *
250
333
  * When a `sigma` is provided, performs a slower, more accurate Gaussian blur.
334
+ *
335
+ * @example
336
+ * const boxBlurred = await sharp(input)
337
+ * .blur()
338
+ * .toBuffer();
339
+ *
340
+ * @example
341
+ * const gaussianBlurred = await sharp(input)
342
+ * .blur(5)
343
+ * .toBuffer();
344
+ *
251
345
  * @param {number} [sigma] a value between 0.3 and 1000 representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
252
346
  * @returns {Sharp}
253
347
  * @throws {Error} Invalid parameters
@@ -269,7 +363,15 @@ function blur (sigma) {
269
363
  }
270
364
 
271
365
  /**
272
- * Merge alpha transparency channel, if any, with a background.
366
+ * Merge alpha transparency channel, if any, with a background, then remove the alpha channel.
367
+ *
368
+ * See also {@link /api-channel#removealpha|removeAlpha}.
369
+ *
370
+ * @example
371
+ * await sharp(rgbaInput)
372
+ * .flatten({ background: '#F0A703' })
373
+ * .toBuffer();
374
+ *
273
375
  * @param {Object} [options]
274
376
  * @param {string|Object} [options.background={r: 0, g: 0, b: 0}] - background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black.
275
377
  * @returns {Sharp}
@@ -318,16 +420,39 @@ function gamma (gamma, gammaOut) {
318
420
 
319
421
  /**
320
422
  * Produce the "negative" of the image.
321
- * @param {Boolean} [negate=true]
423
+ *
424
+ * @example
425
+ * const output = await sharp(input)
426
+ * .negate()
427
+ * .toBuffer();
428
+ *
429
+ * @example
430
+ * const output = await sharp(input)
431
+ * .negate({ alpha: false })
432
+ * .toBuffer();
433
+ *
434
+ * @param {Object} [options]
435
+ * @param {Boolean} [options.alpha=true] Whether or not to negate any alpha channel
322
436
  * @returns {Sharp}
323
437
  */
324
- function negate (negate) {
325
- this.options.negate = is.bool(negate) ? negate : true;
438
+ function negate (options) {
439
+ this.options.negate = is.bool(options) ? options : true;
440
+ if (is.plainObject(options) && 'alpha' in options) {
441
+ if (!is.bool(options.alpha)) {
442
+ throw is.invalidParameterError('alpha', 'should be boolean value', options.alpha);
443
+ } else {
444
+ this.options.negateAlpha = options.alpha;
445
+ }
446
+ }
326
447
  return this;
327
448
  }
328
449
 
329
450
  /**
330
451
  * Enhance output image contrast by stretching its luminance to cover the full dynamic range.
452
+ *
453
+ * @example
454
+ * const output = await sharp(input).normalise().toBuffer();
455
+ *
331
456
  * @param {Boolean} [normalise=true]
332
457
  * @returns {Sharp}
333
458
  */
@@ -338,6 +463,10 @@ function normalise (normalise) {
338
463
 
339
464
  /**
340
465
  * Alternative spelling of normalise.
466
+ *
467
+ * @example
468
+ * const output = await sharp(input).normalize().toBuffer();
469
+ *
341
470
  * @param {Boolean} [normalize=true]
342
471
  * @returns {Sharp}
343
472
  */
@@ -345,6 +474,55 @@ function normalize (normalize) {
345
474
  return this.normalise(normalize);
346
475
  }
347
476
 
477
+ /**
478
+ * Perform contrast limiting adaptive histogram equalization
479
+ * {@link https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE|CLAHE}.
480
+ *
481
+ * This will, in general, enhance the clarity of the image by bringing out darker details.
482
+ *
483
+ * @since 0.28.3
484
+ *
485
+ * @example
486
+ * const output = await sharp(input)
487
+ * .clahe({
488
+ * width: 3,
489
+ * height: 3,
490
+ * })
491
+ * .toBuffer();
492
+ *
493
+ * @param {Object} options
494
+ * @param {number} options.width - integer width of the region in pixels.
495
+ * @param {number} options.height - integer height of the region in pixels.
496
+ * @param {number} [options.maxSlope=3] - maximum value for the slope of the
497
+ * cumulative histogram. A value of 0 disables contrast limiting. Valid values
498
+ * are integers in the range 0-100 (inclusive)
499
+ * @returns {Sharp}
500
+ * @throws {Error} Invalid parameters
501
+ */
502
+ function clahe (options) {
503
+ if (!is.plainObject(options)) {
504
+ throw is.invalidParameterError('options', 'plain object', options);
505
+ }
506
+ if (!('width' in options) || !is.integer(options.width) || options.width <= 0) {
507
+ throw is.invalidParameterError('width', 'integer above zero', options.width);
508
+ } else {
509
+ this.options.claheWidth = options.width;
510
+ }
511
+ if (!('height' in options) || !is.integer(options.height) || options.height <= 0) {
512
+ throw is.invalidParameterError('height', 'integer above zero', options.height);
513
+ } else {
514
+ this.options.claheHeight = options.height;
515
+ }
516
+ if (!is.defined(options.maxSlope)) {
517
+ this.options.claheMaxSlope = 3;
518
+ } else if (!is.integer(options.maxSlope) || options.maxSlope < 0 || options.maxSlope > 100) {
519
+ throw is.invalidParameterError('maxSlope', 'integer 0-100', options.maxSlope);
520
+ } else {
521
+ this.options.claheMaxSlope = options.maxSlope;
522
+ }
523
+ return this;
524
+ }
525
+
348
526
  /**
349
527
  * Convolve the image with the specified kernel.
350
528
  *
@@ -363,7 +541,7 @@ function normalize (normalize) {
363
541
  *
364
542
  * @param {Object} kernel
365
543
  * @param {number} kernel.width - width of the kernel in pixels.
366
- * @param {number} kernel.height - width of the kernel in pixels.
544
+ * @param {number} kernel.height - height of the kernel in pixels.
367
545
  * @param {Array<number>} kernel.kernel - Array of length `width*height` containing the kernel values.
368
546
  * @param {number} [kernel.scale=sum] - the scale of the kernel in pixels.
369
547
  * @param {number} [kernel.offset=0] - the offset of the kernel in pixels.
@@ -397,7 +575,7 @@ function convolve (kernel) {
397
575
  }
398
576
 
399
577
  /**
400
- * Any pixel value greather than or equal to the threshold value will be set to 255, otherwise it will be set to 0.
578
+ * Any pixel value greater than or equal to the threshold value will be set to 255, otherwise it will be set to 0.
401
579
  * @param {number} [threshold=128] - a value in the range 0-255 representing the level at which the threshold will be applied.
402
580
  * @param {Object} [options]
403
581
  * @param {Boolean} [options.greyscale=true] - convert to single channel greyscale.
@@ -514,33 +692,51 @@ function recomb (inputMatrix) {
514
692
  }
515
693
 
516
694
  /**
517
- * Transforms the image using brightness, saturation and hue rotation.
695
+ * Transforms the image using brightness, saturation, hue rotation, and lightness.
696
+ * Brightness and lightness both operate on luminance, with the difference being that
697
+ * brightness is multiplicative whereas lightness is additive.
518
698
  *
519
699
  * @since 0.22.1
520
700
  *
521
701
  * @example
522
- * sharp(input)
702
+ * // increase brightness by a factor of 2
703
+ * const output = await sharp(input)
523
704
  * .modulate({
524
- * brightness: 2 // increase lightness by a factor of 2
525
- * });
705
+ * brightness: 2
706
+ * })
707
+ * .toBuffer();
526
708
  *
527
- * sharp(input)
709
+ * @example
710
+ * // hue-rotate by 180 degrees
711
+ * const output = await sharp(input)
528
712
  * .modulate({
529
- * hue: 180 // hue-rotate by 180 degrees
530
- * });
713
+ * hue: 180
714
+ * })
715
+ * .toBuffer();
531
716
  *
717
+ * @example
718
+ * // increase lightness by +50
719
+ * const output = await sharp(input)
720
+ * .modulate({
721
+ * lightness: 50
722
+ * })
723
+ * .toBuffer();
724
+ *
725
+ * @example
532
726
  * // decreate brightness and saturation while also hue-rotating by 90 degrees
533
- * sharp(input)
727
+ * const output = await sharp(input)
534
728
  * .modulate({
535
729
  * brightness: 0.5,
536
730
  * saturation: 0.5,
537
- * hue: 90
538
- * });
731
+ * hue: 90,
732
+ * })
733
+ * .toBuffer();
539
734
  *
540
735
  * @param {Object} [options]
541
736
  * @param {number} [options.brightness] Brightness multiplier
542
737
  * @param {number} [options.saturation] Saturation multiplier
543
738
  * @param {number} [options.hue] Degrees for hue rotation
739
+ * @param {number} [options.lightness] Lightness addend
544
740
  * @returns {Sharp}
545
741
  */
546
742
  function modulate (options) {
@@ -568,6 +764,13 @@ function modulate (options) {
568
764
  throw is.invalidParameterError('hue', 'number', options.hue);
569
765
  }
570
766
  }
767
+ if ('lightness' in options) {
768
+ if (is.number(options.lightness)) {
769
+ this.options.lightness = options.lightness;
770
+ } else {
771
+ throw is.invalidParameterError('lightness', 'number', options.lightness);
772
+ }
773
+ }
571
774
  return this;
572
775
  }
573
776
 
@@ -589,6 +792,7 @@ module.exports = function (Sharp) {
589
792
  negate,
590
793
  normalise,
591
794
  normalize,
795
+ clahe,
592
796
  convolve,
593
797
  threshold,
594
798
  boolean,