UNPKG

30.7 kBJavaScriptView Raw
1// Copyright 2013 Lovell Fuller and others.
2// SPDX-License-Identifier: Apache-2.0
3
4'use strict';
5
6const color = require('color');
7const is = require('./is');
8
9/**
10 * Rotate the output image by either an explicit angle
11 * or auto-orient based on the EXIF `Orientation` tag.
12 *
13 * If an angle is provided, it is converted to a valid positive degree rotation.
14 * For example, `-450` will produce a 270deg rotation.
15 *
16 * When rotating by an angle other than a multiple of 90,
17 * the background colour can be provided with the `background` option.
18 *
19 * If no angle is provided, it is determined from the EXIF data.
20 * Mirroring is supported and may infer the use of a flip operation.
21 *
22 * The use of `rotate` implies the removal of the EXIF `Orientation` tag, if any.
23 *
24 * Only one rotation can occur per pipeline.
25 * Previous calls to `rotate` in the same pipeline will be ignored.
26 *
27 * Method order is important when rotating, resizing and/or extracting regions,
28 * for example `.rotate(x).extract(y)` will produce a different result to `.extract(y).rotate(x)`.
29 *
30 * @example
31 * const pipeline = sharp()
32 * .rotate()
33 * .resize(null, 200)
34 * .toBuffer(function (err, outputBuffer, info) {
35 * // outputBuffer contains 200px high JPEG image data,
36 * // auto-rotated using EXIF Orientation tag
37 * // info.width and info.height contain the dimensions of the resized image
38 * });
39 * readableStream.pipe(pipeline);
40 *
41 * @example
42 * const rotateThenResize = await sharp(input)
43 * .rotate(90)
44 * .resize({ width: 16, height: 8, fit: 'fill' })
45 * .toBuffer();
46 * const resizeThenRotate = await sharp(input)
47 * .resize({ width: 16, height: 8, fit: 'fill' })
48 * .rotate(90)
49 * .toBuffer();
50 *
51 * @param {number} [angle=auto] angle of rotation.
52 * @param {Object} [options] - if present, is an Object with optional attributes.
53 * @param {string|Object} [options.background="#000000"] parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
54 * @returns {Sharp}
55 * @throws {Error} Invalid parameters
56 */
57function rotate (angle, options) {
58 if (this.options.useExifOrientation || this.options.angle || this.options.rotationAngle) {
59 this.options.debuglog('ignoring previous rotate options');
60 }
61 if (!is.defined(angle)) {
62 this.options.useExifOrientation = true;
63 } else if (is.integer(angle) && !(angle % 90)) {
64 this.options.angle = angle;
65 } else if (is.number(angle)) {
66 this.options.rotationAngle = angle;
67 if (is.object(options) && options.background) {
68 const backgroundColour = color(options.background);
69 this.options.rotationBackground = [
70 backgroundColour.red(),
71 backgroundColour.green(),
72 backgroundColour.blue(),
73 Math.round(backgroundColour.alpha() * 255)
74 ];
75 }
76 } else {
77 throw is.invalidParameterError('angle', 'numeric', angle);
78 }
79 return this;
80}
81
82/**
83 * Mirror the image vertically (up-down) about the x-axis.
84 * This always occurs before rotation, if any.
85 *
86 * The use of `flip` implies the removal of the EXIF `Orientation` tag, if any.
87 *
88 * This operation does not work correctly with multi-page images.
89 *
90 * @example
91 * const output = await sharp(input).flip().toBuffer();
92 *
93 * @param {Boolean} [flip=true]
94 * @returns {Sharp}
95 */
96function flip (flip) {
97 this.options.flip = is.bool(flip) ? flip : true;
98 return this;
99}
100
101/**
102 * Mirror the image horizontally (left-right) about the y-axis.
103 * This always occurs before rotation, if any.
104 *
105 * The use of `flop` implies the removal of the EXIF `Orientation` tag, if any.
106 *
107 * @example
108 * const output = await sharp(input).flop().toBuffer();
109 *
110 * @param {Boolean} [flop=true]
111 * @returns {Sharp}
112 */
113function flop (flop) {
114 this.options.flop = is.bool(flop) ? flop : true;
115 return this;
116}
117
118/**
119 * Perform an affine transform on an image. This operation will always occur after resizing, extraction and rotation, if any.
120 *
121 * You must provide an array of length 4 or a 2x2 affine transformation matrix.
122 * By default, new pixels are filled with a black background. You can provide a background color with the `background` option.
123 * A particular interpolator may also be specified. Set the `interpolator` option to an attribute of the `sharp.interpolators` Object e.g. `sharp.interpolators.nohalo`.
124 *
125 * In the case of a 2x2 matrix, the transform is:
126 * - X = `matrix[0, 0]` \* (x + `idx`) + `matrix[0, 1]` \* (y + `idy`) + `odx`
127 * - Y = `matrix[1, 0]` \* (x + `idx`) + `matrix[1, 1]` \* (y + `idy`) + `ody`
128 *
129 * where:
130 * - x and y are the coordinates in input image.
131 * - X and Y are the coordinates in output image.
132 * - (0,0) is the upper left corner.
133 *
134 * @since 0.27.0
135 *
136 * @example
137 * const pipeline = sharp()
138 * .affine([[1, 0.3], [0.1, 0.7]], {
139 * background: 'white',
140 * interpolator: sharp.interpolators.nohalo
141 * })
142 * .toBuffer((err, outputBuffer, info) => {
143 * // outputBuffer contains the transformed image
144 * // info.width and info.height contain the new dimensions
145 * });
146 *
147 * inputStream
148 * .pipe(pipeline);
149 *
150 * @param {Array<Array<number>>|Array<number>} matrix - affine transformation matrix
151 * @param {Object} [options] - if present, is an Object with optional attributes.
152 * @param {String|Object} [options.background="#000000"] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
153 * @param {Number} [options.idx=0] - input horizontal offset
154 * @param {Number} [options.idy=0] - input vertical offset
155 * @param {Number} [options.odx=0] - output horizontal offset
156 * @param {Number} [options.ody=0] - output vertical offset
157 * @param {String} [options.interpolator=sharp.interpolators.bicubic] - interpolator
158 * @returns {Sharp}
159 * @throws {Error} Invalid parameters
160 */
161function affine (matrix, options) {
162 const flatMatrix = [].concat(...matrix);
163 if (flatMatrix.length === 4 && flatMatrix.every(is.number)) {
164 this.options.affineMatrix = flatMatrix;
165 } else {
166 throw is.invalidParameterError('matrix', '1x4 or 2x2 array', matrix);
167 }
168
169 if (is.defined(options)) {
170 if (is.object(options)) {
171 this._setBackgroundColourOption('affineBackground', options.background);
172 if (is.defined(options.idx)) {
173 if (is.number(options.idx)) {
174 this.options.affineIdx = options.idx;
175 } else {
176 throw is.invalidParameterError('options.idx', 'number', options.idx);
177 }
178 }
179 if (is.defined(options.idy)) {
180 if (is.number(options.idy)) {
181 this.options.affineIdy = options.idy;
182 } else {
183 throw is.invalidParameterError('options.idy', 'number', options.idy);
184 }
185 }
186 if (is.defined(options.odx)) {
187 if (is.number(options.odx)) {
188 this.options.affineOdx = options.odx;
189 } else {
190 throw is.invalidParameterError('options.odx', 'number', options.odx);
191 }
192 }
193 if (is.defined(options.ody)) {
194 if (is.number(options.ody)) {
195 this.options.affineOdy = options.ody;
196 } else {
197 throw is.invalidParameterError('options.ody', 'number', options.ody);
198 }
199 }
200 if (is.defined(options.interpolator)) {
201 if (is.inArray(options.interpolator, Object.values(this.constructor.interpolators))) {
202 this.options.affineInterpolator = options.interpolator;
203 } else {
204 throw is.invalidParameterError('options.interpolator', 'valid interpolator name', options.interpolator);
205 }
206 }
207 } else {
208 throw is.invalidParameterError('options', 'object', options);
209 }
210 }
211
212 return this;
213}
214
215/**
216 * Sharpen the image.
217 *
218 * When used without parameters, performs a fast, mild sharpen of the output image.
219 *
220 * When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space.
221 * Fine-grained control over the level of sharpening in "flat" (m1) and "jagged" (m2) areas is available.
222 *
223 * See {@link https://www.libvips.org/API/current/libvips-convolution.html#vips-sharpen|libvips sharpen} operation.
224 *
225 * @example
226 * const data = await sharp(input).sharpen().toBuffer();
227 *
228 * @example
229 * const data = await sharp(input).sharpen({ sigma: 2 }).toBuffer();
230 *
231 * @example
232 * const data = await sharp(input)
233 * .sharpen({
234 * sigma: 2,
235 * m1: 0,
236 * m2: 3,
237 * x1: 3,
238 * y2: 15,
239 * y3: 15,
240 * })
241 * .toBuffer();
242 *
243 * @param {Object|number} [options] - if present, is an Object with attributes
244 * @param {number} [options.sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`, between 0.000001 and 10
245 * @param {number} [options.m1=1.0] - the level of sharpening to apply to "flat" areas, between 0 and 1000000
246 * @param {number} [options.m2=2.0] - the level of sharpening to apply to "jagged" areas, between 0 and 1000000
247 * @param {number} [options.x1=2.0] - threshold between "flat" and "jagged", between 0 and 1000000
248 * @param {number} [options.y2=10.0] - maximum amount of brightening, between 0 and 1000000
249 * @param {number} [options.y3=20.0] - maximum amount of darkening, between 0 and 1000000
250 * @param {number} [flat] - (deprecated) see `options.m1`.
251 * @param {number} [jagged] - (deprecated) see `options.m2`.
252 * @returns {Sharp}
253 * @throws {Error} Invalid parameters
254 */
255function sharpen (options, flat, jagged) {
256 if (!is.defined(options)) {
257 // No arguments: default to mild sharpen
258 this.options.sharpenSigma = -1;
259 } else if (is.bool(options)) {
260 // Deprecated boolean argument: apply mild sharpen?
261 this.options.sharpenSigma = options ? -1 : 0;
262 } else if (is.number(options) && is.inRange(options, 0.01, 10000)) {
263 // Deprecated numeric argument: specific sigma
264 this.options.sharpenSigma = options;
265 // Deprecated control over flat areas
266 if (is.defined(flat)) {
267 if (is.number(flat) && is.inRange(flat, 0, 10000)) {
268 this.options.sharpenM1 = flat;
269 } else {
270 throw is.invalidParameterError('flat', 'number between 0 and 10000', flat);
271 }
272 }
273 // Deprecated control over jagged areas
274 if (is.defined(jagged)) {
275 if (is.number(jagged) && is.inRange(jagged, 0, 10000)) {
276 this.options.sharpenM2 = jagged;
277 } else {
278 throw is.invalidParameterError('jagged', 'number between 0 and 10000', jagged);
279 }
280 }
281 } else if (is.plainObject(options)) {
282 if (is.number(options.sigma) && is.inRange(options.sigma, 0.000001, 10)) {
283 this.options.sharpenSigma = options.sigma;
284 } else {
285 throw is.invalidParameterError('options.sigma', 'number between 0.000001 and 10', options.sigma);
286 }
287 if (is.defined(options.m1)) {
288 if (is.number(options.m1) && is.inRange(options.m1, 0, 1000000)) {
289 this.options.sharpenM1 = options.m1;
290 } else {
291 throw is.invalidParameterError('options.m1', 'number between 0 and 1000000', options.m1);
292 }
293 }
294 if (is.defined(options.m2)) {
295 if (is.number(options.m2) && is.inRange(options.m2, 0, 1000000)) {
296 this.options.sharpenM2 = options.m2;
297 } else {
298 throw is.invalidParameterError('options.m2', 'number between 0 and 1000000', options.m2);
299 }
300 }
301 if (is.defined(options.x1)) {
302 if (is.number(options.x1) && is.inRange(options.x1, 0, 1000000)) {
303 this.options.sharpenX1 = options.x1;
304 } else {
305 throw is.invalidParameterError('options.x1', 'number between 0 and 1000000', options.x1);
306 }
307 }
308 if (is.defined(options.y2)) {
309 if (is.number(options.y2) && is.inRange(options.y2, 0, 1000000)) {
310 this.options.sharpenY2 = options.y2;
311 } else {
312 throw is.invalidParameterError('options.y2', 'number between 0 and 1000000', options.y2);
313 }
314 }
315 if (is.defined(options.y3)) {
316 if (is.number(options.y3) && is.inRange(options.y3, 0, 1000000)) {
317 this.options.sharpenY3 = options.y3;
318 } else {
319 throw is.invalidParameterError('options.y3', 'number between 0 and 1000000', options.y3);
320 }
321 }
322 } else {
323 throw is.invalidParameterError('sigma', 'number between 0.01 and 10000', options);
324 }
325 return this;
326}
327
328/**
329 * Apply median filter.
330 * When used without parameters the default window is 3x3.
331 *
332 * @example
333 * const output = await sharp(input).median().toBuffer();
334 *
335 * @example
336 * const output = await sharp(input).median(5).toBuffer();
337 *
338 * @param {number} [size=3] square mask size: size x size
339 * @returns {Sharp}
340 * @throws {Error} Invalid parameters
341 */
342function median (size) {
343 if (!is.defined(size)) {
344 // No arguments: default to 3x3
345 this.options.medianSize = 3;
346 } else if (is.integer(size) && is.inRange(size, 1, 1000)) {
347 // Numeric argument: specific sigma
348 this.options.medianSize = size;
349 } else {
350 throw is.invalidParameterError('size', 'integer between 1 and 1000', size);
351 }
352 return this;
353}
354
355/**
356 * Blur the image.
357 *
358 * When used without parameters, performs a fast 3x3 box blur (equivalent to a box linear filter).
359 *
360 * When a `sigma` is provided, performs a slower, more accurate Gaussian blur.
361 *
362 * @example
363 * const boxBlurred = await sharp(input)
364 * .blur()
365 * .toBuffer();
366 *
367 * @example
368 * const gaussianBlurred = await sharp(input)
369 * .blur(5)
370 * .toBuffer();
371 *
372 * @param {number} [sigma] a value between 0.3 and 1000 representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
373 * @returns {Sharp}
374 * @throws {Error} Invalid parameters
375 */
376function blur (sigma) {
377 if (!is.defined(sigma)) {
378 // No arguments: default to mild blur
379 this.options.blurSigma = -1;
380 } else if (is.bool(sigma)) {
381 // Boolean argument: apply mild blur?
382 this.options.blurSigma = sigma ? -1 : 0;
383 } else if (is.number(sigma) && is.inRange(sigma, 0.3, 1000)) {
384 // Numeric argument: specific sigma
385 this.options.blurSigma = sigma;
386 } else {
387 throw is.invalidParameterError('sigma', 'number between 0.3 and 1000', sigma);
388 }
389 return this;
390}
391
392/**
393 * Merge alpha transparency channel, if any, with a background, then remove the alpha channel.
394 *
395 * See also {@link /api-channel#removealpha|removeAlpha}.
396 *
397 * @example
398 * await sharp(rgbaInput)
399 * .flatten({ background: '#F0A703' })
400 * .toBuffer();
401 *
402 * @param {Object} [options]
403 * @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.
404 * @returns {Sharp}
405 */
406function flatten (options) {
407 this.options.flatten = is.bool(options) ? options : true;
408 if (is.object(options)) {
409 this._setBackgroundColourOption('flattenBackground', options.background);
410 }
411 return this;
412}
413
414/**
415 * Ensure the image has an alpha channel
416 * with all white pixel values made fully transparent.
417 *
418 * Existing alpha channel values for non-white pixels remain unchanged.
419 *
420 * This feature is experimental and the API may change.
421 *
422 * @since 0.32.1
423 *
424 * @example
425 * await sharp(rgbInput)
426 * .unflatten()
427 * .toBuffer();
428 *
429 * @example
430 * await sharp(rgbInput)
431 * .threshold(128, { grayscale: false }) // converter bright pixels to white
432 * .unflatten()
433 * .toBuffer();
434 */
435function unflatten () {
436 this.options.unflatten = true;
437 return this;
438}
439
440/**
441 * Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of `1/gamma`
442 * then increasing the encoding (brighten) post-resize at a factor of `gamma`.
443 * This can improve the perceived brightness of a resized image in non-linear colour spaces.
444 * JPEG and WebP input images will not take advantage of the shrink-on-load performance optimisation
445 * when applying a gamma correction.
446 *
447 * Supply a second argument to use a different output gamma value, otherwise the first value is used in both cases.
448 *
449 * @param {number} [gamma=2.2] value between 1.0 and 3.0.
450 * @param {number} [gammaOut] value between 1.0 and 3.0. (optional, defaults to same as `gamma`)
451 * @returns {Sharp}
452 * @throws {Error} Invalid parameters
453 */
454function gamma (gamma, gammaOut) {
455 if (!is.defined(gamma)) {
456 // Default gamma correction of 2.2 (sRGB)
457 this.options.gamma = 2.2;
458 } else if (is.number(gamma) && is.inRange(gamma, 1, 3)) {
459 this.options.gamma = gamma;
460 } else {
461 throw is.invalidParameterError('gamma', 'number between 1.0 and 3.0', gamma);
462 }
463 if (!is.defined(gammaOut)) {
464 // Default gamma correction for output is same as input
465 this.options.gammaOut = this.options.gamma;
466 } else if (is.number(gammaOut) && is.inRange(gammaOut, 1, 3)) {
467 this.options.gammaOut = gammaOut;
468 } else {
469 throw is.invalidParameterError('gammaOut', 'number between 1.0 and 3.0', gammaOut);
470 }
471 return this;
472}
473
474/**
475 * Produce the "negative" of the image.
476 *
477 * @example
478 * const output = await sharp(input)
479 * .negate()
480 * .toBuffer();
481 *
482 * @example
483 * const output = await sharp(input)
484 * .negate({ alpha: false })
485 * .toBuffer();
486 *
487 * @param {Object} [options]
488 * @param {Boolean} [options.alpha=true] Whether or not to negate any alpha channel
489 * @returns {Sharp}
490 */
491function negate (options) {
492 this.options.negate = is.bool(options) ? options : true;
493 if (is.plainObject(options) && 'alpha' in options) {
494 if (!is.bool(options.alpha)) {
495 throw is.invalidParameterError('alpha', 'should be boolean value', options.alpha);
496 } else {
497 this.options.negateAlpha = options.alpha;
498 }
499 }
500 return this;
501}
502
503/**
504 * Enhance output image contrast by stretching its luminance to cover a full dynamic range.
505 *
506 * Uses a histogram-based approach, taking a default range of 1% to 99% to reduce sensitivity to noise at the extremes.
507 *
508 * Luminance values below the `lower` percentile will be underexposed by clipping to zero.
509 * Luminance values above the `upper` percentile will be overexposed by clipping to the max pixel value.
510 *
511 * @example
512 * const output = await sharp(input)
513 * .normalise()
514 * .toBuffer();
515 *
516 * @example
517 * const output = await sharp(input)
518 * .normalise({ lower: 0, upper: 100 })
519 * .toBuffer();
520 *
521 * @param {Object} [options]
522 * @param {number} [options.lower=1] - Percentile below which luminance values will be underexposed.
523 * @param {number} [options.upper=99] - Percentile above which luminance values will be overexposed.
524 * @returns {Sharp}
525 */
526function normalise (options) {
527 if (is.plainObject(options)) {
528 if (is.defined(options.lower)) {
529 if (is.number(options.lower) && is.inRange(options.lower, 0, 99)) {
530 this.options.normaliseLower = options.lower;
531 } else {
532 throw is.invalidParameterError('lower', 'number between 0 and 99', options.lower);
533 }
534 }
535 if (is.defined(options.upper)) {
536 if (is.number(options.upper) && is.inRange(options.upper, 1, 100)) {
537 this.options.normaliseUpper = options.upper;
538 } else {
539 throw is.invalidParameterError('upper', 'number between 1 and 100', options.upper);
540 }
541 }
542 }
543 if (this.options.normaliseLower >= this.options.normaliseUpper) {
544 throw is.invalidParameterError('range', 'lower to be less than upper',
545 `${this.options.normaliseLower} >= ${this.options.normaliseUpper}`);
546 }
547 this.options.normalise = true;
548 return this;
549}
550
551/**
552 * Alternative spelling of normalise.
553 *
554 * @example
555 * const output = await sharp(input)
556 * .normalize()
557 * .toBuffer();
558 *
559 * @param {Object} [options]
560 * @param {number} [options.lower=1] - Percentile below which luminance values will be underexposed.
561 * @param {number} [options.upper=99] - Percentile above which luminance values will be overexposed.
562 * @returns {Sharp}
563 */
564function normalize (options) {
565 return this.normalise(options);
566}
567
568/**
569 * Perform contrast limiting adaptive histogram equalization
570 * {@link https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE|CLAHE}.
571 *
572 * This will, in general, enhance the clarity of the image by bringing out darker details.
573 *
574 * @since 0.28.3
575 *
576 * @example
577 * const output = await sharp(input)
578 * .clahe({
579 * width: 3,
580 * height: 3,
581 * })
582 * .toBuffer();
583 *
584 * @param {Object} options
585 * @param {number} options.width - Integral width of the search window, in pixels.
586 * @param {number} options.height - Integral height of the search window, in pixels.
587 * @param {number} [options.maxSlope=3] - Integral level of brightening, between 0 and 100, where 0 disables contrast limiting.
588 * @returns {Sharp}
589 * @throws {Error} Invalid parameters
590 */
591function clahe (options) {
592 if (is.plainObject(options)) {
593 if (is.integer(options.width) && options.width > 0) {
594 this.options.claheWidth = options.width;
595 } else {
596 throw is.invalidParameterError('width', 'integer greater than zero', options.width);
597 }
598 if (is.integer(options.height) && options.height > 0) {
599 this.options.claheHeight = options.height;
600 } else {
601 throw is.invalidParameterError('height', 'integer greater than zero', options.height);
602 }
603 if (is.defined(options.maxSlope)) {
604 if (is.integer(options.maxSlope) && is.inRange(options.maxSlope, 0, 100)) {
605 this.options.claheMaxSlope = options.maxSlope;
606 } else {
607 throw is.invalidParameterError('maxSlope', 'integer between 0 and 100', options.maxSlope);
608 }
609 }
610 } else {
611 throw is.invalidParameterError('options', 'plain object', options);
612 }
613 return this;
614}
615
616/**
617 * Convolve the image with the specified kernel.
618 *
619 * @example
620 * sharp(input)
621 * .convolve({
622 * width: 3,
623 * height: 3,
624 * kernel: [-1, 0, 1, -2, 0, 2, -1, 0, 1]
625 * })
626 * .raw()
627 * .toBuffer(function(err, data, info) {
628 * // data contains the raw pixel data representing the convolution
629 * // of the input image with the horizontal Sobel operator
630 * });
631 *
632 * @param {Object} kernel
633 * @param {number} kernel.width - width of the kernel in pixels.
634 * @param {number} kernel.height - height of the kernel in pixels.
635 * @param {Array<number>} kernel.kernel - Array of length `width*height` containing the kernel values.
636 * @param {number} [kernel.scale=sum] - the scale of the kernel in pixels.
637 * @param {number} [kernel.offset=0] - the offset of the kernel in pixels.
638 * @returns {Sharp}
639 * @throws {Error} Invalid parameters
640 */
641function convolve (kernel) {
642 if (!is.object(kernel) || !Array.isArray(kernel.kernel) ||
643 !is.integer(kernel.width) || !is.integer(kernel.height) ||
644 !is.inRange(kernel.width, 3, 1001) || !is.inRange(kernel.height, 3, 1001) ||
645 kernel.height * kernel.width !== kernel.kernel.length
646 ) {
647 // must pass in a kernel
648 throw new Error('Invalid convolution kernel');
649 }
650 // Default scale is sum of kernel values
651 if (!is.integer(kernel.scale)) {
652 kernel.scale = kernel.kernel.reduce(function (a, b) {
653 return a + b;
654 }, 0);
655 }
656 // Clip scale to a minimum value of 1
657 if (kernel.scale < 1) {
658 kernel.scale = 1;
659 }
660 if (!is.integer(kernel.offset)) {
661 kernel.offset = 0;
662 }
663 this.options.convKernel = kernel;
664 return this;
665}
666
667/**
668 * Any pixel value greater than or equal to the threshold value will be set to 255, otherwise it will be set to 0.
669 * @param {number} [threshold=128] - a value in the range 0-255 representing the level at which the threshold will be applied.
670 * @param {Object} [options]
671 * @param {Boolean} [options.greyscale=true] - convert to single channel greyscale.
672 * @param {Boolean} [options.grayscale=true] - alternative spelling for greyscale.
673 * @returns {Sharp}
674 * @throws {Error} Invalid parameters
675 */
676function threshold (threshold, options) {
677 if (!is.defined(threshold)) {
678 this.options.threshold = 128;
679 } else if (is.bool(threshold)) {
680 this.options.threshold = threshold ? 128 : 0;
681 } else if (is.integer(threshold) && is.inRange(threshold, 0, 255)) {
682 this.options.threshold = threshold;
683 } else {
684 throw is.invalidParameterError('threshold', 'integer between 0 and 255', threshold);
685 }
686 if (!is.object(options) || options.greyscale === true || options.grayscale === true) {
687 this.options.thresholdGrayscale = true;
688 } else {
689 this.options.thresholdGrayscale = false;
690 }
691 return this;
692}
693
694/**
695 * Perform a bitwise boolean operation with operand image.
696 *
697 * This operation creates an output image where each pixel is the result of
698 * the selected bitwise boolean `operation` between the corresponding pixels of the input images.
699 *
700 * @param {Buffer|string} operand - Buffer containing image data or string containing the path to an image file.
701 * @param {string} operator - one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively.
702 * @param {Object} [options]
703 * @param {Object} [options.raw] - describes operand when using raw pixel data.
704 * @param {number} [options.raw.width]
705 * @param {number} [options.raw.height]
706 * @param {number} [options.raw.channels]
707 * @returns {Sharp}
708 * @throws {Error} Invalid parameters
709 */
710function boolean (operand, operator, options) {
711 this.options.boolean = this._createInputDescriptor(operand, options);
712 if (is.string(operator) && is.inArray(operator, ['and', 'or', 'eor'])) {
713 this.options.booleanOp = operator;
714 } else {
715 throw is.invalidParameterError('operator', 'one of: and, or, eor', operator);
716 }
717 return this;
718}
719
720/**
721 * Apply the linear formula `a` * input + `b` to the image to adjust image levels.
722 *
723 * When a single number is provided, it will be used for all image channels.
724 * When an array of numbers is provided, the array length must match the number of channels.
725 *
726 * @example
727 * await sharp(input)
728 * .linear(0.5, 2)
729 * .toBuffer();
730 *
731 * @example
732 * await sharp(rgbInput)
733 * .linear(
734 * [0.25, 0.5, 0.75],
735 * [150, 100, 50]
736 * )
737 * .toBuffer();
738 *
739 * @param {(number|number[])} [a=[]] multiplier
740 * @param {(number|number[])} [b=[]] offset
741 * @returns {Sharp}
742 * @throws {Error} Invalid parameters
743 */
744function linear (a, b) {
745 if (!is.defined(a) && is.number(b)) {
746 a = 1.0;
747 } else if (is.number(a) && !is.defined(b)) {
748 b = 0.0;
749 }
750 if (!is.defined(a)) {
751 this.options.linearA = [];
752 } else if (is.number(a)) {
753 this.options.linearA = [a];
754 } else if (Array.isArray(a) && a.length && a.every(is.number)) {
755 this.options.linearA = a;
756 } else {
757 throw is.invalidParameterError('a', 'number or array of numbers', a);
758 }
759 if (!is.defined(b)) {
760 this.options.linearB = [];
761 } else if (is.number(b)) {
762 this.options.linearB = [b];
763 } else if (Array.isArray(b) && b.length && b.every(is.number)) {
764 this.options.linearB = b;
765 } else {
766 throw is.invalidParameterError('b', 'number or array of numbers', b);
767 }
768 if (this.options.linearA.length !== this.options.linearB.length) {
769 throw new Error('Expected a and b to be arrays of the same length');
770 }
771 return this;
772}
773
774/**
775 * Recomb the image with the specified matrix.
776 *
777 * @since 0.21.1
778 *
779 * @example
780 * sharp(input)
781 * .recomb([
782 * [0.3588, 0.7044, 0.1368],
783 * [0.2990, 0.5870, 0.1140],
784 * [0.2392, 0.4696, 0.0912],
785 * ])
786 * .raw()
787 * .toBuffer(function(err, data, info) {
788 * // data contains the raw pixel data after applying the recomb
789 * // With this example input, a sepia filter has been applied
790 * });
791 *
792 * @param {Array<Array<number>>} inputMatrix - 3x3 Recombination matrix
793 * @returns {Sharp}
794 * @throws {Error} Invalid parameters
795 */
796function recomb (inputMatrix) {
797 if (!Array.isArray(inputMatrix) || inputMatrix.length !== 3 ||
798 inputMatrix[0].length !== 3 ||
799 inputMatrix[1].length !== 3 ||
800 inputMatrix[2].length !== 3
801 ) {
802 // must pass in a kernel
803 throw new Error('Invalid recombination matrix');
804 }
805 this.options.recombMatrix = [
806 inputMatrix[0][0], inputMatrix[0][1], inputMatrix[0][2],
807 inputMatrix[1][0], inputMatrix[1][1], inputMatrix[1][2],
808 inputMatrix[2][0], inputMatrix[2][1], inputMatrix[2][2]
809 ].map(Number);
810 return this;
811}
812
813/**
814 * Transforms the image using brightness, saturation, hue rotation, and lightness.
815 * Brightness and lightness both operate on luminance, with the difference being that
816 * brightness is multiplicative whereas lightness is additive.
817 *
818 * @since 0.22.1
819 *
820 * @example
821 * // increase brightness by a factor of 2
822 * const output = await sharp(input)
823 * .modulate({
824 * brightness: 2
825 * })
826 * .toBuffer();
827 *
828 * @example
829 * // hue-rotate by 180 degrees
830 * const output = await sharp(input)
831 * .modulate({
832 * hue: 180
833 * })
834 * .toBuffer();
835 *
836 * @example
837 * // increase lightness by +50
838 * const output = await sharp(input)
839 * .modulate({
840 * lightness: 50
841 * })
842 * .toBuffer();
843 *
844 * @example
845 * // decreate brightness and saturation while also hue-rotating by 90 degrees
846 * const output = await sharp(input)
847 * .modulate({
848 * brightness: 0.5,
849 * saturation: 0.5,
850 * hue: 90,
851 * })
852 * .toBuffer();
853 *
854 * @param {Object} [options]
855 * @param {number} [options.brightness] Brightness multiplier
856 * @param {number} [options.saturation] Saturation multiplier
857 * @param {number} [options.hue] Degrees for hue rotation
858 * @param {number} [options.lightness] Lightness addend
859 * @returns {Sharp}
860 */
861function modulate (options) {
862 if (!is.plainObject(options)) {
863 throw is.invalidParameterError('options', 'plain object', options);
864 }
865 if ('brightness' in options) {
866 if (is.number(options.brightness) && options.brightness >= 0) {
867 this.options.brightness = options.brightness;
868 } else {
869 throw is.invalidParameterError('brightness', 'number above zero', options.brightness);
870 }
871 }
872 if ('saturation' in options) {
873 if (is.number(options.saturation) && options.saturation >= 0) {
874 this.options.saturation = options.saturation;
875 } else {
876 throw is.invalidParameterError('saturation', 'number above zero', options.saturation);
877 }
878 }
879 if ('hue' in options) {
880 if (is.integer(options.hue)) {
881 this.options.hue = options.hue % 360;
882 } else {
883 throw is.invalidParameterError('hue', 'number', options.hue);
884 }
885 }
886 if ('lightness' in options) {
887 if (is.number(options.lightness)) {
888 this.options.lightness = options.lightness;
889 } else {
890 throw is.invalidParameterError('lightness', 'number', options.lightness);
891 }
892 }
893 return this;
894}
895
896/**
897 * Decorate the Sharp prototype with operation-related functions.
898 * @private
899 */
900module.exports = function (Sharp) {
901 Object.assign(Sharp.prototype, {
902 rotate,
903 flip,
904 flop,
905 affine,
906 sharpen,
907 median,
908 blur,
909 flatten,
910 unflatten,
911 gamma,
912 negate,
913 normalise,
914 normalize,
915 clahe,
916 convolve,
917 threshold,
918 boolean,
919 linear,
920 recomb,
921 modulate
922 });
923};