input.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. // Copyright 2013 Lovell Fuller and others.
  2. // SPDX-License-Identifier: Apache-2.0
  3. 'use strict';
  4. const color = require('color');
  5. const is = require('./is');
  6. const sharp = require('./sharp');
  7. /**
  8. * Justication alignment
  9. * @member
  10. * @private
  11. */
  12. const align = {
  13. left: 'low',
  14. center: 'centre',
  15. centre: 'centre',
  16. right: 'high'
  17. };
  18. /**
  19. * Extract input options, if any, from an object.
  20. * @private
  21. */
  22. function _inputOptionsFromObject (obj) {
  23. const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd } = obj;
  24. return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd].some(is.defined)
  25. ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd }
  26. : undefined;
  27. }
  28. /**
  29. * Create Object containing input and input-related options.
  30. * @private
  31. */
  32. function _createInputDescriptor (input, inputOptions, containerOptions) {
  33. const inputDescriptor = {
  34. failOn: 'warning',
  35. limitInputPixels: Math.pow(0x3FFF, 2),
  36. ignoreIcc: false,
  37. unlimited: false,
  38. sequentialRead: true
  39. };
  40. if (is.string(input)) {
  41. // filesystem
  42. inputDescriptor.file = input;
  43. } else if (is.buffer(input)) {
  44. // Buffer
  45. if (input.length === 0) {
  46. throw Error('Input Buffer is empty');
  47. }
  48. inputDescriptor.buffer = input;
  49. } else if (is.arrayBuffer(input)) {
  50. if (input.byteLength === 0) {
  51. throw Error('Input bit Array is empty');
  52. }
  53. inputDescriptor.buffer = Buffer.from(input, 0, input.byteLength);
  54. } else if (is.typedArray(input)) {
  55. if (input.length === 0) {
  56. throw Error('Input Bit Array is empty');
  57. }
  58. inputDescriptor.buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
  59. } else if (is.plainObject(input) && !is.defined(inputOptions)) {
  60. // Plain Object descriptor, e.g. create
  61. inputOptions = input;
  62. if (_inputOptionsFromObject(inputOptions)) {
  63. // Stream with options
  64. inputDescriptor.buffer = [];
  65. }
  66. } else if (!is.defined(input) && !is.defined(inputOptions) && is.object(containerOptions) && containerOptions.allowStream) {
  67. // Stream without options
  68. inputDescriptor.buffer = [];
  69. } else {
  70. throw new Error(`Unsupported input '${input}' of type ${typeof input}${
  71. is.defined(inputOptions) ? ` when also providing options of type ${typeof inputOptions}` : ''
  72. }`);
  73. }
  74. if (is.object(inputOptions)) {
  75. // Deprecated: failOnError
  76. if (is.defined(inputOptions.failOnError)) {
  77. if (is.bool(inputOptions.failOnError)) {
  78. inputDescriptor.failOn = inputOptions.failOnError ? 'warning' : 'none';
  79. } else {
  80. throw is.invalidParameterError('failOnError', 'boolean', inputOptions.failOnError);
  81. }
  82. }
  83. // failOn
  84. if (is.defined(inputOptions.failOn)) {
  85. if (is.string(inputOptions.failOn) && is.inArray(inputOptions.failOn, ['none', 'truncated', 'error', 'warning'])) {
  86. inputDescriptor.failOn = inputOptions.failOn;
  87. } else {
  88. throw is.invalidParameterError('failOn', 'one of: none, truncated, error, warning', inputOptions.failOn);
  89. }
  90. }
  91. // Density
  92. if (is.defined(inputOptions.density)) {
  93. if (is.inRange(inputOptions.density, 1, 100000)) {
  94. inputDescriptor.density = inputOptions.density;
  95. } else {
  96. throw is.invalidParameterError('density', 'number between 1 and 100000', inputOptions.density);
  97. }
  98. }
  99. // Ignore embeddded ICC profile
  100. if (is.defined(inputOptions.ignoreIcc)) {
  101. if (is.bool(inputOptions.ignoreIcc)) {
  102. inputDescriptor.ignoreIcc = inputOptions.ignoreIcc;
  103. } else {
  104. throw is.invalidParameterError('ignoreIcc', 'boolean', inputOptions.ignoreIcc);
  105. }
  106. }
  107. // limitInputPixels
  108. if (is.defined(inputOptions.limitInputPixels)) {
  109. if (is.bool(inputOptions.limitInputPixels)) {
  110. inputDescriptor.limitInputPixels = inputOptions.limitInputPixels
  111. ? Math.pow(0x3FFF, 2)
  112. : 0;
  113. } else if (is.integer(inputOptions.limitInputPixels) && is.inRange(inputOptions.limitInputPixels, 0, Number.MAX_SAFE_INTEGER)) {
  114. inputDescriptor.limitInputPixels = inputOptions.limitInputPixels;
  115. } else {
  116. throw is.invalidParameterError('limitInputPixels', 'positive integer', inputOptions.limitInputPixels);
  117. }
  118. }
  119. // unlimited
  120. if (is.defined(inputOptions.unlimited)) {
  121. if (is.bool(inputOptions.unlimited)) {
  122. inputDescriptor.unlimited = inputOptions.unlimited;
  123. } else {
  124. throw is.invalidParameterError('unlimited', 'boolean', inputOptions.unlimited);
  125. }
  126. }
  127. // sequentialRead
  128. if (is.defined(inputOptions.sequentialRead)) {
  129. if (is.bool(inputOptions.sequentialRead)) {
  130. inputDescriptor.sequentialRead = inputOptions.sequentialRead;
  131. } else {
  132. throw is.invalidParameterError('sequentialRead', 'boolean', inputOptions.sequentialRead);
  133. }
  134. }
  135. // Raw pixel input
  136. if (is.defined(inputOptions.raw)) {
  137. if (
  138. is.object(inputOptions.raw) &&
  139. is.integer(inputOptions.raw.width) && inputOptions.raw.width > 0 &&
  140. is.integer(inputOptions.raw.height) && inputOptions.raw.height > 0 &&
  141. is.integer(inputOptions.raw.channels) && is.inRange(inputOptions.raw.channels, 1, 4)
  142. ) {
  143. inputDescriptor.rawWidth = inputOptions.raw.width;
  144. inputDescriptor.rawHeight = inputOptions.raw.height;
  145. inputDescriptor.rawChannels = inputOptions.raw.channels;
  146. inputDescriptor.rawPremultiplied = !!inputOptions.raw.premultiplied;
  147. switch (input.constructor) {
  148. case Uint8Array:
  149. case Uint8ClampedArray:
  150. inputDescriptor.rawDepth = 'uchar';
  151. break;
  152. case Int8Array:
  153. inputDescriptor.rawDepth = 'char';
  154. break;
  155. case Uint16Array:
  156. inputDescriptor.rawDepth = 'ushort';
  157. break;
  158. case Int16Array:
  159. inputDescriptor.rawDepth = 'short';
  160. break;
  161. case Uint32Array:
  162. inputDescriptor.rawDepth = 'uint';
  163. break;
  164. case Int32Array:
  165. inputDescriptor.rawDepth = 'int';
  166. break;
  167. case Float32Array:
  168. inputDescriptor.rawDepth = 'float';
  169. break;
  170. case Float64Array:
  171. inputDescriptor.rawDepth = 'double';
  172. break;
  173. default:
  174. inputDescriptor.rawDepth = 'uchar';
  175. break;
  176. }
  177. } else {
  178. throw new Error('Expected width, height and channels for raw pixel input');
  179. }
  180. }
  181. // Multi-page input (GIF, TIFF, PDF)
  182. if (is.defined(inputOptions.animated)) {
  183. if (is.bool(inputOptions.animated)) {
  184. inputDescriptor.pages = inputOptions.animated ? -1 : 1;
  185. } else {
  186. throw is.invalidParameterError('animated', 'boolean', inputOptions.animated);
  187. }
  188. }
  189. if (is.defined(inputOptions.pages)) {
  190. if (is.integer(inputOptions.pages) && is.inRange(inputOptions.pages, -1, 100000)) {
  191. inputDescriptor.pages = inputOptions.pages;
  192. } else {
  193. throw is.invalidParameterError('pages', 'integer between -1 and 100000', inputOptions.pages);
  194. }
  195. }
  196. if (is.defined(inputOptions.page)) {
  197. if (is.integer(inputOptions.page) && is.inRange(inputOptions.page, 0, 100000)) {
  198. inputDescriptor.page = inputOptions.page;
  199. } else {
  200. throw is.invalidParameterError('page', 'integer between 0 and 100000', inputOptions.page);
  201. }
  202. }
  203. // Multi-level input (OpenSlide)
  204. if (is.defined(inputOptions.level)) {
  205. if (is.integer(inputOptions.level) && is.inRange(inputOptions.level, 0, 256)) {
  206. inputDescriptor.level = inputOptions.level;
  207. } else {
  208. throw is.invalidParameterError('level', 'integer between 0 and 256', inputOptions.level);
  209. }
  210. }
  211. // Sub Image File Directory (TIFF)
  212. if (is.defined(inputOptions.subifd)) {
  213. if (is.integer(inputOptions.subifd) && is.inRange(inputOptions.subifd, -1, 100000)) {
  214. inputDescriptor.subifd = inputOptions.subifd;
  215. } else {
  216. throw is.invalidParameterError('subifd', 'integer between -1 and 100000', inputOptions.subifd);
  217. }
  218. }
  219. // Create new image
  220. if (is.defined(inputOptions.create)) {
  221. if (
  222. is.object(inputOptions.create) &&
  223. is.integer(inputOptions.create.width) && inputOptions.create.width > 0 &&
  224. is.integer(inputOptions.create.height) && inputOptions.create.height > 0 &&
  225. is.integer(inputOptions.create.channels)
  226. ) {
  227. inputDescriptor.createWidth = inputOptions.create.width;
  228. inputDescriptor.createHeight = inputOptions.create.height;
  229. inputDescriptor.createChannels = inputOptions.create.channels;
  230. // Noise
  231. if (is.defined(inputOptions.create.noise)) {
  232. if (!is.object(inputOptions.create.noise)) {
  233. throw new Error('Expected noise to be an object');
  234. }
  235. if (!is.inArray(inputOptions.create.noise.type, ['gaussian'])) {
  236. throw new Error('Only gaussian noise is supported at the moment');
  237. }
  238. if (!is.inRange(inputOptions.create.channels, 1, 4)) {
  239. throw is.invalidParameterError('create.channels', 'number between 1 and 4', inputOptions.create.channels);
  240. }
  241. inputDescriptor.createNoiseType = inputOptions.create.noise.type;
  242. if (is.number(inputOptions.create.noise.mean) && is.inRange(inputOptions.create.noise.mean, 0, 10000)) {
  243. inputDescriptor.createNoiseMean = inputOptions.create.noise.mean;
  244. } else {
  245. throw is.invalidParameterError('create.noise.mean', 'number between 0 and 10000', inputOptions.create.noise.mean);
  246. }
  247. if (is.number(inputOptions.create.noise.sigma) && is.inRange(inputOptions.create.noise.sigma, 0, 10000)) {
  248. inputDescriptor.createNoiseSigma = inputOptions.create.noise.sigma;
  249. } else {
  250. throw is.invalidParameterError('create.noise.sigma', 'number between 0 and 10000', inputOptions.create.noise.sigma);
  251. }
  252. } else if (is.defined(inputOptions.create.background)) {
  253. if (!is.inRange(inputOptions.create.channels, 3, 4)) {
  254. throw is.invalidParameterError('create.channels', 'number between 3 and 4', inputOptions.create.channels);
  255. }
  256. const background = color(inputOptions.create.background);
  257. inputDescriptor.createBackground = [
  258. background.red(),
  259. background.green(),
  260. background.blue(),
  261. Math.round(background.alpha() * 255)
  262. ];
  263. } else {
  264. throw new Error('Expected valid noise or background to create a new input image');
  265. }
  266. delete inputDescriptor.buffer;
  267. } else {
  268. throw new Error('Expected valid width, height and channels to create a new input image');
  269. }
  270. }
  271. // Create a new image with text
  272. if (is.defined(inputOptions.text)) {
  273. if (is.object(inputOptions.text) && is.string(inputOptions.text.text)) {
  274. inputDescriptor.textValue = inputOptions.text.text;
  275. if (is.defined(inputOptions.text.height) && is.defined(inputOptions.text.dpi)) {
  276. throw new Error('Expected only one of dpi or height');
  277. }
  278. if (is.defined(inputOptions.text.font)) {
  279. if (is.string(inputOptions.text.font)) {
  280. inputDescriptor.textFont = inputOptions.text.font;
  281. } else {
  282. throw is.invalidParameterError('text.font', 'string', inputOptions.text.font);
  283. }
  284. }
  285. if (is.defined(inputOptions.text.fontfile)) {
  286. if (is.string(inputOptions.text.fontfile)) {
  287. inputDescriptor.textFontfile = inputOptions.text.fontfile;
  288. } else {
  289. throw is.invalidParameterError('text.fontfile', 'string', inputOptions.text.fontfile);
  290. }
  291. }
  292. if (is.defined(inputOptions.text.width)) {
  293. if (is.number(inputOptions.text.width)) {
  294. inputDescriptor.textWidth = inputOptions.text.width;
  295. } else {
  296. throw is.invalidParameterError('text.textWidth', 'number', inputOptions.text.width);
  297. }
  298. }
  299. if (is.defined(inputOptions.text.height)) {
  300. if (is.number(inputOptions.text.height)) {
  301. inputDescriptor.textHeight = inputOptions.text.height;
  302. } else {
  303. throw is.invalidParameterError('text.height', 'number', inputOptions.text.height);
  304. }
  305. }
  306. if (is.defined(inputOptions.text.align)) {
  307. if (is.string(inputOptions.text.align) && is.string(this.constructor.align[inputOptions.text.align])) {
  308. inputDescriptor.textAlign = this.constructor.align[inputOptions.text.align];
  309. } else {
  310. throw is.invalidParameterError('text.align', 'valid alignment', inputOptions.text.align);
  311. }
  312. }
  313. if (is.defined(inputOptions.text.justify)) {
  314. if (is.bool(inputOptions.text.justify)) {
  315. inputDescriptor.textJustify = inputOptions.text.justify;
  316. } else {
  317. throw is.invalidParameterError('text.justify', 'boolean', inputOptions.text.justify);
  318. }
  319. }
  320. if (is.defined(inputOptions.text.dpi)) {
  321. if (is.number(inputOptions.text.dpi) && is.inRange(inputOptions.text.dpi, 1, 100000)) {
  322. inputDescriptor.textDpi = inputOptions.text.dpi;
  323. } else {
  324. throw is.invalidParameterError('text.dpi', 'number between 1 and 100000', inputOptions.text.dpi);
  325. }
  326. }
  327. if (is.defined(inputOptions.text.rgba)) {
  328. if (is.bool(inputOptions.text.rgba)) {
  329. inputDescriptor.textRgba = inputOptions.text.rgba;
  330. } else {
  331. throw is.invalidParameterError('text.rgba', 'bool', inputOptions.text.rgba);
  332. }
  333. }
  334. if (is.defined(inputOptions.text.spacing)) {
  335. if (is.number(inputOptions.text.spacing)) {
  336. inputDescriptor.textSpacing = inputOptions.text.spacing;
  337. } else {
  338. throw is.invalidParameterError('text.spacing', 'number', inputOptions.text.spacing);
  339. }
  340. }
  341. if (is.defined(inputOptions.text.wrap)) {
  342. if (is.string(inputOptions.text.wrap) && is.inArray(inputOptions.text.wrap, ['word', 'char', 'wordChar', 'none'])) {
  343. inputDescriptor.textWrap = inputOptions.text.wrap;
  344. } else {
  345. throw is.invalidParameterError('text.wrap', 'one of: word, char, wordChar, none', inputOptions.text.wrap);
  346. }
  347. }
  348. delete inputDescriptor.buffer;
  349. } else {
  350. throw new Error('Expected a valid string to create an image with text.');
  351. }
  352. }
  353. } else if (is.defined(inputOptions)) {
  354. throw new Error('Invalid input options ' + inputOptions);
  355. }
  356. return inputDescriptor;
  357. }
  358. /**
  359. * Handle incoming Buffer chunk on Writable Stream.
  360. * @private
  361. * @param {Buffer} chunk
  362. * @param {string} encoding - unused
  363. * @param {Function} callback
  364. */
  365. function _write (chunk, encoding, callback) {
  366. /* istanbul ignore else */
  367. if (Array.isArray(this.options.input.buffer)) {
  368. /* istanbul ignore else */
  369. if (is.buffer(chunk)) {
  370. if (this.options.input.buffer.length === 0) {
  371. this.on('finish', () => {
  372. this.streamInFinished = true;
  373. });
  374. }
  375. this.options.input.buffer.push(chunk);
  376. callback();
  377. } else {
  378. callback(new Error('Non-Buffer data on Writable Stream'));
  379. }
  380. } else {
  381. callback(new Error('Unexpected data on Writable Stream'));
  382. }
  383. }
  384. /**
  385. * Flattens the array of chunks accumulated in input.buffer.
  386. * @private
  387. */
  388. function _flattenBufferIn () {
  389. if (this._isStreamInput()) {
  390. this.options.input.buffer = Buffer.concat(this.options.input.buffer);
  391. }
  392. }
  393. /**
  394. * Are we expecting Stream-based input?
  395. * @private
  396. * @returns {boolean}
  397. */
  398. function _isStreamInput () {
  399. return Array.isArray(this.options.input.buffer);
  400. }
  401. /**
  402. * Fast access to (uncached) image metadata without decoding any compressed pixel data.
  403. *
  404. * This is read from the header of the input image.
  405. * It does not take into consideration any operations to be applied to the output image,
  406. * such as resize or rotate.
  407. *
  408. * Dimensions in the response will respect the `page` and `pages` properties of the
  409. * {@link /api-constructor#parameters|constructor parameters}.
  410. *
  411. * A `Promise` is returned when `callback` is not provided.
  412. *
  413. * - `format`: Name of decoder used to decompress image data e.g. `jpeg`, `png`, `webp`, `gif`, `svg`
  414. * - `size`: Total size of image in bytes, for Stream and Buffer input only
  415. * - `width`: Number of pixels wide (EXIF orientation is not taken into consideration, see example below)
  416. * - `height`: Number of pixels high (EXIF orientation is not taken into consideration, see example below)
  417. * - `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://www.libvips.org/API/current/VipsImage.html#VipsInterpretation)
  418. * - `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK
  419. * - `depth`: Name of pixel depth format e.g. `uchar`, `char`, `ushort`, `float` [...](https://www.libvips.org/API/current/VipsImage.html#VipsBandFormat)
  420. * - `density`: Number of pixels per inch (DPI), if present
  421. * - `chromaSubsampling`: String containing JPEG chroma subsampling, `4:2:0` or `4:4:4` for RGB, `4:2:0:4` or `4:4:4:4` for CMYK
  422. * - `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan
  423. * - `pages`: Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP
  424. * - `pageHeight`: Number of pixels high each page in a multi-page image will be.
  425. * - `paletteBitDepth`: Bit depth of palette-based image (GIF, PNG).
  426. * - `loop`: Number of times to loop an animated image, zero refers to a continuous loop.
  427. * - `delay`: Delay in ms between each page in an animated image, provided as an array of integers.
  428. * - `pagePrimary`: Number of the primary page in a HEIF image
  429. * - `levels`: Details of each level in a multi-level image provided as an array of objects, requires libvips compiled with support for OpenSlide
  430. * - `subifds`: Number of Sub Image File Directories in an OME-TIFF image
  431. * - `background`: Default background colour, if present, for PNG (bKGD) and GIF images, either an RGB Object or a single greyscale value
  432. * - `compression`: The encoder used to compress an HEIF file, `av1` (AVIF) or `hevc` (HEIC)
  433. * - `resolutionUnit`: The unit of resolution (density), either `inch` or `cm`, if present
  434. * - `hasProfile`: Boolean indicating the presence of an embedded ICC profile
  435. * - `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
  436. * - `orientation`: Number value of the EXIF Orientation header, if present
  437. * - `exif`: Buffer containing raw EXIF data, if present
  438. * - `icc`: Buffer containing raw [ICC](https://www.npmjs.com/package/icc) profile data, if present
  439. * - `iptc`: Buffer containing raw IPTC data, if present
  440. * - `xmp`: Buffer containing raw XMP data, if present
  441. * - `tifftagPhotoshop`: Buffer containing raw TIFFTAG_PHOTOSHOP data, if present
  442. * - `formatMagick`: String containing format for images loaded via *magick
  443. *
  444. * @example
  445. * const metadata = await sharp(input).metadata();
  446. *
  447. * @example
  448. * const image = sharp(inputJpg);
  449. * image
  450. * .metadata()
  451. * .then(function(metadata) {
  452. * return image
  453. * .resize(Math.round(metadata.width / 2))
  454. * .webp()
  455. * .toBuffer();
  456. * })
  457. * .then(function(data) {
  458. * // data contains a WebP image half the width and height of the original JPEG
  459. * });
  460. *
  461. * @example
  462. * // Based on EXIF rotation metadata, get the right-side-up width and height:
  463. *
  464. * const size = getNormalSize(await sharp(input).metadata());
  465. *
  466. * function getNormalSize({ width, height, orientation }) {
  467. * return (orientation || 0) >= 5
  468. * ? { width: height, height: width }
  469. * : { width, height };
  470. * }
  471. *
  472. * @param {Function} [callback] - called with the arguments `(err, metadata)`
  473. * @returns {Promise<Object>|Sharp}
  474. */
  475. function metadata (callback) {
  476. if (is.fn(callback)) {
  477. if (this._isStreamInput()) {
  478. this.on('finish', () => {
  479. this._flattenBufferIn();
  480. sharp.metadata(this.options, callback);
  481. });
  482. } else {
  483. sharp.metadata(this.options, callback);
  484. }
  485. return this;
  486. } else {
  487. if (this._isStreamInput()) {
  488. return new Promise((resolve, reject) => {
  489. const finished = () => {
  490. this._flattenBufferIn();
  491. sharp.metadata(this.options, (err, metadata) => {
  492. if (err) {
  493. reject(err);
  494. } else {
  495. resolve(metadata);
  496. }
  497. });
  498. };
  499. if (this.writableFinished) {
  500. finished();
  501. } else {
  502. this.once('finish', finished);
  503. }
  504. });
  505. } else {
  506. return new Promise((resolve, reject) => {
  507. sharp.metadata(this.options, (err, metadata) => {
  508. if (err) {
  509. reject(err);
  510. } else {
  511. resolve(metadata);
  512. }
  513. });
  514. });
  515. }
  516. }
  517. }
  518. /**
  519. * Access to pixel-derived image statistics for every channel in the image.
  520. * A `Promise` is returned when `callback` is not provided.
  521. *
  522. * - `channels`: Array of channel statistics for each channel in the image. Each channel statistic contains
  523. * - `min` (minimum value in the channel)
  524. * - `max` (maximum value in the channel)
  525. * - `sum` (sum of all values in a channel)
  526. * - `squaresSum` (sum of squared values in a channel)
  527. * - `mean` (mean of the values in a channel)
  528. * - `stdev` (standard deviation for the values in a channel)
  529. * - `minX` (x-coordinate of one of the pixel where the minimum lies)
  530. * - `minY` (y-coordinate of one of the pixel where the minimum lies)
  531. * - `maxX` (x-coordinate of one of the pixel where the maximum lies)
  532. * - `maxY` (y-coordinate of one of the pixel where the maximum lies)
  533. * - `isOpaque`: Is the image fully opaque? Will be `true` if the image has no alpha channel or if every pixel is fully opaque.
  534. * - `entropy`: Histogram-based estimation of greyscale entropy, discarding alpha channel if any.
  535. * - `sharpness`: Estimation of greyscale sharpness based on the standard deviation of a Laplacian convolution, discarding alpha channel if any.
  536. * - `dominant`: Object containing most dominant sRGB colour based on a 4096-bin 3D histogram.
  537. *
  538. * **Note**: Statistics are derived from the original input image. Any operations performed on the image must first be
  539. * written to a buffer in order to run `stats` on the result (see third example).
  540. *
  541. * @example
  542. * const image = sharp(inputJpg);
  543. * image
  544. * .stats()
  545. * .then(function(stats) {
  546. * // stats contains the channel-wise statistics array and the isOpaque value
  547. * });
  548. *
  549. * @example
  550. * const { entropy, sharpness, dominant } = await sharp(input).stats();
  551. * const { r, g, b } = dominant;
  552. *
  553. * @example
  554. * const image = sharp(input);
  555. * // store intermediate result
  556. * const part = await image.extract(region).toBuffer();
  557. * // create new instance to obtain statistics of extracted region
  558. * const stats = await sharp(part).stats();
  559. *
  560. * @param {Function} [callback] - called with the arguments `(err, stats)`
  561. * @returns {Promise<Object>}
  562. */
  563. function stats (callback) {
  564. if (is.fn(callback)) {
  565. if (this._isStreamInput()) {
  566. this.on('finish', () => {
  567. this._flattenBufferIn();
  568. sharp.stats(this.options, callback);
  569. });
  570. } else {
  571. sharp.stats(this.options, callback);
  572. }
  573. return this;
  574. } else {
  575. if (this._isStreamInput()) {
  576. return new Promise((resolve, reject) => {
  577. this.on('finish', function () {
  578. this._flattenBufferIn();
  579. sharp.stats(this.options, (err, stats) => {
  580. if (err) {
  581. reject(err);
  582. } else {
  583. resolve(stats);
  584. }
  585. });
  586. });
  587. });
  588. } else {
  589. return new Promise((resolve, reject) => {
  590. sharp.stats(this.options, (err, stats) => {
  591. if (err) {
  592. reject(err);
  593. } else {
  594. resolve(stats);
  595. }
  596. });
  597. });
  598. }
  599. }
  600. }
  601. /**
  602. * Decorate the Sharp prototype with input-related functions.
  603. * @private
  604. */
  605. module.exports = function (Sharp) {
  606. Object.assign(Sharp.prototype, {
  607. // Private
  608. _inputOptionsFromObject,
  609. _createInputDescriptor,
  610. _write,
  611. _flattenBufferIn,
  612. _isStreamInput,
  613. // Public
  614. metadata,
  615. stats
  616. });
  617. // Class attributes
  618. Sharp.align = align;
  619. };