sharp/index.js
Lovell Fuller 928edfd1dd Ensure jsdoc comments/types exist for all public methods.
This is a precursor to fully-automated docs and typings.
2016-11-02 09:25:20 +00:00

1910 lines
66 KiB
JavaScript

'use strict';
const path = require('path');
const util = require('util');
const stream = require('stream');
const events = require('events');
const semver = require('semver');
const color = require('color');
const sharp = require('./build/Release/sharp.node');
// Versioning
let versions = {
vips: sharp.libvipsVersion()
};
(function () {
// Does libvips meet minimum requirement?
const libvipsVersionMin = require('./package.json').config.libvips;
if (semver.lt(versions.vips, libvipsVersionMin)) {
throw new Error('Found libvips ' + versions.vips + ' but require at least ' + libvipsVersionMin);
}
// Include versions of dependencies, if present
try {
versions = require('./lib/versions.json');
} catch (err) {}
})();
// Limits
const maximum = {
width: 0x3FFF,
height: 0x3FFF,
pixels: Math.pow(0x3FFF, 2)
};
/**
* Constructor factory to which further methods are chained.
*
* JPEG, PNG or WebP format image data can be streamed out from this object.
* When using Stream based output, derived attributes are available from the `info` event.
*
* @example
* sharp('input.jpg')
* .resize(300, 200)
* .toFile('output.jpg', function(err) {
* // output.jpg is a 300 pixels wide and 200 pixels high image
* // containing a scaled and cropped version of input.jpg
* });
*
* @example
* // Read image data from readableStream,
* // resize to 300 pixels wide,
* // emit an 'info' event with calculated dimensions
* // and finally write image data to writableStream
* var transformer = sharp()
* .resize(300)
* .on('info', function(info) {
* console.log('Image height is ' + info.height);
* });
* readableStream.pipe(transformer).pipe(writableStream);
*
* @param {Buffer|String} [input] - if present, can be
* a Buffer containing JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data, or
* a String containing the path to an JPEG, PNG, WebP, GIF, SVG or TIFF image file.
* JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when null or undefined.
*
* @param {Object} [options] - if present, is an Object with optional attributes.
* @param {Number} [options.density=72] - integral number representing the DPI for vector images.
* @param {Object} [options.raw] - describes raw pixel image data. See `raw()` for pixel ordering.
* @param {Number} [options.raw.width]
* @param {Number} [options.raw.height]
* @param {Number} [options.raw.channels]
* @returns {Sharp} - Implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
* @throws {Error} Invalid parameters
*/
const Sharp = function (input, options) {
if (!(this instanceof Sharp)) {
return new Sharp(input, options);
}
stream.Duplex.call(this);
this.options = {
// input options
sequentialRead: false,
limitInputPixels: maximum.pixels,
// ICC profiles
iccProfilePath: path.join(__dirname, 'icc') + path.sep,
// resize options
topOffsetPre: -1,
leftOffsetPre: -1,
widthPre: -1,
heightPre: -1,
topOffsetPost: -1,
leftOffsetPost: -1,
widthPost: -1,
heightPost: -1,
width: -1,
height: -1,
canvas: 'crop',
crop: 0,
angle: 0,
rotateBeforePreExtract: false,
flip: false,
flop: false,
extendTop: 0,
extendBottom: 0,
extendLeft: 0,
extendRight: 0,
withoutEnlargement: false,
kernel: 'lanczos3',
interpolator: 'bicubic',
// operations
background: [0, 0, 0, 255],
flatten: false,
negate: false,
blurSigma: 0,
sharpenSigma: 0,
sharpenFlat: 1,
sharpenJagged: 2,
threshold: 0,
thresholdGrayscale: true,
trimTolerance: 0,
gamma: 0,
greyscale: false,
normalize: 0,
booleanBufferIn: null,
booleanFileIn: '',
joinChannelIn: [],
extractChannel: -1,
colourspace: 'srgb',
// overlay
overlayGravity: 0,
overlayXOffset: -1,
overlayYOffset: -1,
overlayTile: false,
overlayCutout: false,
// output
fileOut: '',
formatOut: 'input',
streamOut: false,
withMetadata: false,
withMetadataOrientation: -1,
// output format
jpegQuality: 80,
jpegProgressive: false,
jpegChromaSubsampling: '4:2:0',
jpegTrellisQuantisation: false,
jpegOvershootDeringing: false,
jpegOptimiseScans: false,
pngProgressive: false,
pngCompressionLevel: 6,
pngAdaptiveFiltering: true,
webpQuality: 80,
tiffQuality: 80,
tileSize: 256,
tileOverlap: 0,
// Function to notify of queue length changes
queueListener: function (queueLength) {
module.exports.queue.emit('change', queueLength);
}
};
this.options.input = this._createInputDescriptor(input, options, { allowStream: true });
return this;
};
module.exports = Sharp;
util.inherits(Sharp, stream.Duplex);
/**
* An EventEmitter that emits a `change` event when a task is either:
* - queued, waiting for _libuv_ to provide a worker thread
* - complete
* @member
* @example
* sharp.queue.on('change', function(queueLength) {
* console.log('Queue contains ' + queueLength + ' task(s)');
* });
*/
module.exports.queue = new events.EventEmitter();
/**
* An Object containing nested boolean values representing the available input and output formats/methods.
* @example
* console.log(sharp.format());
* @returns {Object}
*/
module.exports.format = sharp.format();
/**
* An Object containing the version numbers of libvips and its dependencies.
* @member
* @example
* console.log(sharp.versions);
*/
module.exports.versions = versions;
// Validation helpers
const isDefined = function (val) {
return typeof val !== 'undefined' && val !== null;
};
const isObject = function (val) {
return typeof val === 'object';
};
const isFunction = function (val) {
return typeof val === 'function';
};
const isBoolean = function (val) {
return typeof val === 'boolean';
};
const isBuffer = function (val) {
return typeof val === 'object' && val instanceof Buffer;
};
const isString = function (val) {
return typeof val === 'string' && val.length > 0;
};
const isNumber = function (val) {
return typeof val === 'number' && !Number.isNaN(val);
};
const isInteger = function (val) {
return isNumber(val) && val % 1 === 0;
};
const inRange = function (val, min, max) {
return val >= min && val <= max;
};
const contains = function (val, list) {
return list.indexOf(val) !== -1;
};
// Create Object containing input and input-related options
Sharp.prototype._createInputDescriptor = function (input, inputOptions, containerOptions) {
const inputDescriptor = {};
if (isString(input)) {
// filesystem
inputDescriptor.file = input;
} else if (isBuffer(input)) {
// Buffer
inputDescriptor.buffer = input;
} else if (!isDefined(input) && isObject(containerOptions) && containerOptions.allowStream) {
// Stream
inputDescriptor.buffer = [];
} else {
throw new Error('Unsupported input ' + typeof input);
}
if (isObject(inputOptions)) {
// Density
if (isDefined(inputOptions.density)) {
if (isInteger(inputOptions.density) && inRange(inputOptions.density, 1, 2400)) {
inputDescriptor.density = inputOptions.density;
} else {
throw new Error('Invalid density (1 to 2400) ' + inputOptions.density);
}
}
// Raw pixel input
if (isDefined(inputOptions.raw)) {
if (
isObject(inputOptions.raw) &&
isInteger(inputOptions.raw.width) && inRange(inputOptions.raw.width, 1, maximum.width) &&
isInteger(inputOptions.raw.height) && inRange(inputOptions.raw.height, 1, maximum.height) &&
isInteger(inputOptions.raw.channels) && 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');
}
}
} else if (isDefined(inputOptions)) {
throw new Error('Invalid input options ' + inputOptions);
}
return inputDescriptor;
};
/**
* Handle incoming Buffer chunk on Writable Stream.
* @param {Buffer} chunk
* @param {String} encoding - unused
* @param {Function} callback
*/
Sharp.prototype._write = function (chunk, encoding, callback) {
if (Array.isArray(this.options.input.buffer)) {
if (isBuffer(chunk)) {
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
Sharp.prototype._flattenBufferIn = function () {
if (this._isStreamInput()) {
this.options.input.buffer = Buffer.concat(this.options.input.buffer);
}
};
Sharp.prototype._isStreamInput = function () {
return Array.isArray(this.options.input.buffer);
};
/**
* Weighting to apply to image crop.
* @member
*/
module.exports.gravity = {
center: 0,
centre: 0,
north: 1,
east: 2,
south: 3,
west: 4,
northeast: 5,
southeast: 6,
southwest: 7,
northwest: 8
};
/**
* Strategies for automagic crop behaviour.
* @member
*/
module.exports.strategy = {
entropy: 16,
attention: 17
};
/**
* Crop the resized image to the exact size specified, the default behaviour.
*
* Possible attributes of the optional `sharp.gravity` are `north`, `northeast`, `east`, `southeast`, `south`,
* `southwest`, `west`, `northwest`, `center` and `centre`.
*
* The experimental strategy-based approach resizes so one dimension is at its target length
* then repeatedly ranks edge regions, discarding the edge with the lowest score based on the selected strategy.
* - `entropy`: focus on the region with the highest [Shannon entropy](https://en.wikipedia.org/wiki/Entropy_%28information_theory%29).
* - `attention`: focus on the region with the highest luminance frequency, colour saturation and presence of skin tones.
*
* @example
* const transformer = sharp()
* .resize(200, 200)
* .crop(sharp.strategy.entropy)
* .on('error', function(err) {
* console.log(err);
* });
* // Read image data from readableStream
* // Write 200px square auto-cropped image data to writableStream
* readableStream.pipe(transformer).pipe(writableStream);
*
* @param {String} [crop='centre'] - A member of `sharp.gravity` to crop to an edge/corner or `sharp.strategy` to crop dynamically.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.crop = function (crop) {
this.options.canvas = 'crop';
if (!isDefined(crop)) {
// Default
this.options.crop = module.exports.gravity.center;
} else if (isInteger(crop) && inRange(crop, 0, 8)) {
// Gravity (numeric)
this.options.crop = crop;
} else if (isString(crop) && isInteger(module.exports.gravity[crop])) {
// Gravity (string)
this.options.crop = module.exports.gravity[crop];
} else if (isInteger(crop) && crop >= module.exports.strategy.entropy) {
// Strategy
this.options.crop = crop;
} else {
throw new Error('Unsupported crop ' + crop);
}
return this;
};
/**
* Extract a region of the image.
*
* - Use `extract` before `resize` for pre-resize extraction.
* - Use `extract` after `resize` for post-resize extraction.
* - Use `extract` before and after for both.
*
* @example
* sharp(input)
* .extract({ left: left, top: top, width: width, height: height })
* .toFile(output, function(err) {
* // Extract a region of the input image, saving in the same format.
* });
* @example
* sharp(input)
* .extract({ left: leftOffsetPre, top: topOffsetPre, width: widthPre, height: heightPre })
* .resize(width, height)
* .extract({ left: leftOffsetPost, top: topOffsetPost, width: widthPost, height: heightPost })
* .toFile(output, function(err) {
* // Extract a region, resize, then extract from the resized image
* });
*
* @param {Object} options
* @param {Number} options.left - zero-indexed offset from left edge
* @param {Number} options.top - zero-indexed offset from top edge
* @param {Number} options.width - dimension of extracted image
* @param {Number} options.height - dimension of extracted image
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.extract = function (options) {
const suffix = this.options.width === -1 && this.options.height === -1 ? 'Pre' : 'Post';
['left', 'top', 'width', 'height'].forEach(function (name) {
const value = options[name];
if (isInteger(value) && value >= 0) {
this.options[name + (name === 'left' || name === 'top' ? 'Offset' : '') + suffix] = value;
} else {
throw new Error('Non-integer value for ' + name + ' of ' + value);
}
}, this);
// Ensure existing rotation occurs before pre-resize extraction
if (suffix === 'Pre' && this.options.angle !== 0) {
this.options.rotateBeforePreExtract = true;
}
return this;
};
/**
* Extract a single channel from a multi-channel image.
*
* @example
* sharp(input)
* .extractChannel('green')
* .toFile('input_green.jpg', function(err, info) {
* // info.channels === 1
* // input_green.jpg contains the green channel of the input image
* });
*
* @param {Number|String} channel - zero-indexed band number to extract, or `red`, `green` or `blue` as alternative to `0`, `1` or `2` respectively.
* @returns {Sharp}
* @throws {Error} Invalid channel
*/
Sharp.prototype.extractChannel = function (channel) {
if (channel === 'red') {
channel = 0;
} else if (channel === 'green') {
channel = 1;
} else if (channel === 'blue') {
channel = 2;
}
if (isInteger(channel) && inRange(channel, 0, 4)) {
this.options.extractChannel = channel;
} else {
throw new Error('Cannot extract invalid channel ' + channel);
}
return this;
};
/**
* Set the background for the `embed`, `flatten` and `extend` operations.
* The default background is `{r: 0, g: 0, b: 0, a: 1}`, black without transparency.
*
* Delegates to the _color_ module, which can throw an Error
* but is liberal in what it accepts, clipping values to sensible min/max.
* The alpha value is a float between `0` (transparent) and `1` (opaque).
*
* @param {String|Object} rgba - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
* @returns {Sharp}
* @throws {Error} Invalid parameter
*/
Sharp.prototype.background = function (rgba) {
const colour = color(rgba);
this.options.background = colour.rgbArray();
this.options.background.push(colour.alpha() * 255);
return this;
};
/**
* Preserving aspect ratio, resize the image to the maximum `width` or `height` specified
* then embed on a background of the exact `width` and `height` specified.
*
* If the background contains an alpha value then WebP and PNG format output images will
* contain an alpha channel, even when the input image does not.
*
* @example
* sharp('input.gif')
* .resize(200, 300)
* .background({r: 0, g: 0, b: 0, a: 0})
* .embed()
* .toFormat(sharp.format.webp)
* .toBuffer(function(err, outputBuffer) {
* if (err) {
* throw err;
* }
* // outputBuffer contains WebP image data of a 200 pixels wide and 300 pixels high
* // containing a scaled version, embedded on a transparent canvas, of input.gif
* });
*
* @returns {Sharp}
*/
Sharp.prototype.embed = function () {
this.options.canvas = 'embed';
return this;
};
/**
* Preserving aspect ratio, resize the image to be as large as possible
* while ensuring its dimensions are less than or equal to the `width` and `height` specified.
*
* Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
*
* @example
* sharp(inputBuffer)
* .resize(200, 200)
* .max()
* .toFormat('jpeg')
* .toBuffer()
* .then(function(outputBuffer) {
* // outputBuffer contains JPEG image data no wider than 200 pixels and no higher
* // than 200 pixels regardless of the inputBuffer image dimensions
* });
*
* @returns {Sharp}
*/
Sharp.prototype.max = function () {
this.options.canvas = 'max';
return this;
};
/**
* Preserving aspect ratio, resize the image to be as small as possible
* while ensuring its dimensions are greater than or equal to the `width` and `height` specified.
*
* Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
*
* @returns {Sharp}
*/
Sharp.prototype.min = function () {
this.options.canvas = 'min';
return this;
};
/**
* Ignoring the aspect ratio of the input, stretch the image to
* the exact `width` and/or `height` provided via `resize`.
* @returns {Sharp}
*/
Sharp.prototype.ignoreAspectRatio = function () {
this.options.canvas = 'ignore_aspect';
return this;
};
/**
* Merge alpha transparency channel, if any, with `background`.
* @param {Boolean} [flatten=true]
* @returns {Sharp}
*/
Sharp.prototype.flatten = function (flatten) {
this.options.flatten = isBoolean(flatten) ? flatten : true;
return this;
};
/**
* Produce the "negative" of the image.
* White => Black, Black => White, Blue => Yellow, etc.
* @param {Boolean} [negate=true]
* @returns {Sharp}
*/
Sharp.prototype.negate = function (negate) {
this.options.negate = isBoolean(negate) ? negate : true;
return this;
};
/**
* Perform a bitwise boolean operation with operand image.
*
* This operation creates an output image where each pixel is the result of
* the selected bitwise boolean `operation` between the corresponding pixels of the input images.
*
* @param {Buffer|String} operand - Buffer containing image data or String containing the path to an image file.
* @param {String} operator - one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively.
* @param {Object} [options]
* @param {Object} [options.raw] - describes operand when using raw pixel data.
* @param {Number} [options.raw.width]
* @param {Number} [options.raw.height]
* @param {Number} [options.raw.channels]
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.boolean = function (operand, operator, options) {
this.options.boolean = this._createInputDescriptor(operand, options);
if (isString(operator) && contains(operator, ['and', 'or', 'eor'])) {
this.options.booleanOp = operator;
} else {
throw new Error('Invalid boolean operator ' + operator);
}
return this;
};
/**
* Overlay (composite) an image over the processed (resized, extracted etc.) image.
*
* The overlay image must be the same size or smaller than the processed image.
* If both `top` and `left` options are provided, they take precedence over `gravity`.
*
* @example
* sharp('input.png')
* .rotate(180)
* .resize(300)
* .flatten()
* .background('#ff6600')
* .overlayWith('overlay.png', { gravity: sharp.gravity.southeast } )
* .sharpen()
* .withMetadata()
* .quality(90)
* .webp()
* .toBuffer()
* .then(function(outputBuffer) {
* // outputBuffer contains upside down, 300px wide, alpha channel flattened
* // onto orange background, composited with overlay.png with SE gravity,
* // sharpened, with metadata, 90% quality WebP image data. Phew!
* });
*
* @param {Buffer|String} overlay - Buffer containing image data or String containing the path to an image file.
* @param {Object} [options]
* @param {String} [options.gravity='centre'] - gravity at which to place the overlay.
* @param {Number} [options.top] - the pixel offset from the top edge.
* @param {Number} [options.left] - the pixel offset from the left edge.
* @param {Boolean} [options.tile=false] - set to true to repeat the overlay image across the entire image with the given `gravity`.
* @param {Boolean} [options.cutout=false] - set to true to apply only the alpha channel of the overlay image to the input image, giving the appearance of one image being cut out of another.
* @param {Object} [options.raw] - describes overlay when using raw pixel data.
* @param {Number} [options.raw.width]
* @param {Number} [options.raw.height]
* @param {Number} [options.raw.channels]
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.overlayWith = function (overlay, options) {
this.options.overlay = this._createInputDescriptor(overlay, options, {
allowStream: false
});
if (isObject(options)) {
if (isDefined(options.tile)) {
if (isBoolean(options.tile)) {
this.options.overlayTile = options.tile;
} else {
throw new Error('Invalid overlay tile ' + options.tile);
}
}
if (isDefined(options.cutout)) {
if (isBoolean(options.cutout)) {
this.options.overlayCutout = options.cutout;
} else {
throw new Error('Invalid overlay cutout ' + options.cutout);
}
}
if (isDefined(options.left) || isDefined(options.top)) {
if (
isInteger(options.left) && inRange(options.left, 0, maximum.width) &&
isInteger(options.top) && inRange(options.top, 0, maximum.height)
) {
this.options.overlayXOffset = options.left;
this.options.overlayYOffset = options.top;
} else {
throw new Error('Invalid overlay left ' + options.left + ' and/or top ' + options.top);
}
}
if (isDefined(options.gravity)) {
if (isInteger(options.gravity) && inRange(options.gravity, 0, 8)) {
this.options.overlayGravity = options.gravity;
} else if (isString(options.gravity) && isInteger(module.exports.gravity[options.gravity])) {
this.options.overlayGravity = module.exports.gravity[options.gravity];
} else {
throw new Error('Unsupported overlay gravity ' + options.gravity);
}
}
}
return this;
};
/**
* Join one or more channels to the image.
* The meaning of the added channels depends on the output colourspace, set with `toColourspace()`.
* By default the output image will be web-friendly sRGB, with additional channels interpreted as alpha channels.
* Channel ordering follows vips convention:
* - sRGB: 0: Red, 1: Green, 2: Blue, 3: Alpha.
* - CMYK: 0: Magenta, 1: Cyan, 2: Yellow, 3: Black, 4: Alpha.
*
* Buffers may be any of the image formats supported by sharp: JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data.
* For raw pixel input, the `options` object should contain a `raw` attribute, which follows the format of the attribute of the same name in the `sharp()` constructor.
*
* @param {Array|String|Buffer} images - one or more images (file paths, Buffers).
* @param {Object} - image options, see `sharp()` constructor.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.joinChannel = function (images, options) {
if (Array.isArray(images)) {
images.forEach(function (image) {
this.options.joinChannelIn.push(this._createInputDescriptor(image, options));
}, this);
} else {
this.options.joinChannelIn.push(this._createInputDescriptor(images, options));
}
return this;
};
/**
* Rotate the output image by either an explicit angle
* or auto-orient based on the EXIF `Orientation` tag.
*
* Use this method without angle to determine the angle from EXIF data.
* Mirroring is supported and may infer the use of a flip operation.
*
* The use of `rotate` implies the removal of the EXIF `Orientation` tag, if any.
*
* Method order is important when both rotating and extracting regions,
* for example `rotate(x).extract(y)` will produce a different result to `extract(y).rotate(x)`.
*
* @example
* const pipeline = sharp()
* .rotate()
* .resize(null, 200)
* .toBuffer(function (err, outputBuffer, info) {
* // outputBuffer contains 200px high JPEG image data,
* // auto-rotated using EXIF Orientation tag
* // info.width and info.height contain the dimensions of the resized image
* });
* readableStream.pipe(pipeline);
*
* @param {Number} [angle=auto] 0, 90, 180 or 270.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.rotate = function (angle) {
if (!isDefined(angle)) {
this.options.angle = -1;
} else if (isInteger(angle) && contains(angle, [0, 90, 180, 270])) {
this.options.angle = angle;
} else {
throw new Error('Unsupported angle (0, 90, 180, 270) ' + angle);
}
return this;
};
/**
* Flip the image about the vertical Y axis. This always occurs after rotation, if any.
* The use of `flip` implies the removal of the EXIF `Orientation` tag, if any.
* @param {Boolean} [flip=true]
* @returns {Sharp}
*/
Sharp.prototype.flip = function (flip) {
this.options.flip = isBoolean(flip) ? flip : true;
return this;
};
/**
* Flop the image about the horizontal X axis. This always occurs after rotation, if any.
* The use of `flop` implies the removal of the EXIF `Orientation` tag, if any.
* @param {Boolean} [flop=true]
* @returns {Sharp}
*/
Sharp.prototype.flop = function (flop) {
this.options.flop = isBoolean(flop) ? flop : true;
return this;
};
/**
* Do not enlarge the output image if the input image width *or* height are already less than the required dimensions.
* This is equivalent to GraphicsMagick's `>` geometry option:
* "*change the dimensions of the image only if its width or height exceeds the geometry specification*".
* @param {Boolean} [withoutEnlargement=true]
* @returns {Sharp}
*/
Sharp.prototype.withoutEnlargement = function (withoutEnlargement) {
this.options.withoutEnlargement = isBoolean(withoutEnlargement) ? withoutEnlargement : true;
return this;
};
/**
* Blur the image.
* When used without parameters, performs a fast, mild blur of the output image.
* When a `sigma` is provided, performs a slower, more accurate Gaussian blur.
* @param {Number} [sigma] a value between 0.3 and 1000 representing the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.blur = function (sigma) {
if (!isDefined(sigma)) {
// No arguments: default to mild blur
this.options.blurSigma = -1;
} else if (isBoolean(sigma)) {
// Boolean argument: apply mild blur?
this.options.blurSigma = sigma ? -1 : 0;
} else if (isNumber(sigma) && inRange(sigma, 0.3, 1000)) {
// Numeric argument: specific sigma
this.options.blurSigma = sigma;
} else {
throw new Error('Invalid blur sigma (0.3 - 1000.0) ' + sigma);
}
return this;
};
/**
* Convolve the image with the specified kernel.
*
* @example
* sharp(input)
* .convolve({
* width: 3,
* height: 3,
* kernel: [-1, 0, 1, -2, 0, 2, -1, 0, 1]
* })
* .raw()
* .toBuffer(function(err, data, info) {
* // data contains the raw pixel data representing the convolution
* // of the input image with the horizontal Sobel operator
* });
*
* @param {Object} kernel
* @param {Number} kernel.width - width of the kernel in pixels.
* @param {Number} kernel.height - width of the kernel in pixels.
* @param {Array} kernel.kernel - Array of length `width*height` containing the kernel values.
* @param {Number} [kernel.scale=sum] - the scale of the kernel in pixels.
* @param {Number} [kernel.offset=0] - the offset of the kernel in pixels.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.convolve = function (kernel) {
if (!isObject(kernel) || !Array.isArray(kernel.kernel) ||
!isInteger(kernel.width) || !isInteger(kernel.height) ||
!inRange(kernel.width, 3, 1001) || !inRange(kernel.height, 3, 1001) ||
kernel.height * kernel.width !== kernel.kernel.length
) {
// must pass in a kernel
throw new Error('Invalid convolution kernel');
}
// Default scale is sum of kernel values
if (!isInteger(kernel.scale)) {
kernel.scale = kernel.kernel.reduce(function (a, b) {
return a + b;
}, 0);
}
// Clip scale to a minimum value of 1
if (kernel.scale < 1) {
kernel.scale = 1;
}
if (!isInteger(kernel.offset)) {
kernel.offset = 0;
}
this.options.convKernel = kernel;
return this;
};
/**
* Sharpen the image.
* When used without parameters, performs a fast, mild sharpen of the output image.
* When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space.
* Separate control over the level of sharpening in "flat" and "jagged" areas is available.
*
* @param {Number} [sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
* @param {Number} [flat=1.0] - the level of sharpening to apply to "flat" areas.
* @param {Number} [jagged=2.0] - the level of sharpening to apply to "jagged" areas.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.sharpen = function (sigma, flat, jagged) {
if (!isDefined(sigma)) {
// No arguments: default to mild sharpen
this.options.sharpenSigma = -1;
} else if (isBoolean(sigma)) {
// Boolean argument: apply mild sharpen?
this.options.sharpenSigma = sigma ? -1 : 0;
} else if (isNumber(sigma) && inRange(sigma, 0.01, 10000)) {
// Numeric argument: specific sigma
this.options.sharpenSigma = sigma;
// Control over flat areas
if (isDefined(flat)) {
if (isNumber(flat) && inRange(flat, 0, 10000)) {
this.options.sharpenFlat = flat;
} else {
throw new Error('Invalid sharpen level for flat areas (0 - 10000) ' + flat);
}
}
// Control over jagged areas
if (isDefined(jagged)) {
if (isNumber(jagged) && inRange(jagged, 0, 10000)) {
this.options.sharpenJagged = jagged;
} else {
throw new Error('Invalid sharpen level for jagged areas (0 - 10000) ' + jagged);
}
}
} else {
throw new Error('Invalid sharpen sigma (0.01 - 10000) ' + sigma);
}
return this;
};
/**
* Any pixel value greather than or equal to the threshold value will be set to 255, otherwise it will be set to 0.
* @param {Number} [threshold=128] - a value in the range 0-255 representing the level at which the threshold will be applied.
* @param {Object} [options]
* @param {Boolean} [options.greyscale=true] - convert to single channel greyscale.
* @param {Boolean} [options.grayscale=true] - alternative spelling for greyscale.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.threshold = function (threshold, options) {
if (!isDefined(threshold)) {
this.options.threshold = 128;
} else if (isBoolean(threshold)) {
this.options.threshold = threshold ? 128 : 0;
} else if (isInteger(threshold) && inRange(threshold, 0, 255)) {
this.options.threshold = threshold;
} else {
throw new Error('Invalid threshold (0 to 255) ' + threshold);
}
if (!isObject(options) || options.greyscale === true || options.grayscale === true) {
this.options.thresholdGrayscale = true;
} else {
this.options.thresholdGrayscale = false;
}
return this;
};
/**
* Trim "boring" pixels from all edges that contain values within a percentage similarity of the top-left pixel.
* @param {Number} [tolerance=10] value between 1 and 99 representing the percentage similarity.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.trim = function (tolerance) {
if (!isDefined(tolerance)) {
this.options.trimTolerance = 10;
} else if (isInteger(tolerance) && inRange(tolerance, 1, 99)) {
this.options.trimTolerance = tolerance;
} else {
throw new Error('Invalid trim tolerance (1 to 99) ' + tolerance);
}
return this;
};
/**
* Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of `1/gamma`
* then increasing the encoding (brighten) post-resize at a factor of `gamma`.
* This can improve the perceived brightness of a resized image in non-linear colour spaces.
* JPEG and WebP input images will not take advantage of the shrink-on-load performance optimisation
* when applying a gamma correction.
* @param {Number} [gamma=2.2] value between 1.0 and 3.0.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.gamma = function (gamma) {
if (!isDefined(gamma)) {
// Default gamma correction of 2.2 (sRGB)
this.options.gamma = 2.2;
} else if (isNumber(gamma) && inRange(gamma, 1, 3)) {
this.options.gamma = gamma;
} else {
throw new Error('Invalid gamma correction (1.0 to 3.0) ' + gamma);
}
return this;
};
/**
* Enhance output image contrast by stretching its luminance to cover the full dynamic range.
* @param {Boolean} [normalize=true]
* @returns {Sharp}
*/
Sharp.prototype.normalize = function (normalize) {
this.options.normalize = isBoolean(normalize) ? normalize : true;
return this;
};
/**
* Alternative spelling of normalize.
* @param {Boolean} [normalise=true]
* @returns {Sharp}
*/
Sharp.prototype.normalise = Sharp.prototype.normalize;
/**
* Perform a bitwise boolean operation on all input image channels (bands) to produce a single channel output image.
*
* @example
* sharp('3-channel-rgb-input.png')
* .bandbool(sharp.bool.and)
* .toFile('1-channel-output.png', function (err, info) {
* // The output will be a single channel image where each pixel `P = R & G & B`.
* // If `I(1,1) = [247, 170, 14] = [0b11110111, 0b10101010, 0b00001111]`
* // then `O(1,1) = 0b11110111 & 0b10101010 & 0b00001111 = 0b00000010 = 2`.
* });
*
* @param {String} boolOp - one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.bandbool = function (boolOp) {
if (isString(boolOp) && contains(boolOp, ['and', 'or', 'eor'])) {
this.options.bandBoolOp = boolOp;
} else {
throw new Error('Invalid bandbool operation ' + boolOp);
}
return this;
};
/**
* Convert to 8-bit greyscale; 256 shades of grey.
* This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use `gamma()` with `greyscale()` for the best results.
* By default the output image will be web-friendly sRGB and contain three (identical) color channels.
* This may be overridden by other sharp operations such as `toColourspace('b-w')`,
* which will produce an output image containing one color channel.
* An alpha channel may be present, and will be unchanged by the operation.
* @param {Boolean} [greyscale=true]
* @returns {Sharp}
*/
Sharp.prototype.greyscale = function (greyscale) {
this.options.greyscale = isBoolean(greyscale) ? greyscale : true;
return this;
};
/**
* Alternative spelling of `greyscale`.
* @param {Boolean} [grayscale=true]
* @returns {Sharp}
*/
Sharp.prototype.grayscale = Sharp.prototype.greyscale;
/**
* Set the output colourspace.
* By default output image will be web-friendly sRGB, with additional channels interpreted as alpha channels.
* @param {String} [colourspace] - output colourspace e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L568)
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.toColourspace = function (colourspace) {
if (!isString(colourspace)) {
throw new Error('Invalid output colourspace ' + colourspace);
}
this.options.colourspace = colourspace;
return this;
};
/**
* Alternative spelling of `toColourspace`.
* @param {String} [colorspace] - output colorspace.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.toColorspace = Sharp.prototype.toColourspace;
/**
* An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`.
* This will reduce memory usage and can improve performance on some systems.
* @param {Boolean} [sequentialRead=true]
* @returns {Sharp}
*/
Sharp.prototype.sequentialRead = function (sequentialRead) {
this.options.sequentialRead = isBoolean(sequentialRead) ? sequentialRead : true;
return this;
};
// Deprecated output options
Sharp.prototype.quality = util.deprecate(function (quality) {
const formatOut = this.options.formatOut;
const options = { quality: quality };
this.jpeg(options).webp(options).tiff(options);
this.options.formatOut = formatOut;
return this;
}, 'quality: use jpeg({ quality: ... }), webp({ quality: ... }) and/or tiff({ quality: ... }) instead');
Sharp.prototype.progressive = util.deprecate(function (progressive) {
const formatOut = this.options.formatOut;
const options = { progressive: (progressive !== false) };
this.jpeg(options).png(options);
this.options.formatOut = formatOut;
return this;
}, 'progressive: use jpeg({ progressive: ... }) and/or png({ progressive: ... }) instead');
Sharp.prototype.compressionLevel = util.deprecate(function (compressionLevel) {
const formatOut = this.options.formatOut;
this.png({ compressionLevel: compressionLevel });
this.options.formatOut = formatOut;
return this;
}, 'compressionLevel: use png({ compressionLevel: ... }) instead');
Sharp.prototype.withoutAdaptiveFiltering = util.deprecate(function (withoutAdaptiveFiltering) {
const formatOut = this.options.formatOut;
this.png({ adaptiveFiltering: (withoutAdaptiveFiltering === false) });
this.options.formatOut = formatOut;
return this;
}, 'withoutAdaptiveFiltering: use png({ adaptiveFiltering: ... }) instead');
Sharp.prototype.withoutChromaSubsampling = util.deprecate(function (withoutChromaSubsampling) {
const formatOut = this.options.formatOut;
this.jpeg({ chromaSubsampling: (withoutChromaSubsampling === false) ? '4:2:0' : '4:4:4' });
this.options.formatOut = formatOut;
return this;
}, 'withoutChromaSubsampling: use jpeg({ chromaSubsampling: "4:4:4" }) instead');
Sharp.prototype.trellisQuantisation = util.deprecate(function (trellisQuantisation) {
const formatOut = this.options.formatOut;
this.jpeg({ trellisQuantisation: (trellisQuantisation !== false) });
this.options.formatOut = formatOut;
return this;
}, 'trellisQuantisation: use jpeg({ trellisQuantisation: ... }) instead');
Sharp.prototype.trellisQuantization = Sharp.prototype.trellisQuantisation;
Sharp.prototype.overshootDeringing = util.deprecate(function (overshootDeringing) {
const formatOut = this.options.formatOut;
this.jpeg({ overshootDeringing: (overshootDeringing !== false) });
this.options.formatOut = formatOut;
return this;
}, 'overshootDeringing: use jpeg({ overshootDeringing: ... }) instead');
Sharp.prototype.optimiseScans = util.deprecate(function (optimiseScans) {
const formatOut = this.options.formatOut;
this.jpeg({ optimiseScans: (optimiseScans !== false) });
this.options.formatOut = formatOut;
return this;
}, 'optimiseScans: use jpeg({ optimiseScans: ... }) instead');
Sharp.prototype.optimizeScans = Sharp.prototype.optimiseScans;
/**
* Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
* The default behaviour, when `withMetadata` is not used, is to strip all metadata and convert to the device-independent sRGB colour space.
* This will also convert to and add a web-friendly sRGB ICC profile.
* @param {Object} [withMetadata]
* @param {Number} [withMetadata.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.withMetadata = function (withMetadata) {
this.options.withMetadata = isBoolean(withMetadata) ? withMetadata : true;
if (isObject(withMetadata)) {
if (isDefined(withMetadata.orientation)) {
if (isInteger(withMetadata.orientation) && inRange(withMetadata.orientation, 1, 8)) {
this.options.withMetadataOrientation = withMetadata.orientation;
} else {
throw new Error('Invalid orientation (1 to 8) ' + withMetadata.orientation);
}
}
}
return this;
};
/**
* Use tile-based deep zoom (image pyramid) output.
* You can also use a `.zip` or `.szi` file extension with `toFile` to write to a compressed archive file format.
*
* @example
* sharp('input.tiff')
* .tile({
* size: 512
* })
* .toFile('output.dzi', function(err, info) {
* // output.dzi is the Deep Zoom XML definition
* // output_files contains 512x512 tiles grouped by zoom level
* });
*
* @param {Object} [tile]
* @param {Number} [tile.size=256] tile size in pixels, a value between 1 and 8192.
* @param {Number} [tile.overlap=0] tile overlap in pixels, a value between 0 and 8192.
* @param {String} [tile.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file).
* @param {String} [tile.layout='dz'] filesystem layout, possible values are `dz`, `zoomify` or `google`.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.tile = function (tile) {
if (isObject(tile)) {
// Size of square tiles, in pixels
if (isDefined(tile.size)) {
if (isInteger(tile.size) && inRange(tile.size, 1, 8192)) {
this.options.tileSize = tile.size;
} else {
throw new Error('Invalid tile size (1 to 8192) ' + tile.size);
}
}
// Overlap of tiles, in pixels
if (isDefined(tile.overlap)) {
if (isInteger(tile.overlap) && inRange(tile.overlap, 0, 8192)) {
if (tile.overlap > this.options.tileSize) {
throw new Error('Tile overlap ' + tile.overlap + ' cannot be larger than tile size ' + this.options.tileSize);
}
this.options.tileOverlap = tile.overlap;
} else {
throw new Error('Invalid tile overlap (0 to 8192) ' + tile.overlap);
}
}
// Container
if (isDefined(tile.container)) {
if (isString(tile.container) && contains(tile.container, ['fs', 'zip'])) {
this.options.tileContainer = tile.container;
} else {
throw new Error('Invalid tile container ' + tile.container);
}
}
// Layout
if (isDefined(tile.layout)) {
if (isString(tile.layout) && contains(tile.layout, ['dz', 'google', 'zoomify'])) {
this.options.tileLayout = tile.layout;
} else {
throw new Error('Invalid tile layout ' + tile.layout);
}
}
}
return this;
};
/**
* Extends/pads the edges of the image with the colour provided to the `background` method.
* This operation will always occur after resizing and extraction, if any.
*
* @example
* // Resize to 140 pixels wide, then add 10 transparent pixels
* // to the top, left and right edges and 20 to the bottom edge
* sharp(input)
* .resize(140)
* .background({r: 0, g: 0, b: 0, a: 0})
* .extend({top: 10, bottom: 20, left: 10, right: 10})
* ...
*
* @param {Number|Object} extend - single pixel count to add to all edges or an Object with per-edge counts
* @param {Number} [extend.top]
* @param {Number} [extend.left]
* @param {Number} [extend.bottom]
* @param {Number} [extend.right]
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.extend = function (extend) {
if (isInteger(extend) && extend > 0) {
this.options.extendTop = extend;
this.options.extendBottom = extend;
this.options.extendLeft = extend;
this.options.extendRight = extend;
} else if (
isObject(extend) &&
isInteger(extend.top) && extend.top >= 0 &&
isInteger(extend.bottom) && extend.bottom >= 0 &&
isInteger(extend.left) && extend.left >= 0 &&
isInteger(extend.right) && extend.right >= 0
) {
this.options.extendTop = extend.top;
this.options.extendBottom = extend.bottom;
this.options.extendLeft = extend.left;
this.options.extendRight = extend.right;
} else {
throw new Error('Invalid edge extension ' + extend);
}
return this;
};
/**
* Reduction kernels.
* @member
*/
module.exports.kernel = {
cubic: 'cubic',
lanczos2: 'lanczos2',
lanczos3: 'lanczos3'
};
/**
* Enlargement interpolators.
* @member
*/
module.exports.interpolator = {
nearest: 'nearest',
bilinear: 'bilinear',
bicubic: 'bicubic',
nohalo: 'nohalo',
lbb: 'lbb',
locallyBoundedBicubic: 'lbb',
vsqbs: 'vsqbs',
vertexSplitQuadraticBasisSpline: 'vsqbs'
};
/**
* Boolean operations for bandbool.
* @member
*/
module.exports.bool = {
and: 'and',
or: 'or',
eor: 'eor'
};
/**
* Colourspaces.
* @member
*/
module.exports.colourspace = {
multiband: 'multiband',
'b-w': 'b-w',
bw: 'b-w',
cmyk: 'cmyk',
srgb: 'srgb'
};
/**
* Alternative spelling of colourspace.
* @member
*/
module.exports.colorspace = module.exports.colourspace;
/**
* Resize image to `width` x `height`.
* By default, the resized image is centre cropped to the exact size specified.
*
* Possible reduction kernels are:
* - `cubic`: Use a [Catmull-Rom spline](https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline).
* - `lanczos2`: Use a [Lanczos kernel](https://en.wikipedia.org/wiki/Lanczos_resampling#Lanczos_kernel) with `a=2`.
* - `lanczos3`: Use a Lanczos kernel with `a=3` (the default).
*
* Possible enlargement interpolators are:
* - `nearest`: Use [nearest neighbour interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation).
* - `bilinear`: Use [bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation), faster than bicubic but with less smooth results.
* - `vertexSplitQuadraticBasisSpline`: Use the smoother [VSQBS interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/vsqbs.cpp#L48) to prevent "staircasing" when enlarging.
* - `bicubic`: Use [bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation) (the default).
* - `locallyBoundedBicubic`: Use [LBB interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/lbb.cpp#L100), which prevents some "[acutance](http://en.wikipedia.org/wiki/Acutance)" but typically reduces performance by a factor of 2.
* - `nohalo`: Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/), which prevents acutance but typically reduces performance by a factor of 3.
*
* @example
* sharp(inputBuffer)
* .resize(200, 300, {
* kernel: sharp.kernel.lanczos2,
* interpolator: sharp.interpolator.nohalo
* })
* .background('white')
* .embed()
* .toFile('output.tiff')
* .then(function() {
* // output.tiff is a 200 pixels wide and 300 pixels high image
* // containing a lanczos2/nohalo scaled version, embedded on a white canvas,
* // of the image data in inputBuffer
* });
*
* @param {Number} [width] - pixels wide the resultant image should be, between 1 and 16383 (0x3FFF). Use `null` or `undefined` to auto-scale the width to match the height.
* @param {Number} [height] - pixels high the resultant image should be, between 1 and 16383. Use `null` or `undefined` to auto-scale the height to match the width.
* @param {Object} [options]
* @param {String} [options.kernel='lanczos3'] - the kernel to use for image reduction.
* @param {String} [options.interpolator='bicubic'] - the interpolator to use for image enlargement.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
Sharp.prototype.resize = function (width, height, options) {
if (isDefined(width)) {
if (isInteger(width) && inRange(width, 1, maximum.width)) {
this.options.width = width;
} else {
throw new Error('Invalid width (1 to ' + maximum.width + ') ' + width);
}
} else {
this.options.width = -1;
}
if (isDefined(height)) {
if (isInteger(height) && inRange(height, 1, maximum.height)) {
this.options.height = height;
} else {
throw new Error('Invalid height (1 to ' + maximum.height + ') ' + height);
}
} else {
this.options.height = -1;
}
if (isObject(options)) {
// Kernel
if (isDefined(options.kernel)) {
if (isString(module.exports.kernel[options.kernel])) {
this.options.kernel = module.exports.kernel[options.kernel];
} else {
throw new Error('Invalid kernel ' + options.kernel);
}
}
// Interpolator
if (isDefined(options.interpolator)) {
if (isString(module.exports.interpolator[options.interpolator])) {
this.options.interpolator = module.exports.interpolator[options.interpolator];
} else {
throw new Error('Invalid interpolator ' + options.interpolator);
}
}
}
return this;
};
/**
* Do not process input images where the number of pixels (width * height) exceeds this limit.
* Assumes image dimensions contained in the input metadata can be trusted.
* The default limit is 268402689 (0x3FFF * 0x3FFF) pixels.
* @param {Number|Boolean} limit - an integral Number of pixels, zero or false to remove limit, true to use default limit.
* @returns {Sharp}
* @throws {Error} Invalid limit
*/
Sharp.prototype.limitInputPixels = function (limit) {
// if we pass in false we represent the integer as 0 to disable
if (limit === false) {
limit = 0;
} else if (limit === true) {
limit = maximum.pixels;
}
if (isInteger(limit) && limit >= 0) {
this.options.limitInputPixels = limit;
} else {
throw new Error('Invalid pixel limit (0 to ' + maximum.pixels + ') ' + limit);
}
return this;
};
/**
* Write output image data to a file.
*
* If an explicit output format is not selected, it will be inferred from the extension,
* with JPEG, PNG, WebP, TIFF, DZI, and libvips' V format supported.
* Note that raw pixel data is only supported for buffer output.
*
* A Promises/A+ promise is returned when `callback` is not provided.
*
* @param {String} fileOut - the path to write the image data to.
* @param {Function} [callback] - called on completion with two arguments `(err, info)`.
* `info` contains the output image `format`, `size` (bytes), `width`, `height` and `channels`.
* @returns {Promise} - when no callback is provided
* @throws {Error} Invalid parameters
*/
Sharp.prototype.toFile = function (fileOut, callback) {
if (!fileOut || fileOut.length === 0) {
const errOutputInvalid = new Error('Invalid output');
if (isFunction(callback)) {
callback(errOutputInvalid);
} else {
return Promise.reject(errOutputInvalid);
}
} else {
if (this.options.input.file === fileOut) {
const errOutputIsInput = new Error('Cannot use same file for input and output');
if (isFunction(callback)) {
callback(errOutputIsInput);
} else {
return Promise.reject(errOutputIsInput);
}
} else {
this.options.fileOut = fileOut;
return this._pipeline(callback);
}
}
return this;
};
/**
* Write output to a Buffer.
* By default, the format will match the input image. JPEG, PNG, WebP, and RAW are supported.
* `callback`, if present, gets three arguments `(err, buffer, info)` where:
* - `err` is an error message, if any.
* - `buffer` is the output image data.
* - `info` contains the output image `format`, `size` (bytes), `width`, `height` and `channels`.
* A Promises/A+ promise is returned when `callback` is not provided.
*
* @param {Function} [callback]
* @returns {Promise|Sharp}
*/
Sharp.prototype.toBuffer = function (callback) {
return this._pipeline(callback);
};
/**
* Update the output format unless options.force is false,
* in which case revert to input format.
* @private
* @param {String} formatOut
* @param {Object} [options]
* @param {Boolean} [options.force=true] - force output format, otherwise attempt to use input format
* @returns {Sharp}
*/
Sharp.prototype._updateFormatOut = function (formatOut, options) {
this.options.formatOut = (isObject(options) && options.force === false) ? 'input' : formatOut;
return this;
};
/**
* Update a Boolean attribute of the this.options Object.
* @private
* @param {String} key
* @param {Boolean} val
* @throws {Error} Invalid key
*/
Sharp.prototype._setBooleanOption = function (key, val) {
if (isBoolean(val)) {
this.options[key] = val;
} else {
throw new Error('Invalid ' + key + ' (boolean) ' + val);
}
};
/**
* Use these JPEG options for output image.
* @param {Object} [options] - output options
* @param {Number} [options.quality=80] - quality, integer 1-100
* @param {Boolean} [options.progressive=false] - use progressive (interlace) scan
* @param {String} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling when quality <= 90
* @param {Boolean} [trellisQuantisation=false] - apply trellis quantisation, requires mozjpeg
* @param {Boolean} [overshootDeringing=false] - apply overshoot deringing, requires mozjpeg
* @param {Boolean} [optimiseScans=false] - optimise progressive scans, assumes progressive=true, requires mozjpeg
* @param {Boolean} [options.force=true] - force JPEG output, otherwise attempt to use input format
* @returns {Sharp}
* @throws {Error} Invalid options
*/
Sharp.prototype.jpeg = function (options) {
if (isObject(options)) {
if (isDefined(options.quality)) {
if (isInteger(options.quality) && inRange(options.quality, 1, 100)) {
this.options.jpegQuality = options.quality;
} else {
throw new Error('Invalid quality (integer, 1-100) ' + options.quality);
}
}
if (isDefined(options.progressive)) {
this._setBooleanOption('jpegProgressive', options.progressive);
}
if (isDefined(options.chromaSubsampling)) {
if (isString(options.chromaSubsampling) && contains(options.chromaSubsampling, ['4:2:0', '4:4:4'])) {
this.options.jpegChromaSubsampling = options.chromaSubsampling;
} else {
throw new Error('Invalid chromaSubsampling (4:2:0, 4:4:4) ' + options.chromaSubsampling);
}
}
options.trellisQuantisation = options.trellisQuantisation || options.trellisQuantization;
if (isDefined(options.trellisQuantisation)) {
this._setBooleanOption('jpegTrellisQuantisation', options.trellisQuantisation);
}
if (isDefined(options.overshootDeringing)) {
this._setBooleanOption('jpegOvershootDeringing', options.overshootDeringing);
}
options.optimiseScans = options.optimiseScans || options.optimizeScans;
if (isDefined(options.optimiseScans)) {
this._setBooleanOption('jpegOptimiseScans', options.optimiseScans);
if (options.optimiseScans) {
this.options.jpegProgressive = true;
}
}
}
return this._updateFormatOut('jpeg', options);
};
/**
* Use these PNG options for output image.
* @param {Object} [options]
* @param {Boolean} [options.progressive=false] - use progressive (interlace) scan
* @param {Number} [options.compressionLevel=6] - zlib compression level
* @param {Boolean} [options.adaptiveFiltering=true] - use adaptive row filtering
* @param {Boolean} [options.force=true] - force PNG output, otherwise attempt to use input format
* @returns {Sharp}
* @throws {Error} Invalid options
*/
Sharp.prototype.png = function (options) {
if (isObject(options)) {
if (isDefined(options.progressive)) {
this._setBooleanOption('pngProgressive', options.progressive);
}
if (isDefined(options.compressionLevel)) {
if (isInteger(options.compressionLevel) && inRange(options.compressionLevel, 0, 9)) {
this.options.pngCompressionLevel = options.compressionLevel;
} else {
throw new Error('Invalid compressionLevel (integer, 0-9) ' + options.compressionLevel);
}
}
if (isDefined(options.adaptiveFiltering)) {
this._setBooleanOption('pngAdaptiveFiltering', options.adaptiveFiltering);
}
}
return this._updateFormatOut('png', options);
};
/**
* Use these WebP options for output image.
* @param {Object} [options] - output options
* @param {Number} [options.quality=80] - quality, integer 1-100
* @param {Boolean} [options.force=true] - force WebP output, otherwise attempt to use input format
* @returns {Sharp}
* @throws {Error} Invalid options
*/
Sharp.prototype.webp = function (options) {
if (isObject(options)) {
if (isDefined(options.quality)) {
if (isInteger(options.quality) && inRange(options.quality, 1, 100)) {
this.options.webpQuality = options.quality;
} else {
throw new Error('Invalid quality (integer, 1-100) ' + options.quality);
}
}
}
return this._updateFormatOut('webp', options);
};
/**
* Use these TIFF options for output image.
* @param {Object} [options] - output options
* @param {Number} [options.quality=80] - quality, integer 1-100
* @param {Boolean} [options.force=true] - force TIFF output, otherwise attempt to use input format
* @returns {Sharp}
* @throws {Error} Invalid options
*/
Sharp.prototype.tiff = function (options) {
if (isObject(options)) {
if (isDefined(options.quality)) {
if (isInteger(options.quality) && inRange(options.quality, 1, 100)) {
this.options.tiffQuality = options.quality;
} else {
throw new Error('Invalid quality (integer, 1-100) ' + options.quality);
}
}
}
return this._updateFormatOut('tiff', options);
};
/**
* Force output to be raw, uncompressed uint8 pixel data.
* @returns {Sharp}
*/
Sharp.prototype.raw = function () {
return this._updateFormatOut('raw');
};
/**
* Force output to a given format.
* @param {String|Object} format - as a String or an Object with an 'id' attribute
* @param {Object} options - output options
* @returns {Sharp}
* @throws {Error} unsupported format or options
*/
Sharp.prototype.toFormat = function (format, options) {
if (isObject(format) && isString(format.id)) {
format = format.id;
}
if (!contains(format, ['jpeg', 'png', 'webp', 'tiff', 'raw'])) {
throw new Error('Unsupported output format ' + format);
}
return this[format](options);
};
/**
* Called by a WriteableStream to notify us it is ready for data.
* @private
*/
Sharp.prototype._read = function () {
if (!this.options.streamOut) {
this.options.streamOut = true;
this._pipeline();
}
};
/**
* Invoke the C++ image processing pipeline
* Supports callback, stream and promise variants
* @private
*/
Sharp.prototype._pipeline = function (callback) {
const that = this;
if (typeof callback === 'function') {
// output=file/buffer
if (this._isStreamInput()) {
// output=file/buffer, input=stream
this.on('finish', function () {
that._flattenBufferIn();
sharp.pipeline(that.options, callback);
});
} else {
// output=file/buffer, input=file/buffer
sharp.pipeline(this.options, callback);
}
return this;
} else if (this.options.streamOut) {
// output=stream
if (this._isStreamInput()) {
// output=stream, input=stream
this.on('finish', function () {
that._flattenBufferIn();
sharp.pipeline(that.options, function (err, data, info) {
if (err) {
that.emit('error', err);
} else {
that.emit('info', info);
that.push(data);
}
that.push(null);
});
});
} else {
// output=stream, input=file/buffer
sharp.pipeline(this.options, function (err, data, info) {
if (err) {
that.emit('error', err);
} else {
that.emit('info', info);
that.push(data);
}
that.push(null);
});
}
return this;
} else {
// output=promise
if (this._isStreamInput()) {
// output=promise, input=stream
return new Promise(function (resolve, reject) {
that.on('finish', function () {
that._flattenBufferIn();
sharp.pipeline(that.options, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
});
} else {
// output=promise, input=file/buffer
return new Promise(function (resolve, reject) {
sharp.pipeline(that.options, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
}
};
/**
* Fast access to image metadata without decoding any compressed image data.
* A Promises/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`
* - `width`: Number of pixels wide
* - `height`: Number of pixels high
* - `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L568)
* - `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK
* - `density`: Number of pixels per inch (DPI), if present
* - `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
*
* @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}
*/
Sharp.prototype.metadata = function (callback) {
const that = this;
if (typeof callback === 'function') {
if (this._isStreamInput()) {
this.on('finish', function () {
that._flattenBufferIn();
sharp.metadata(that.options, callback);
});
} else {
sharp.metadata(this.options, callback);
}
return this;
} else {
if (this._isStreamInput()) {
return new Promise(function (resolve, reject) {
that.on('finish', function () {
that._flattenBufferIn();
sharp.metadata(that.options, function (err, metadata) {
if (err) {
reject(err);
} else {
resolve(metadata);
}
});
});
});
} else {
return new Promise(function (resolve, reject) {
sharp.metadata(that.options, function (err, metadata) {
if (err) {
reject(err);
} else {
resolve(metadata);
}
});
});
}
}
};
/**
* Take a "snapshot" of the Sharp instance, returning a new instance.
* Cloned instances inherit the input of their parent instance.
* This allows multiple output Streams and therefore multiple processing pipelines to share a single input Stream.
*
* @example
* const pipeline = sharp().rotate();
* pipeline.clone().resize(800, 600).pipe(firstWritableStream);
* pipeline.clone().extract({ left: 20, top: 20, width: 100, height: 100 }).pipe(secondWritableStream);
* readableStream.pipe(pipeline);
* // firstWritableStream receives auto-rotated, resized readableStream
* // secondWritableStream receives auto-rotated, extracted region of readableStream
*
* @returns {Sharp}
*/
Sharp.prototype.clone = function () {
const that = this;
// Clone existing options
const clone = new Sharp();
util._extend(clone.options, this.options);
// Pass 'finish' event to clone for Stream-based input
this.on('finish', function () {
// Clone inherits input data
that._flattenBufferIn();
clone.options.bufferIn = that.options.bufferIn;
clone.emit('finish');
});
return clone;
};
/**
* Gets, or when options are provided sets, the limits of _libvips'_ operation cache.
* Existing entries in the cache will be trimmed after any change in limits.
* This method always returns cache statistics,
* useful for determining how much working memory is required for a particular task.
*
* @example
* const stats = sharp.cache();
* @example
* sharp.cache( { items: 200 } );
* sharp.cache( { files: 0 } );
* sharp.cache(false);
*
* @param {Object|Boolean} Object with the following attributes, or Boolean where true uses default cache settings and false removes all caching.
* @param {Number} [options.memory=50] is the maximum memory in MB to use for this cache
* @param {Number} [options.files=20] is the maximum number of files to hold open
* @param {Number} [options.items=100] is the maximum number of operations to cache
* @returns {Object}
*/
module.exports.cache = function (options) {
if (isBoolean(options)) {
if (options) {
// Default cache settings of 50MB, 20 files, 100 items
return sharp.cache(50, 20, 100);
} else {
return sharp.cache(0, 0, 0);
}
} else if (isObject(options)) {
return sharp.cache(options.memory, options.files, options.items);
} else {
return sharp.cache();
}
};
// Ensure default cache settings are set
module.exports.cache(true);
/**
* Gets, or when a concurrency is provided sets,
* the number of threads _libvips'_ should create to process each image.
* The default value is the number of CPU cores.
* A value of `0` will reset to this default.
*
* The maximum number of images that can be processed in parallel
* is limited by libuv's `UV_THREADPOOL_SIZE` environment variable.
*
* This method always returns the current concurrency.
*
* @example
* const threads = sharp.concurrency(); // 4
* sharp.concurrency(2); // 2
* sharp.concurrency(0); // 4
*
* @param {Number} [concurrency]
* @returns {Number} concurrency
*/
module.exports.concurrency = function (concurrency) {
return sharp.concurrency(isInteger(concurrency) ? concurrency : null);
};
/**
* Provides access to internal task counters.
* - queue is the number of tasks this module has queued waiting for _libuv_ to provide a worker thread from its pool.
* - process is the number of resize tasks currently being processed.
*
* @example
* const counters = sharp.counters(); // { queue: 2, process: 4 }
*
* @returns {Object}
*/
module.exports.counters = function () {
return sharp.counters();
};
/**
* Get and set use of SIMD vector unit instructions.
* Requires libvips to have been compiled with liborc support.
*
* Improves the performance of `resize`, `blur` and `sharpen` operations
* by taking advantage of the SIMD vector unit of the CPU, e.g. Intel SSE and ARM NEON.
*
* This feature is currently off by default but future versions may reverse this.
* Versions of liborc prior to 0.4.25 are prone to segfault under heavy load.
*
* @example
* const simd = sharp.simd();
* // simd is `true` if SIMD is currently enabled
* @example
* const simd = sharp.simd(true);
* // attempts to enable the use of SIMD, returning true if available
*
* @param {Boolean} [simd=false]
* @returns {Boolean}
*/
module.exports.simd = function (simd) {
return sharp.simd(isBoolean(simd) ? simd : null);
};
// Switch off default
module.exports.simd(false);