'use strict'; const color = require('color'); const is = require('./is'); const sharp = require('../build/Release/sharp.node'); /** * Extract input options, if any, from an object. * @private */ function _inputOptionsFromObject (obj) { const { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages } = obj; return [raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages].some(is.defined) ? { raw, density, limitInputPixels, sequentialRead, failOnError, animated, page, pages } : undefined; } /** * Create Object containing input and input-related options. * @private */ function _createInputDescriptor (input, inputOptions, containerOptions) { const inputDescriptor = { failOnError: true, limitInputPixels: Math.pow(0x3FFF, 2), sequentialRead: false }; if (is.string(input)) { // filesystem inputDescriptor.file = input; } else if (is.buffer(input)) { // Buffer inputDescriptor.buffer = input; } else if (is.plainObject(input) && !is.defined(inputOptions)) { // Plain Object descriptor, e.g. create inputOptions = input; if (_inputOptionsFromObject(inputOptions)) { // Stream with options inputDescriptor.buffer = []; } } else if (!is.defined(input) && !is.defined(inputOptions) && is.object(containerOptions) && containerOptions.allowStream) { // Stream without options inputDescriptor.buffer = []; } else { throw new Error(`Unsupported input '${input}' of type ${typeof input}${ is.defined(inputOptions) ? ` when also providing options of type ${typeof inputOptions}` : '' }`); } if (is.object(inputOptions)) { // Fail on error if (is.defined(inputOptions.failOnError)) { if (is.bool(inputOptions.failOnError)) { inputDescriptor.failOnError = inputOptions.failOnError; } else { throw is.invalidParameterError('failOnError', 'boolean', inputOptions.failOnError); } } // Density if (is.defined(inputOptions.density)) { if (is.inRange(inputOptions.density, 1, 100000)) { inputDescriptor.density = inputOptions.density; } else { throw is.invalidParameterError('density', 'number between 1 and 100000', inputOptions.density); } } // limitInputPixels if (is.defined(inputOptions.limitInputPixels)) { if (is.bool(inputOptions.limitInputPixels)) { inputDescriptor.limitInputPixels = inputOptions.limitInputPixels ? Math.pow(0x3FFF, 2) : 0; } else if (is.integer(inputOptions.limitInputPixels) && inputOptions.limitInputPixels >= 0) { inputDescriptor.limitInputPixels = inputOptions.limitInputPixels; } else { throw is.invalidParameterError('limitInputPixels', 'integer >= 0', inputOptions.limitInputPixels); } } // sequentialRead if (is.defined(inputOptions.sequentialRead)) { if (is.bool(inputOptions.sequentialRead)) { inputDescriptor.sequentialRead = inputOptions.sequentialRead; } else { throw is.invalidParameterError('sequentialRead', 'boolean', inputOptions.sequentialRead); } } // Raw pixel input if (is.defined(inputOptions.raw)) { if ( is.object(inputOptions.raw) && is.integer(inputOptions.raw.width) && inputOptions.raw.width > 0 && is.integer(inputOptions.raw.height) && inputOptions.raw.height > 0 && is.integer(inputOptions.raw.channels) && is.inRange(inputOptions.raw.channels, 1, 4) ) { inputDescriptor.rawWidth = inputOptions.raw.width; inputDescriptor.rawHeight = inputOptions.raw.height; inputDescriptor.rawChannels = inputOptions.raw.channels; } else { throw new Error('Expected width, height and channels for raw pixel input'); } } // Multi-page input (GIF, TIFF, PDF) if (is.defined(inputOptions.animated)) { if (is.bool(inputOptions.animated)) { inputDescriptor.pages = inputOptions.animated ? -1 : 1; } else { throw is.invalidParameterError('animated', 'boolean', inputOptions.animated); } } if (is.defined(inputOptions.pages)) { if (is.integer(inputOptions.pages) && is.inRange(inputOptions.pages, -1, 100000)) { inputDescriptor.pages = inputOptions.pages; } else { throw is.invalidParameterError('pages', 'integer between -1 and 100000', inputOptions.pages); } } if (is.defined(inputOptions.page)) { if (is.integer(inputOptions.page) && is.inRange(inputOptions.page, 0, 100000)) { inputDescriptor.page = inputOptions.page; } else { throw is.invalidParameterError('page', 'integer between 0 and 100000', inputOptions.page); } } // Multi-level input (OpenSlide) if (is.defined(inputOptions.level)) { if (is.integer(inputOptions.level) && is.inRange(inputOptions.level, 0, 256)) { inputDescriptor.level = inputOptions.level; } else { throw is.invalidParameterError('level', 'integer between 0 and 256', inputOptions.level); } } // Create new image if (is.defined(inputOptions.create)) { if ( is.object(inputOptions.create) && is.integer(inputOptions.create.width) && inputOptions.create.width > 0 && is.integer(inputOptions.create.height) && inputOptions.create.height > 0 && is.integer(inputOptions.create.channels) && is.inRange(inputOptions.create.channels, 3, 4) && is.defined(inputOptions.create.background) ) { inputDescriptor.createWidth = inputOptions.create.width; inputDescriptor.createHeight = inputOptions.create.height; inputDescriptor.createChannels = inputOptions.create.channels; const background = color(inputOptions.create.background); inputDescriptor.createBackground = [ background.red(), background.green(), background.blue(), Math.round(background.alpha() * 255) ]; delete inputDescriptor.buffer; } else { throw new Error('Expected width, height, channels and background to create a new input image'); } } } else if (is.defined(inputOptions)) { throw new Error('Invalid input options ' + inputOptions); } return inputDescriptor; } /** * Handle incoming Buffer chunk on Writable Stream. * @private * @param {Buffer} chunk * @param {string} encoding - unused * @param {Function} callback */ function _write (chunk, encoding, callback) { /* istanbul ignore else */ if (Array.isArray(this.options.input.buffer)) { /* istanbul ignore else */ if (is.buffer(chunk)) { if (this.options.input.buffer.length === 0) { this.on('finish', () => { this.streamInFinished = true; }); } this.options.input.buffer.push(chunk); callback(); } else { callback(new Error('Non-Buffer data on Writable Stream')); } } else { callback(new Error('Unexpected data on Writable Stream')); } } /** * Flattens the array of chunks accumulated in input.buffer. * @private */ function _flattenBufferIn () { if (this._isStreamInput()) { this.options.input.buffer = Buffer.concat(this.options.input.buffer); } } /** * Are we expecting Stream-based input? * @private * @returns {boolean} */ function _isStreamInput () { return Array.isArray(this.options.input.buffer); } /** * Fast access to (uncached) image metadata without decoding any compressed image data. * A `Promise` is returned when `callback` is not provided. * * - `format`: Name of decoder used to decompress image data e.g. `jpeg`, `png`, `webp`, `gif`, `svg` * - `size`: Total size of image in bytes, for Stream and Buffer input only * - `width`: Number of pixels wide (EXIF orientation is not taken into consideration) * - `height`: Number of pixels high (EXIF orientation is not taken into consideration) * - `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://libvips.github.io/libvips/API/current/VipsImage.html#VipsInterpretation) * - `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK * - `depth`: Name of pixel depth format e.g. `uchar`, `char`, `ushort`, `float` [...](https://libvips.github.io/libvips/API/current/VipsImage.html#VipsBandFormat) * - `density`: Number of pixels per inch (DPI), if present * - `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 * - `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan * - `pages`: Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP * - `pageHeight`: Number of pixels high each page in a multi-page image will be. * - `loop`: Number of times to loop an animated image, zero refers to a continuous loop. * - `delay`: Delay in ms between each page in an animated image, provided as an array of integers. * - `pagePrimary`: Number of the primary page in a HEIF image * - `levels`: Details of each level in a multi-level image provided as an array of objects, requires libvips compiled with support for OpenSlide * - `hasProfile`: Boolean indicating the presence of an embedded ICC profile * - `hasAlpha`: Boolean indicating the presence of an alpha transparency channel * - `orientation`: Number value of the EXIF Orientation header, if present * - `exif`: Buffer containing raw EXIF data, if present * - `icc`: Buffer containing raw [ICC](https://www.npmjs.com/package/icc) profile data, if present * - `iptc`: Buffer containing raw IPTC data, if present * - `xmp`: Buffer containing raw XMP data, if present * - `tifftagPhotoshop`: Buffer containing raw TIFFTAG_PHOTOSHOP data, if present * * @example * const image = sharp(inputJpg); * image * .metadata() * .then(function(metadata) { * return image * .resize(Math.round(metadata.width / 2)) * .webp() * .toBuffer(); * }) * .then(function(data) { * // data contains a WebP image half the width and height of the original JPEG * }); * * @param {Function} [callback] - called with the arguments `(err, metadata)` * @returns {Promise|Sharp} */ function metadata (callback) { if (is.fn(callback)) { if (this._isStreamInput()) { this.on('finish', () => { this._flattenBufferIn(); sharp.metadata(this.options, callback); }); } else { sharp.metadata(this.options, callback); } return this; } else { if (this._isStreamInput()) { return new Promise((resolve, reject) => { this.on('finish', () => { this._flattenBufferIn(); sharp.metadata(this.options, (err, metadata) => { if (err) { reject(err); } else { resolve(metadata); } }); }); }); } else { return new Promise((resolve, reject) => { sharp.metadata(this.options, (err, metadata) => { if (err) { reject(err); } else { resolve(metadata); } }); }); } } } /** * Access to pixel-derived image statistics for every channel in the image. * A `Promise` is returned when `callback` is not provided. * * - `channels`: Array of channel statistics for each channel in the image. Each channel statistic contains * - `min` (minimum value in the channel) * - `max` (maximum value in the channel) * - `sum` (sum of all values in a channel) * - `squaresSum` (sum of squared values in a channel) * - `mean` (mean of the values in a channel) * - `stdev` (standard deviation for the values in a channel) * - `minX` (x-coordinate of one of the pixel where the minimum lies) * - `minY` (y-coordinate of one of the pixel where the minimum lies) * - `maxX` (x-coordinate of one of the pixel where the maximum lies) * - `maxY` (y-coordinate of one of the pixel where the maximum lies) * - `isOpaque`: Is the image fully opaque? Will be `true` if the image has no alpha channel or if every pixel is fully opaque. * - `entropy`: Histogram-based estimation of greyscale entropy, discarding alpha channel if any (experimental) * - `sharpness`: Estimation of greyscale sharpness based on the standard deviation of a Laplacian convolution, discarding alpha channel if any (experimental) * - `dominant`: Object containing most dominant sRGB colour based on a 4096-bin 3D histogram (experimental) * * @example * const image = sharp(inputJpg); * image * .stats() * .then(function(stats) { * // stats contains the channel-wise statistics array and the isOpaque value * }); * * @example * const { entropy, sharpness, dominant } = await sharp(input).stats(); * const { r, g, b } = dominant; * * @param {Function} [callback] - called with the arguments `(err, stats)` * @returns {Promise} */ function stats (callback) { if (is.fn(callback)) { if (this._isStreamInput()) { this.on('finish', () => { this._flattenBufferIn(); sharp.stats(this.options, callback); }); } else { sharp.stats(this.options, callback); } return this; } else { if (this._isStreamInput()) { return new Promise((resolve, reject) => { this.on('finish', function () { this._flattenBufferIn(); sharp.stats(this.options, (err, stats) => { if (err) { reject(err); } else { resolve(stats); } }); }); }); } else { return new Promise((resolve, reject) => { sharp.stats(this.options, (err, stats) => { if (err) { reject(err); } else { resolve(stats); } }); }); } } } /** * Decorate the Sharp prototype with input-related functions. * @private */ module.exports = function (Sharp) { Object.assign(Sharp.prototype, { // Private _inputOptionsFromObject, _createInputDescriptor, _write, _flattenBufferIn, _isStreamInput, // Public metadata, stats }); };