mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
PNG is mostly used for logos and line art, so these defaults will produce the smallest files for most output most of the time.
539 lines
19 KiB
JavaScript
539 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
const is = require('./is');
|
|
const sharp = require('../build/Release/sharp.node');
|
|
|
|
/**
|
|
* 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`,
|
|
* `channels` and `premultiplied` (indicating if premultiplication was used).
|
|
* When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`.
|
|
* @returns {Promise<Object>} - when no callback is provided
|
|
* @throws {Error} Invalid parameters
|
|
*/
|
|
function toFile (fileOut, callback) {
|
|
if (!fileOut || fileOut.length === 0) {
|
|
const errOutputInvalid = new Error('Invalid output');
|
|
if (is.fn(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 (is.fn(callback)) {
|
|
callback(errOutputIsInput);
|
|
} else {
|
|
return Promise.reject(errOutputIsInput);
|
|
}
|
|
} else {
|
|
this.options.fileOut = fileOut;
|
|
return this._pipeline(callback);
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Write output to a Buffer.
|
|
* JPEG, PNG, WebP, TIFF and RAW output are supported.
|
|
* By default, the format will match the input image, except GIF and SVG input which become PNG output.
|
|
*
|
|
* `callback`, if present, gets three arguments `(err, data, info)` where:
|
|
* - `err` is an error, if any.
|
|
* - `data` is the output image data.
|
|
* - `info` contains the output image `format`, `size` (bytes), `width`, `height`,
|
|
* `channels` and `premultiplied` (indicating if premultiplication was used).
|
|
* A Promise is returned when `callback` is not provided.
|
|
*
|
|
* @param {Object} [options]
|
|
* @param {Boolean} [options.resolveWithObject] Resolve the Promise with an Object containing `data` and `info` properties instead of resolving only with `data`.
|
|
* @param {Function} [callback]
|
|
* @returns {Promise<Buffer>} - when no callback is provided
|
|
*/
|
|
function toBuffer (options, callback) {
|
|
if (is.object(options)) {
|
|
if (is.bool(options.resolveWithObject)) {
|
|
this.options.resolveWithObject = options.resolveWithObject;
|
|
}
|
|
}
|
|
return this._pipeline(is.fn(options) ? options : callback);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function withMetadata (withMetadata) {
|
|
this.options.withMetadata = is.bool(withMetadata) ? withMetadata : true;
|
|
if (is.object(withMetadata)) {
|
|
if (is.defined(withMetadata.orientation)) {
|
|
if (is.integer(withMetadata.orientation) && is.inRange(withMetadata.orientation, 1, 8)) {
|
|
this.options.withMetadataOrientation = withMetadata.orientation;
|
|
} else {
|
|
throw new Error('Invalid orientation (1 to 8) ' + withMetadata.orientation);
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* 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} [options.trellisQuantisation=false] - apply trellis quantisation, requires mozjpeg
|
|
* @param {Boolean} [options.overshootDeringing=false] - apply overshoot deringing, requires mozjpeg
|
|
* @param {Boolean} [options.optimiseScans=false] - optimise progressive scans, forces progressive, requires mozjpeg
|
|
* @param {Boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans
|
|
* @param {Boolean} [options.force=true] - force JPEG output, otherwise attempt to use input format
|
|
* @returns {Sharp}
|
|
* @throws {Error} Invalid options
|
|
*/
|
|
function jpeg (options) {
|
|
if (is.object(options)) {
|
|
if (is.defined(options.quality)) {
|
|
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
|
|
this.options.jpegQuality = options.quality;
|
|
} else {
|
|
throw new Error('Invalid quality (integer, 1-100) ' + options.quality);
|
|
}
|
|
}
|
|
if (is.defined(options.progressive)) {
|
|
this._setBooleanOption('jpegProgressive', options.progressive);
|
|
}
|
|
if (is.defined(options.chromaSubsampling)) {
|
|
if (is.string(options.chromaSubsampling) && is.inArray(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 = is.bool(options.trellisQuantization) ? options.trellisQuantization : options.trellisQuantisation;
|
|
if (is.defined(options.trellisQuantisation)) {
|
|
this._setBooleanOption('jpegTrellisQuantisation', options.trellisQuantisation);
|
|
}
|
|
if (is.defined(options.overshootDeringing)) {
|
|
this._setBooleanOption('jpegOvershootDeringing', options.overshootDeringing);
|
|
}
|
|
options.optimiseScans = is.bool(options.optimizeScans) ? options.optimizeScans : options.optimiseScans;
|
|
if (is.defined(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=9] - zlib compression level, 0-9
|
|
* @param {Boolean} [options.adaptiveFiltering=false] - use adaptive row filtering
|
|
* @param {Boolean} [options.force=true] - force PNG output, otherwise attempt to use input format
|
|
* @returns {Sharp}
|
|
* @throws {Error} Invalid options
|
|
*/
|
|
function png (options) {
|
|
if (is.object(options)) {
|
|
if (is.defined(options.progressive)) {
|
|
this._setBooleanOption('pngProgressive', options.progressive);
|
|
}
|
|
if (is.defined(options.compressionLevel)) {
|
|
if (is.integer(options.compressionLevel) && is.inRange(options.compressionLevel, 0, 9)) {
|
|
this.options.pngCompressionLevel = options.compressionLevel;
|
|
} else {
|
|
throw new Error('Invalid compressionLevel (integer, 0-9) ' + options.compressionLevel);
|
|
}
|
|
}
|
|
if (is.defined(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 {Number} [options.alphaQuality=100] - quality of alpha layer, integer 0-100
|
|
* @param {Boolean} [options.lossless=false] - use lossless compression mode
|
|
* @param {Boolean} [options.nearLossless=false] - use near_lossless compression mode
|
|
* @param {Boolean} [options.force=true] - force WebP output, otherwise attempt to use input format
|
|
* @returns {Sharp}
|
|
* @throws {Error} Invalid options
|
|
*/
|
|
function webp (options) {
|
|
if (is.object(options) && is.defined(options.quality)) {
|
|
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
|
|
this.options.webpQuality = options.quality;
|
|
} else {
|
|
throw new Error('Invalid quality (integer, 1-100) ' + options.quality);
|
|
}
|
|
}
|
|
if (is.object(options) && is.defined(options.alphaQuality)) {
|
|
if (is.integer(options.alphaQuality) && is.inRange(options.alphaQuality, 1, 100)) {
|
|
this.options.webpAlphaQuality = options.alphaQuality;
|
|
} else {
|
|
throw new Error('Invalid webp alpha quality (integer, 1-100) ' + options.alphaQuality);
|
|
}
|
|
}
|
|
if (is.object(options) && is.defined(options.lossless)) {
|
|
this._setBooleanOption('webpLossless', options.lossless);
|
|
}
|
|
if (is.object(options) && is.defined(options.nearLossless)) {
|
|
this._setBooleanOption('webpNearLossless', options.nearLossless);
|
|
}
|
|
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
|
|
* @param {Boolean} [options.compression='jpeg'] - compression options: lzw, deflate, jpeg
|
|
* @param {Boolean} [options.predictor='none'] - compression predictor options: none, horizontal, float
|
|
* @param {Number} [options.xres=1.0] - horizontal resolution in pixels/mm
|
|
* @param {Number} [options.yres=1.0] - vertical resolution in pixels/mm
|
|
* @param {Boolean} [options.squash=false] - squash 8-bit images down to 1 bit
|
|
* @returns {Sharp}
|
|
* @throws {Error} Invalid options
|
|
*/
|
|
function tiff (options) {
|
|
if (is.object(options) && is.defined(options.quality)) {
|
|
if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
|
|
this.options.tiffQuality = options.quality;
|
|
} else {
|
|
throw new Error('Invalid quality (integer, 1-100) ' + options.quality);
|
|
}
|
|
}
|
|
if (is.object(options) && is.defined(options.squash)) {
|
|
if (is.bool(options.squash)) {
|
|
this.options.tiffSquash = options.squash;
|
|
} else {
|
|
throw new Error('Invalid Value for squash ' + options.squash + ' Only Boolean Values allowed for options.squash.');
|
|
}
|
|
}
|
|
// resolution
|
|
if (is.object(options) && is.defined(options.xres)) {
|
|
if (is.number(options.xres)) {
|
|
this.options.tiffXres = options.xres;
|
|
} else {
|
|
throw new Error('Invalid Value for xres ' + options.xres + ' Only numeric values allowed for options.xres');
|
|
}
|
|
}
|
|
if (is.object(options) && is.defined(options.yres)) {
|
|
if (is.number(options.yres)) {
|
|
this.options.tiffYres = options.yres;
|
|
} else {
|
|
throw new Error('Invalid Value for yres ' + options.yres + ' Only numeric values allowed for options.yres');
|
|
}
|
|
}
|
|
// compression
|
|
if (is.defined(options) && is.defined(options.compression)) {
|
|
if (is.string(options.compression) && is.inArray(options.compression, ['lzw', 'deflate', 'jpeg', 'none'])) {
|
|
this.options.tiffCompression = options.compression;
|
|
} else {
|
|
const message = `Invalid compression option "${options.compression}". Should be one of: lzw, deflate, jpeg, none`;
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
// predictor
|
|
if (is.defined(options) && is.defined(options.predictor)) {
|
|
if (is.string(options.predictor) && is.inArray(options.predictor, ['none', 'horizontal', 'float'])) {
|
|
this.options.tiffPredictor = options.predictor;
|
|
} else {
|
|
const message = `Invalid predictor option "${options.predictor}". Should be one of: none, horizontal, float`;
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
return this._updateFormatOut('tiff', options);
|
|
}
|
|
|
|
/**
|
|
* Force output to be raw, uncompressed uint8 pixel data.
|
|
* @returns {Sharp}
|
|
*/
|
|
function raw () {
|
|
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
|
|
*/
|
|
function toFormat (format, options) {
|
|
if (is.object(format) && is.string(format.id)) {
|
|
format = format.id;
|
|
}
|
|
if (format === 'jpg') format = 'jpeg';
|
|
if (!is.inArray(format, ['jpeg', 'png', 'webp', 'tiff', 'raw'])) {
|
|
throw new Error('Unsupported output format ' + format);
|
|
}
|
|
return this[format](options);
|
|
}
|
|
|
|
/**
|
|
* Use tile-based deep zoom (image pyramid) output.
|
|
* Set the format and options for tile images via the `toFormat`, `jpeg`, `png` or `webp` functions.
|
|
* Use a `.zip` or `.szi` file extension with `toFile` to write to a compressed archive file format.
|
|
*
|
|
* @example
|
|
* sharp('input.tiff')
|
|
* .png()
|
|
* .tile({
|
|
* size: 512
|
|
* })
|
|
* .toFile('output.dz', 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
|
|
*/
|
|
function tile (tile) {
|
|
if (is.object(tile)) {
|
|
// Size of square tiles, in pixels
|
|
if (is.defined(tile.size)) {
|
|
if (is.integer(tile.size) && is.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 (is.defined(tile.overlap)) {
|
|
if (is.integer(tile.overlap) && is.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 (is.defined(tile.container)) {
|
|
if (is.string(tile.container) && is.inArray(tile.container, ['fs', 'zip'])) {
|
|
this.options.tileContainer = tile.container;
|
|
} else {
|
|
throw new Error('Invalid tile container ' + tile.container);
|
|
}
|
|
}
|
|
// Layout
|
|
if (is.defined(tile.layout)) {
|
|
if (is.string(tile.layout) && is.inArray(tile.layout, ['dz', 'google', 'zoomify'])) {
|
|
this.options.tileLayout = tile.layout;
|
|
} else {
|
|
throw new Error('Invalid tile layout ' + tile.layout);
|
|
}
|
|
}
|
|
}
|
|
// Format
|
|
if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
|
|
this.options.tileFormat = this.options.formatOut;
|
|
} else if (this.options.formatOut !== 'input') {
|
|
throw new Error('Invalid tile format ' + this.options.formatOut);
|
|
}
|
|
return this._updateFormatOut('dz');
|
|
}
|
|
|
|
/**
|
|
* 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}
|
|
*/
|
|
function _updateFormatOut (formatOut, options) {
|
|
this.options.formatOut = (is.object(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
|
|
*/
|
|
function _setBooleanOption (key, val) {
|
|
if (is.bool(val)) {
|
|
this.options[key] = val;
|
|
} else {
|
|
throw new Error('Invalid ' + key + ' (boolean) ' + val);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by a WriteableStream to notify us it is ready for data.
|
|
* @private
|
|
*/
|
|
function _read () {
|
|
if (!this.options.streamOut) {
|
|
this.options.streamOut = true;
|
|
this._pipeline();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoke the C++ image processing pipeline
|
|
* Supports callback, stream and promise variants
|
|
* @private
|
|
*/
|
|
function _pipeline (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
|
|
if (this.streamInFinished) {
|
|
this._flattenBufferIn();
|
|
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);
|
|
});
|
|
} else {
|
|
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, info) {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
if (that.options.resolveWithObject) {
|
|
resolve({ data: data, info: info });
|
|
} else {
|
|
resolve(data);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
} else {
|
|
// output=promise, input=file/buffer
|
|
return new Promise(function (resolve, reject) {
|
|
sharp.pipeline(that.options, function (err, data, info) {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
if (that.options.resolveWithObject) {
|
|
resolve({ data: data, info: info });
|
|
} else {
|
|
resolve(data);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decorate the Sharp prototype with output-related functions.
|
|
* @private
|
|
*/
|
|
module.exports = function (Sharp) {
|
|
[
|
|
// Public
|
|
toFile,
|
|
toBuffer,
|
|
withMetadata,
|
|
jpeg,
|
|
png,
|
|
webp,
|
|
tiff,
|
|
raw,
|
|
toFormat,
|
|
tile,
|
|
// Private
|
|
_updateFormatOut,
|
|
_setBooleanOption,
|
|
_read,
|
|
_pipeline
|
|
].forEach(function (f) {
|
|
Sharp.prototype[f.name] = f;
|
|
});
|
|
};
|