UNPKG

53.7 kBJavaScriptView Raw
1// Copyright 2013 Lovell Fuller and others.
2// SPDX-License-Identifier: Apache-2.0
3
4'use strict';
5
6const path = require('path');
7const is = require('./is');
8const sharp = require('./sharp');
9
10const formats = new Map([
11 ['heic', 'heif'],
12 ['heif', 'heif'],
13 ['avif', 'avif'],
14 ['jpeg', 'jpeg'],
15 ['jpg', 'jpeg'],
16 ['jpe', 'jpeg'],
17 ['tile', 'tile'],
18 ['dz', 'tile'],
19 ['png', 'png'],
20 ['raw', 'raw'],
21 ['tiff', 'tiff'],
22 ['tif', 'tiff'],
23 ['webp', 'webp'],
24 ['gif', 'gif'],
25 ['jp2', 'jp2'],
26 ['jpx', 'jp2'],
27 ['j2k', 'jp2'],
28 ['j2c', 'jp2'],
29 ['jxl', 'jxl']
30]);
31
32const jp2Regex = /\.(jp[2x]|j2[kc])$/i;
33
34const errJp2Save = () => new Error('JP2 output requires libvips with support for OpenJPEG');
35
36const bitdepthFromColourCount = (colours) => 1 << 31 - Math.clz32(Math.ceil(Math.log2(colours)));
37
38/**
39 * Write output image data to a file.
40 *
41 * If an explicit output format is not selected, it will be inferred from the extension,
42 * with JPEG, PNG, WebP, AVIF, TIFF, GIF, DZI, and libvips' V format supported.
43 * Note that raw pixel data is only supported for buffer output.
44 *
45 * By default all metadata will be removed, which includes EXIF-based orientation.
46 * See {@link #withmetadata|withMetadata} for control over this.
47 *
48 * The caller is responsible for ensuring directory structures and permissions exist.
49 *
50 * A `Promise` is returned when `callback` is not provided.
51 *
52 * @example
53 * sharp(input)
54 * .toFile('output.png', (err, info) => { ... });
55 *
56 * @example
57 * sharp(input)
58 * .toFile('output.png')
59 * .then(info => { ... })
60 * .catch(err => { ... });
61 *
62 * @param {string} fileOut - the path to write the image data to.
63 * @param {Function} [callback] - called on completion with two arguments `(err, info)`.
64 * `info` contains the output image `format`, `size` (bytes), `width`, `height`,
65 * `channels` and `premultiplied` (indicating if premultiplication was used).
66 * When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`.
67 * When using the attention crop strategy also contains `attentionX` and `attentionY`, the focal point of the cropped region.
68 * May also contain `textAutofitDpi` (dpi the font was rendered at) if image was created from text.
69 * @returns {Promise<Object>} - when no callback is provided
70 * @throws {Error} Invalid parameters
71 */
72function toFile (fileOut, callback) {
73 let err;
74 if (!is.string(fileOut)) {
75 err = new Error('Missing output file path');
76 } else if (is.string(this.options.input.file) && path.resolve(this.options.input.file) === path.resolve(fileOut)) {
77 err = new Error('Cannot use same file for input and output');
78 } else if (jp2Regex.test(path.extname(fileOut)) && !this.constructor.format.jp2k.output.file) {
79 err = errJp2Save();
80 }
81 if (err) {
82 if (is.fn(callback)) {
83 callback(err);
84 } else {
85 return Promise.reject(err);
86 }
87 } else {
88 this.options.fileOut = fileOut;
89 return this._pipeline(callback);
90 }
91 return this;
92}
93
94/**
95 * Write output to a Buffer.
96 * JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.
97 *
98 * Use {@link #toformat|toFormat} or one of the format-specific functions such as {@link jpeg}, {@link png} etc. to set the output format.
99 *
100 * If no explicit format is set, the output format will match the input image, except SVG input which becomes PNG output.
101 *
102 * By default all metadata will be removed, which includes EXIF-based orientation.
103 * See {@link #withmetadata|withMetadata} for control over this.
104 *
105 * `callback`, if present, gets three arguments `(err, data, info)` where:
106 * - `err` is an error, if any.
107 * - `data` is the output image data.
108 * - `info` contains the output image `format`, `size` (bytes), `width`, `height`,
109 * `channels` and `premultiplied` (indicating if premultiplication was used).
110 * When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`.
111 * May also contain `textAutofitDpi` (dpi the font was rendered at) if image was created from text.
112 *
113 * A `Promise` is returned when `callback` is not provided.
114 *
115 * @example
116 * sharp(input)
117 * .toBuffer((err, data, info) => { ... });
118 *
119 * @example
120 * sharp(input)
121 * .toBuffer()
122 * .then(data => { ... })
123 * .catch(err => { ... });
124 *
125 * @example
126 * sharp(input)
127 * .png()
128 * .toBuffer({ resolveWithObject: true })
129 * .then(({ data, info }) => { ... })
130 * .catch(err => { ... });
131 *
132 * @example
133 * const { data, info } = await sharp('my-image.jpg')
134 * // output the raw pixels
135 * .raw()
136 * .toBuffer({ resolveWithObject: true });
137 *
138 * // create a more type safe way to work with the raw pixel data
139 * // this will not copy the data, instead it will change `data`s underlying ArrayBuffer
140 * // so `data` and `pixelArray` point to the same memory location
141 * const pixelArray = new Uint8ClampedArray(data.buffer);
142 *
143 * // When you are done changing the pixelArray, sharp takes the `pixelArray` as an input
144 * const { width, height, channels } = info;
145 * await sharp(pixelArray, { raw: { width, height, channels } })
146 * .toFile('my-changed-image.jpg');
147 *
148 * @param {Object} [options]
149 * @param {boolean} [options.resolveWithObject] Resolve the Promise with an Object containing `data` and `info` properties instead of resolving only with `data`.
150 * @param {Function} [callback]
151 * @returns {Promise<Buffer>} - when no callback is provided
152 */
153function toBuffer (options, callback) {
154 if (is.object(options)) {
155 this._setBooleanOption('resolveWithObject', options.resolveWithObject);
156 } else if (this.options.resolveWithObject) {
157 this.options.resolveWithObject = false;
158 }
159 this.options.fileOut = '';
160 return this._pipeline(is.fn(options) ? options : callback);
161}
162
163/**
164 * Include all metadata (EXIF, XMP, IPTC) from the input image in the output image.
165 * This will also convert to and add a web-friendly sRGB ICC profile unless a custom
166 * output profile is provided.
167 *
168 * The default behaviour, when `withMetadata` is not used, is to convert to the device-independent
169 * sRGB colour space and strip all metadata, including the removal of any ICC profile.
170 *
171 * EXIF metadata is unsupported for TIFF output.
172 *
173 * @example
174 * sharp('input.jpg')
175 * .withMetadata()
176 * .toFile('output-with-metadata.jpg')
177 * .then(info => { ... });
178 *
179 * @example
180 * // Set output EXIF metadata
181 * const data = await sharp(input)
182 * .withMetadata({
183 * exif: {
184 * IFD0: {
185 * Copyright: 'The National Gallery'
186 * },
187 * IFD3: {
188 * GPSLatitudeRef: 'N',
189 * GPSLatitude: '51/1 30/1 3230/100',
190 * GPSLongitudeRef: 'W',
191 * GPSLongitude: '0/1 7/1 4366/100'
192 * }
193 * }
194 * })
195 * .toBuffer();
196 *
197 * @example
198 * // Set output metadata to 96 DPI
199 * const data = await sharp(input)
200 * .withMetadata({ density: 96 })
201 * .toBuffer();
202 *
203 * @param {Object} [options]
204 * @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag.
205 * @param {string} [options.icc='srgb'] Filesystem path to output ICC profile, relative to `process.cwd()`, defaults to built-in sRGB.
206 * @param {Object<Object>} [options.exif={}] Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
207 * @param {number} [options.density] Number of pixels per inch (DPI).
208 * @returns {Sharp}
209 * @throws {Error} Invalid parameters
210 */
211function withMetadata (options) {
212 this.options.withMetadata = is.bool(options) ? options : true;
213 if (is.object(options)) {
214 if (is.defined(options.orientation)) {
215 if (is.integer(options.orientation) && is.inRange(options.orientation, 1, 8)) {
216 this.options.withMetadataOrientation = options.orientation;
217 } else {
218 throw is.invalidParameterError('orientation', 'integer between 1 and 8', options.orientation);
219 }
220 }
221 if (is.defined(options.density)) {
222 if (is.number(options.density) && options.density > 0) {
223 this.options.withMetadataDensity = options.density;
224 } else {
225 throw is.invalidParameterError('density', 'positive number', options.density);
226 }
227 }
228 if (is.defined(options.icc)) {
229 if (is.string(options.icc)) {
230 this.options.withMetadataIcc = options.icc;
231 } else {
232 throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', options.icc);
233 }
234 }
235 if (is.defined(options.exif)) {
236 if (is.object(options.exif)) {
237 for (const [ifd, entries] of Object.entries(options.exif)) {
238 if (is.object(entries)) {
239 for (const [k, v] of Object.entries(entries)) {
240 if (is.string(v)) {
241 this.options.withMetadataStrs[`exif-${ifd.toLowerCase()}-${k}`] = v;
242 } else {
243 throw is.invalidParameterError(`exif.${ifd}.${k}`, 'string', v);
244 }
245 }
246 } else {
247 throw is.invalidParameterError(`exif.${ifd}`, 'object', entries);
248 }
249 }
250 } else {
251 throw is.invalidParameterError('exif', 'object', options.exif);
252 }
253 }
254 }
255 return this;
256}
257
258/**
259 * Force output to a given format.
260 *
261 * @example
262 * // Convert any input to PNG output
263 * const data = await sharp(input)
264 * .toFormat('png')
265 * .toBuffer();
266 *
267 * @param {(string|Object)} format - as a string or an Object with an 'id' attribute
268 * @param {Object} options - output options
269 * @returns {Sharp}
270 * @throws {Error} unsupported format or options
271 */
272function toFormat (format, options) {
273 const actualFormat = formats.get((is.object(format) && is.string(format.id) ? format.id : format).toLowerCase());
274 if (!actualFormat) {
275 throw is.invalidParameterError('format', `one of: ${[...formats.keys()].join(', ')}`, format);
276 }
277 return this[actualFormat](options);
278}
279
280/**
281 * Use these JPEG options for output image.
282 *
283 * @example
284 * // Convert any input to very high quality JPEG output
285 * const data = await sharp(input)
286 * .jpeg({
287 * quality: 100,
288 * chromaSubsampling: '4:4:4'
289 * })
290 * .toBuffer();
291 *
292 * @example
293 * // Use mozjpeg to reduce output JPEG file size (slower)
294 * const data = await sharp(input)
295 * .jpeg({ mozjpeg: true })
296 * .toBuffer();
297 *
298 * @param {Object} [options] - output options
299 * @param {number} [options.quality=80] - quality, integer 1-100
300 * @param {boolean} [options.progressive=false] - use progressive (interlace) scan
301 * @param {string} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling
302 * @param {boolean} [options.optimiseCoding=true] - optimise Huffman coding tables
303 * @param {boolean} [options.optimizeCoding=true] - alternative spelling of optimiseCoding
304 * @param {boolean} [options.mozjpeg=false] - use mozjpeg defaults, equivalent to `{ trellisQuantisation: true, overshootDeringing: true, optimiseScans: true, quantisationTable: 3 }`
305 * @param {boolean} [options.trellisQuantisation=false] - apply trellis quantisation
306 * @param {boolean} [options.overshootDeringing=false] - apply overshoot deringing
307 * @param {boolean} [options.optimiseScans=false] - optimise progressive scans, forces progressive
308 * @param {boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans
309 * @param {number} [options.quantisationTable=0] - quantization table to use, integer 0-8
310 * @param {number} [options.quantizationTable=0] - alternative spelling of quantisationTable
311 * @param {boolean} [options.force=true] - force JPEG output, otherwise attempt to use input format
312 * @returns {Sharp}
313 * @throws {Error} Invalid options
314 */
315function jpeg (options) {
316 if (is.object(options)) {
317 if (is.defined(options.quality)) {
318 if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
319 this.options.jpegQuality = options.quality;
320 } else {
321 throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
322 }
323 }
324 if (is.defined(options.progressive)) {
325 this._setBooleanOption('jpegProgressive', options.progressive);
326 }
327 if (is.defined(options.chromaSubsampling)) {
328 if (is.string(options.chromaSubsampling) && is.inArray(options.chromaSubsampling, ['4:2:0', '4:4:4'])) {
329 this.options.jpegChromaSubsampling = options.chromaSubsampling;
330 } else {
331 throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling);
332 }
333 }
334 const optimiseCoding = is.bool(options.optimizeCoding) ? options.optimizeCoding : options.optimiseCoding;
335 if (is.defined(optimiseCoding)) {
336 this._setBooleanOption('jpegOptimiseCoding', optimiseCoding);
337 }
338 if (is.defined(options.mozjpeg)) {
339 if (is.bool(options.mozjpeg)) {
340 if (options.mozjpeg) {
341 this.options.jpegTrellisQuantisation = true;
342 this.options.jpegOvershootDeringing = true;
343 this.options.jpegOptimiseScans = true;
344 this.options.jpegProgressive = true;
345 this.options.jpegQuantisationTable = 3;
346 }
347 } else {
348 throw is.invalidParameterError('mozjpeg', 'boolean', options.mozjpeg);
349 }
350 }
351 const trellisQuantisation = is.bool(options.trellisQuantization) ? options.trellisQuantization : options.trellisQuantisation;
352 if (is.defined(trellisQuantisation)) {
353 this._setBooleanOption('jpegTrellisQuantisation', trellisQuantisation);
354 }
355 if (is.defined(options.overshootDeringing)) {
356 this._setBooleanOption('jpegOvershootDeringing', options.overshootDeringing);
357 }
358 const optimiseScans = is.bool(options.optimizeScans) ? options.optimizeScans : options.optimiseScans;
359 if (is.defined(optimiseScans)) {
360 this._setBooleanOption('jpegOptimiseScans', optimiseScans);
361 if (optimiseScans) {
362 this.options.jpegProgressive = true;
363 }
364 }
365 const quantisationTable = is.number(options.quantizationTable) ? options.quantizationTable : options.quantisationTable;
366 if (is.defined(quantisationTable)) {
367 if (is.integer(quantisationTable) && is.inRange(quantisationTable, 0, 8)) {
368 this.options.jpegQuantisationTable = quantisationTable;
369 } else {
370 throw is.invalidParameterError('quantisationTable', 'integer between 0 and 8', quantisationTable);
371 }
372 }
373 }
374 return this._updateFormatOut('jpeg', options);
375}
376
377/**
378 * Use these PNG options for output image.
379 *
380 * By default, PNG output is full colour at 8 or 16 bits per pixel.
381 * Indexed PNG input at 1, 2 or 4 bits per pixel is converted to 8 bits per pixel.
382 * Set `palette` to `true` for slower, indexed PNG output.
383 *
384 * @example
385 * // Convert any input to full colour PNG output
386 * const data = await sharp(input)
387 * .png()
388 * .toBuffer();
389 *
390 * @example
391 * // Convert any input to indexed PNG output (slower)
392 * const data = await sharp(input)
393 * .png({ palette: true })
394 * .toBuffer();
395 *
396 * @param {Object} [options]
397 * @param {boolean} [options.progressive=false] - use progressive (interlace) scan
398 * @param {number} [options.compressionLevel=6] - zlib compression level, 0 (fastest, largest) to 9 (slowest, smallest)
399 * @param {boolean} [options.adaptiveFiltering=false] - use adaptive row filtering
400 * @param {boolean} [options.palette=false] - quantise to a palette-based image with alpha transparency support
401 * @param {number} [options.quality=100] - use the lowest number of colours needed to achieve given quality, sets `palette` to `true`
402 * @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest), sets `palette` to `true`
403 * @param {number} [options.colours=256] - maximum number of palette entries, sets `palette` to `true`
404 * @param {number} [options.colors=256] - alternative spelling of `options.colours`, sets `palette` to `true`
405 * @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, sets `palette` to `true`
406 * @param {boolean} [options.force=true] - force PNG output, otherwise attempt to use input format
407 * @returns {Sharp}
408 * @throws {Error} Invalid options
409 */
410function png (options) {
411 if (is.object(options)) {
412 if (is.defined(options.progressive)) {
413 this._setBooleanOption('pngProgressive', options.progressive);
414 }
415 if (is.defined(options.compressionLevel)) {
416 if (is.integer(options.compressionLevel) && is.inRange(options.compressionLevel, 0, 9)) {
417 this.options.pngCompressionLevel = options.compressionLevel;
418 } else {
419 throw is.invalidParameterError('compressionLevel', 'integer between 0 and 9', options.compressionLevel);
420 }
421 }
422 if (is.defined(options.adaptiveFiltering)) {
423 this._setBooleanOption('pngAdaptiveFiltering', options.adaptiveFiltering);
424 }
425 const colours = options.colours || options.colors;
426 if (is.defined(colours)) {
427 if (is.integer(colours) && is.inRange(colours, 2, 256)) {
428 this.options.pngBitdepth = bitdepthFromColourCount(colours);
429 } else {
430 throw is.invalidParameterError('colours', 'integer between 2 and 256', colours);
431 }
432 }
433 if (is.defined(options.palette)) {
434 this._setBooleanOption('pngPalette', options.palette);
435 } else if ([options.quality, options.effort, options.colours, options.colors, options.dither].some(is.defined)) {
436 this._setBooleanOption('pngPalette', true);
437 }
438 if (this.options.pngPalette) {
439 if (is.defined(options.quality)) {
440 if (is.integer(options.quality) && is.inRange(options.quality, 0, 100)) {
441 this.options.pngQuality = options.quality;
442 } else {
443 throw is.invalidParameterError('quality', 'integer between 0 and 100', options.quality);
444 }
445 }
446 if (is.defined(options.effort)) {
447 if (is.integer(options.effort) && is.inRange(options.effort, 1, 10)) {
448 this.options.pngEffort = options.effort;
449 } else {
450 throw is.invalidParameterError('effort', 'integer between 1 and 10', options.effort);
451 }
452 }
453 if (is.defined(options.dither)) {
454 if (is.number(options.dither) && is.inRange(options.dither, 0, 1)) {
455 this.options.pngDither = options.dither;
456 } else {
457 throw is.invalidParameterError('dither', 'number between 0.0 and 1.0', options.dither);
458 }
459 }
460 }
461 }
462 return this._updateFormatOut('png', options);
463}
464
465/**
466 * Use these WebP options for output image.
467 *
468 * @example
469 * // Convert any input to lossless WebP output
470 * const data = await sharp(input)
471 * .webp({ lossless: true })
472 * .toBuffer();
473 *
474 * @example
475 * // Optimise the file size of an animated WebP
476 * const outputWebp = await sharp(inputWebp, { animated: true })
477 * .webp({ effort: 6 })
478 * .toBuffer();
479 *
480 * @param {Object} [options] - output options
481 * @param {number} [options.quality=80] - quality, integer 1-100
482 * @param {number} [options.alphaQuality=100] - quality of alpha layer, integer 0-100
483 * @param {boolean} [options.lossless=false] - use lossless compression mode
484 * @param {boolean} [options.nearLossless=false] - use near_lossless compression mode
485 * @param {boolean} [options.smartSubsample=false] - use high quality chroma subsampling
486 * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 6 (slowest)
487 * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation
488 * @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds)
489 * @param {boolean} [options.minSize=false] - prevent use of animation key frames to minimise file size (slow)
490 * @param {boolean} [options.mixed=false] - allow mixture of lossy and lossless animation frames (slow)
491 * @param {boolean} [options.force=true] - force WebP output, otherwise attempt to use input format
492 * @returns {Sharp}
493 * @throws {Error} Invalid options
494 */
495function webp (options) {
496 if (is.object(options)) {
497 if (is.defined(options.quality)) {
498 if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
499 this.options.webpQuality = options.quality;
500 } else {
501 throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
502 }
503 }
504 if (is.defined(options.alphaQuality)) {
505 if (is.integer(options.alphaQuality) && is.inRange(options.alphaQuality, 0, 100)) {
506 this.options.webpAlphaQuality = options.alphaQuality;
507 } else {
508 throw is.invalidParameterError('alphaQuality', 'integer between 0 and 100', options.alphaQuality);
509 }
510 }
511 if (is.defined(options.lossless)) {
512 this._setBooleanOption('webpLossless', options.lossless);
513 }
514 if (is.defined(options.nearLossless)) {
515 this._setBooleanOption('webpNearLossless', options.nearLossless);
516 }
517 if (is.defined(options.smartSubsample)) {
518 this._setBooleanOption('webpSmartSubsample', options.smartSubsample);
519 }
520 if (is.defined(options.effort)) {
521 if (is.integer(options.effort) && is.inRange(options.effort, 0, 6)) {
522 this.options.webpEffort = options.effort;
523 } else {
524 throw is.invalidParameterError('effort', 'integer between 0 and 6', options.effort);
525 }
526 }
527 if (is.defined(options.minSize)) {
528 this._setBooleanOption('webpMinSize', options.minSize);
529 }
530 if (is.defined(options.mixed)) {
531 this._setBooleanOption('webpMixed', options.mixed);
532 }
533 }
534 trySetAnimationOptions(options, this.options);
535 return this._updateFormatOut('webp', options);
536}
537
538/**
539 * Use these GIF options for the output image.
540 *
541 * The first entry in the palette is reserved for transparency.
542 *
543 * The palette of the input image will be re-used if possible.
544 *
545 * @since 0.30.0
546 *
547 * @example
548 * // Convert PNG to GIF
549 * await sharp(pngBuffer)
550 * .gif()
551 * .toBuffer();
552 *
553 * @example
554 * // Convert animated WebP to animated GIF
555 * await sharp('animated.webp', { animated: true })
556 * .toFile('animated.gif');
557 *
558 * @example
559 * // Create a 128x128, cropped, non-dithered, animated thumbnail of an animated GIF
560 * const out = await sharp('in.gif', { animated: true })
561 * .resize({ width: 128, height: 128 })
562 * .gif({ dither: 0 })
563 * .toBuffer();
564 *
565 * @example
566 * // Lossy file size reduction of animated GIF
567 * await sharp('in.gif', { animated: true })
568 * .gif({ interFrameMaxError: 8 })
569 * .toFile('optim.gif');
570 *
571 * @param {Object} [options] - output options
572 * @param {boolean} [options.reuse=true] - re-use existing palette, otherwise generate new (slow)
573 * @param {boolean} [options.progressive=false] - use progressive (interlace) scan
574 * @param {number} [options.colours=256] - maximum number of palette entries, including transparency, between 2 and 256
575 * @param {number} [options.colors=256] - alternative spelling of `options.colours`
576 * @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest)
577 * @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, between 0 (least) and 1 (most)
578 * @param {number} [options.interFrameMaxError=0] - maximum inter-frame error for transparency, between 0 (lossless) and 32
579 * @param {number} [options.interPaletteMaxError=3] - maximum inter-palette error for palette reuse, between 0 and 256
580 * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation
581 * @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds)
582 * @param {boolean} [options.force=true] - force GIF output, otherwise attempt to use input format
583 * @returns {Sharp}
584 * @throws {Error} Invalid options
585 */
586function gif (options) {
587 if (is.object(options)) {
588 if (is.defined(options.reuse)) {
589 this._setBooleanOption('gifReuse', options.reuse);
590 }
591 if (is.defined(options.progressive)) {
592 this._setBooleanOption('gifProgressive', options.progressive);
593 }
594 const colours = options.colours || options.colors;
595 if (is.defined(colours)) {
596 if (is.integer(colours) && is.inRange(colours, 2, 256)) {
597 this.options.gifBitdepth = bitdepthFromColourCount(colours);
598 } else {
599 throw is.invalidParameterError('colours', 'integer between 2 and 256', colours);
600 }
601 }
602 if (is.defined(options.effort)) {
603 if (is.number(options.effort) && is.inRange(options.effort, 1, 10)) {
604 this.options.gifEffort = options.effort;
605 } else {
606 throw is.invalidParameterError('effort', 'integer between 1 and 10', options.effort);
607 }
608 }
609 if (is.defined(options.dither)) {
610 if (is.number(options.dither) && is.inRange(options.dither, 0, 1)) {
611 this.options.gifDither = options.dither;
612 } else {
613 throw is.invalidParameterError('dither', 'number between 0.0 and 1.0', options.dither);
614 }
615 }
616 if (is.defined(options.interFrameMaxError)) {
617 if (is.number(options.interFrameMaxError) && is.inRange(options.interFrameMaxError, 0, 32)) {
618 this.options.gifInterFrameMaxError = options.interFrameMaxError;
619 } else {
620 throw is.invalidParameterError('interFrameMaxError', 'number between 0.0 and 32.0', options.interFrameMaxError);
621 }
622 }
623 if (is.defined(options.interPaletteMaxError)) {
624 if (is.number(options.interPaletteMaxError) && is.inRange(options.interPaletteMaxError, 0, 256)) {
625 this.options.gifInterPaletteMaxError = options.interPaletteMaxError;
626 } else {
627 throw is.invalidParameterError('interPaletteMaxError', 'number between 0.0 and 256.0', options.interPaletteMaxError);
628 }
629 }
630 }
631 trySetAnimationOptions(options, this.options);
632 return this._updateFormatOut('gif', options);
633}
634
635/* istanbul ignore next */
636/**
637 * Use these JP2 options for output image.
638 *
639 * Requires libvips compiled with support for OpenJPEG.
640 * The prebuilt binaries do not include this - see
641 * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}.
642 *
643 * @example
644 * // Convert any input to lossless JP2 output
645 * const data = await sharp(input)
646 * .jp2({ lossless: true })
647 * .toBuffer();
648 *
649 * @example
650 * // Convert any input to very high quality JP2 output
651 * const data = await sharp(input)
652 * .jp2({
653 * quality: 100,
654 * chromaSubsampling: '4:4:4'
655 * })
656 * .toBuffer();
657 *
658 * @since 0.29.1
659 *
660 * @param {Object} [options] - output options
661 * @param {number} [options.quality=80] - quality, integer 1-100
662 * @param {boolean} [options.lossless=false] - use lossless compression mode
663 * @param {number} [options.tileWidth=512] - horizontal tile size
664 * @param {number} [options.tileHeight=512] - vertical tile size
665 * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
666 * @returns {Sharp}
667 * @throws {Error} Invalid options
668 */
669function jp2 (options) {
670 if (!this.constructor.format.jp2k.output.buffer) {
671 throw errJp2Save();
672 }
673 if (is.object(options)) {
674 if (is.defined(options.quality)) {
675 if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
676 this.options.jp2Quality = options.quality;
677 } else {
678 throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
679 }
680 }
681 if (is.defined(options.lossless)) {
682 if (is.bool(options.lossless)) {
683 this.options.jp2Lossless = options.lossless;
684 } else {
685 throw is.invalidParameterError('lossless', 'boolean', options.lossless);
686 }
687 }
688 if (is.defined(options.tileWidth)) {
689 if (is.integer(options.tileWidth) && is.inRange(options.tileWidth, 1, 32768)) {
690 this.options.jp2TileWidth = options.tileWidth;
691 } else {
692 throw is.invalidParameterError('tileWidth', 'integer between 1 and 32768', options.tileWidth);
693 }
694 }
695 if (is.defined(options.tileHeight)) {
696 if (is.integer(options.tileHeight) && is.inRange(options.tileHeight, 1, 32768)) {
697 this.options.jp2TileHeight = options.tileHeight;
698 } else {
699 throw is.invalidParameterError('tileHeight', 'integer between 1 and 32768', options.tileHeight);
700 }
701 }
702 if (is.defined(options.chromaSubsampling)) {
703 if (is.string(options.chromaSubsampling) && is.inArray(options.chromaSubsampling, ['4:2:0', '4:4:4'])) {
704 this.options.jp2ChromaSubsampling = options.chromaSubsampling;
705 } else {
706 throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling);
707 }
708 }
709 }
710 return this._updateFormatOut('jp2', options);
711}
712
713/**
714 * Set animation options if available.
715 * @private
716 *
717 * @param {Object} [source] - output options
718 * @param {number} [source.loop=0] - number of animation iterations, use 0 for infinite animation
719 * @param {number[]} [source.delay] - list of delays between animation frames (in milliseconds)
720 * @param {Object} [target] - target object for valid options
721 * @throws {Error} Invalid options
722 */
723function trySetAnimationOptions (source, target) {
724 if (is.object(source) && is.defined(source.loop)) {
725 if (is.integer(source.loop) && is.inRange(source.loop, 0, 65535)) {
726 target.loop = source.loop;
727 } else {
728 throw is.invalidParameterError('loop', 'integer between 0 and 65535', source.loop);
729 }
730 }
731 if (is.object(source) && is.defined(source.delay)) {
732 // We allow singular values as well
733 if (is.integer(source.delay) && is.inRange(source.delay, 0, 65535)) {
734 target.delay = [source.delay];
735 } else if (
736 Array.isArray(source.delay) &&
737 source.delay.every(is.integer) &&
738 source.delay.every(v => is.inRange(v, 0, 65535))) {
739 target.delay = source.delay;
740 } else {
741 throw is.invalidParameterError('delay', 'integer or an array of integers between 0 and 65535', source.delay);
742 }
743 }
744}
745
746/**
747 * Use these TIFF options for output image.
748 *
749 * The `density` can be set in pixels/inch via {@link #withmetadata|withMetadata}
750 * instead of providing `xres` and `yres` in pixels/mm.
751 *
752 * @example
753 * // Convert SVG input to LZW-compressed, 1 bit per pixel TIFF output
754 * sharp('input.svg')
755 * .tiff({
756 * compression: 'lzw',
757 * bitdepth: 1
758 * })
759 * .toFile('1-bpp-output.tiff')
760 * .then(info => { ... });
761 *
762 * @param {Object} [options] - output options
763 * @param {number} [options.quality=80] - quality, integer 1-100
764 * @param {boolean} [options.force=true] - force TIFF output, otherwise attempt to use input format
765 * @param {string} [options.compression='jpeg'] - compression options: none, jpeg, deflate, packbits, ccittfax4, lzw, webp, zstd, jp2k
766 * @param {string} [options.predictor='horizontal'] - compression predictor options: none, horizontal, float
767 * @param {boolean} [options.pyramid=false] - write an image pyramid
768 * @param {boolean} [options.tile=false] - write a tiled tiff
769 * @param {number} [options.tileWidth=256] - horizontal tile size
770 * @param {number} [options.tileHeight=256] - vertical tile size
771 * @param {number} [options.xres=1.0] - horizontal resolution in pixels/mm
772 * @param {number} [options.yres=1.0] - vertical resolution in pixels/mm
773 * @param {string} [options.resolutionUnit='inch'] - resolution unit options: inch, cm
774 * @param {number} [options.bitdepth=8] - reduce bitdepth to 1, 2 or 4 bit
775 * @returns {Sharp}
776 * @throws {Error} Invalid options
777 */
778function tiff (options) {
779 if (is.object(options)) {
780 if (is.defined(options.quality)) {
781 if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
782 this.options.tiffQuality = options.quality;
783 } else {
784 throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
785 }
786 }
787 if (is.defined(options.bitdepth)) {
788 if (is.integer(options.bitdepth) && is.inArray(options.bitdepth, [1, 2, 4, 8])) {
789 this.options.tiffBitdepth = options.bitdepth;
790 } else {
791 throw is.invalidParameterError('bitdepth', '1, 2, 4 or 8', options.bitdepth);
792 }
793 }
794 // tiling
795 if (is.defined(options.tile)) {
796 this._setBooleanOption('tiffTile', options.tile);
797 }
798 if (is.defined(options.tileWidth)) {
799 if (is.integer(options.tileWidth) && options.tileWidth > 0) {
800 this.options.tiffTileWidth = options.tileWidth;
801 } else {
802 throw is.invalidParameterError('tileWidth', 'integer greater than zero', options.tileWidth);
803 }
804 }
805 if (is.defined(options.tileHeight)) {
806 if (is.integer(options.tileHeight) && options.tileHeight > 0) {
807 this.options.tiffTileHeight = options.tileHeight;
808 } else {
809 throw is.invalidParameterError('tileHeight', 'integer greater than zero', options.tileHeight);
810 }
811 }
812 // pyramid
813 if (is.defined(options.pyramid)) {
814 this._setBooleanOption('tiffPyramid', options.pyramid);
815 }
816 // resolution
817 if (is.defined(options.xres)) {
818 if (is.number(options.xres) && options.xres > 0) {
819 this.options.tiffXres = options.xres;
820 } else {
821 throw is.invalidParameterError('xres', 'number greater than zero', options.xres);
822 }
823 }
824 if (is.defined(options.yres)) {
825 if (is.number(options.yres) && options.yres > 0) {
826 this.options.tiffYres = options.yres;
827 } else {
828 throw is.invalidParameterError('yres', 'number greater than zero', options.yres);
829 }
830 }
831 // compression
832 if (is.defined(options.compression)) {
833 if (is.string(options.compression) && is.inArray(options.compression, ['none', 'jpeg', 'deflate', 'packbits', 'ccittfax4', 'lzw', 'webp', 'zstd', 'jp2k'])) {
834 this.options.tiffCompression = options.compression;
835 } else {
836 throw is.invalidParameterError('compression', 'one of: none, jpeg, deflate, packbits, ccittfax4, lzw, webp, zstd, jp2k', options.compression);
837 }
838 }
839 // predictor
840 if (is.defined(options.predictor)) {
841 if (is.string(options.predictor) && is.inArray(options.predictor, ['none', 'horizontal', 'float'])) {
842 this.options.tiffPredictor = options.predictor;
843 } else {
844 throw is.invalidParameterError('predictor', 'one of: none, horizontal, float', options.predictor);
845 }
846 }
847 // resolutionUnit
848 if (is.defined(options.resolutionUnit)) {
849 if (is.string(options.resolutionUnit) && is.inArray(options.resolutionUnit, ['inch', 'cm'])) {
850 this.options.tiffResolutionUnit = options.resolutionUnit;
851 } else {
852 throw is.invalidParameterError('resolutionUnit', 'one of: inch, cm', options.resolutionUnit);
853 }
854 }
855 }
856 return this._updateFormatOut('tiff', options);
857}
858
859/**
860 * Use these AVIF options for output image.
861 *
862 * Whilst it is possible to create AVIF images smaller than 16x16 pixels,
863 * most web browsers do not display these properly.
864 *
865 * AVIF image sequences are not supported.
866 *
867 * @example
868 * const data = await sharp(input)
869 * .avif({ effort: 2 })
870 * .toBuffer();
871 *
872 * @example
873 * const data = await sharp(input)
874 * .avif({ lossless: true })
875 * .toBuffer();
876 *
877 * @since 0.27.0
878 *
879 * @param {Object} [options] - output options
880 * @param {number} [options.quality=50] - quality, integer 1-100
881 * @param {boolean} [options.lossless=false] - use lossless compression
882 * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest)
883 * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
884 * @returns {Sharp}
885 * @throws {Error} Invalid options
886 */
887function avif (options) {
888 return this.heif({ ...options, compression: 'av1' });
889}
890
891/**
892 * Use these HEIF options for output image.
893 *
894 * Support for patent-encumbered HEIC images using `hevc` compression requires the use of a
895 * globally-installed libvips compiled with support for libheif, libde265 and x265.
896 *
897 * @example
898 * const data = await sharp(input)
899 * .heif({ compression: 'hevc' })
900 * .toBuffer();
901 *
902 * @since 0.23.0
903 *
904 * @param {Object} [options] - output options
905 * @param {number} [options.quality=50] - quality, integer 1-100
906 * @param {string} [options.compression='av1'] - compression format: av1, hevc
907 * @param {boolean} [options.lossless=false] - use lossless compression
908 * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest)
909 * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
910 * @returns {Sharp}
911 * @throws {Error} Invalid options
912 */
913function heif (options) {
914 if (is.object(options)) {
915 if (is.defined(options.quality)) {
916 if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
917 this.options.heifQuality = options.quality;
918 } else {
919 throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
920 }
921 }
922 if (is.defined(options.lossless)) {
923 if (is.bool(options.lossless)) {
924 this.options.heifLossless = options.lossless;
925 } else {
926 throw is.invalidParameterError('lossless', 'boolean', options.lossless);
927 }
928 }
929 if (is.defined(options.compression)) {
930 if (is.string(options.compression) && is.inArray(options.compression, ['av1', 'hevc'])) {
931 this.options.heifCompression = options.compression;
932 } else {
933 throw is.invalidParameterError('compression', 'one of: av1, hevc', options.compression);
934 }
935 }
936 if (is.defined(options.effort)) {
937 if (is.integer(options.effort) && is.inRange(options.effort, 0, 9)) {
938 this.options.heifEffort = options.effort;
939 } else {
940 throw is.invalidParameterError('effort', 'integer between 0 and 9', options.effort);
941 }
942 }
943 if (is.defined(options.chromaSubsampling)) {
944 if (is.string(options.chromaSubsampling) && is.inArray(options.chromaSubsampling, ['4:2:0', '4:4:4'])) {
945 this.options.heifChromaSubsampling = options.chromaSubsampling;
946 } else {
947 throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling);
948 }
949 }
950 }
951 return this._updateFormatOut('heif', options);
952}
953
954/**
955 * Use these JPEG-XL (JXL) options for output image.
956 *
957 * This feature is experimental, please do not use in production systems.
958 *
959 * Requires libvips compiled with support for libjxl.
960 * The prebuilt binaries do not include this - see
961 * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}.
962 *
963 * Image metadata (EXIF, XMP) is unsupported.
964 *
965 * @since 0.31.3
966 *
967 * @param {Object} [options] - output options
968 * @param {number} [options.distance=1.0] - maximum encoding error, between 0 (highest quality) and 15 (lowest quality)
969 * @param {number} [options.quality] - calculate `distance` based on JPEG-like quality, between 1 and 100, overrides distance if specified
970 * @param {number} [options.decodingTier=0] - target decode speed tier, between 0 (highest quality) and 4 (lowest quality)
971 * @param {boolean} [options.lossless=false] - use lossless compression
972 * @param {number} [options.effort=7] - CPU effort, between 3 (fastest) and 9 (slowest)
973 * @returns {Sharp}
974 * @throws {Error} Invalid options
975 */
976function jxl (options) {
977 if (is.object(options)) {
978 if (is.defined(options.quality)) {
979 if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
980 // https://github.com/libjxl/libjxl/blob/0aeea7f180bafd6893c1db8072dcb67d2aa5b03d/tools/cjxl_main.cc#L640-L644
981 this.options.jxlDistance = options.quality >= 30
982 ? 0.1 + (100 - options.quality) * 0.09
983 : 53 / 3000 * options.quality * options.quality - 23 / 20 * options.quality + 25;
984 } else {
985 throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
986 }
987 } else if (is.defined(options.distance)) {
988 if (is.number(options.distance) && is.inRange(options.distance, 0, 15)) {
989 this.options.jxlDistance = options.distance;
990 } else {
991 throw is.invalidParameterError('distance', 'number between 0.0 and 15.0', options.distance);
992 }
993 }
994 if (is.defined(options.decodingTier)) {
995 if (is.integer(options.decodingTier) && is.inRange(options.decodingTier, 0, 4)) {
996 this.options.jxlDecodingTier = options.decodingTier;
997 } else {
998 throw is.invalidParameterError('decodingTier', 'integer between 0 and 4', options.decodingTier);
999 }
1000 }
1001 if (is.defined(options.lossless)) {
1002 if (is.bool(options.lossless)) {
1003 this.options.jxlLossless = options.lossless;
1004 } else {
1005 throw is.invalidParameterError('lossless', 'boolean', options.lossless);
1006 }
1007 }
1008 if (is.defined(options.effort)) {
1009 if (is.integer(options.effort) && is.inRange(options.effort, 3, 9)) {
1010 this.options.jxlEffort = options.effort;
1011 } else {
1012 throw is.invalidParameterError('effort', 'integer between 3 and 9', options.effort);
1013 }
1014 }
1015 }
1016 return this._updateFormatOut('jxl', options);
1017}
1018
1019/**
1020 * Force output to be raw, uncompressed pixel data.
1021 * Pixel ordering is left-to-right, top-to-bottom, without padding.
1022 * Channel ordering will be RGB or RGBA for non-greyscale colourspaces.
1023 *
1024 * @example
1025 * // Extract raw, unsigned 8-bit RGB pixel data from JPEG input
1026 * const { data, info } = await sharp('input.jpg')
1027 * .raw()
1028 * .toBuffer({ resolveWithObject: true });
1029 *
1030 * @example
1031 * // Extract alpha channel as raw, unsigned 16-bit pixel data from PNG input
1032 * const data = await sharp('input.png')
1033 * .ensureAlpha()
1034 * .extractChannel(3)
1035 * .toColourspace('b-w')
1036 * .raw({ depth: 'ushort' })
1037 * .toBuffer();
1038 *
1039 * @param {Object} [options] - output options
1040 * @param {string} [options.depth='uchar'] - bit depth, one of: char, uchar (default), short, ushort, int, uint, float, complex, double, dpcomplex
1041 * @throws {Error} Invalid options
1042 */
1043function raw (options) {
1044 if (is.object(options)) {
1045 if (is.defined(options.depth)) {
1046 if (is.string(options.depth) && is.inArray(options.depth,
1047 ['char', 'uchar', 'short', 'ushort', 'int', 'uint', 'float', 'complex', 'double', 'dpcomplex']
1048 )) {
1049 this.options.rawDepth = options.depth;
1050 } else {
1051 throw is.invalidParameterError('depth', 'one of: char, uchar, short, ushort, int, uint, float, complex, double, dpcomplex', options.depth);
1052 }
1053 }
1054 }
1055 return this._updateFormatOut('raw');
1056}
1057
1058/**
1059 * Use tile-based deep zoom (image pyramid) output.
1060 *
1061 * Set the format and options for tile images via the `toFormat`, `jpeg`, `png` or `webp` functions.
1062 * Use a `.zip` or `.szi` file extension with `toFile` to write to a compressed archive file format.
1063 *
1064 * The container will be set to `zip` when the output is a Buffer or Stream, otherwise it will default to `fs`.
1065 *
1066 * Requires libvips compiled with support for libgsf.
1067 * The prebuilt binaries do not include this - see
1068 * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}.
1069 *
1070 * @example
1071 * sharp('input.tiff')
1072 * .png()
1073 * .tile({
1074 * size: 512
1075 * })
1076 * .toFile('output.dz', function(err, info) {
1077 * // output.dzi is the Deep Zoom XML definition
1078 * // output_files contains 512x512 tiles grouped by zoom level
1079 * });
1080 *
1081 * @example
1082 * const zipFileWithTiles = await sharp(input)
1083 * .tile({ basename: "tiles" })
1084 * .toBuffer();
1085 *
1086 * @example
1087 * const iiififier = sharp().tile({ layout: "iiif" });
1088 * readableStream
1089 * .pipe(iiififier)
1090 * .pipe(writeableStream);
1091 *
1092 * @param {Object} [options]
1093 * @param {number} [options.size=256] tile size in pixels, a value between 1 and 8192.
1094 * @param {number} [options.overlap=0] tile overlap in pixels, a value between 0 and 8192.
1095 * @param {number} [options.angle=0] tile angle of rotation, must be a multiple of 90.
1096 * @param {string|Object} [options.background={r: 255, g: 255, b: 255, alpha: 1}] - background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to white without transparency.
1097 * @param {string} [options.depth] how deep to make the pyramid, possible values are `onepixel`, `onetile` or `one`, default based on layout.
1098 * @param {number} [options.skipBlanks=-1] threshold to skip tile generation, a value 0 - 255 for 8-bit images or 0 - 65535 for 16-bit images
1099 * @param {string} [options.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file).
1100 * @param {string} [options.layout='dz'] filesystem layout, possible values are `dz`, `iiif`, `iiif3`, `zoomify` or `google`.
1101 * @param {boolean} [options.centre=false] centre image in tile.
1102 * @param {boolean} [options.center=false] alternative spelling of centre.
1103 * @param {string} [options.id='https://example.com/iiif'] when `layout` is `iiif`/`iiif3`, sets the `@id`/`id` attribute of `info.json`
1104 * @param {string} [options.basename] the name of the directory within the zip file when container is `zip`.
1105 * @returns {Sharp}
1106 * @throws {Error} Invalid parameters
1107 */
1108function tile (options) {
1109 if (is.object(options)) {
1110 // Size of square tiles, in pixels
1111 if (is.defined(options.size)) {
1112 if (is.integer(options.size) && is.inRange(options.size, 1, 8192)) {
1113 this.options.tileSize = options.size;
1114 } else {
1115 throw is.invalidParameterError('size', 'integer between 1 and 8192', options.size);
1116 }
1117 }
1118 // Overlap of tiles, in pixels
1119 if (is.defined(options.overlap)) {
1120 if (is.integer(options.overlap) && is.inRange(options.overlap, 0, 8192)) {
1121 if (options.overlap > this.options.tileSize) {
1122 throw is.invalidParameterError('overlap', `<= size (${this.options.tileSize})`, options.overlap);
1123 }
1124 this.options.tileOverlap = options.overlap;
1125 } else {
1126 throw is.invalidParameterError('overlap', 'integer between 0 and 8192', options.overlap);
1127 }
1128 }
1129 // Container
1130 if (is.defined(options.container)) {
1131 if (is.string(options.container) && is.inArray(options.container, ['fs', 'zip'])) {
1132 this.options.tileContainer = options.container;
1133 } else {
1134 throw is.invalidParameterError('container', 'one of: fs, zip', options.container);
1135 }
1136 }
1137 // Layout
1138 if (is.defined(options.layout)) {
1139 if (is.string(options.layout) && is.inArray(options.layout, ['dz', 'google', 'iiif', 'iiif3', 'zoomify'])) {
1140 this.options.tileLayout = options.layout;
1141 } else {
1142 throw is.invalidParameterError('layout', 'one of: dz, google, iiif, iiif3, zoomify', options.layout);
1143 }
1144 }
1145 // Angle of rotation,
1146 if (is.defined(options.angle)) {
1147 if (is.integer(options.angle) && !(options.angle % 90)) {
1148 this.options.tileAngle = options.angle;
1149 } else {
1150 throw is.invalidParameterError('angle', 'positive/negative multiple of 90', options.angle);
1151 }
1152 }
1153 // Background colour
1154 this._setBackgroundColourOption('tileBackground', options.background);
1155 // Depth of tiles
1156 if (is.defined(options.depth)) {
1157 if (is.string(options.depth) && is.inArray(options.depth, ['onepixel', 'onetile', 'one'])) {
1158 this.options.tileDepth = options.depth;
1159 } else {
1160 throw is.invalidParameterError('depth', 'one of: onepixel, onetile, one', options.depth);
1161 }
1162 }
1163 // Threshold to skip blank tiles
1164 if (is.defined(options.skipBlanks)) {
1165 if (is.integer(options.skipBlanks) && is.inRange(options.skipBlanks, -1, 65535)) {
1166 this.options.tileSkipBlanks = options.skipBlanks;
1167 } else {
1168 throw is.invalidParameterError('skipBlanks', 'integer between -1 and 255/65535', options.skipBlanks);
1169 }
1170 } else if (is.defined(options.layout) && options.layout === 'google') {
1171 this.options.tileSkipBlanks = 5;
1172 }
1173 // Center image in tile
1174 const centre = is.bool(options.center) ? options.center : options.centre;
1175 if (is.defined(centre)) {
1176 this._setBooleanOption('tileCentre', centre);
1177 }
1178 // @id attribute for IIIF layout
1179 if (is.defined(options.id)) {
1180 if (is.string(options.id)) {
1181 this.options.tileId = options.id;
1182 } else {
1183 throw is.invalidParameterError('id', 'string', options.id);
1184 }
1185 }
1186 // Basename for zip container
1187 if (is.defined(options.basename)) {
1188 if (is.string(options.basename)) {
1189 this.options.tileBasename = options.basename;
1190 } else {
1191 throw is.invalidParameterError('basename', 'string', options.basename);
1192 }
1193 }
1194 }
1195 // Format
1196 if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
1197 this.options.tileFormat = this.options.formatOut;
1198 } else if (this.options.formatOut !== 'input') {
1199 throw is.invalidParameterError('format', 'one of: jpeg, png, webp', this.options.formatOut);
1200 }
1201 return this._updateFormatOut('dz');
1202}
1203
1204/**
1205 * Set a timeout for processing, in seconds.
1206 * Use a value of zero to continue processing indefinitely, the default behaviour.
1207 *
1208 * The clock starts when libvips opens an input image for processing.
1209 * Time spent waiting for a libuv thread to become available is not included.
1210 *
1211 * @example
1212 * // Ensure processing takes no longer than 3 seconds
1213 * try {
1214 * const data = await sharp(input)
1215 * .blur(1000)
1216 * .timeout({ seconds: 3 })
1217 * .toBuffer();
1218 * } catch (err) {
1219 * if (err.message.includes('timeout')) { ... }
1220 * }
1221 *
1222 * @since 0.29.2
1223 *
1224 * @param {Object} options
1225 * @param {number} options.seconds - Number of seconds after which processing will be stopped
1226 * @returns {Sharp}
1227 */
1228function timeout (options) {
1229 if (!is.plainObject(options)) {
1230 throw is.invalidParameterError('options', 'object', options);
1231 }
1232 if (is.integer(options.seconds) && is.inRange(options.seconds, 0, 3600)) {
1233 this.options.timeoutSeconds = options.seconds;
1234 } else {
1235 throw is.invalidParameterError('seconds', 'integer between 0 and 3600', options.seconds);
1236 }
1237 return this;
1238}
1239
1240/**
1241 * Update the output format unless options.force is false,
1242 * in which case revert to input format.
1243 * @private
1244 * @param {string} formatOut
1245 * @param {Object} [options]
1246 * @param {boolean} [options.force=true] - force output format, otherwise attempt to use input format
1247 * @returns {Sharp}
1248 */
1249function _updateFormatOut (formatOut, options) {
1250 if (!(is.object(options) && options.force === false)) {
1251 this.options.formatOut = formatOut;
1252 }
1253 return this;
1254}
1255
1256/**
1257 * Update a boolean attribute of the this.options Object.
1258 * @private
1259 * @param {string} key
1260 * @param {boolean} val
1261 * @throws {Error} Invalid key
1262 */
1263function _setBooleanOption (key, val) {
1264 if (is.bool(val)) {
1265 this.options[key] = val;
1266 } else {
1267 throw is.invalidParameterError(key, 'boolean', val);
1268 }
1269}
1270
1271/**
1272 * Called by a WriteableStream to notify us it is ready for data.
1273 * @private
1274 */
1275function _read () {
1276 /* istanbul ignore else */
1277 if (!this.options.streamOut) {
1278 this.options.streamOut = true;
1279 this._pipeline();
1280 }
1281}
1282
1283/**
1284 * Invoke the C++ image processing pipeline
1285 * Supports callback, stream and promise variants
1286 * @private
1287 */
1288function _pipeline (callback) {
1289 if (typeof callback === 'function') {
1290 // output=file/buffer
1291 if (this._isStreamInput()) {
1292 // output=file/buffer, input=stream
1293 this.on('finish', () => {
1294 this._flattenBufferIn();
1295 sharp.pipeline(this.options, callback);
1296 });
1297 } else {
1298 // output=file/buffer, input=file/buffer
1299 sharp.pipeline(this.options, callback);
1300 }
1301 return this;
1302 } else if (this.options.streamOut) {
1303 // output=stream
1304 if (this._isStreamInput()) {
1305 // output=stream, input=stream
1306 this.once('finish', () => {
1307 this._flattenBufferIn();
1308 sharp.pipeline(this.options, (err, data, info) => {
1309 if (err) {
1310 this.emit('error', err);
1311 } else {
1312 this.emit('info', info);
1313 this.push(data);
1314 }
1315 this.push(null);
1316 this.on('end', () => this.emit('close'));
1317 });
1318 });
1319 if (this.streamInFinished) {
1320 this.emit('finish');
1321 }
1322 } else {
1323 // output=stream, input=file/buffer
1324 sharp.pipeline(this.options, (err, data, info) => {
1325 if (err) {
1326 this.emit('error', err);
1327 } else {
1328 this.emit('info', info);
1329 this.push(data);
1330 }
1331 this.push(null);
1332 this.on('end', () => this.emit('close'));
1333 });
1334 }
1335 return this;
1336 } else {
1337 // output=promise
1338 if (this._isStreamInput()) {
1339 // output=promise, input=stream
1340 return new Promise((resolve, reject) => {
1341 this.once('finish', () => {
1342 this._flattenBufferIn();
1343 sharp.pipeline(this.options, (err, data, info) => {
1344 if (err) {
1345 reject(err);
1346 } else {
1347 if (this.options.resolveWithObject) {
1348 resolve({ data, info });
1349 } else {
1350 resolve(data);
1351 }
1352 }
1353 });
1354 });
1355 });
1356 } else {
1357 // output=promise, input=file/buffer
1358 return new Promise((resolve, reject) => {
1359 sharp.pipeline(this.options, (err, data, info) => {
1360 if (err) {
1361 reject(err);
1362 } else {
1363 if (this.options.resolveWithObject) {
1364 resolve({ data: data, info: info });
1365 } else {
1366 resolve(data);
1367 }
1368 }
1369 });
1370 });
1371 }
1372 }
1373}
1374
1375/**
1376 * Decorate the Sharp prototype with output-related functions.
1377 * @private
1378 */
1379module.exports = function (Sharp) {
1380 Object.assign(Sharp.prototype, {
1381 // Public
1382 toFile,
1383 toBuffer,
1384 withMetadata,
1385 toFormat,
1386 jpeg,
1387 jp2,
1388 png,
1389 webp,
1390 tiff,
1391 avif,
1392 heif,
1393 jxl,
1394 gif,
1395 raw,
1396 tile,
1397 timeout,
1398 // Private
1399 _updateFormatOut,
1400 _setBooleanOption,
1401 _read,
1402 _pipeline
1403 });
1404};