UNPKG

16.4 kBJavaScriptView Raw
1'use strict';
2
3const color = require('color');
4const is = require('./is');
5
6/**
7 * Rotate the output image by either an explicit angle
8 * or auto-orient based on the EXIF `Orientation` tag.
9 *
10 * If an angle is provided, it is converted to a valid positive degree rotation.
11 * For example, `-450` will produce a 270deg rotation.
12 *
13 * When rotating by an angle other than a multiple of 90,
14 * the background colour can be provided with the `background` option.
15 *
16 * If no angle is provided, it is determined from the EXIF data.
17 * Mirroring is supported and may infer the use of a flip operation.
18 *
19 * The use of `rotate` implies the removal of the EXIF `Orientation` tag, if any.
20 *
21 * Method order is important when both rotating and extracting regions,
22 * for example `rotate(x).extract(y)` will produce a different result to `extract(y).rotate(x)`.
23 *
24 * @example
25 * const pipeline = sharp()
26 * .rotate()
27 * .resize(null, 200)
28 * .toBuffer(function (err, outputBuffer, info) {
29 * // outputBuffer contains 200px high JPEG image data,
30 * // auto-rotated using EXIF Orientation tag
31 * // info.width and info.height contain the dimensions of the resized image
32 * });
33 * readableStream.pipe(pipeline);
34 *
35 * @param {number} [angle=auto] angle of rotation.
36 * @param {Object} [options] - if present, is an Object with optional attributes.
37 * @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.
38 * @returns {Sharp}
39 * @throws {Error} Invalid parameters
40 */
41function rotate (angle, options) {
42 if (!is.defined(angle)) {
43 this.options.useExifOrientation = true;
44 } else if (is.integer(angle) && !(angle % 90)) {
45 this.options.angle = angle;
46 } else if (is.number(angle)) {
47 this.options.rotationAngle = angle;
48 if (is.object(options) && options.background) {
49 const backgroundColour = color(options.background);
50 this.options.rotationBackground = [
51 backgroundColour.red(),
52 backgroundColour.green(),
53 backgroundColour.blue(),
54 Math.round(backgroundColour.alpha() * 255)
55 ];
56 }
57 } else {
58 throw is.invalidParameterError('angle', 'numeric', angle);
59 }
60 return this;
61}
62
63/**
64 * Flip the image about the vertical Y axis. This always occurs after rotation, if any.
65 * The use of `flip` implies the removal of the EXIF `Orientation` tag, if any.
66 * @param {Boolean} [flip=true]
67 * @returns {Sharp}
68 */
69function flip (flip) {
70 this.options.flip = is.bool(flip) ? flip : true;
71 return this;
72}
73
74/**
75 * Flop the image about the horizontal X axis. This always occurs after rotation, if any.
76 * The use of `flop` implies the removal of the EXIF `Orientation` tag, if any.
77 * @param {Boolean} [flop=true]
78 * @returns {Sharp}
79 */
80function flop (flop) {
81 this.options.flop = is.bool(flop) ? flop : true;
82 return this;
83}
84
85/**
86 * Sharpen the image.
87 * When used without parameters, performs a fast, mild sharpen of the output image.
88 * When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space.
89 * Separate control over the level of sharpening in "flat" and "jagged" areas is available.
90 *
91 * @param {number} [sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
92 * @param {number} [flat=1.0] - the level of sharpening to apply to "flat" areas.
93 * @param {number} [jagged=2.0] - the level of sharpening to apply to "jagged" areas.
94 * @returns {Sharp}
95 * @throws {Error} Invalid parameters
96 */
97function sharpen (sigma, flat, jagged) {
98 if (!is.defined(sigma)) {
99 // No arguments: default to mild sharpen
100 this.options.sharpenSigma = -1;
101 } else if (is.bool(sigma)) {
102 // Boolean argument: apply mild sharpen?
103 this.options.sharpenSigma = sigma ? -1 : 0;
104 } else if (is.number(sigma) && is.inRange(sigma, 0.01, 10000)) {
105 // Numeric argument: specific sigma
106 this.options.sharpenSigma = sigma;
107 // Control over flat areas
108 if (is.defined(flat)) {
109 if (is.number(flat) && is.inRange(flat, 0, 10000)) {
110 this.options.sharpenFlat = flat;
111 } else {
112 throw is.invalidParameterError('flat', 'number between 0 and 10000', flat);
113 }
114 }
115 // Control over jagged areas
116 if (is.defined(jagged)) {
117 if (is.number(jagged) && is.inRange(jagged, 0, 10000)) {
118 this.options.sharpenJagged = jagged;
119 } else {
120 throw is.invalidParameterError('jagged', 'number between 0 and 10000', jagged);
121 }
122 }
123 } else {
124 throw is.invalidParameterError('sigma', 'number between 0.01 and 10000', sigma);
125 }
126 return this;
127}
128
129/**
130 * Apply median filter.
131 * When used without parameters the default window is 3x3.
132 * @param {number} [size=3] square mask size: size x size
133 * @returns {Sharp}
134 * @throws {Error} Invalid parameters
135 */
136function median (size) {
137 if (!is.defined(size)) {
138 // No arguments: default to 3x3
139 this.options.medianSize = 3;
140 } else if (is.integer(size) && is.inRange(size, 1, 1000)) {
141 // Numeric argument: specific sigma
142 this.options.medianSize = size;
143 } else {
144 throw is.invalidParameterError('size', 'integer between 1 and 1000', size);
145 }
146 return this;
147}
148
149/**
150 * Blur the image.
151 * When used without parameters, performs a fast, mild blur of the output image.
152 * When a `sigma` is provided, performs a slower, more accurate Gaussian blur.
153 * @param {number} [sigma] a value between 0.3 and 1000 representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
154 * @returns {Sharp}
155 * @throws {Error} Invalid parameters
156 */
157function blur (sigma) {
158 if (!is.defined(sigma)) {
159 // No arguments: default to mild blur
160 this.options.blurSigma = -1;
161 } else if (is.bool(sigma)) {
162 // Boolean argument: apply mild blur?
163 this.options.blurSigma = sigma ? -1 : 0;
164 } else if (is.number(sigma) && is.inRange(sigma, 0.3, 1000)) {
165 // Numeric argument: specific sigma
166 this.options.blurSigma = sigma;
167 } else {
168 throw is.invalidParameterError('sigma', 'number between 0.3 and 1000', sigma);
169 }
170 return this;
171}
172
173/**
174 * Merge alpha transparency channel, if any, with a background.
175 * @param {Object} [options]
176 * @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.
177 * @returns {Sharp}
178 */
179function flatten (options) {
180 this.options.flatten = is.bool(options) ? options : true;
181 if (is.object(options)) {
182 this._setBackgroundColourOption('flattenBackground', options.background);
183 }
184 return this;
185}
186
187/**
188 * Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of `1/gamma`
189 * then increasing the encoding (brighten) post-resize at a factor of `gamma`.
190 * This can improve the perceived brightness of a resized image in non-linear colour spaces.
191 * JPEG and WebP input images will not take advantage of the shrink-on-load performance optimisation
192 * when applying a gamma correction.
193 *
194 * Supply a second argument to use a different output gamma value, otherwise the first value is used in both cases.
195 *
196 * @param {number} [gamma=2.2] value between 1.0 and 3.0.
197 * @param {number} [gammaOut] value between 1.0 and 3.0. (optional, defaults to same as `gamma`)
198 * @returns {Sharp}
199 * @throws {Error} Invalid parameters
200 */
201function gamma (gamma, gammaOut) {
202 if (!is.defined(gamma)) {
203 // Default gamma correction of 2.2 (sRGB)
204 this.options.gamma = 2.2;
205 } else if (is.number(gamma) && is.inRange(gamma, 1, 3)) {
206 this.options.gamma = gamma;
207 } else {
208 throw is.invalidParameterError('gamma', 'number between 1.0 and 3.0', gamma);
209 }
210 if (!is.defined(gammaOut)) {
211 // Default gamma correction for output is same as input
212 this.options.gammaOut = this.options.gamma;
213 } else if (is.number(gammaOut) && is.inRange(gammaOut, 1, 3)) {
214 this.options.gammaOut = gammaOut;
215 } else {
216 throw is.invalidParameterError('gammaOut', 'number between 1.0 and 3.0', gammaOut);
217 }
218 return this;
219}
220
221/**
222 * Produce the "negative" of the image.
223 * @param {Boolean} [negate=true]
224 * @returns {Sharp}
225 */
226function negate (negate) {
227 this.options.negate = is.bool(negate) ? negate : true;
228 return this;
229}
230
231/**
232 * Enhance output image contrast by stretching its luminance to cover the full dynamic range.
233 * @param {Boolean} [normalise=true]
234 * @returns {Sharp}
235 */
236function normalise (normalise) {
237 this.options.normalise = is.bool(normalise) ? normalise : true;
238 return this;
239}
240
241/**
242 * Alternative spelling of normalise.
243 * @param {Boolean} [normalize=true]
244 * @returns {Sharp}
245 */
246function normalize (normalize) {
247 return this.normalise(normalize);
248}
249
250/**
251 * Convolve the image with the specified kernel.
252 *
253 * @example
254 * sharp(input)
255 * .convolve({
256 * width: 3,
257 * height: 3,
258 * kernel: [-1, 0, 1, -2, 0, 2, -1, 0, 1]
259 * })
260 * .raw()
261 * .toBuffer(function(err, data, info) {
262 * // data contains the raw pixel data representing the convolution
263 * // of the input image with the horizontal Sobel operator
264 * });
265 *
266 * @param {Object} kernel
267 * @param {number} kernel.width - width of the kernel in pixels.
268 * @param {number} kernel.height - width of the kernel in pixels.
269 * @param {Array<number>} kernel.kernel - Array of length `width*height` containing the kernel values.
270 * @param {number} [kernel.scale=sum] - the scale of the kernel in pixels.
271 * @param {number} [kernel.offset=0] - the offset of the kernel in pixels.
272 * @returns {Sharp}
273 * @throws {Error} Invalid parameters
274 */
275function convolve (kernel) {
276 if (!is.object(kernel) || !Array.isArray(kernel.kernel) ||
277 !is.integer(kernel.width) || !is.integer(kernel.height) ||
278 !is.inRange(kernel.width, 3, 1001) || !is.inRange(kernel.height, 3, 1001) ||
279 kernel.height * kernel.width !== kernel.kernel.length
280 ) {
281 // must pass in a kernel
282 throw new Error('Invalid convolution kernel');
283 }
284 // Default scale is sum of kernel values
285 if (!is.integer(kernel.scale)) {
286 kernel.scale = kernel.kernel.reduce(function (a, b) {
287 return a + b;
288 }, 0);
289 }
290 // Clip scale to a minimum value of 1
291 if (kernel.scale < 1) {
292 kernel.scale = 1;
293 }
294 if (!is.integer(kernel.offset)) {
295 kernel.offset = 0;
296 }
297 this.options.convKernel = kernel;
298 return this;
299}
300
301/**
302 * Any pixel value greather than or equal to the threshold value will be set to 255, otherwise it will be set to 0.
303 * @param {number} [threshold=128] - a value in the range 0-255 representing the level at which the threshold will be applied.
304 * @param {Object} [options]
305 * @param {Boolean} [options.greyscale=true] - convert to single channel greyscale.
306 * @param {Boolean} [options.grayscale=true] - alternative spelling for greyscale.
307 * @returns {Sharp}
308 * @throws {Error} Invalid parameters
309 */
310function threshold (threshold, options) {
311 if (!is.defined(threshold)) {
312 this.options.threshold = 128;
313 } else if (is.bool(threshold)) {
314 this.options.threshold = threshold ? 128 : 0;
315 } else if (is.integer(threshold) && is.inRange(threshold, 0, 255)) {
316 this.options.threshold = threshold;
317 } else {
318 throw is.invalidParameterError('threshold', 'integer between 0 and 255', threshold);
319 }
320 if (!is.object(options) || options.greyscale === true || options.grayscale === true) {
321 this.options.thresholdGrayscale = true;
322 } else {
323 this.options.thresholdGrayscale = false;
324 }
325 return this;
326}
327
328/**
329 * Perform a bitwise boolean operation with operand image.
330 *
331 * This operation creates an output image where each pixel is the result of
332 * the selected bitwise boolean `operation` between the corresponding pixels of the input images.
333 *
334 * @param {Buffer|string} operand - Buffer containing image data or string containing the path to an image file.
335 * @param {string} operator - one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively.
336 * @param {Object} [options]
337 * @param {Object} [options.raw] - describes operand when using raw pixel data.
338 * @param {number} [options.raw.width]
339 * @param {number} [options.raw.height]
340 * @param {number} [options.raw.channels]
341 * @returns {Sharp}
342 * @throws {Error} Invalid parameters
343 */
344function boolean (operand, operator, options) {
345 this.options.boolean = this._createInputDescriptor(operand, options);
346 if (is.string(operator) && is.inArray(operator, ['and', 'or', 'eor'])) {
347 this.options.booleanOp = operator;
348 } else {
349 throw is.invalidParameterError('operator', 'one of: and, or, eor', operator);
350 }
351 return this;
352}
353
354/**
355 * Apply the linear formula a * input + b to the image (levels adjustment)
356 * @param {number} [a=1.0] multiplier
357 * @param {number} [b=0.0] offset
358 * @returns {Sharp}
359 * @throws {Error} Invalid parameters
360 */
361function linear (a, b) {
362 if (!is.defined(a)) {
363 this.options.linearA = 1.0;
364 } else if (is.number(a)) {
365 this.options.linearA = a;
366 } else {
367 throw is.invalidParameterError('a', 'numeric', a);
368 }
369 if (!is.defined(b)) {
370 this.options.linearB = 0.0;
371 } else if (is.number(b)) {
372 this.options.linearB = b;
373 } else {
374 throw is.invalidParameterError('b', 'numeric', b);
375 }
376 return this;
377}
378
379/**
380 * Recomb the image with the specified matrix.
381 *
382 * @since 0.21.1
383 *
384 * @example
385 * sharp(input)
386 * .recomb([
387 * [0.3588, 0.7044, 0.1368],
388 * [0.2990, 0.5870, 0.1140],
389 * [0.2392, 0.4696, 0.0912],
390 * ])
391 * .raw()
392 * .toBuffer(function(err, data, info) {
393 * // data contains the raw pixel data after applying the recomb
394 * // With this example input, a sepia filter has been applied
395 * });
396 *
397 * @param {Array<Array<number>>} inputMatrix - 3x3 Recombination matrix
398 * @returns {Sharp}
399 * @throws {Error} Invalid parameters
400 */
401function recomb (inputMatrix) {
402 if (!Array.isArray(inputMatrix) || inputMatrix.length !== 3 ||
403 inputMatrix[0].length !== 3 ||
404 inputMatrix[1].length !== 3 ||
405 inputMatrix[2].length !== 3
406 ) {
407 // must pass in a kernel
408 throw new Error('Invalid recombination matrix');
409 }
410 this.options.recombMatrix = [
411 inputMatrix[0][0], inputMatrix[0][1], inputMatrix[0][2],
412 inputMatrix[1][0], inputMatrix[1][1], inputMatrix[1][2],
413 inputMatrix[2][0], inputMatrix[2][1], inputMatrix[2][2]
414 ].map(Number);
415 return this;
416}
417
418/**
419 * Transforms the image using brightness, saturation and hue rotation.
420 *
421 * @since 0.22.1
422 *
423 * @example
424 * sharp(input)
425 * .modulate({
426 * brightness: 2 // increase lightness by a factor of 2
427 * });
428 *
429 * sharp(input)
430 * .modulate({
431 * hue: 180 // hue-rotate by 180 degrees
432 * });
433 *
434 * // decreate brightness and saturation while also hue-rotating by 90 degrees
435 * sharp(input)
436 * .modulate({
437 * brightness: 0.5,
438 * saturation: 0.5,
439 * hue: 90
440 * });
441 *
442 * @param {Object} [options]
443 * @param {number} [options.brightness] Brightness multiplier
444 * @param {number} [options.saturation] Saturation multiplier
445 * @param {number} [options.hue] Degrees for hue rotation
446 * @returns {Sharp}
447 */
448function modulate (options) {
449 if (!is.plainObject(options)) {
450 throw is.invalidParameterError('options', 'plain object', options);
451 }
452 if ('brightness' in options) {
453 if (is.number(options.brightness) && options.brightness >= 0) {
454 this.options.brightness = options.brightness;
455 } else {
456 throw is.invalidParameterError('brightness', 'number above zero', options.brightness);
457 }
458 }
459 if ('saturation' in options) {
460 if (is.number(options.saturation) && options.saturation >= 0) {
461 this.options.saturation = options.saturation;
462 } else {
463 throw is.invalidParameterError('saturation', 'number above zero', options.saturation);
464 }
465 }
466 if ('hue' in options) {
467 if (is.integer(options.hue)) {
468 this.options.hue = options.hue % 360;
469 } else {
470 throw is.invalidParameterError('hue', 'number', options.hue);
471 }
472 }
473 return this;
474}
475
476/**
477 * Decorate the Sharp prototype with operation-related functions.
478 * @private
479 */
480module.exports = function (Sharp) {
481 Object.assign(Sharp.prototype, {
482 rotate,
483 flip,
484 flop,
485 sharpen,
486 median,
487 blur,
488 flatten,
489 gamma,
490 negate,
491 normalise,
492 normalize,
493 convolve,
494 threshold,
495 boolean,
496 linear,
497 recomb,
498 modulate
499 });
500};