sharp 0.27.1 → 0.30.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/output.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('path');
3
4
  const is = require('./is');
4
- const sharp = require('../build/Release/sharp.node');
5
+ const sharp = require('./sharp');
5
6
 
6
7
  const formats = new Map([
7
8
  ['heic', 'heif'],
@@ -12,20 +13,31 @@ const formats = new Map([
12
13
  ['png', 'png'],
13
14
  ['raw', 'raw'],
14
15
  ['tiff', 'tiff'],
16
+ ['tif', 'tiff'],
15
17
  ['webp', 'webp'],
16
- ['gif', 'gif']
18
+ ['gif', 'gif'],
19
+ ['jp2', 'jp2'],
20
+ ['jpx', 'jp2'],
21
+ ['j2k', 'jp2'],
22
+ ['j2c', 'jp2']
17
23
  ]);
18
24
 
25
+ const errJp2Save = new Error('JP2 output requires libvips with support for OpenJPEG');
26
+
27
+ const bitdepthFromColourCount = (colours) => 1 << 31 - Math.clz32(Math.ceil(Math.log2(colours)));
28
+
19
29
  /**
20
30
  * Write output image data to a file.
21
31
  *
22
32
  * If an explicit output format is not selected, it will be inferred from the extension,
23
- * with JPEG, PNG, WebP, AVIF, TIFF, DZI, and libvips' V format supported.
33
+ * with JPEG, PNG, WebP, AVIF, TIFF, GIF, DZI, and libvips' V format supported.
24
34
  * Note that raw pixel data is only supported for buffer output.
25
35
  *
26
36
  * By default all metadata will be removed, which includes EXIF-based orientation.
27
37
  * See {@link withMetadata} for control over this.
28
38
  *
39
+ * The caller is responsible for ensuring directory structures and permissions exist.
40
+ *
29
41
  * A `Promise` is returned when `callback` is not provided.
30
42
  *
31
43
  * @example
@@ -47,34 +59,32 @@ const formats = new Map([
47
59
  * @throws {Error} Invalid parameters
48
60
  */
49
61
  function toFile (fileOut, callback) {
50
- if (!fileOut || fileOut.length === 0) {
51
- const errOutputInvalid = new Error('Missing output file path');
62
+ let err;
63
+ if (!is.string(fileOut)) {
64
+ err = new Error('Missing output file path');
65
+ } else if (is.string(this.options.input.file) && path.resolve(this.options.input.file) === path.resolve(fileOut)) {
66
+ err = new Error('Cannot use same file for input and output');
67
+ }
68
+ if (err) {
52
69
  if (is.fn(callback)) {
53
- callback(errOutputInvalid);
70
+ callback(err);
54
71
  } else {
55
- return Promise.reject(errOutputInvalid);
72
+ return Promise.reject(err);
56
73
  }
57
74
  } else {
58
- if (this.options.input.file === fileOut) {
59
- const errOutputIsInput = new Error('Cannot use same file for input and output');
60
- if (is.fn(callback)) {
61
- callback(errOutputIsInput);
62
- } else {
63
- return Promise.reject(errOutputIsInput);
64
- }
65
- } else {
66
- this.options.fileOut = fileOut;
67
- return this._pipeline(callback);
68
- }
75
+ this.options.fileOut = fileOut;
76
+ return this._pipeline(callback);
69
77
  }
70
78
  return this;
71
79
  }
72
80
 
73
81
  /**
74
82
  * Write output to a Buffer.
75
- * JPEG, PNG, WebP, AVIF, TIFF and raw pixel data output are supported.
83
+ * JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.
84
+ *
85
+ * Use {@link toFormat} or one of the format-specific functions such as {@link jpeg}, {@link png} etc. to set the output format.
76
86
  *
77
- * If no explicit format is set, the output format will match the input image, except GIF and SVG input which become PNG output.
87
+ * If no explicit format is set, the output format will match the input image, except SVG input which becomes PNG output.
78
88
  *
79
89
  * By default all metadata will be removed, which includes EXIF-based orientation.
80
90
  * See {@link withMetadata} for control over this.
@@ -100,15 +110,16 @@ function toFile (fileOut, callback) {
100
110
  *
101
111
  * @example
102
112
  * sharp(input)
113
+ * .png()
103
114
  * .toBuffer({ resolveWithObject: true })
104
115
  * .then(({ data, info }) => { ... })
105
116
  * .catch(err => { ... });
106
117
  *
107
118
  * @example
108
- * const data = await sharp('my-image.jpg')
119
+ * const { data, info } = await sharp('my-image.jpg')
109
120
  * // output the raw pixels
110
121
  * .raw()
111
- * .toBuffer();
122
+ * .toBuffer({ resolveWithObject: true });
112
123
  *
113
124
  * // create a more type safe way to work with the raw pixel data
114
125
  * // this will not copy the data, instead it will change `data`s underlying ArrayBuffer
@@ -116,7 +127,9 @@ function toFile (fileOut, callback) {
116
127
  * const pixelArray = new Uint8ClampedArray(data.buffer);
117
128
  *
118
129
  * // When you are done changing the pixelArray, sharp takes the `pixelArray` as an input
119
- * await sharp(pixelArray).toFile('my-changed-image.jpg');
130
+ * const { width, height, channels } = info;
131
+ * await sharp(pixelArray, { raw: { width, height, channels } })
132
+ * .toFile('my-changed-image.jpg');
120
133
  *
121
134
  * @param {Object} [options]
122
135
  * @param {boolean} [options.resolveWithObject] Resolve the Promise with an Object containing `data` and `info` properties instead of resolving only with `data`.
@@ -129,6 +142,7 @@ function toBuffer (options, callback) {
129
142
  } else if (this.options.resolveWithObject) {
130
143
  this.options.resolveWithObject = false;
131
144
  }
145
+ this.options.fileOut = '';
132
146
  return this._pipeline(is.fn(options) ? options : callback);
133
147
  }
134
148
 
@@ -140,15 +154,37 @@ function toBuffer (options, callback) {
140
154
  * The default behaviour, when `withMetadata` is not used, is to convert to the device-independent
141
155
  * sRGB colour space and strip all metadata, including the removal of any ICC profile.
142
156
  *
157
+ * EXIF metadata is unsupported for TIFF output.
158
+ *
143
159
  * @example
144
160
  * sharp('input.jpg')
145
161
  * .withMetadata()
146
162
  * .toFile('output-with-metadata.jpg')
147
163
  * .then(info => { ... });
148
164
  *
165
+ * @example
166
+ * // Set "IFD0-Copyright" in output EXIF metadata
167
+ * const data = await sharp(input)
168
+ * .withMetadata({
169
+ * exif: {
170
+ * IFD0: {
171
+ * Copyright: 'Wernham Hogg'
172
+ * }
173
+ * }
174
+ * })
175
+ * .toBuffer();
176
+ *
177
+ * @example
178
+ * // Set output metadata to 96 DPI
179
+ * const data = await sharp(input)
180
+ * .withMetadata({ density: 96 })
181
+ * .toBuffer();
182
+ *
149
183
  * @param {Object} [options]
150
184
  * @param {number} [options.orientation] value between 1 and 8, used to update the EXIF `Orientation` tag.
151
185
  * @param {string} [options.icc] filesystem path to output ICC profile, defaults to sRGB.
186
+ * @param {Object<Object>} [options.exif={}] Object keyed by IFD0, IFD1 etc. of key/value string pairs to write as EXIF data.
187
+ * @param {number} [options.density] Number of pixels per inch (DPI).
152
188
  * @returns {Sharp}
153
189
  * @throws {Error} Invalid parameters
154
190
  */
@@ -162,6 +198,13 @@ function withMetadata (options) {
162
198
  throw is.invalidParameterError('orientation', 'integer between 1 and 8', options.orientation);
163
199
  }
164
200
  }
201
+ if (is.defined(options.density)) {
202
+ if (is.number(options.density) && options.density > 0) {
203
+ this.options.withMetadataDensity = options.density;
204
+ } else {
205
+ throw is.invalidParameterError('density', 'positive number', options.density);
206
+ }
207
+ }
165
208
  if (is.defined(options.icc)) {
166
209
  if (is.string(options.icc)) {
167
210
  this.options.withMetadataIcc = options.icc;
@@ -169,6 +212,25 @@ function withMetadata (options) {
169
212
  throw is.invalidParameterError('icc', 'string filesystem path to ICC profile', options.icc);
170
213
  }
171
214
  }
215
+ if (is.defined(options.exif)) {
216
+ if (is.object(options.exif)) {
217
+ for (const [ifd, entries] of Object.entries(options.exif)) {
218
+ if (is.object(entries)) {
219
+ for (const [k, v] of Object.entries(entries)) {
220
+ if (is.string(v)) {
221
+ this.options.withMetadataStrs[`exif-${ifd.toLowerCase()}-${k}`] = v;
222
+ } else {
223
+ throw is.invalidParameterError(`exif.${ifd}.${k}`, 'string', v);
224
+ }
225
+ }
226
+ } else {
227
+ throw is.invalidParameterError(`exif.${ifd}`, 'object', entries);
228
+ }
229
+ }
230
+ } else {
231
+ throw is.invalidParameterError('exif', 'object', options.exif);
232
+ }
233
+ }
172
234
  }
173
235
  return this;
174
236
  }
@@ -188,7 +250,7 @@ function withMetadata (options) {
188
250
  * @throws {Error} unsupported format or options
189
251
  */
190
252
  function toFormat (format, options) {
191
- const actualFormat = formats.get(is.object(format) && is.string(format.id) ? format.id : format);
253
+ const actualFormat = formats.get((is.object(format) && is.string(format.id) ? format.id : format).toLowerCase());
192
254
  if (!actualFormat) {
193
255
  throw is.invalidParameterError('format', `one of: ${[...formats.keys()].join(', ')}`, format);
194
256
  }
@@ -198,8 +260,6 @@ function toFormat (format, options) {
198
260
  /**
199
261
  * Use these JPEG options for output image.
200
262
  *
201
- * Some of these options require the use of a globally-installed libvips compiled with support for mozjpeg.
202
- *
203
263
  * @example
204
264
  * // Convert any input to very high quality JPEG output
205
265
  * const data = await sharp(input)
@@ -209,18 +269,25 @@ function toFormat (format, options) {
209
269
  * })
210
270
  * .toBuffer();
211
271
  *
272
+ * @example
273
+ * // Use mozjpeg to reduce output JPEG file size (slower)
274
+ * const data = await sharp(input)
275
+ * .jpeg({ mozjpeg: true })
276
+ * .toBuffer();
277
+ *
212
278
  * @param {Object} [options] - output options
213
279
  * @param {number} [options.quality=80] - quality, integer 1-100
214
280
  * @param {boolean} [options.progressive=false] - use progressive (interlace) scan
215
281
  * @param {string} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling
216
282
  * @param {boolean} [options.optimiseCoding=true] - optimise Huffman coding tables
217
283
  * @param {boolean} [options.optimizeCoding=true] - alternative spelling of optimiseCoding
218
- * @param {boolean} [options.trellisQuantisation=false] - apply trellis quantisation, requires libvips compiled with support for mozjpeg
219
- * @param {boolean} [options.overshootDeringing=false] - apply overshoot deringing, requires libvips compiled with support for mozjpeg
220
- * @param {boolean} [options.optimiseScans=false] - optimise progressive scans, forces progressive, requires libvips compiled with support for mozjpeg
221
- * @param {boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans, requires libvips compiled with support for mozjpeg
222
- * @param {number} [options.quantisationTable=0] - quantization table to use, integer 0-8, requires libvips compiled with support for mozjpeg
223
- * @param {number} [options.quantizationTable=0] - alternative spelling of quantisationTable, requires libvips compiled with support for mozjpeg
284
+ * @param {boolean} [options.mozjpeg=false] - use mozjpeg defaults, equivalent to `{ trellisQuantisation: true, overshootDeringing: true, optimiseScans: true, quantisationTable: 3 }`
285
+ * @param {boolean} [options.trellisQuantisation=false] - apply trellis quantisation
286
+ * @param {boolean} [options.overshootDeringing=false] - apply overshoot deringing
287
+ * @param {boolean} [options.optimiseScans=false] - optimise progressive scans, forces progressive
288
+ * @param {boolean} [options.optimizeScans=false] - alternative spelling of optimiseScans
289
+ * @param {number} [options.quantisationTable=0] - quantization table to use, integer 0-8
290
+ * @param {number} [options.quantizationTable=0] - alternative spelling of quantisationTable
224
291
  * @param {boolean} [options.force=true] - force JPEG output, otherwise attempt to use input format
225
292
  * @returns {Sharp}
226
293
  * @throws {Error} Invalid options
@@ -244,6 +311,23 @@ function jpeg (options) {
244
311
  throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling);
245
312
  }
246
313
  }
314
+ const optimiseCoding = is.bool(options.optimizeCoding) ? options.optimizeCoding : options.optimiseCoding;
315
+ if (is.defined(optimiseCoding)) {
316
+ this._setBooleanOption('jpegOptimiseCoding', optimiseCoding);
317
+ }
318
+ if (is.defined(options.mozjpeg)) {
319
+ if (is.bool(options.mozjpeg)) {
320
+ if (options.mozjpeg) {
321
+ this.options.jpegTrellisQuantisation = true;
322
+ this.options.jpegOvershootDeringing = true;
323
+ this.options.jpegOptimiseScans = true;
324
+ this.options.jpegProgressive = true;
325
+ this.options.jpegQuantisationTable = 3;
326
+ }
327
+ } else {
328
+ throw is.invalidParameterError('mozjpeg', 'boolean', options.mozjpeg);
329
+ }
330
+ }
247
331
  const trellisQuantisation = is.bool(options.trellisQuantization) ? options.trellisQuantization : options.trellisQuantisation;
248
332
  if (is.defined(trellisQuantisation)) {
249
333
  this._setBooleanOption('jpegTrellisQuantisation', trellisQuantisation);
@@ -258,10 +342,6 @@ function jpeg (options) {
258
342
  this.options.jpegProgressive = true;
259
343
  }
260
344
  }
261
- const optimiseCoding = is.bool(options.optimizeCoding) ? options.optimizeCoding : options.optimiseCoding;
262
- if (is.defined(optimiseCoding)) {
263
- this._setBooleanOption('jpegOptimiseCoding', optimiseCoding);
264
- }
265
345
  const quantisationTable = is.number(options.quantizationTable) ? options.quantizationTable : options.quantisationTable;
266
346
  if (is.defined(quantisationTable)) {
267
347
  if (is.integer(quantisationTable) && is.inRange(quantisationTable, 0, 8)) {
@@ -277,26 +357,32 @@ function jpeg (options) {
277
357
  /**
278
358
  * Use these PNG options for output image.
279
359
  *
280
- * PNG output is always full colour at 8 or 16 bits per pixel.
360
+ * By default, PNG output is full colour at 8 or 16 bits per pixel.
281
361
  * Indexed PNG input at 1, 2 or 4 bits per pixel is converted to 8 bits per pixel.
282
- *
283
- * Some of these options require the use of a globally-installed libvips compiled with support for libimagequant (GPL).
362
+ * Set `palette` to `true` for slower, indexed PNG output.
284
363
  *
285
364
  * @example
286
- * // Convert any input to PNG output
365
+ * // Convert any input to full colour PNG output
287
366
  * const data = await sharp(input)
288
367
  * .png()
289
368
  * .toBuffer();
290
369
  *
370
+ * @example
371
+ * // Convert any input to indexed PNG output (slower)
372
+ * const data = await sharp(input)
373
+ * .png({ palette: true })
374
+ * .toBuffer();
375
+ *
291
376
  * @param {Object} [options]
292
377
  * @param {boolean} [options.progressive=false] - use progressive (interlace) scan
293
- * @param {number} [options.compressionLevel=9] - zlib compression level, 0-9
378
+ * @param {number} [options.compressionLevel=6] - zlib compression level, 0 (fastest, largest) to 9 (slowest, smallest)
294
379
  * @param {boolean} [options.adaptiveFiltering=false] - use adaptive row filtering
295
- * @param {boolean} [options.palette=false] - quantise to a palette-based image with alpha transparency support, requires libvips compiled with support for libimagequant
296
- * @param {number} [options.quality=100] - use the lowest number of colours needed to achieve given quality, sets `palette` to `true`, requires libvips compiled with support for libimagequant
297
- * @param {number} [options.colours=256] - maximum number of palette entries, sets `palette` to `true`, requires libvips compiled with support for libimagequant
298
- * @param {number} [options.colors=256] - alternative spelling of `options.colours`, sets `palette` to `true`, requires libvips compiled with support for libimagequant
299
- * @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, sets `palette` to `true`, requires libvips compiled with support for libimagequant
380
+ * @param {boolean} [options.palette=false] - quantise to a palette-based image with alpha transparency support
381
+ * @param {number} [options.quality=100] - use the lowest number of colours needed to achieve given quality, sets `palette` to `true`
382
+ * @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest), sets `palette` to `true`
383
+ * @param {number} [options.colours=256] - maximum number of palette entries, sets `palette` to `true`
384
+ * @param {number} [options.colors=256] - alternative spelling of `options.colours`, sets `palette` to `true`
385
+ * @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, sets `palette` to `true`
300
386
  * @param {boolean} [options.force=true] - force PNG output, otherwise attempt to use input format
301
387
  * @returns {Sharp}
302
388
  * @throws {Error} Invalid options
@@ -318,7 +404,7 @@ function png (options) {
318
404
  }
319
405
  if (is.defined(options.palette)) {
320
406
  this._setBooleanOption('pngPalette', options.palette);
321
- } else if (is.defined(options.quality) || is.defined(options.colours || options.colors) || is.defined(options.dither)) {
407
+ } else if ([options.quality, options.effort, options.colours, options.colors, options.dither].some(is.defined)) {
322
408
  this._setBooleanOption('pngPalette', true);
323
409
  }
324
410
  if (this.options.pngPalette) {
@@ -329,10 +415,17 @@ function png (options) {
329
415
  throw is.invalidParameterError('quality', 'integer between 0 and 100', options.quality);
330
416
  }
331
417
  }
418
+ if (is.defined(options.effort)) {
419
+ if (is.integer(options.effort) && is.inRange(options.effort, 1, 10)) {
420
+ this.options.pngEffort = options.effort;
421
+ } else {
422
+ throw is.invalidParameterError('effort', 'integer between 1 and 10', options.effort);
423
+ }
424
+ }
332
425
  const colours = options.colours || options.colors;
333
426
  if (is.defined(colours)) {
334
427
  if (is.integer(colours) && is.inRange(colours, 2, 256)) {
335
- this.options.pngColours = colours;
428
+ this.options.pngBitdepth = bitdepthFromColourCount(colours);
336
429
  } else {
337
430
  throw is.invalidParameterError('colours', 'integer between 2 and 256', colours);
338
431
  }
@@ -358,99 +451,217 @@ function png (options) {
358
451
  * .webp({ lossless: true })
359
452
  * .toBuffer();
360
453
  *
454
+ * @example
455
+ * // Optimise the file size of an animated WebP
456
+ * const outputWebp = await sharp(inputWebp, { animated: true })
457
+ * .webp({ effort: 6 })
458
+ * .toBuffer();
459
+ *
361
460
  * @param {Object} [options] - output options
362
461
  * @param {number} [options.quality=80] - quality, integer 1-100
363
462
  * @param {number} [options.alphaQuality=100] - quality of alpha layer, integer 0-100
364
463
  * @param {boolean} [options.lossless=false] - use lossless compression mode
365
464
  * @param {boolean} [options.nearLossless=false] - use near_lossless compression mode
366
465
  * @param {boolean} [options.smartSubsample=false] - use high quality chroma subsampling
367
- * @param {number} [options.reductionEffort=4] - level of CPU effort to reduce file size, integer 0-6
368
- * @param {number} [options.pageHeight] - page height for animated output
466
+ * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 6 (slowest)
369
467
  * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation
370
- * @param {number[]} [options.delay] - list of delays between animation frames (in milliseconds)
468
+ * @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds)
371
469
  * @param {boolean} [options.force=true] - force WebP output, otherwise attempt to use input format
372
470
  * @returns {Sharp}
373
471
  * @throws {Error} Invalid options
374
472
  */
375
473
  function webp (options) {
376
- if (is.object(options) && is.defined(options.quality)) {
377
- if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
378
- this.options.webpQuality = options.quality;
379
- } else {
380
- throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
474
+ if (is.object(options)) {
475
+ if (is.defined(options.quality)) {
476
+ if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
477
+ this.options.webpQuality = options.quality;
478
+ } else {
479
+ throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
480
+ }
381
481
  }
382
- }
383
- if (is.object(options) && is.defined(options.alphaQuality)) {
384
- if (is.integer(options.alphaQuality) && is.inRange(options.alphaQuality, 0, 100)) {
385
- this.options.webpAlphaQuality = options.alphaQuality;
386
- } else {
387
- throw is.invalidParameterError('alphaQuality', 'integer between 0 and 100', options.alphaQuality);
482
+ if (is.defined(options.alphaQuality)) {
483
+ if (is.integer(options.alphaQuality) && is.inRange(options.alphaQuality, 0, 100)) {
484
+ this.options.webpAlphaQuality = options.alphaQuality;
485
+ } else {
486
+ throw is.invalidParameterError('alphaQuality', 'integer between 0 and 100', options.alphaQuality);
487
+ }
388
488
  }
389
- }
390
- if (is.object(options) && is.defined(options.lossless)) {
391
- this._setBooleanOption('webpLossless', options.lossless);
392
- }
393
- if (is.object(options) && is.defined(options.nearLossless)) {
394
- this._setBooleanOption('webpNearLossless', options.nearLossless);
395
- }
396
- if (is.object(options) && is.defined(options.smartSubsample)) {
397
- this._setBooleanOption('webpSmartSubsample', options.smartSubsample);
398
- }
399
- if (is.object(options) && is.defined(options.reductionEffort)) {
400
- if (is.integer(options.reductionEffort) && is.inRange(options.reductionEffort, 0, 6)) {
401
- this.options.webpReductionEffort = options.reductionEffort;
402
- } else {
403
- throw is.invalidParameterError('reductionEffort', 'integer between 0 and 6', options.reductionEffort);
489
+ if (is.defined(options.lossless)) {
490
+ this._setBooleanOption('webpLossless', options.lossless);
491
+ }
492
+ if (is.defined(options.nearLossless)) {
493
+ this._setBooleanOption('webpNearLossless', options.nearLossless);
494
+ }
495
+ if (is.defined(options.smartSubsample)) {
496
+ this._setBooleanOption('webpSmartSubsample', options.smartSubsample);
497
+ }
498
+ const effort = options.effort || options.reductionEffort;
499
+ if (is.defined(effort)) {
500
+ if (is.integer(effort) && is.inRange(effort, 0, 6)) {
501
+ this.options.webpEffort = effort;
502
+ } else {
503
+ throw is.invalidParameterError('effort', 'integer between 0 and 6', effort);
504
+ }
404
505
  }
405
506
  }
406
-
407
507
  trySetAnimationOptions(options, this.options);
408
508
  return this._updateFormatOut('webp', options);
409
509
  }
410
510
 
411
511
  /**
412
- * Use these GIF options for output image.
512
+ * Use these GIF options for the output image.
413
513
  *
414
- * Requires libvips compiled with support for ImageMagick or GraphicsMagick.
415
- * The prebuilt binaries do not include this - see
416
- * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}.
514
+ * The first entry in the palette is reserved for transparency.
515
+ *
516
+ * @since 0.30.0
517
+ *
518
+ * @example
519
+ * // Convert PNG to GIF
520
+ * await sharp(pngBuffer)
521
+ * .gif()
522
+ * .toBuffer();
523
+ *
524
+ * @example
525
+ * // Convert animated WebP to animated GIF
526
+ * await sharp('animated.webp', { animated: true })
527
+ * .toFile('animated.gif');
528
+ *
529
+ * @example
530
+ * // Create a 128x128, cropped, non-dithered, animated thumbnail of an animated GIF
531
+ * const out = await sharp('in.gif', { animated: true })
532
+ * .resize({ width: 128, height: 128 })
533
+ * .gif({ dither: 0 })
534
+ * .toBuffer();
417
535
  *
418
536
  * @param {Object} [options] - output options
419
- * @param {number} [options.pageHeight] - page height for animated output
537
+ * @param {number} [options.colours=256] - maximum number of palette entries, including transparency, between 2 and 256
538
+ * @param {number} [options.colors=256] - alternative spelling of `options.colours`
539
+ * @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest)
540
+ * @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, between 0 (least) and 1 (most)
420
541
  * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation
421
- * @param {number[]} [options.delay] - list of delays between animation frames (in milliseconds)
542
+ * @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds)
422
543
  * @param {boolean} [options.force=true] - force GIF output, otherwise attempt to use input format
423
544
  * @returns {Sharp}
424
545
  * @throws {Error} Invalid options
425
546
  */
426
- /* istanbul ignore next */
427
547
  function gif (options) {
428
- if (!this.constructor.format.magick.output.buffer) {
429
- throw new Error('The gif operation requires libvips to have been installed with support for ImageMagick');
548
+ if (is.object(options)) {
549
+ const colours = options.colours || options.colors;
550
+ if (is.defined(colours)) {
551
+ if (is.integer(colours) && is.inRange(colours, 2, 256)) {
552
+ this.options.gifBitdepth = bitdepthFromColourCount(colours);
553
+ } else {
554
+ throw is.invalidParameterError('colours', 'integer between 2 and 256', colours);
555
+ }
556
+ }
557
+ if (is.defined(options.effort)) {
558
+ if (is.number(options.effort) && is.inRange(options.effort, 1, 10)) {
559
+ this.options.gifEffort = options.effort;
560
+ } else {
561
+ throw is.invalidParameterError('effort', 'integer between 1 and 10', options.effort);
562
+ }
563
+ }
564
+ if (is.defined(options.dither)) {
565
+ if (is.number(options.dither) && is.inRange(options.dither, 0, 1)) {
566
+ this.options.gifDither = options.dither;
567
+ } else {
568
+ throw is.invalidParameterError('dither', 'number between 0.0 and 1.0', options.dither);
569
+ }
570
+ }
430
571
  }
431
572
  trySetAnimationOptions(options, this.options);
432
573
  return this._updateFormatOut('gif', options);
433
574
  }
434
575
 
576
+ /**
577
+ * Use these JP2 options for output image.
578
+ *
579
+ * Requires libvips compiled with support for OpenJPEG.
580
+ * The prebuilt binaries do not include this - see
581
+ * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}.
582
+ *
583
+ * @example
584
+ * // Convert any input to lossless JP2 output
585
+ * const data = await sharp(input)
586
+ * .jp2({ lossless: true })
587
+ * .toBuffer();
588
+ *
589
+ * @example
590
+ * // Convert any input to very high quality JP2 output
591
+ * const data = await sharp(input)
592
+ * .jp2({
593
+ * quality: 100,
594
+ * chromaSubsampling: '4:4:4'
595
+ * })
596
+ * .toBuffer();
597
+ *
598
+ * @since 0.29.1
599
+ *
600
+ * @param {Object} [options] - output options
601
+ * @param {number} [options.quality=80] - quality, integer 1-100
602
+ * @param {boolean} [options.lossless=false] - use lossless compression mode
603
+ * @param {number} [options.tileWidth=512] - horizontal tile size
604
+ * @param {number} [options.tileHeight=512] - vertical tile size
605
+ * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
606
+ * @returns {Sharp}
607
+ * @throws {Error} Invalid options
608
+ */
609
+ /* istanbul ignore next */
610
+ function jp2 (options) {
611
+ if (!this.constructor.format.jp2k.output.buffer) {
612
+ throw errJp2Save;
613
+ }
614
+ if (is.object(options)) {
615
+ if (is.defined(options.quality)) {
616
+ if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
617
+ this.options.jp2Quality = options.quality;
618
+ } else {
619
+ throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
620
+ }
621
+ }
622
+ if (is.defined(options.lossless)) {
623
+ if (is.bool(options.lossless)) {
624
+ this.options.jp2Lossless = options.lossless;
625
+ } else {
626
+ throw is.invalidParameterError('lossless', 'boolean', options.lossless);
627
+ }
628
+ }
629
+ if (is.defined(options.tileWidth)) {
630
+ if (is.integer(options.tileWidth) && is.inRange(options.tileWidth, 1, 32768)) {
631
+ this.options.jp2TileWidth = options.tileWidth;
632
+ } else {
633
+ throw is.invalidParameterError('tileWidth', 'integer between 1 and 32768', options.tileWidth);
634
+ }
635
+ }
636
+ if (is.defined(options.tileHeight)) {
637
+ if (is.integer(options.tileHeight) && is.inRange(options.tileHeight, 1, 32768)) {
638
+ this.options.jp2TileHeight = options.tileHeight;
639
+ } else {
640
+ throw is.invalidParameterError('tileHeight', 'integer between 1 and 32768', options.tileHeight);
641
+ }
642
+ }
643
+ if (is.defined(options.chromaSubsampling)) {
644
+ if (is.string(options.chromaSubsampling) && is.inArray(options.chromaSubsampling, ['4:2:0', '4:4:4'])) {
645
+ this.options.heifChromaSubsampling = options.chromaSubsampling;
646
+ } else {
647
+ throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling);
648
+ }
649
+ }
650
+ }
651
+ return this._updateFormatOut('jp2', options);
652
+ }
653
+
435
654
  /**
436
655
  * Set animation options if available.
437
656
  * @private
438
657
  *
439
658
  * @param {Object} [source] - output options
440
- * @param {number} [source.pageHeight] - page height for animated output
441
659
  * @param {number} [source.loop=0] - number of animation iterations, use 0 for infinite animation
442
660
  * @param {number[]} [source.delay] - list of delays between animation frames (in milliseconds)
443
661
  * @param {Object} [target] - target object for valid options
444
662
  * @throws {Error} Invalid options
445
663
  */
446
664
  function trySetAnimationOptions (source, target) {
447
- if (is.object(source) && is.defined(source.pageHeight)) {
448
- if (is.integer(source.pageHeight) && source.pageHeight > 0) {
449
- target.pageHeight = source.pageHeight;
450
- } else {
451
- throw is.invalidParameterError('pageHeight', 'integer larger than 0', source.pageHeight);
452
- }
453
- }
454
665
  if (is.object(source) && is.defined(source.loop)) {
455
666
  if (is.integer(source.loop) && is.inRange(source.loop, 0, 65535)) {
456
667
  target.loop = source.loop;
@@ -459,13 +670,16 @@ function trySetAnimationOptions (source, target) {
459
670
  }
460
671
  }
461
672
  if (is.object(source) && is.defined(source.delay)) {
462
- if (
673
+ // We allow singular values as well
674
+ if (is.integer(source.delay) && is.inRange(source.delay, 0, 65535)) {
675
+ target.delay = [source.delay];
676
+ } else if (
463
677
  Array.isArray(source.delay) &&
464
678
  source.delay.every(is.integer) &&
465
679
  source.delay.every(v => is.inRange(v, 0, 65535))) {
466
680
  target.delay = source.delay;
467
681
  } else {
468
- throw is.invalidParameterError('delay', 'array of integers between 0 and 65535', source.delay);
682
+ throw is.invalidParameterError('delay', 'integer or an array of integers between 0 and 65535', source.delay);
469
683
  }
470
684
  }
471
685
  }
@@ -473,6 +687,8 @@ function trySetAnimationOptions (source, target) {
473
687
  /**
474
688
  * Use these TIFF options for output image.
475
689
  *
690
+ * The `density` can be set in pixels/inch via {@link withMetadata} instead of providing `xres` and `yres` in pixels/mm.
691
+ *
476
692
  * @example
477
693
  * // Convert SVG input to LZW-compressed, 1 bit per pixel TIFF output
478
694
  * sharp('input.svg')
@@ -494,6 +710,7 @@ function trySetAnimationOptions (source, target) {
494
710
  * @param {number} [options.tileHeight=256] - vertical tile size
495
711
  * @param {number} [options.xres=1.0] - horizontal resolution in pixels/mm
496
712
  * @param {number} [options.yres=1.0] - vertical resolution in pixels/mm
713
+ * @param {string} [options.resolutionUnit='inch'] - resolution unit options: inch, cm
497
714
  * @param {number} [options.bitdepth=8] - reduce bitdepth to 1, 2 or 4 bit
498
715
  * @returns {Sharp}
499
716
  * @throws {Error} Invalid options
@@ -567,6 +784,14 @@ function tiff (options) {
567
784
  throw is.invalidParameterError('predictor', 'one of: none, horizontal, float', options.predictor);
568
785
  }
569
786
  }
787
+ // resolutionUnit
788
+ if (is.defined(options.resolutionUnit)) {
789
+ if (is.string(options.resolutionUnit) && is.inArray(options.resolutionUnit, ['inch', 'cm'])) {
790
+ this.options.tiffResolutionUnit = options.resolutionUnit;
791
+ } else {
792
+ throw is.invalidParameterError('resolutionUnit', 'one of: inch, cm', options.resolutionUnit);
793
+ }
794
+ }
570
795
  }
571
796
  return this._updateFormatOut('tiff', options);
572
797
  }
@@ -577,13 +802,15 @@ function tiff (options) {
577
802
  * Whilst it is possible to create AVIF images smaller than 16x16 pixels,
578
803
  * most web browsers do not display these properly.
579
804
  *
805
+ * AVIF image sequences are not supported.
806
+ *
580
807
  * @since 0.27.0
581
808
  *
582
809
  * @param {Object} [options] - output options
583
810
  * @param {number} [options.quality=50] - quality, integer 1-100
584
811
  * @param {boolean} [options.lossless=false] - use lossless compression
585
- * @param {boolean} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest)
586
- * @param {string} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling, requires libvips v8.11.0
812
+ * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest)
813
+ * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
587
814
  * @returns {Sharp}
588
815
  * @throws {Error} Invalid options
589
816
  */
@@ -603,8 +830,8 @@ function avif (options) {
603
830
  * @param {number} [options.quality=50] - quality, integer 1-100
604
831
  * @param {string} [options.compression='av1'] - compression format: av1, hevc
605
832
  * @param {boolean} [options.lossless=false] - use lossless compression
606
- * @param {number} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest)
607
- * @param {string} [options.chromaSubsampling='4:2:0'] - set to '4:4:4' to prevent chroma subsampling otherwise defaults to '4:2:0' chroma subsampling, requires libvips v8.11.0
833
+ * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest)
834
+ * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
608
835
  * @returns {Sharp}
609
836
  * @throws {Error} Invalid options
610
837
  */
@@ -631,11 +858,17 @@ function heif (options) {
631
858
  throw is.invalidParameterError('compression', 'one of: av1, hevc', options.compression);
632
859
  }
633
860
  }
634
- if (is.defined(options.speed)) {
635
- if (is.integer(options.speed) && is.inRange(options.speed, 0, 8)) {
636
- this.options.heifSpeed = options.speed;
861
+ if (is.defined(options.effort)) {
862
+ if (is.integer(options.effort) && is.inRange(options.effort, 0, 9)) {
863
+ this.options.heifEffort = options.effort;
637
864
  } else {
638
- throw is.invalidParameterError('speed', 'integer between 0 and 8', options.speed);
865
+ throw is.invalidParameterError('effort', 'integer between 0 and 9', options.effort);
866
+ }
867
+ } else if (is.defined(options.speed)) {
868
+ if (is.integer(options.speed) && is.inRange(options.speed, 0, 9)) {
869
+ this.options.heifEffort = 9 - options.speed;
870
+ } else {
871
+ throw is.invalidParameterError('speed', 'integer between 0 and 9', options.speed);
639
872
  }
640
873
  }
641
874
  if (is.defined(options.chromaSubsampling)) {
@@ -650,28 +883,41 @@ function heif (options) {
650
883
  }
651
884
 
652
885
  /**
653
- * Force output to be raw, uncompressed, 8-bit unsigned integer (unit8) pixel data.
886
+ * Force output to be raw, uncompressed pixel data.
654
887
  * Pixel ordering is left-to-right, top-to-bottom, without padding.
655
888
  * Channel ordering will be RGB or RGBA for non-greyscale colourspaces.
656
889
  *
657
890
  * @example
658
- * // Extract raw RGB pixel data from JPEG input
891
+ * // Extract raw, unsigned 8-bit RGB pixel data from JPEG input
659
892
  * const { data, info } = await sharp('input.jpg')
660
893
  * .raw()
661
894
  * .toBuffer({ resolveWithObject: true });
662
895
  *
663
896
  * @example
664
- * // Extract alpha channel as raw pixel data from PNG input
897
+ * // Extract alpha channel as raw, unsigned 16-bit pixel data from PNG input
665
898
  * const data = await sharp('input.png')
666
899
  * .ensureAlpha()
667
900
  * .extractChannel(3)
668
901
  * .toColourspace('b-w')
669
- * .raw()
902
+ * .raw({ depth: 'ushort' })
670
903
  * .toBuffer();
671
904
  *
672
- * @returns {Sharp}
905
+ * @param {Object} [options] - output options
906
+ * @param {string} [options.depth='uchar'] - bit depth, one of: char, uchar (default), short, ushort, int, uint, float, complex, double, dpcomplex
907
+ * @throws {Error} Invalid options
673
908
  */
674
- function raw () {
909
+ function raw (options) {
910
+ if (is.object(options)) {
911
+ if (is.defined(options.depth)) {
912
+ if (is.string(options.depth) && is.inArray(options.depth,
913
+ ['char', 'uchar', 'short', 'ushort', 'int', 'uint', 'float', 'complex', 'double', 'dpcomplex']
914
+ )) {
915
+ this.options.rawDepth = options.depth;
916
+ } else {
917
+ throw is.invalidParameterError('depth', 'one of: char, uchar, short, ushort, int, uint, float, complex, double, dpcomplex', options.depth);
918
+ }
919
+ }
920
+ }
675
921
  return this._updateFormatOut('raw');
676
922
  }
677
923
 
@@ -680,8 +926,6 @@ function raw () {
680
926
  * Set the format and options for tile images via the `toFormat`, `jpeg`, `png` or `webp` functions.
681
927
  * Use a `.zip` or `.szi` file extension with `toFile` to write to a compressed archive file format.
682
928
  *
683
- * Warning: multiple sharp instances concurrently producing tile output can expose a possible race condition in some versions of libgsf.
684
- *
685
929
  * @example
686
930
  * sharp('input.tiff')
687
931
  * .png()
@@ -701,9 +945,10 @@ function raw () {
701
945
  * @param {string} [options.depth] how deep to make the pyramid, possible values are `onepixel`, `onetile` or `one`, default based on layout.
702
946
  * @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
703
947
  * @param {string} [options.container='fs'] tile container, with value `fs` (filesystem) or `zip` (compressed file).
704
- * @param {string} [options.layout='dz'] filesystem layout, possible values are `dz`, `iiif`, `zoomify` or `google`.
948
+ * @param {string} [options.layout='dz'] filesystem layout, possible values are `dz`, `iiif`, `iiif3`, `zoomify` or `google`.
705
949
  * @param {boolean} [options.centre=false] centre image in tile.
706
950
  * @param {boolean} [options.center=false] alternative spelling of centre.
951
+ * @param {string} [options.id='https://example.com/iiif'] when `layout` is `iiif`/`iiif3`, sets the `@id`/`id` attribute of `info.json`
707
952
  * @returns {Sharp}
708
953
  * @throws {Error} Invalid parameters
709
954
  */
@@ -738,10 +983,10 @@ function tile (options) {
738
983
  }
739
984
  // Layout
740
985
  if (is.defined(options.layout)) {
741
- if (is.string(options.layout) && is.inArray(options.layout, ['dz', 'google', 'iiif', 'zoomify'])) {
986
+ if (is.string(options.layout) && is.inArray(options.layout, ['dz', 'google', 'iiif', 'iiif3', 'zoomify'])) {
742
987
  this.options.tileLayout = options.layout;
743
988
  } else {
744
- throw is.invalidParameterError('layout', 'one of: dz, google, iiif, zoomify', options.layout);
989
+ throw is.invalidParameterError('layout', 'one of: dz, google, iiif, iiif3, zoomify', options.layout);
745
990
  }
746
991
  }
747
992
  // Angle of rotation,
@@ -777,6 +1022,14 @@ function tile (options) {
777
1022
  if (is.defined(centre)) {
778
1023
  this._setBooleanOption('tileCentre', centre);
779
1024
  }
1025
+ // @id attribute for IIIF layout
1026
+ if (is.defined(options.id)) {
1027
+ if (is.string(options.id)) {
1028
+ this.options.tileId = options.id;
1029
+ } else {
1030
+ throw is.invalidParameterError('id', 'string', options.id);
1031
+ }
1032
+ }
780
1033
  }
781
1034
  // Format
782
1035
  if (is.inArray(this.options.formatOut, ['jpeg', 'png', 'webp'])) {
@@ -787,6 +1040,31 @@ function tile (options) {
787
1040
  return this._updateFormatOut('dz');
788
1041
  }
789
1042
 
1043
+ /**
1044
+ * Set a timeout for processing, in seconds.
1045
+ * Use a value of zero to continue processing indefinitely, the default behaviour.
1046
+ *
1047
+ * The clock starts when libvips opens an input image for processing.
1048
+ * Time spent waiting for a libuv thread to become available is not included.
1049
+ *
1050
+ * @since 0.29.2
1051
+ *
1052
+ * @param {Object} options
1053
+ * @param {number} options.seconds - Number of seconds after which processing will be stopped
1054
+ * @returns {Sharp}
1055
+ */
1056
+ function timeout (options) {
1057
+ if (!is.plainObject(options)) {
1058
+ throw is.invalidParameterError('options', 'object', options);
1059
+ }
1060
+ if (is.integer(options.seconds) && is.inRange(options.seconds, 0, 3600)) {
1061
+ this.options.timeoutSeconds = options.seconds;
1062
+ } else {
1063
+ throw is.invalidParameterError('seconds', 'integer between 0 and 3600', options.seconds);
1064
+ }
1065
+ return this;
1066
+ }
1067
+
790
1068
  /**
791
1069
  * Update the output format unless options.force is false,
792
1070
  * in which case revert to input format.
@@ -863,6 +1141,7 @@ function _pipeline (callback) {
863
1141
  this.push(data);
864
1142
  }
865
1143
  this.push(null);
1144
+ this.emit('close');
866
1145
  });
867
1146
  });
868
1147
  if (this.streamInFinished) {
@@ -878,6 +1157,7 @@ function _pipeline (callback) {
878
1157
  this.push(data);
879
1158
  }
880
1159
  this.push(null);
1160
+ this.emit('close');
881
1161
  });
882
1162
  }
883
1163
  return this;
@@ -932,6 +1212,7 @@ module.exports = function (Sharp) {
932
1212
  withMetadata,
933
1213
  toFormat,
934
1214
  jpeg,
1215
+ jp2,
935
1216
  png,
936
1217
  webp,
937
1218
  tiff,
@@ -940,6 +1221,7 @@ module.exports = function (Sharp) {
940
1221
  gif,
941
1222
  raw,
942
1223
  tile,
1224
+ timeout,
943
1225
  // Private
944
1226
  _updateFormatOut,
945
1227
  _setBooleanOption,