operation.js 33 KB

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