344 lines
11 KiB
JavaScript
344 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
const util = require('util');
|
|
const stream = require('stream');
|
|
const is = require('./is');
|
|
|
|
require('./libvips').hasVendoredLibvips();
|
|
|
|
/* istanbul ignore next */
|
|
try {
|
|
require('../build/Release/sharp.node');
|
|
} catch (err) {
|
|
// Bail early if bindings aren't available
|
|
const help = ['', 'Something went wrong installing the "sharp" module', '', err.message, ''];
|
|
if (/NODE_MODULE_VERSION/.test(err.message)) {
|
|
help.push('- Ensure the version of Node.js used at install time matches that used at runtime');
|
|
} else if (/invalid ELF header/.test(err.message)) {
|
|
help.push(`- Ensure "${process.platform}" is used at install time as well as runtime`);
|
|
} else if (/dylib/.test(err.message) && /Incompatible library version/.test(err.message)) {
|
|
help.push('- Run "brew update && brew upgrade vips"');
|
|
} else if (/Cannot find module/.test(err.message)) {
|
|
help.push('- Run "npm rebuild --verbose sharp" and look for errors');
|
|
} else {
|
|
help.push(
|
|
'- Remove the "node_modules/sharp" directory then run',
|
|
' "npm install --ignore-scripts=false --verbose" and look for errors'
|
|
);
|
|
}
|
|
help.push(
|
|
'- Consult the installation documentation at https://sharp.pixelplumbing.com/install',
|
|
'- Search for this error at https://github.com/lovell/sharp/issues', ''
|
|
);
|
|
const error = help.join('\n');
|
|
throw new Error(error);
|
|
}
|
|
|
|
// Use NODE_DEBUG=sharp to enable libvips warnings
|
|
const debuglog = util.debuglog('sharp');
|
|
|
|
/**
|
|
* Constructor factory to create an instance of `sharp`, to which further methods are chained.
|
|
*
|
|
* JPEG, PNG, WebP or TIFF format image data can be streamed out from this object.
|
|
* When using Stream based output, derived attributes are available from the `info` event.
|
|
*
|
|
* Non-critical problems encountered during processing are emitted as `warning` events.
|
|
*
|
|
* Implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
|
|
*
|
|
* @constructs Sharp
|
|
*
|
|
* @emits Sharp#info
|
|
* @emits Sharp#warning
|
|
*
|
|
* @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);
|
|
*
|
|
* @example
|
|
* // Create a blank 300x200 PNG image of semi-transluent red pixels
|
|
* sharp({
|
|
* create: {
|
|
* width: 300,
|
|
* height: 200,
|
|
* channels: 4,
|
|
* background: { r: 255, g: 0, b: 0, alpha: 0.5 }
|
|
* }
|
|
* })
|
|
* .png()
|
|
* .toBuffer()
|
|
* .then( ... );
|
|
*
|
|
* @example
|
|
* // Convert an animated GIF to an animated WebP
|
|
* await sharp('in.gif', { animated: true }).toFile('out.webp');
|
|
*
|
|
* @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 filesystem 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 not present.
|
|
* @param {Object} [options] - if present, is an Object with optional attributes.
|
|
* @param {boolean} [options.failOnError=true] - by default halt processing and raise an error when loading invalid images.
|
|
* Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid.
|
|
* @param {number|boolean} [options.limitInputPixels=268402689] - Do not process input images where the number of pixels
|
|
* (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted.
|
|
* An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF).
|
|
* @param {boolean} [options.sequentialRead=false] - Set this to `true` to use sequential rather than random access where possible.
|
|
* This can reduce memory usage and might improve performance on some systems.
|
|
* @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000.
|
|
* @param {number} [options.pages=1] - number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages.
|
|
* @param {number} [options.page=0] - page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based.
|
|
* @param {number} [options.level=0] - level to extract from a multi-level input (OpenSlide), zero based.
|
|
* @param {boolean} [options.animated=false] - Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`).
|
|
* @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering.
|
|
* @param {number} [options.raw.width]
|
|
* @param {number} [options.raw.height]
|
|
* @param {number} [options.raw.channels] - 1-4
|
|
* @param {Object} [options.create] - describes a new image to be created.
|
|
* @param {number} [options.create.width]
|
|
* @param {number} [options.create.height]
|
|
* @param {number} [options.create.channels] - 3-4
|
|
* @param {string|Object} [options.create.background] - 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 parameters
|
|
*/
|
|
const Sharp = function (input, options) {
|
|
if (arguments.length === 1 && !is.defined(input)) {
|
|
throw new Error('Invalid input');
|
|
}
|
|
if (!(this instanceof Sharp)) {
|
|
return new Sharp(input, options);
|
|
}
|
|
stream.Duplex.call(this);
|
|
this.options = {
|
|
// resize options
|
|
topOffsetPre: -1,
|
|
leftOffsetPre: -1,
|
|
widthPre: -1,
|
|
heightPre: -1,
|
|
topOffsetPost: -1,
|
|
leftOffsetPost: -1,
|
|
widthPost: -1,
|
|
heightPost: -1,
|
|
width: -1,
|
|
height: -1,
|
|
canvas: 'crop',
|
|
position: 0,
|
|
resizeBackground: [0, 0, 0, 255],
|
|
useExifOrientation: false,
|
|
angle: 0,
|
|
rotationAngle: 0,
|
|
rotationBackground: [0, 0, 0, 255],
|
|
rotateBeforePreExtract: false,
|
|
flip: false,
|
|
flop: false,
|
|
extendTop: 0,
|
|
extendBottom: 0,
|
|
extendLeft: 0,
|
|
extendRight: 0,
|
|
extendBackground: [0, 0, 0, 255],
|
|
withoutEnlargement: false,
|
|
kernel: 'lanczos3',
|
|
fastShrinkOnLoad: true,
|
|
// operations
|
|
tintA: 128,
|
|
tintB: 128,
|
|
flatten: false,
|
|
flattenBackground: [0, 0, 0],
|
|
negate: false,
|
|
medianSize: 0,
|
|
blurSigma: 0,
|
|
sharpenSigma: 0,
|
|
sharpenFlat: 1,
|
|
sharpenJagged: 2,
|
|
threshold: 0,
|
|
thresholdGrayscale: true,
|
|
trimThreshold: 0,
|
|
gamma: 0,
|
|
gammaOut: 0,
|
|
greyscale: false,
|
|
normalise: false,
|
|
brightness: 1,
|
|
saturation: 1,
|
|
hue: 0,
|
|
booleanBufferIn: null,
|
|
booleanFileIn: '',
|
|
joinChannelIn: [],
|
|
extractChannel: -1,
|
|
removeAlpha: false,
|
|
ensureAlpha: false,
|
|
colourspace: 'srgb',
|
|
composite: [],
|
|
// output
|
|
fileOut: '',
|
|
formatOut: 'input',
|
|
streamOut: false,
|
|
withMetadata: false,
|
|
withMetadataOrientation: -1,
|
|
withMetadataIcc: '',
|
|
resolveWithObject: false,
|
|
// output format
|
|
jpegQuality: 80,
|
|
jpegProgressive: false,
|
|
jpegChromaSubsampling: '4:2:0',
|
|
jpegTrellisQuantisation: false,
|
|
jpegOvershootDeringing: false,
|
|
jpegOptimiseScans: false,
|
|
jpegOptimiseCoding: true,
|
|
jpegQuantisationTable: 0,
|
|
pngProgressive: false,
|
|
pngCompressionLevel: 9,
|
|
pngAdaptiveFiltering: false,
|
|
pngPalette: false,
|
|
pngQuality: 100,
|
|
pngColours: 256,
|
|
pngDither: 1,
|
|
webpQuality: 80,
|
|
webpAlphaQuality: 100,
|
|
webpLossless: false,
|
|
webpNearLossless: false,
|
|
webpSmartSubsample: false,
|
|
webpReductionEffort: 4,
|
|
tiffQuality: 80,
|
|
tiffCompression: 'jpeg',
|
|
tiffPredictor: 'horizontal',
|
|
tiffPyramid: false,
|
|
tiffBitdepth: 8,
|
|
tiffTile: false,
|
|
tiffTileHeight: 256,
|
|
tiffTileWidth: 256,
|
|
tiffXres: 1.0,
|
|
tiffYres: 1.0,
|
|
heifQuality: 80,
|
|
heifLossless: false,
|
|
heifCompression: 'hevc',
|
|
tileSize: 256,
|
|
tileOverlap: 0,
|
|
tileContainer: 'fs',
|
|
tileLayout: 'dz',
|
|
tileFormat: 'last',
|
|
tileDepth: 'last',
|
|
tileAngle: 0,
|
|
tileSkipBlanks: -1,
|
|
tileBackground: [255, 255, 255, 255],
|
|
tileCentre: false,
|
|
linearA: 1,
|
|
linearB: 0,
|
|
// Function to notify of libvips warnings
|
|
debuglog: warning => {
|
|
this.emit('warning', warning);
|
|
debuglog(warning);
|
|
},
|
|
// Function to notify of queue length changes
|
|
queueListener: function (queueLength) {
|
|
Sharp.queue.emit('change', queueLength);
|
|
}
|
|
};
|
|
this.options.input = this._createInputDescriptor(input, options, { allowStream: true });
|
|
return this;
|
|
};
|
|
util.inherits(Sharp, stream.Duplex);
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @example
|
|
* // Create a pipeline that will download an image, resize it and format it to different files
|
|
* // Using Promises to know when the pipeline is complete
|
|
* const fs = require("fs");
|
|
* const got = require("got");
|
|
* const sharpStream = sharp({
|
|
* failOnError: false
|
|
* });
|
|
*
|
|
* const promises = [];
|
|
*
|
|
* promises.push(
|
|
* sharpStream
|
|
* .clone()
|
|
* .jpeg({ quality: 100 })
|
|
* .toFile("originalFile.jpg")
|
|
* );
|
|
*
|
|
* promises.push(
|
|
* sharpStream
|
|
* .clone()
|
|
* .resize({ width: 500 })
|
|
* .jpeg({ quality: 80 })
|
|
* .toFile("optimized-500.jpg")
|
|
* );
|
|
*
|
|
* promises.push(
|
|
* sharpStream
|
|
* .clone()
|
|
* .resize({ width: 500 })
|
|
* .webp({ quality: 80 })
|
|
* .toFile("optimized-500.webp")
|
|
* );
|
|
*
|
|
* // https://github.com/sindresorhus/got#gotstreamurl-options
|
|
* got.stream("https://www.example.com/some-file.jpg").pipe(sharpStream);
|
|
*
|
|
* Promise.all(promises)
|
|
* .then(res => { console.log("Done!", res); })
|
|
* .catch(err => {
|
|
* console.error("Error processing files, let's clean it up", err);
|
|
* try {
|
|
* fs.unlinkSync("originalFile.jpg");
|
|
* fs.unlinkSync("optimized-500.jpg");
|
|
* fs.unlinkSync("optimized-500.webp");
|
|
* } catch (e) {}
|
|
* });
|
|
*
|
|
* @returns {Sharp}
|
|
*/
|
|
function clone () {
|
|
// Clone existing options
|
|
const clone = this.constructor.call();
|
|
clone.options = Object.assign({}, this.options);
|
|
// Pass 'finish' event to clone for Stream-based input
|
|
if (this._isStreamInput()) {
|
|
this.on('finish', () => {
|
|
// Clone inherits input data
|
|
this._flattenBufferIn();
|
|
clone.options.bufferIn = this.options.bufferIn;
|
|
clone.emit('finish');
|
|
});
|
|
}
|
|
return clone;
|
|
}
|
|
Object.assign(Sharp.prototype, { clone });
|
|
|
|
/**
|
|
* Export constructor.
|
|
* @private
|
|
*/
|
|
module.exports = Sharp;
|