composite.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. 'use strict';
  2. const is = require('./is');
  3. /**
  4. * Blend modes.
  5. * @member
  6. * @private
  7. */
  8. const blend = {
  9. clear: 'clear',
  10. source: 'source',
  11. over: 'over',
  12. in: 'in',
  13. out: 'out',
  14. atop: 'atop',
  15. dest: 'dest',
  16. 'dest-over': 'dest-over',
  17. 'dest-in': 'dest-in',
  18. 'dest-out': 'dest-out',
  19. 'dest-atop': 'dest-atop',
  20. xor: 'xor',
  21. add: 'add',
  22. saturate: 'saturate',
  23. multiply: 'multiply',
  24. screen: 'screen',
  25. overlay: 'overlay',
  26. darken: 'darken',
  27. lighten: 'lighten',
  28. 'colour-dodge': 'colour-dodge',
  29. 'color-dodge': 'colour-dodge',
  30. 'colour-burn': 'colour-burn',
  31. 'color-burn': 'colour-burn',
  32. 'hard-light': 'hard-light',
  33. 'soft-light': 'soft-light',
  34. difference: 'difference',
  35. exclusion: 'exclusion'
  36. };
  37. /**
  38. * Composite image(s) over the processed (resized, extracted etc.) image.
  39. *
  40. * The images to composite must be the same size or smaller than the processed image.
  41. * If both `top` and `left` options are provided, they take precedence over `gravity`.
  42. *
  43. * The `blend` option can be one of `clear`, `source`, `over`, `in`, `out`, `atop`,
  44. * `dest`, `dest-over`, `dest-in`, `dest-out`, `dest-atop`,
  45. * `xor`, `add`, `saturate`, `multiply`, `screen`, `overlay`, `darken`, `lighten`,
  46. * `colour-dodge`, `color-dodge`, `colour-burn`,`color-burn`,
  47. * `hard-light`, `soft-light`, `difference`, `exclusion`.
  48. *
  49. * More information about blend modes can be found at
  50. * https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsBlendMode
  51. * and https://www.cairographics.org/operators/
  52. *
  53. * @since 0.22.0
  54. *
  55. * @example
  56. * sharp('input.png')
  57. * .rotate(180)
  58. * .resize(300)
  59. * .flatten( { background: '#ff6600' } )
  60. * .composite([{ input: 'overlay.png', gravity: 'southeast' }])
  61. * .sharpen()
  62. * .withMetadata()
  63. * .webp( { quality: 90 } )
  64. * .toBuffer()
  65. * .then(function(outputBuffer) {
  66. * // outputBuffer contains upside down, 300px wide, alpha channel flattened
  67. * // onto orange background, composited with overlay.png with SE gravity,
  68. * // sharpened, with metadata, 90% quality WebP image data. Phew!
  69. * });
  70. *
  71. * @param {Object[]} images - Ordered list of images to composite
  72. * @param {Buffer|String} [images[].input] - Buffer containing image data, String containing the path to an image file, or Create object (see below)
  73. * @param {Object} [images[].input.create] - describes a blank overlay to be created.
  74. * @param {Number} [images[].input.create.width]
  75. * @param {Number} [images[].input.create.height]
  76. * @param {Number} [images[].input.create.channels] - 3-4
  77. * @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.
  78. * @param {String} [images[].blend='over'] - how to blend this image with the image below.
  79. * @param {String} [images[].gravity='centre'] - gravity at which to place the overlay.
  80. * @param {Number} [images[].top] - the pixel offset from the top edge.
  81. * @param {Number} [images[].left] - the pixel offset from the left edge.
  82. * @param {Boolean} [images[].tile=false] - set to true to repeat the overlay image across the entire image with the given `gravity`.
  83. * @param {Boolean} [images[].premultiplied=false] - set to true to avoid premultipling the image below. Equivalent to the `--premultiplied` vips option.
  84. * @param {Number} [images[].density=72] - number representing the DPI for vector overlay image.
  85. * @param {Object} [images[].raw] - describes overlay when using raw pixel data.
  86. * @param {Number} [images[].raw.width]
  87. * @param {Number} [images[].raw.height]
  88. * @param {Number} [images[].raw.channels]
  89. * @returns {Sharp}
  90. * @throws {Error} Invalid parameters
  91. */
  92. function composite (images) {
  93. if (!Array.isArray(images)) {
  94. throw is.invalidParameterError('images to composite', 'array', images);
  95. }
  96. this.options.composite = images.map(image => {
  97. if (!is.object(image)) {
  98. throw is.invalidParameterError('image to composite', 'object', image);
  99. }
  100. const inputOptions = this._inputOptionsFromObject(image);
  101. const composite = {
  102. input: this._createInputDescriptor(image.input, inputOptions, { allowStream: false }),
  103. blend: 'over',
  104. tile: false,
  105. left: -1,
  106. top: -1,
  107. gravity: 0,
  108. premultiplied: false
  109. };
  110. if (is.defined(image.blend)) {
  111. if (is.string(blend[image.blend])) {
  112. composite.blend = blend[image.blend];
  113. } else {
  114. throw is.invalidParameterError('blend', 'valid blend name', image.blend);
  115. }
  116. }
  117. if (is.defined(image.tile)) {
  118. if (is.bool(image.tile)) {
  119. composite.tile = image.tile;
  120. } else {
  121. throw is.invalidParameterError('tile', 'boolean', image.tile);
  122. }
  123. }
  124. if (is.defined(image.left)) {
  125. if (is.integer(image.left) && image.left >= 0) {
  126. composite.left = image.left;
  127. } else {
  128. throw is.invalidParameterError('left', 'positive integer', image.left);
  129. }
  130. }
  131. if (is.defined(image.top)) {
  132. if (is.integer(image.top) && image.top >= 0) {
  133. composite.top = image.top;
  134. } else {
  135. throw is.invalidParameterError('top', 'positive integer', image.top);
  136. }
  137. }
  138. if (composite.left !== composite.top && Math.min(composite.left, composite.top) === -1) {
  139. throw new Error('Expected both left and top to be set');
  140. }
  141. if (is.defined(image.gravity)) {
  142. if (is.integer(image.gravity) && is.inRange(image.gravity, 0, 8)) {
  143. composite.gravity = image.gravity;
  144. } else if (is.string(image.gravity) && is.integer(this.constructor.gravity[image.gravity])) {
  145. composite.gravity = this.constructor.gravity[image.gravity];
  146. } else {
  147. throw is.invalidParameterError('gravity', 'valid gravity', image.gravity);
  148. }
  149. }
  150. if (is.defined(image.premultiplied)) {
  151. if (is.bool(image.premultiplied)) {
  152. composite.premultiplied = image.premultiplied;
  153. } else {
  154. throw is.invalidParameterError('premultiplied', 'boolean', image.premultiplied);
  155. }
  156. }
  157. return composite;
  158. });
  159. return this;
  160. }
  161. /**
  162. * Decorate the Sharp prototype with composite-related functions.
  163. * @private
  164. */
  165. module.exports = function (Sharp) {
  166. Sharp.prototype.composite = composite;
  167. Sharp.blend = blend;
  168. };