UNPKG

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