composite.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /*!
  2. Copyright 2013 Lovell Fuller and others.
  3. SPDX-License-Identifier: Apache-2.0
  4. */
  5. const is = require('./is');
  6. /**
  7. * Blend modes.
  8. * @member
  9. * @private
  10. */
  11. const blend = {
  12. clear: 'clear',
  13. source: 'source',
  14. over: 'over',
  15. in: 'in',
  16. out: 'out',
  17. atop: 'atop',
  18. dest: 'dest',
  19. 'dest-over': 'dest-over',
  20. 'dest-in': 'dest-in',
  21. 'dest-out': 'dest-out',
  22. 'dest-atop': 'dest-atop',
  23. xor: 'xor',
  24. add: 'add',
  25. saturate: 'saturate',
  26. multiply: 'multiply',
  27. screen: 'screen',
  28. overlay: 'overlay',
  29. darken: 'darken',
  30. lighten: 'lighten',
  31. 'colour-dodge': 'colour-dodge',
  32. 'color-dodge': 'colour-dodge',
  33. 'colour-burn': 'colour-burn',
  34. 'color-burn': 'colour-burn',
  35. 'hard-light': 'hard-light',
  36. 'soft-light': 'soft-light',
  37. difference: 'difference',
  38. exclusion: 'exclusion'
  39. };
  40. /**
  41. * Composite image(s) over the processed (resized, extracted etc.) image.
  42. *
  43. * The images to composite must be the same size or smaller than the processed image.
  44. * If both `top` and `left` options are provided, they take precedence over `gravity`.
  45. *
  46. * Other operations in the same processing pipeline (e.g. resize, rotate, flip,
  47. * flop, extract) will always be applied to the input image before composition.
  48. *
  49. * The `blend` option can be one of `clear`, `source`, `over`, `in`, `out`, `atop`,
  50. * `dest`, `dest-over`, `dest-in`, `dest-out`, `dest-atop`,
  51. * `xor`, `add`, `saturate`, `multiply`, `screen`, `overlay`, `darken`, `lighten`,
  52. * `colour-dodge`, `color-dodge`, `colour-burn`,`color-burn`,
  53. * `hard-light`, `soft-light`, `difference`, `exclusion`.
  54. *
  55. * More information about blend modes can be found at
  56. * https://www.libvips.org/API/current/enum.BlendMode.html
  57. * and https://www.cairographics.org/operators/
  58. *
  59. * @since 0.22.0
  60. *
  61. * @example
  62. * await sharp(background)
  63. * .composite([
  64. * { input: layer1, gravity: 'northwest' },
  65. * { input: layer2, gravity: 'southeast' },
  66. * ])
  67. * .toFile('combined.png');
  68. *
  69. * @example
  70. * const output = await sharp('input.gif', { animated: true })
  71. * .composite([
  72. * { input: 'overlay.png', tile: true, blend: 'saturate' }
  73. * ])
  74. * .toBuffer();
  75. *
  76. * @example
  77. * sharp('input.png')
  78. * .rotate(180)
  79. * .resize(300)
  80. * .flatten( { background: '#ff6600' } )
  81. * .composite([{ input: 'overlay.png', gravity: 'southeast' }])
  82. * .sharpen()
  83. * .withMetadata()
  84. * .webp( { quality: 90 } )
  85. * .toBuffer()
  86. * .then(function(outputBuffer) {
  87. * // outputBuffer contains upside down, 300px wide, alpha channel flattened
  88. * // onto orange background, composited with overlay.png with SE gravity,
  89. * // sharpened, with metadata, 90% quality WebP image data. Phew!
  90. * });
  91. *
  92. * @param {Object[]} images - Ordered list of images to composite
  93. * @param {Buffer|String} [images[].input] - Buffer containing image data, String containing the path to an image file, or Create object (see below)
  94. * @param {Object} [images[].input.create] - describes a blank overlay to be created.
  95. * @param {Number} [images[].input.create.width]
  96. * @param {Number} [images[].input.create.height]
  97. * @param {Number} [images[].input.create.channels] - 3-4
  98. * @param {String|Object} [images[].input.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
  99. * @param {Object} [images[].input.text] - describes a new text image to be created.
  100. * @param {string} [images[].input.text.text] - text to render as a UTF-8 string. It can contain Pango markup, for example `<i>Le</i>Monde`.
  101. * @param {string} [images[].input.text.font] - font name to render with.
  102. * @param {string} [images[].input.text.fontfile] - absolute filesystem path to a font file that can be used by `font`.
  103. * @param {number} [images[].input.text.width=0] - integral number of pixels to word-wrap at. Lines of text wider than this will be broken at word boundaries.
  104. * @param {number} [images[].input.text.height=0] - integral number of pixels high. When defined, `dpi` will be ignored and the text will automatically fit the pixel resolution defined by `width` and `height`. Will be ignored if `width` is not specified or set to 0.
  105. * @param {string} [images[].input.text.align='left'] - text alignment (`'left'`, `'centre'`, `'center'`, `'right'`).
  106. * @param {boolean} [images[].input.text.justify=false] - set this to true to apply justification to the text.
  107. * @param {number} [images[].input.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified.
  108. * @param {boolean} [images[].input.text.rgba=false] - set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for Pango markup features like `<span foreground="red">Red!</span>`.
  109. * @param {number} [images[].input.text.spacing=0] - text line height in points. Will use the font line height if none is specified.
  110. * @param {Boolean} [images[].autoOrient=false] - set to true to use EXIF orientation data, if present, to orient the image.
  111. * @param {String} [images[].blend='over'] - how to blend this image with the image below.
  112. * @param {String} [images[].gravity='centre'] - gravity at which to place the overlay.
  113. * @param {Number} [images[].top] - the pixel offset from the top edge.
  114. * @param {Number} [images[].left] - the pixel offset from the left edge.
  115. * @param {Boolean} [images[].tile=false] - set to true to repeat the overlay image across the entire image with the given `gravity`.
  116. * @param {Boolean} [images[].premultiplied=false] - set to true to avoid premultiplying the image below. Equivalent to the `--premultiplied` vips option.
  117. * @param {Number} [images[].density=72] - number representing the DPI for vector overlay image.
  118. * @param {Object} [images[].raw] - describes overlay when using raw pixel data.
  119. * @param {Number} [images[].raw.width]
  120. * @param {Number} [images[].raw.height]
  121. * @param {Number} [images[].raw.channels]
  122. * @param {boolean} [images[].animated=false] - Set to `true` to read all frames/pages of an animated image.
  123. * @param {string} [images[].failOn='warning'] - @see {@link /api-constructor/ constructor parameters}
  124. * @param {number|boolean} [images[].limitInputPixels=268402689] - @see {@link /api-constructor/ constructor parameters}
  125. * @returns {Sharp}
  126. * @throws {Error} Invalid parameters
  127. */
  128. function composite (images) {
  129. if (!Array.isArray(images)) {
  130. throw is.invalidParameterError('images to composite', 'array', images);
  131. }
  132. this.options.composite = images.map(image => {
  133. if (!is.object(image)) {
  134. throw is.invalidParameterError('image to composite', 'object', image);
  135. }
  136. const inputOptions = this._inputOptionsFromObject(image);
  137. const composite = {
  138. input: this._createInputDescriptor(image.input, inputOptions, { allowStream: false }),
  139. blend: 'over',
  140. tile: false,
  141. left: 0,
  142. top: 0,
  143. hasOffset: false,
  144. gravity: 0,
  145. premultiplied: false
  146. };
  147. if (is.defined(image.blend)) {
  148. if (is.string(blend[image.blend])) {
  149. composite.blend = blend[image.blend];
  150. } else {
  151. throw is.invalidParameterError('blend', 'valid blend name', image.blend);
  152. }
  153. }
  154. if (is.defined(image.tile)) {
  155. if (is.bool(image.tile)) {
  156. composite.tile = image.tile;
  157. } else {
  158. throw is.invalidParameterError('tile', 'boolean', image.tile);
  159. }
  160. }
  161. if (is.defined(image.left)) {
  162. if (is.integer(image.left)) {
  163. composite.left = image.left;
  164. } else {
  165. throw is.invalidParameterError('left', 'integer', image.left);
  166. }
  167. }
  168. if (is.defined(image.top)) {
  169. if (is.integer(image.top)) {
  170. composite.top = image.top;
  171. } else {
  172. throw is.invalidParameterError('top', 'integer', image.top);
  173. }
  174. }
  175. if (is.defined(image.top) !== is.defined(image.left)) {
  176. throw new Error('Expected both left and top to be set');
  177. } else {
  178. composite.hasOffset = is.integer(image.top) && is.integer(image.left);
  179. }
  180. if (is.defined(image.gravity)) {
  181. if (is.integer(image.gravity) && is.inRange(image.gravity, 0, 8)) {
  182. composite.gravity = image.gravity;
  183. } else if (is.string(image.gravity) && is.integer(this.constructor.gravity[image.gravity])) {
  184. composite.gravity = this.constructor.gravity[image.gravity];
  185. } else {
  186. throw is.invalidParameterError('gravity', 'valid gravity', image.gravity);
  187. }
  188. }
  189. if (is.defined(image.premultiplied)) {
  190. if (is.bool(image.premultiplied)) {
  191. composite.premultiplied = image.premultiplied;
  192. } else {
  193. throw is.invalidParameterError('premultiplied', 'boolean', image.premultiplied);
  194. }
  195. }
  196. return composite;
  197. });
  198. return this;
  199. }
  200. /**
  201. * Decorate the Sharp prototype with composite-related functions.
  202. * @module Sharp
  203. * @private
  204. */
  205. module.exports = (Sharp) => {
  206. Sharp.prototype.composite = composite;
  207. Sharp.blend = blend;
  208. };