sharp 0.31.2 → 0.31.3

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.
@@ -138,7 +138,8 @@ try {
138
138
  const libcFamily = detectLibc.familySync();
139
139
  const libcVersion = detectLibc.versionSync();
140
140
  if (libcFamily === detectLibc.GLIBC && libcVersion && minimumGlibcVersionByArch[arch]) {
141
- if (semverLessThan(`${libcVersion}.0`, `${minimumGlibcVersionByArch[arch]}.0`)) {
141
+ const libcVersionWithoutPatch = libcVersion.split('.').slice(0, 2).join('.');
142
+ if (semverLessThan(`${libcVersionWithoutPatch}.0`, `${minimumGlibcVersionByArch[arch]}.0`)) {
142
143
  handleError(new Error(`Use with glibc ${libcVersion} requires manual installation of libvips >= ${minimumLibvipsVersion}`));
143
144
  }
144
145
  }
@@ -167,7 +168,7 @@ try {
167
168
  } else {
168
169
  const url = distBaseUrl + tarFilename;
169
170
  libvips.log(`Downloading ${url}`);
170
- simpleGet({ url: url, agent: agent() }, function (err, response) {
171
+ simpleGet({ url: url, agent: agent(libvips.log) }, function (err, response) {
171
172
  if (err) {
172
173
  fail(err);
173
174
  } else if (response.statusCode === 404) {
package/lib/agent.js CHANGED
@@ -18,7 +18,7 @@ function env (key) {
18
18
  return process.env[key];
19
19
  }
20
20
 
21
- module.exports = function () {
21
+ module.exports = function (log) {
22
22
  try {
23
23
  const proxy = new url.URL(proxies.map(env).find(is.string));
24
24
  const tunnel = proxy.protocol === 'https:'
@@ -27,6 +27,7 @@ module.exports = function () {
27
27
  const proxyAuth = proxy.username && proxy.password
28
28
  ? `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`
29
29
  : null;
30
+ log(`Via proxy ${proxy.protocol}://${proxy.hostname}:${proxy.port} ${proxyAuth ? 'with' : 'no'} credentials`);
30
31
  return tunnel({
31
32
  proxy: {
32
33
  port: Number(proxy.port),
package/lib/channel.js CHANGED
@@ -98,7 +98,7 @@ function extractChannel (channel) {
98
98
  } else {
99
99
  throw is.invalidParameterError('channel', 'integer or one of: red, green, blue, alpha', channel);
100
100
  }
101
- return this.toColourspace('b-w');
101
+ return this;
102
102
  }
103
103
 
104
104
  /**
@@ -119,7 +119,7 @@ const debuglog = util.debuglog('sharp');
119
119
  * a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file.
120
120
  * JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
121
121
  * @param {Object} [options] - if present, is an Object with optional attributes.
122
- * @param {string} [options.failOn='warning'] - level of sensitivity to invalid images, one of (in order of sensitivity): 'none' (least), 'truncated', 'error' or 'warning' (most), highers level imply lower levels.
122
+ * @param {string} [options.failOn='warning'] - when to abort processing of invalid pixel data, one of (in order of sensitivity): 'none' (least), 'truncated', 'error' or 'warning' (most), highers level imply lower levels, invalid metadata will always abort.
123
123
  * @param {number|boolean} [options.limitInputPixels=268402689] - Do not process input images where the number of pixels
124
124
  * (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted.
125
125
  * An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF).
@@ -289,6 +289,8 @@ const Sharp = function (input, options) {
289
289
  gifBitdepth: 8,
290
290
  gifEffort: 7,
291
291
  gifDither: 1,
292
+ gifInterFrameMaxError: 0,
293
+ gifInterPaletteMaxError: 3,
292
294
  gifReoptimise: false,
293
295
  tiffQuality: 80,
294
296
  tiffCompression: 'jpeg',
@@ -306,6 +308,10 @@ const Sharp = function (input, options) {
306
308
  heifCompression: 'av1',
307
309
  heifEffort: 4,
308
310
  heifChromaSubsampling: '4:4:4',
311
+ jxlDistance: 1,
312
+ jxlDecodingTier: 0,
313
+ jxlEffort: 7,
314
+ jxlLossless: false,
309
315
  rawDepth: 'uchar',
310
316
  tileSize: 256,
311
317
  tileOverlap: 0,
package/lib/input.js CHANGED
@@ -469,7 +469,7 @@ function metadata (callback) {
469
469
  } else {
470
470
  if (this._isStreamInput()) {
471
471
  return new Promise((resolve, reject) => {
472
- this.on('finish', () => {
472
+ const finished = () => {
473
473
  this._flattenBufferIn();
474
474
  sharp.metadata(this.options, (err, metadata) => {
475
475
  if (err) {
@@ -478,7 +478,12 @@ function metadata (callback) {
478
478
  resolve(metadata);
479
479
  }
480
480
  });
481
- });
481
+ };
482
+ if (this.writableFinished) {
483
+ finished();
484
+ } else {
485
+ this.once('finish', finished);
486
+ }
482
487
  });
483
488
  } else {
484
489
  return new Promise((resolve, reject) => {
package/lib/operation.js CHANGED
@@ -205,9 +205,11 @@ function affine (matrix, options) {
205
205
 
206
206
  /**
207
207
  * Sharpen the image.
208
+ *
208
209
  * When used without parameters, performs a fast, mild sharpen of the output image.
210
+ *
209
211
  * When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space.
210
- * Separate control over the level of sharpening in "flat" and "jagged" areas is available.
212
+ * Fine-grained control over the level of sharpening in "flat" (m1) and "jagged" (m2) areas is available.
211
213
  *
212
214
  * See {@link https://www.libvips.org/API/current/libvips-convolution.html#vips-sharpen|libvips sharpen} operation.
213
215
  *
@@ -229,13 +231,13 @@ function affine (matrix, options) {
229
231
  * })
230
232
  * .toBuffer();
231
233
  *
232
- * @param {Object|number} [options] - if present, is an Object with attributes or (deprecated) a number for `options.sigma`.
233
- * @param {number} [options.sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
234
- * @param {number} [options.m1=1.0] - the level of sharpening to apply to "flat" areas.
235
- * @param {number} [options.m2=2.0] - the level of sharpening to apply to "jagged" areas.
236
- * @param {number} [options.x1=2.0] - threshold between "flat" and "jagged"
237
- * @param {number} [options.y2=10.0] - maximum amount of brightening.
238
- * @param {number} [options.y3=20.0] - maximum amount of darkening.
234
+ * @param {Object|number} [options] - if present, is an Object with attributes
235
+ * @param {number} [options.sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`, between 0.000001 and 10000
236
+ * @param {number} [options.m1=1.0] - the level of sharpening to apply to "flat" areas, between 0 and 1000000
237
+ * @param {number} [options.m2=2.0] - the level of sharpening to apply to "jagged" areas, between 0 and 1000000
238
+ * @param {number} [options.x1=2.0] - threshold between "flat" and "jagged", between 0 and 1000000
239
+ * @param {number} [options.y2=10.0] - maximum amount of brightening, between 0 and 1000000
240
+ * @param {number} [options.y3=20.0] - maximum amount of darkening, between 0 and 1000000
239
241
  * @param {number} [flat] - (deprecated) see `options.m1`.
240
242
  * @param {number} [jagged] - (deprecated) see `options.m2`.
241
243
  * @returns {Sharp}
@@ -268,44 +270,44 @@ function sharpen (options, flat, jagged) {
268
270
  }
269
271
  }
270
272
  } else if (is.plainObject(options)) {
271
- if (is.number(options.sigma) && is.inRange(options.sigma, 0.01, 10000)) {
273
+ if (is.number(options.sigma) && is.inRange(options.sigma, 0.000001, 10000)) {
272
274
  this.options.sharpenSigma = options.sigma;
273
275
  } else {
274
- throw is.invalidParameterError('options.sigma', 'number between 0.01 and 10000', options.sigma);
276
+ throw is.invalidParameterError('options.sigma', 'number between 0.000001 and 10000', options.sigma);
275
277
  }
276
278
  if (is.defined(options.m1)) {
277
- if (is.number(options.m1) && is.inRange(options.m1, 0, 10000)) {
279
+ if (is.number(options.m1) && is.inRange(options.m1, 0, 1000000)) {
278
280
  this.options.sharpenM1 = options.m1;
279
281
  } else {
280
- throw is.invalidParameterError('options.m1', 'number between 0 and 10000', options.m1);
282
+ throw is.invalidParameterError('options.m1', 'number between 0 and 1000000', options.m1);
281
283
  }
282
284
  }
283
285
  if (is.defined(options.m2)) {
284
- if (is.number(options.m2) && is.inRange(options.m2, 0, 10000)) {
286
+ if (is.number(options.m2) && is.inRange(options.m2, 0, 1000000)) {
285
287
  this.options.sharpenM2 = options.m2;
286
288
  } else {
287
- throw is.invalidParameterError('options.m2', 'number between 0 and 10000', options.m2);
289
+ throw is.invalidParameterError('options.m2', 'number between 0 and 1000000', options.m2);
288
290
  }
289
291
  }
290
292
  if (is.defined(options.x1)) {
291
- if (is.number(options.x1) && is.inRange(options.x1, 0, 10000)) {
293
+ if (is.number(options.x1) && is.inRange(options.x1, 0, 1000000)) {
292
294
  this.options.sharpenX1 = options.x1;
293
295
  } else {
294
- throw is.invalidParameterError('options.x1', 'number between 0 and 10000', options.x1);
296
+ throw is.invalidParameterError('options.x1', 'number between 0 and 1000000', options.x1);
295
297
  }
296
298
  }
297
299
  if (is.defined(options.y2)) {
298
- if (is.number(options.y2) && is.inRange(options.y2, 0, 10000)) {
300
+ if (is.number(options.y2) && is.inRange(options.y2, 0, 1000000)) {
299
301
  this.options.sharpenY2 = options.y2;
300
302
  } else {
301
- throw is.invalidParameterError('options.y2', 'number between 0 and 10000', options.y2);
303
+ throw is.invalidParameterError('options.y2', 'number between 0 and 1000000', options.y2);
302
304
  }
303
305
  }
304
306
  if (is.defined(options.y3)) {
305
- if (is.number(options.y3) && is.inRange(options.y3, 0, 10000)) {
307
+ if (is.number(options.y3) && is.inRange(options.y3, 0, 1000000)) {
306
308
  this.options.sharpenY3 = options.y3;
307
309
  } else {
308
- throw is.invalidParameterError('options.y3', 'number between 0 and 10000', options.y3);
310
+ throw is.invalidParameterError('options.y3', 'number between 0 and 1000000', options.y3);
309
311
  }
310
312
  }
311
313
  } else {
package/lib/output.js CHANGED
@@ -22,10 +22,13 @@ const formats = new Map([
22
22
  ['jp2', 'jp2'],
23
23
  ['jpx', 'jp2'],
24
24
  ['j2k', 'jp2'],
25
- ['j2c', 'jp2']
25
+ ['j2c', 'jp2'],
26
+ ['jxl', 'jxl']
26
27
  ]);
27
28
 
28
- const errJp2Save = new Error('JP2 output requires libvips with support for OpenJPEG');
29
+ const jp2Regex = /\.jp[2x]|j2[kc]$/i;
30
+
31
+ const errJp2Save = () => new Error('JP2 output requires libvips with support for OpenJPEG');
29
32
 
30
33
  const bitdepthFromColourCount = (colours) => 1 << 31 - Math.clz32(Math.ceil(Math.log2(colours)));
31
34
 
@@ -68,6 +71,8 @@ function toFile (fileOut, callback) {
68
71
  err = new Error('Missing output file path');
69
72
  } else if (is.string(this.options.input.file) && path.resolve(this.options.input.file) === path.resolve(fileOut)) {
70
73
  err = new Error('Cannot use same file for input and output');
74
+ } else if (jp2Regex.test(fileOut) && !this.constructor.format.jp2k.output.file) {
75
+ err = errJp2Save();
71
76
  }
72
77
  if (err) {
73
78
  if (is.fn(callback)) {
@@ -547,6 +552,12 @@ function webp (options) {
547
552
  * .gif({ dither: 0 })
548
553
  * .toBuffer();
549
554
  *
555
+ * @example
556
+ * // Lossy file size reduction of animated GIF
557
+ * await sharp('in.gif', { animated: true })
558
+ * .gif({ interFrameMaxError: 8 })
559
+ * .toFile('optim.gif');
560
+ *
550
561
  * @param {Object} [options] - output options
551
562
  * @param {boolean} [options.reoptimise=false] - always generate new palettes (slow), re-use existing by default
552
563
  * @param {boolean} [options.reoptimize=false] - alternative spelling of `options.reoptimise`
@@ -554,6 +565,8 @@ function webp (options) {
554
565
  * @param {number} [options.colors=256] - alternative spelling of `options.colours`
555
566
  * @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest)
556
567
  * @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, between 0 (least) and 1 (most)
568
+ * @param {number} [options.interFrameMaxError=0] - maximum inter-frame error for transparency, between 0 (lossless) and 32
569
+ * @param {number} [options.interPaletteMaxError=3] - maximum inter-palette error for palette reuse, between 0 and 256
557
570
  * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation
558
571
  * @param {number|number[]} [options.delay] - delay(s) between animation frames (in milliseconds)
559
572
  * @param {boolean} [options.force=true] - force GIF output, otherwise attempt to use input format
@@ -589,6 +602,20 @@ function gif (options) {
589
602
  throw is.invalidParameterError('dither', 'number between 0.0 and 1.0', options.dither);
590
603
  }
591
604
  }
605
+ if (is.defined(options.interFrameMaxError)) {
606
+ if (is.number(options.interFrameMaxError) && is.inRange(options.interFrameMaxError, 0, 32)) {
607
+ this.options.gifInterFrameMaxError = options.interFrameMaxError;
608
+ } else {
609
+ throw is.invalidParameterError('interFrameMaxError', 'number between 0.0 and 32.0', options.interFrameMaxError);
610
+ }
611
+ }
612
+ if (is.defined(options.interPaletteMaxError)) {
613
+ if (is.number(options.interPaletteMaxError) && is.inRange(options.interPaletteMaxError, 0, 256)) {
614
+ this.options.gifInterPaletteMaxError = options.interPaletteMaxError;
615
+ } else {
616
+ throw is.invalidParameterError('interPaletteMaxError', 'number between 0.0 and 256.0', options.interPaletteMaxError);
617
+ }
618
+ }
592
619
  }
593
620
  trySetAnimationOptions(options, this.options);
594
621
  return this._updateFormatOut('gif', options);
@@ -630,7 +657,7 @@ function gif (options) {
630
657
  /* istanbul ignore next */
631
658
  function jp2 (options) {
632
659
  if (!this.constructor.format.jp2k.output.buffer) {
633
- throw errJp2Save;
660
+ throw errJp2Save();
634
661
  }
635
662
  if (is.object(options)) {
636
663
  if (is.defined(options.quality)) {
@@ -912,6 +939,71 @@ function heif (options) {
912
939
  return this._updateFormatOut('heif', options);
913
940
  }
914
941
 
942
+ /**
943
+ * Use these JPEG-XL (JXL) options for output image.
944
+ *
945
+ * This feature is experimental, please do not use in production systems.
946
+ *
947
+ * Requires libvips compiled with support for libjxl.
948
+ * The prebuilt binaries do not include this - see
949
+ * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}.
950
+ *
951
+ * Image metadata (EXIF, XMP) is unsupported.
952
+ *
953
+ * @since 0.31.3
954
+ *
955
+ * @param {Object} [options] - output options
956
+ * @param {number} [options.distance=1.0] - maximum encoding error, between 0 (highest quality) and 15 (lowest quality)
957
+ * @param {number} [options.quality] - calculate `distance` based on JPEG-like quality, between 1 and 100, overrides distance if specified
958
+ * @param {number} [options.decodingTier=0] - target decode speed tier, between 0 (highest quality) and 4 (lowest quality)
959
+ * @param {boolean} [options.lossless=false] - use lossless compression
960
+ * @param {number} [options.effort=7] - CPU effort, between 3 (fastest) and 9 (slowest)
961
+ * @returns {Sharp}
962
+ * @throws {Error} Invalid options
963
+ */
964
+ function jxl (options) {
965
+ if (is.object(options)) {
966
+ if (is.defined(options.quality)) {
967
+ if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
968
+ // https://github.com/libjxl/libjxl/blob/0aeea7f180bafd6893c1db8072dcb67d2aa5b03d/tools/cjxl_main.cc#L640-L644
969
+ this.options.jxlDistance = options.quality >= 30
970
+ ? 0.1 + (100 - options.quality) * 0.09
971
+ : 53 / 3000 * options.quality * options.quality - 23 / 20 * options.quality + 25;
972
+ } else {
973
+ throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
974
+ }
975
+ } else if (is.defined(options.distance)) {
976
+ if (is.number(options.distance) && is.inRange(options.distance, 0, 15)) {
977
+ this.options.jxlDistance = options.distance;
978
+ } else {
979
+ throw is.invalidParameterError('distance', 'number between 0.0 and 15.0', options.distance);
980
+ }
981
+ }
982
+ if (is.defined(options.decodingTier)) {
983
+ if (is.integer(options.decodingTier) && is.inRange(options.decodingTier, 0, 4)) {
984
+ this.options.jxlDecodingTier = options.decodingTier;
985
+ } else {
986
+ throw is.invalidParameterError('decodingTier', 'integer between 0 and 4', options.decodingTier);
987
+ }
988
+ }
989
+ if (is.defined(options.lossless)) {
990
+ if (is.bool(options.lossless)) {
991
+ this.options.jxlLossless = options.lossless;
992
+ } else {
993
+ throw is.invalidParameterError('lossless', 'boolean', options.lossless);
994
+ }
995
+ }
996
+ if (is.defined(options.effort)) {
997
+ if (is.integer(options.effort) && is.inRange(options.effort, 3, 9)) {
998
+ this.options.jxlEffort = options.effort;
999
+ } else {
1000
+ throw is.invalidParameterError('effort', 'integer between 3 and 9', options.effort);
1001
+ }
1002
+ }
1003
+ }
1004
+ return this._updateFormatOut('jxl', options);
1005
+ }
1006
+
915
1007
  /**
916
1008
  * Force output to be raw, uncompressed pixel data.
917
1009
  * Pixel ordering is left-to-right, top-to-bottom, without padding.
@@ -1282,6 +1374,7 @@ module.exports = function (Sharp) {
1282
1374
  tiff,
1283
1375
  avif,
1284
1376
  heif,
1377
+ jxl,
1285
1378
  gif,
1286
1379
  raw,
1287
1380
  tile,
package/lib/resize.js CHANGED
@@ -111,6 +111,8 @@ function isResizeExpected (options) {
111
111
  *
112
112
  * Some of these values are based on the [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) CSS property.
113
113
  *
114
+ * <img alt="Examples of various values for the fit property when resizing" width="100%" style="aspect-ratio: 998/243" src="https://cdn.jsdelivr.net/gh/lovell/sharp@main/docs/image/api-resize-fit.png">
115
+ *
114
116
  * When using a **fit** of `cover` or `contain`, the default **position** is `centre`. Other options are:
115
117
  * - `sharp.position`: `top`, `right top`, `right`, `right bottom`, `bottom`, `left bottom`, `left`, `left top`.
116
118
  * - `sharp.gravity`: `north`, `northeast`, `east`, `southeast`, `south`, `southwest`, `west`, `northwest`, `center` or `centre`.
@@ -217,8 +219,8 @@ function isResizeExpected (options) {
217
219
  * @param {number} [width] - pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height.
218
220
  * @param {number} [height] - pixels high the resultant image should be. Use `null` or `undefined` to auto-scale the height to match the width.
219
221
  * @param {Object} [options]
220
- * @param {String} [options.width] - alternative means of specifying `width`. If both are present this take priority.
221
- * @param {String} [options.height] - alternative means of specifying `height`. If both are present this take priority.
222
+ * @param {String} [options.width] - alternative means of specifying `width`. If both are present this takes priority.
223
+ * @param {String} [options.height] - alternative means of specifying `height`. If both are present this takes priority.
222
224
  * @param {String} [options.fit='cover'] - how the image should be resized to fit both provided dimensions, one of `cover`, `contain`, `fill`, `inside` or `outside`.
223
225
  * @param {String} [options.position='centre'] - position, gravity or strategy to use when `fit` is `cover` or `contain`.
224
226
  * @param {String|Object} [options.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour when `fit` is `contain`, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sharp",
3
3
  "description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, GIF, AVIF and TIFF images",
4
- "version": "0.31.2",
4
+ "version": "0.31.3",
5
5
  "author": "Lovell Fuller <npm@lovell.info>",
6
6
  "homepage": "https://github.com/lovell/sharp",
7
7
  "contributors": [
@@ -92,7 +92,7 @@
92
92
  "clean": "rm -rf node_modules/ build/ vendor/ .nyc_output/ coverage/ test/fixtures/output.*",
93
93
  "test": "npm run test-lint && npm run test-unit && npm run test-licensing",
94
94
  "test-lint": "semistandard && cpplint",
95
- "test-unit": "nyc --reporter=lcov --reporter=text --check-coverage --branches=100 mocha --slow=1000 --timeout=30000 ./test/unit/*.js",
95
+ "test-unit": "nyc --reporter=lcov --reporter=text --check-coverage --branches=100 mocha",
96
96
  "test-licensing": "license-checker --production --summary --onlyAllow=\"Apache-2.0;BSD;ISC;MIT\"",
97
97
  "test-leak": "./test/leak/leak.sh",
98
98
  "docs-build": "documentation lint lib && node docs/build && node docs/search-index/build",
@@ -141,12 +141,12 @@
141
141
  "devDependencies": {
142
142
  "async": "^3.2.4",
143
143
  "cc": "^3.0.1",
144
- "documentation": "^14.0.0",
144
+ "documentation": "^14.0.1",
145
145
  "exif-reader": "^1.0.3",
146
146
  "extract-zip": "^2.0.1",
147
147
  "icc": "^2.0.0",
148
148
  "license-checker": "^25.0.1",
149
- "mocha": "^10.1.0",
149
+ "mocha": "^10.2.0",
150
150
  "mock-fs": "^5.2.0",
151
151
  "nyc": "^15.1.0",
152
152
  "prebuild": "^11.0.4",
package/src/common.cc CHANGED
@@ -76,6 +76,14 @@ namespace sharp {
76
76
  }
77
77
  return vector;
78
78
  }
79
+ Napi::Buffer<char> NewOrCopyBuffer(Napi::Env env, char* data, size_t len) {
80
+ try {
81
+ return Napi::Buffer<char>::New(env, data, len, FreeCallback);
82
+ } catch (Napi::Error const &err) {}
83
+ Napi::Buffer<char> buf = Napi::Buffer<char>::Copy(env, data, len);
84
+ FreeCallback(nullptr, data);
85
+ return buf;
86
+ }
79
87
 
80
88
  // Create an InputDescriptor instance from a Napi::Object describing an input image
81
89
  InputDescriptor* CreateInputDescriptor(Napi::Object input) {
@@ -207,6 +215,9 @@ namespace sharp {
207
215
  bool IsAvif(std::string const &str) {
208
216
  return EndsWith(str, ".avif") || EndsWith(str, ".AVIF");
209
217
  }
218
+ bool IsJxl(std::string const &str) {
219
+ return EndsWith(str, ".jxl") || EndsWith(str, ".JXL");
220
+ }
210
221
  bool IsDz(std::string const &str) {
211
222
  return EndsWith(str, ".dzi") || EndsWith(str, ".DZI");
212
223
  }
@@ -237,6 +248,7 @@ namespace sharp {
237
248
  case ImageType::PPM: id = "ppm"; break;
238
249
  case ImageType::FITS: id = "fits"; break;
239
250
  case ImageType::EXR: id = "exr"; break;
251
+ case ImageType::JXL: id = "jxl"; break;
240
252
  case ImageType::VIPS: id = "vips"; break;
241
253
  case ImageType::RAW: id = "raw"; break;
242
254
  case ImageType::UNKNOWN: id = "unknown"; break;
@@ -281,6 +293,8 @@ namespace sharp {
281
293
  { "VipsForeignLoadPpmFile", ImageType::PPM },
282
294
  { "VipsForeignLoadFitsFile", ImageType::FITS },
283
295
  { "VipsForeignLoadOpenexr", ImageType::EXR },
296
+ { "VipsForeignLoadJxlFile", ImageType::JXL },
297
+ { "VipsForeignLoadJxlBuffer", ImageType::JXL },
284
298
  { "VipsForeignLoadVips", ImageType::VIPS },
285
299
  { "VipsForeignLoadVipsFile", ImageType::VIPS },
286
300
  { "VipsForeignLoadRaw", ImageType::RAW }
@@ -913,7 +927,7 @@ namespace sharp {
913
927
  // Add non-transparent alpha channel, if required
914
928
  if (colour[3] < 255.0 && !HasAlpha(image)) {
915
929
  image = image.bandjoin(
916
- VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier));
930
+ VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier).cast(image.format()));
917
931
  }
918
932
  return std::make_tuple(image, alphaColour);
919
933
  }
package/src/common.h CHANGED
@@ -133,6 +133,7 @@ namespace sharp {
133
133
  return static_cast<T>(
134
134
  vips_enum_from_nick(nullptr, type, AttrAsStr(obj, attr).data()));
135
135
  }
136
+ Napi::Buffer<char> NewOrCopyBuffer(Napi::Env env, char* data, size_t len);
136
137
 
137
138
  // Create an InputDescriptor instance from a Napi::Object describing an input image
138
139
  InputDescriptor* CreateInputDescriptor(Napi::Object input);
@@ -152,6 +153,7 @@ namespace sharp {
152
153
  PPM,
153
154
  FITS,
154
155
  EXR,
156
+ JXL,
155
157
  VIPS,
156
158
  RAW,
157
159
  UNKNOWN,
@@ -182,6 +184,7 @@ namespace sharp {
182
184
  bool IsHeic(std::string const &str);
183
185
  bool IsHeif(std::string const &str);
184
186
  bool IsAvif(std::string const &str);
187
+ bool IsJxl(std::string const &str);
185
188
  bool IsDz(std::string const &str);
186
189
  bool IsDzZip(std::string const &str);
187
190
  bool IsV(std::string const &str);
package/src/metadata.cc CHANGED
@@ -235,20 +235,20 @@ class MetadataWorker : public Napi::AsyncWorker {
235
235
  info.Set("orientation", baton->orientation);
236
236
  }
237
237
  if (baton->exifLength > 0) {
238
- info.Set("exif", Napi::Buffer<char>::New(env, baton->exif, baton->exifLength, sharp::FreeCallback));
238
+ info.Set("exif", sharp::NewOrCopyBuffer(env, baton->exif, baton->exifLength));
239
239
  }
240
240
  if (baton->iccLength > 0) {
241
- info.Set("icc", Napi::Buffer<char>::New(env, baton->icc, baton->iccLength, sharp::FreeCallback));
241
+ info.Set("icc", sharp::NewOrCopyBuffer(env, baton->icc, baton->iccLength));
242
242
  }
243
243
  if (baton->iptcLength > 0) {
244
- info.Set("iptc", Napi::Buffer<char>::New(env, baton->iptc, baton->iptcLength, sharp::FreeCallback));
244
+ info.Set("iptc", sharp::NewOrCopyBuffer(env, baton->iptc, baton->iptcLength));
245
245
  }
246
246
  if (baton->xmpLength > 0) {
247
- info.Set("xmp", Napi::Buffer<char>::New(env, baton->xmp, baton->xmpLength, sharp::FreeCallback));
247
+ info.Set("xmp", sharp::NewOrCopyBuffer(env, baton->xmp, baton->xmpLength));
248
248
  }
249
249
  if (baton->tifftagPhotoshopLength > 0) {
250
250
  info.Set("tifftagPhotoshop",
251
- Napi::Buffer<char>::New(env, baton->tifftagPhotoshop, baton->tifftagPhotoshopLength, sharp::FreeCallback));
251
+ sharp::NewOrCopyBuffer(env, baton->tifftagPhotoshop, baton->tifftagPhotoshopLength));
252
252
  }
253
253
  Callback().MakeCallback(Receiver().Value(), { env.Null(), info });
254
254
  } else {
package/src/operations.cc CHANGED
@@ -345,9 +345,9 @@ namespace sharp {
345
345
  if (HasAlpha(image) && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) {
346
346
  // Separate alpha channel
347
347
  VImage alpha = image[bands - 1];
348
- return RemoveAlpha(image).linear(a, b).bandjoin(alpha);
348
+ return RemoveAlpha(image).linear(a, b, VImage::option()->set("uchar", TRUE)).bandjoin(alpha);
349
349
  } else {
350
- return image.linear(a, b);
350
+ return image.linear(a, b, VImage::option()->set("uchar", TRUE));
351
351
  }
352
352
  }
353
353
 
package/src/pipeline.cc CHANGED
@@ -701,24 +701,6 @@ class PipelineWorker : public Napi::AsyncWorker {
701
701
  image = sharp::Tint(image, baton->tintA, baton->tintB);
702
702
  }
703
703
 
704
- // Extract an image channel (aka vips band)
705
- if (baton->extractChannel > -1) {
706
- if (baton->extractChannel >= image.bands()) {
707
- if (baton->extractChannel == 3 && sharp::HasAlpha(image)) {
708
- baton->extractChannel = image.bands() - 1;
709
- } else {
710
- (baton->err).append("Cannot extract channel from image. Too few channels in image.");
711
- return Error();
712
- }
713
- }
714
- VipsInterpretation const interpretation = sharp::Is16Bit(image.interpretation())
715
- ? VIPS_INTERPRETATION_GREY16
716
- : VIPS_INTERPRETATION_B_W;
717
- image = image
718
- .extract_band(baton->extractChannel)
719
- .copy(VImage::option()->set("interpretation", interpretation));
720
- }
721
-
722
704
  // Remove alpha channel, if any
723
705
  if (baton->removeAlpha) {
724
706
  image = sharp::RemoveAlpha(image);
@@ -744,6 +726,26 @@ class PipelineWorker : public Napi::AsyncWorker {
744
726
  }
745
727
  }
746
728
 
729
+ // Extract channel
730
+ if (baton->extractChannel > -1) {
731
+ if (baton->extractChannel >= image.bands()) {
732
+ if (baton->extractChannel == 3 && sharp::HasAlpha(image)) {
733
+ baton->extractChannel = image.bands() - 1;
734
+ } else {
735
+ (baton->err)
736
+ .append("Cannot extract channel ").append(std::to_string(baton->extractChannel))
737
+ .append(" from image with channels 0-").append(std::to_string(image.bands() - 1));
738
+ return Error();
739
+ }
740
+ }
741
+ VipsInterpretation colourspace = sharp::Is16Bit(image.interpretation())
742
+ ? VIPS_INTERPRETATION_GREY16
743
+ : VIPS_INTERPRETATION_B_W;
744
+ image = image
745
+ .extract_band(baton->extractChannel)
746
+ .copy(VImage::option()->set("interpretation", colourspace));
747
+ }
748
+
747
749
  // Apply output ICC profile
748
750
  if (!baton->withMetadataIcc.empty()) {
749
751
  image = image.icc_transform(
@@ -869,6 +871,8 @@ class PipelineWorker : public Napi::AsyncWorker {
869
871
  ->set("bitdepth", baton->gifBitdepth)
870
872
  ->set("effort", baton->gifEffort)
871
873
  ->set("reoptimise", baton->gifReoptimise)
874
+ ->set("interframe_maxerror", baton->gifInterFrameMaxError)
875
+ ->set("interpalette_maxerror", baton->gifInterPaletteMaxError)
872
876
  ->set("dither", baton->gifDither)));
873
877
  baton->bufferOut = static_cast<char*>(area->data);
874
878
  baton->bufferOutLength = area->length;
@@ -935,6 +939,21 @@ class PipelineWorker : public Napi::AsyncWorker {
935
939
  area->free_fn = nullptr;
936
940
  vips_area_unref(area);
937
941
  baton->formatOut = "dz";
942
+ } else if (baton->formatOut == "jxl" ||
943
+ (baton->formatOut == "input" && inputImageType == sharp::ImageType::JXL)) {
944
+ // Write JXL to buffer
945
+ image = sharp::RemoveAnimationProperties(image);
946
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.jxlsave_buffer(VImage::option()
947
+ ->set("strip", !baton->withMetadata)
948
+ ->set("distance", baton->jxlDistance)
949
+ ->set("tier", baton->jxlDecodingTier)
950
+ ->set("effort", baton->jxlEffort)
951
+ ->set("lossless", baton->jxlLossless)));
952
+ baton->bufferOut = static_cast<char*>(area->data);
953
+ baton->bufferOutLength = area->length;
954
+ area->free_fn = nullptr;
955
+ vips_area_unref(area);
956
+ baton->formatOut = "jxl";
938
957
  } else if (baton->formatOut == "raw" ||
939
958
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::RAW)) {
940
959
  // Write raw, uncompressed image data to buffer
@@ -973,6 +992,7 @@ class PipelineWorker : public Napi::AsyncWorker {
973
992
  bool const isTiff = sharp::IsTiff(baton->fileOut);
974
993
  bool const isJp2 = sharp::IsJp2(baton->fileOut);
975
994
  bool const isHeif = sharp::IsHeif(baton->fileOut);
995
+ bool const isJxl = sharp::IsJxl(baton->fileOut);
976
996
  bool const isDz = sharp::IsDz(baton->fileOut);
977
997
  bool const isDzZip = sharp::IsDzZip(baton->fileOut);
978
998
  bool const isV = sharp::IsV(baton->fileOut);
@@ -1090,6 +1110,17 @@ class PipelineWorker : public Napi::AsyncWorker {
1090
1110
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1091
1111
  ->set("lossless", baton->heifLossless));
1092
1112
  baton->formatOut = "heif";
1113
+ } else if (baton->formatOut == "jxl" || (mightMatchInput && isJxl) ||
1114
+ (willMatchInput && inputImageType == sharp::ImageType::JXL)) {
1115
+ // Write JXL to file
1116
+ image = sharp::RemoveAnimationProperties(image);
1117
+ image.jxlsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1118
+ ->set("strip", !baton->withMetadata)
1119
+ ->set("distance", baton->jxlDistance)
1120
+ ->set("tier", baton->jxlDecodingTier)
1121
+ ->set("effort", baton->jxlEffort)
1122
+ ->set("lossless", baton->jxlLossless));
1123
+ baton->formatOut = "jxl";
1093
1124
  } else if (baton->formatOut == "dz" || isDz || isDzZip) {
1094
1125
  // Write DZ to file
1095
1126
  if (isDzZip) {
@@ -1175,8 +1206,8 @@ class PipelineWorker : public Napi::AsyncWorker {
1175
1206
  // Add buffer size to info
1176
1207
  info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));
1177
1208
  // Pass ownership of output data to Buffer instance
1178
- Napi::Buffer<char> data = Napi::Buffer<char>::New(env, static_cast<char*>(baton->bufferOut),
1179
- baton->bufferOutLength, sharp::FreeCallback);
1209
+ Napi::Buffer<char> data = sharp::NewOrCopyBuffer(env, static_cast<char*>(baton->bufferOut),
1210
+ baton->bufferOutLength);
1180
1211
  Callback().MakeCallback(Receiver().Value(), { env.Null(), data, info });
1181
1212
  } else {
1182
1213
  // Add file size to info
@@ -1549,6 +1580,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1549
1580
  baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1550
1581
  baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1551
1582
  baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1583
+ baton->gifInterFrameMaxError = sharp::AttrAsDouble(options, "gifInterFrameMaxError");
1584
+ baton->gifInterPaletteMaxError = sharp::AttrAsDouble(options, "gifInterPaletteMaxError");
1552
1585
  baton->gifReoptimise = sharp::AttrAsBool(options, "gifReoptimise");
1553
1586
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1554
1587
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
@@ -1573,6 +1606,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1573
1606
  options, "heifCompression", VIPS_TYPE_FOREIGN_HEIF_COMPRESSION);
1574
1607
  baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1575
1608
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1609
+ baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
1610
+ baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
1611
+ baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");
1612
+ baton->jxlLossless = sharp::AttrAsBool(options, "jxlLossless");
1576
1613
  baton->rawDepth = sharp::AttrAsEnum<VipsBandFormat>(options, "rawDepth", VIPS_TYPE_BAND_FORMAT);
1577
1614
  // Animated output properties
1578
1615
  if (sharp::HasAttr(options, "loop")) {
package/src/pipeline.h CHANGED
@@ -163,6 +163,8 @@ struct PipelineBaton {
163
163
  int gifBitdepth;
164
164
  int gifEffort;
165
165
  double gifDither;
166
+ double gifInterFrameMaxError;
167
+ double gifInterPaletteMaxError;
166
168
  bool gifReoptimise;
167
169
  int tiffQuality;
168
170
  VipsForeignTiffCompression tiffCompression;
@@ -180,6 +182,10 @@ struct PipelineBaton {
180
182
  int heifEffort;
181
183
  std::string heifChromaSubsampling;
182
184
  bool heifLossless;
185
+ double jxlDistance;
186
+ int jxlDecodingTier;
187
+ int jxlEffort;
188
+ bool jxlLossless;
183
189
  VipsBandFormat rawDepth;
184
190
  std::string err;
185
191
  bool withMetadata;
@@ -314,6 +320,8 @@ struct PipelineBaton {
314
320
  gifBitdepth(8),
315
321
  gifEffort(7),
316
322
  gifDither(1.0),
323
+ gifInterFrameMaxError(0.0),
324
+ gifInterPaletteMaxError(3.0),
317
325
  gifReoptimise(false),
318
326
  tiffQuality(80),
319
327
  tiffCompression(VIPS_FOREIGN_TIFF_COMPRESSION_JPEG),
@@ -331,6 +339,10 @@ struct PipelineBaton {
331
339
  heifEffort(4),
332
340
  heifChromaSubsampling("4:4:4"),
333
341
  heifLossless(false),
342
+ jxlDistance(1.0),
343
+ jxlDecodingTier(0),
344
+ jxlEffort(7),
345
+ jxlLossless(false),
334
346
  rawDepth(VIPS_FORMAT_UCHAR),
335
347
  withMetadata(false),
336
348
  withMetadataOrientation(-1),
package/src/stats.cc CHANGED
@@ -176,6 +176,7 @@ Napi::Value stats(const Napi::CallbackInfo& info) {
176
176
 
177
177
  // Input
178
178
  baton->input = sharp::CreateInputDescriptor(options.Get("input").As<Napi::Object>());
179
+ baton->input->access = VIPS_ACCESS_RANDOM;
179
180
 
180
181
  // Function to notify of libvips warnings
181
182
  Napi::Function debuglog = options.Get("debuglog").As<Napi::Function>();
package/src/utilities.cc CHANGED
@@ -115,7 +115,7 @@ Napi::Value format(const Napi::CallbackInfo& info) {
115
115
  Napi::Object format = Napi::Object::New(env);
116
116
  for (std::string const f : {
117
117
  "jpeg", "png", "webp", "tiff", "magick", "openslide", "dz",
118
- "ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k"
118
+ "ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k", "jxl"
119
119
  }) {
120
120
  // Input
121
121
  const VipsObjectClass *oc = vips_class_find("VipsOperation", (f + "load").c_str());