// Copyright 2013 Lovell Fuller and others. // SPDX-License-Identifier: Apache-2.0 'use strict'; const util = require('node:util'); const stream = require('node:stream'); const is = require('./is'); require('./sharp'); // 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, GIF, AVIF 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. * * When loading more than one page/frame of an animated image, * these are combined as a vertically-stacked "toilet roll" image * where the overall height is the `pageHeight` multiplied by the number of `pages`. * * @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 remote URL, * // resize to 300 pixels wide, * // emit an 'info' event with calculated dimensions * // and finally write image data to writableStream * const { body } = fetch('https://...'); * const readableStream = Readable.fromWeb(body); * const transformer = sharp() * .resize(300) * .on('info', ({ height }) => { * console.log(`Image height is ${height}`); * }); * readableStream.pipe(transformer).pipe(writableStream); * * @example * // Create a blank 300x200 PNG image of semi-translucent 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'); * * @example * // Read a raw array of pixels and save it to a png * const input = Uint8Array.from([255, 255, 255, 0, 0, 0]); // or Uint8ClampedArray * const image = sharp(input, { * // because the input does not contain its dimensions or how many channels it has * // we need to specify it in the constructor options * raw: { * width: 2, * height: 1, * channels: 3 * } * }); * await image.toFile('my-two-pixels.png'); * * @example * // Generate RGB Gaussian noise * await sharp({ * create: { * width: 300, * height: 200, * channels: 3, * noise: { * type: 'gaussian', * mean: 128, * sigma: 30 * } * } * }).toFile('noise.png'); * * @example * // Generate an image from text * await sharp({ * text: { * text: 'Hello, world!', * width: 400, // max width * height: 300 // max height * } * }).toFile('text_bw.png'); * * @example * // Generate an rgba image from text using pango markup and font * await sharp({ * text: { * text: 'Red!blue', * font: 'sans', * rgba: true, * dpi: 300 * } * }).toFile('text_rgba.png'); * * @example * // Join four input images as a 2x2 grid with a 4 pixel gutter * const data = await sharp( * [image1, image2, image3, image4], * { join: { across: 2, shim: 4 } } * ).toBuffer(); * * @example * // Generate a two-frame animated image from emoji * const images = ['😀', '😛'].map(text => ({ * text: { text, width: 64, height: 64, channels: 4, rgba: true } * })); * await sharp(images, { join: { animated: true } }).toFile('out.gif'); * * @param {(Buffer|ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string|Array)} [input] - if present, can be * a Buffer / ArrayBuffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image data, or * a TypedArray containing raw pixel image data, or * a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file. * An array of inputs can be provided, and these will be joined together. * JPEG, PNG, WebP, AVIF, 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 {string} [options.failOn='warning'] - When to abort processing of invalid pixel data, one of (in order of sensitivity, least to most): 'none', 'truncated', 'error', 'warning'. Higher levels imply lower levels. Invalid metadata will always abort. * @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.unlimited=false] - Set this to `true` to remove safety features that help prevent memory exhaustion (JPEG, PNG, SVG, HEIF). * @param {boolean} [options.autoOrient=false] - Set this to `true` to rotate/flip the image to match EXIF `Orientation`, if any. * @param {boolean} [options.sequentialRead=true] - Set this to `false` to use random access rather than sequential read. Some operations will do this automatically. * @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000. * @param {number} [options.ignoreIcc=false] - should the embedded ICC profile, if any, be ignored. * @param {number} [options.pages=1] - Number of pages to extract for multi-page input (GIF, WebP, TIFF), use -1 for all pages. * @param {number} [options.page=0] - Page number to start extracting from for multi-page input (GIF, WebP, TIFF), zero based. * @param {boolean} [options.animated=false] - Set to `true` to read all frames/pages of an animated image (GIF, WebP, TIFF), 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] - integral number of pixels wide. * @param {number} [options.raw.height] - integral number of pixels high. * @param {number} [options.raw.channels] - integral number of channels, between 1 and 4. * @param {boolean} [options.raw.premultiplied] - specifies that the raw input has already been premultiplied, set to `true` * to avoid sharp premultiplying the image. (optional, default `false`) * @param {number} [options.raw.pageHeight] - The pixel height of each page/frame for animated images, must be an integral factor of `raw.height`. * @param {Object} [options.create] - describes a new image to be created. * @param {number} [options.create.width] - integral number of pixels wide. * @param {number} [options.create.height] - integral number of pixels high. * @param {number} [options.create.channels] - integral number of channels, either 3 (RGB) or 4 (RGBA). * @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. * @param {number} [options.create.pageHeight] - The pixel height of each page/frame for animated images, must be an integral factor of `create.height`. * @param {Object} [options.create.noise] - describes a noise to be created. * @param {string} [options.create.noise.type] - type of generated noise, currently only `gaussian` is supported. * @param {number} [options.create.noise.mean=128] - Mean value of pixels in the generated noise. * @param {number} [options.create.noise.sigma=30] - Standard deviation of pixel values in the generated noise. * @param {Object} [options.text] - describes a new text image to be created. * @param {string} [options.text.text] - text to render as a UTF-8 string. It can contain Pango markup, for example `LeMonde`. * @param {string} [options.text.font] - font name to render with. * @param {string} [options.text.fontfile] - absolute filesystem path to a font file that can be used by `font`. * @param {number} [options.text.width=0] - Integral number of pixels to word-wrap at. Lines of text wider than this will be broken at word boundaries. * @param {number} [options.text.height=0] - Maximum integral number of pixels high. When defined, `dpi` will be ignored and the text will automatically fit the pixel resolution defined by `width` and `height`. Will be ignored if `width` is not specified or set to 0. * @param {string} [options.text.align='left'] - Alignment style for multi-line text (`'left'`, `'centre'`, `'center'`, `'right'`). * @param {boolean} [options.text.justify=false] - set this to true to apply justification to the text. * @param {number} [options.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified. * @param {boolean} [options.text.rgba=false] - set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for pango markup features like `Red!`. * @param {number} [options.text.spacing=0] - text line height in points. Will use the font line height if none is specified. * @param {string} [options.text.wrap='word'] - word wrapping style when width is provided, one of: 'word', 'char', 'word-char' (prefer word, fallback to char) or 'none'. * @param {Object} [options.join] - describes how an array of input images should be joined. * @param {number} [options.join.across=1] - number of images to join horizontally. * @param {boolean} [options.join.animated=false] - set this to `true` to join the images as an animated image. * @param {number} [options.join.shim=0] - number of pixels to insert between joined images. * @param {string|Object} [options.join.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. * @param {string} [options.join.halign='left'] - horizontal alignment style for images joined horizontally (`'left'`, `'centre'`, `'center'`, `'right'`). * @param {string} [options.join.valign='top'] - vertical alignment style for images joined vertically (`'top'`, `'centre'`, `'center'`, `'bottom'`). * @param {Object} [options.tiff] - Describes TIFF specific options. * @param {number} [options.tiff.subifd=-1] - Sub Image File Directory to extract for OME-TIFF, defaults to main image. * @param {Object} [options.svg] - Describes SVG specific options. * @param {string} [options.svg.stylesheet] - Custom CSS for SVG input, applied with a User Origin during the CSS cascade. * @param {boolean} [options.svg.highBitdepth=false] - Set to `true` to render SVG input at 32-bits per channel (128-bit) instead of 8-bits per channel (32-bit) RGBA. * @param {Object} [options.pdf] - Describes PDF specific options. Requires the use of a globally-installed libvips compiled with support for PDFium, Poppler, ImageMagick or GraphicsMagick. * @param {string|Object} [options.pdf.background] - Background colour to use when PDF is partially transparent. Parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. * @param {Object} [options.openSlide] - Describes OpenSlide specific options. Requires the use of a globally-installed libvips compiled with support for OpenSlide. * @param {number} [options.openSlide.level=0] - Level to extract from a multi-level input, zero based. * @param {Object} [options.jp2] - Describes JPEG 2000 specific options. Requires the use of a globally-installed libvips compiled with support for OpenJPEG. * @param {boolean} [options.jp2.oneshot=false] - Set to `true` to decode tiled JPEG 2000 images in a single operation, improving compatibility. * @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], 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], extendWith: 'background', withoutEnlargement: false, withoutReduction: false, affineMatrix: [], affineBackground: [0, 0, 0, 255], affineIdx: 0, affineIdy: 0, affineOdx: 0, affineOdy: 0, affineInterpolator: this.constructor.interpolators.bilinear, kernel: 'lanczos3', fastShrinkOnLoad: true, // operations tint: [-1, 0, 0, 0], flatten: false, flattenBackground: [0, 0, 0], unflatten: false, negate: false, negateAlpha: true, medianSize: 0, blurSigma: 0, precision: 'integer', minAmpl: 0.2, sharpenSigma: 0, sharpenM1: 1, sharpenM2: 2, sharpenX1: 2, sharpenY2: 10, sharpenY3: 20, threshold: 0, thresholdGrayscale: true, trimBackground: [], trimThreshold: -1, trimLineArt: false, dilateWidth: 0, erodeWidth: 0, gamma: 0, gammaOut: 0, greyscale: false, normalise: false, normaliseLower: 1, normaliseUpper: 99, claheWidth: 0, claheHeight: 0, claheMaxSlope: 3, brightness: 1, saturation: 1, hue: 0, lightness: 0, booleanBufferIn: null, booleanFileIn: '', joinChannelIn: [], extractChannel: -1, removeAlpha: false, ensureAlpha: -1, colourspace: 'srgb', colourspacePipeline: 'last', composite: [], // output fileOut: '', formatOut: 'input', streamOut: false, keepMetadata: 0, withMetadataOrientation: -1, withMetadataDensity: 0, withIccProfile: '', withExif: {}, withExifMerge: true, withXmp: '', resolveWithObject: false, loop: -1, delay: [], // output format jpegQuality: 80, jpegProgressive: false, jpegChromaSubsampling: '4:2:0', jpegTrellisQuantisation: false, jpegOvershootDeringing: false, jpegOptimiseScans: false, jpegOptimiseCoding: true, jpegQuantisationTable: 0, pngProgressive: false, pngCompressionLevel: 6, pngAdaptiveFiltering: false, pngPalette: false, pngQuality: 100, pngEffort: 7, pngBitdepth: 8, pngDither: 1, jp2Quality: 80, jp2TileHeight: 512, jp2TileWidth: 512, jp2Lossless: false, jp2ChromaSubsampling: '4:4:4', webpQuality: 80, webpAlphaQuality: 100, webpLossless: false, webpNearLossless: false, webpSmartSubsample: false, webpSmartDeblock: false, webpPreset: 'default', webpEffort: 4, webpMinSize: false, webpMixed: false, gifBitdepth: 8, gifEffort: 7, gifDither: 1, gifInterFrameMaxError: 0, gifInterPaletteMaxError: 3, gifKeepDuplicateFrames: false, gifReuse: true, gifProgressive: false, tiffQuality: 80, tiffCompression: 'jpeg', tiffPredictor: 'horizontal', tiffPyramid: false, tiffMiniswhite: false, tiffBitdepth: 8, tiffTile: false, tiffTileHeight: 256, tiffTileWidth: 256, tiffXres: 1.0, tiffYres: 1.0, tiffResolutionUnit: 'inch', heifQuality: 50, heifLossless: false, heifCompression: 'av1', heifEffort: 4, heifChromaSubsampling: '4:4:4', heifBitdepth: 8, jxlDistance: 1, jxlDecodingTier: 0, jxlEffort: 7, jxlLossless: false, rawDepth: 'uchar', tileSize: 256, tileOverlap: 0, tileContainer: 'fs', tileLayout: 'dz', tileFormat: 'last', tileDepth: 'last', tileAngle: 0, tileSkipBlanks: -1, tileBackground: [255, 255, 255, 255], tileCentre: false, tileId: 'https://example.com/iiif', tileBasename: '', timeoutSeconds: 0, linearA: [], linearB: [], pdfBackground: [255, 255, 255, 255], // 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; }; Object.setPrototypeOf(Sharp.prototype, stream.Duplex.prototype); Object.setPrototypeOf(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({ failOn: 'none' }); * * 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/blob/main/documentation/3-streams.md * 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(); const { debuglog, queueListener, ...options } = this.options; clone.options = structuredClone(options); clone.options.debuglog = debuglog; clone.options.queueListener = queueListener; // Pass 'finish' event to clone for Stream-based input if (this._isStreamInput()) { this.on('finish', () => { // Clone inherits input data this._flattenBufferIn(); clone.options.input.buffer = this.options.input.buffer; clone.emit('finish'); }); } return clone; } Object.assign(Sharp.prototype, { clone }); /** * Export constructor. * @module Sharp * @private */ module.exports = Sharp;