sharp 0.31.1 → 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,7 +111,9 @@ 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
- * When using a `fit` of `cover` or `contain`, the default **position** is `centre`. Other options are:
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
+ *
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`.
117
119
  * - `sharp.strategy`: `cover` only, dynamically crop using either the `entropy` or `attention` strategy.
@@ -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.1",
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=20000 ./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",
@@ -133,7 +133,7 @@
133
133
  "detect-libc": "^2.0.1",
134
134
  "node-addon-api": "^5.0.0",
135
135
  "prebuild-install": "^7.1.1",
136
- "semver": "^7.3.7",
136
+ "semver": "^7.3.8",
137
137
  "simple-get": "^4.0.1",
138
138
  "tar-fs": "^2.1.1",
139
139
  "tunnel-agent": "^0.6.0"
@@ -141,13 +141,13 @@
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.0.0",
150
- "mock-fs": "^5.1.4",
149
+ "mocha": "^10.2.0",
150
+ "mock-fs": "^5.2.0",
151
151
  "nyc": "^15.1.0",
152
152
  "prebuild": "^11.0.4",
153
153
  "rimraf": "^3.0.2",
@@ -155,19 +155,19 @@
155
155
  },
156
156
  "license": "Apache-2.0",
157
157
  "config": {
158
- "libvips": "8.13.2",
158
+ "libvips": "8.13.3",
159
159
  "integrity": {
160
- "darwin-arm64v8": "sha512-4tsE/HMQDT9srV/ovSJlr7IxKnhvH9qpArCAf5Xpb/uNcAiT7BcZ+HYwX2lbf3UY8REB1TR4ThEL/lmPnzMUHw==",
161
- "darwin-x64": "sha512-D4ZSvlgLpf+KzKB2OD+K8NWl0JKzzIbvWwIjjwBycIHTMkaiams3Kp/AQ/bKudqof02Ks6LtP0X4XWvCaoRoUA==",
162
- "linux-arm64v8": "sha512-9ZvUM2NBluhoeUz9X7/zJ48xJ5d7KzI1cO6lsiv4HKo5fOYw/vEY28XodFJzhyfu9NuKxh3Hs9FtoQGNvvAFkw==",
163
- "linux-armv6": "sha512-vu0R8DF0k7KseU62fzrJadHNk5oeJriFLVn3KxCKEfV+Wkj7rX4lQhiPmOuD7/wRcUY+GGdoZ52vysDwMQhfzA==",
164
- "linux-armv7": "sha512-UdfhJTjGFgrwc3Kaos5G1ZAK2+t/16Prtnl6FAT+m7cG5EXzYAqzgvk4qtakAH7UTnVe8MUgOfbTLt0YiRpfsg==",
165
- "linux-x64": "sha512-sv92VpPyN+3oBv0vi4wDjx51demGdtyhEjd+vDfC3h8S/RSuIUE9Pt/+dBFuf+iv9tRdIq9hH9vzAvsLVy6NYg==",
166
- "linuxmusl-arm64v8": "sha512-TjhK/wHAS/m55l46T8PZ0qvlK+PKYFZGTQfh+c9aG8/z1v/VtG7TQOLNmPWfg0SFDTkXV7YqnJCqvgYLmJPZUg==",
167
- "linuxmusl-x64": "sha512-/su96pn/H9+lDdnlM1xB2whWEoeEDJICFp/RNRJb0+bJPJhnL/IDVIhF4VnVNBq/9AlldBWii3hqMq5rY2eEAA==",
168
- "win32-arm64v8": "sha512-UnSmwCcx3F5u4UOXyrdwTdYsuMK/RtQYc+1y+QxqIkBHiSL7dOlTIH/vKOSQvSaDQTPqxVLFt3wkMN1U7LZwyg==",
169
- "win32-ia32": "sha512-KH/H6vpx5lJ6NEzLQmwxU/QnDg8p1Jxd+WKaPiyWmXq/HpwyKrZhi3WDoyKD4fLwnlfhAXEfVLZbUbhX21pDpQ==",
170
- "win32-x64": "sha512-Xim5F21pqx7MuVQViaQNhSz24zWIiKHC9bm4KCdi7q/ytbvdMhm6bzWDI/mvFGNjI62NRB2SBkTTaqwJvM/pUg=="
160
+ "darwin-arm64v8": "sha512-xFgYt7CtQSZcWoyUdzPTDNHbioZIrZSEU+gkMxzH4Cgjhi4/N49UsonnIZhKQoTBGloAqEexHeMx4rYTQ2Kgvw==",
161
+ "darwin-x64": "sha512-6SivWKzu15aUMMohe0wg7sNYMPETVnOe40BuWsnKOgzl3o5FpQqNSgs+68Mi8Za3Qti9/DaR+H/fyD0x48Af2w==",
162
+ "linux-arm64v8": "sha512-b+iI9V/ehgDabXYRQcvqa5CEysh+1FQsgFmYc358StCrJCDahwNmsQdsiH1GOVd5WaWh5wHUGByPwMmFOO16Aw==",
163
+ "linux-armv6": "sha512-zRP2F+EiustLE4bXSH8AHCxwfemh9d+QuvmPjira/HL6uJOUuA7SyQgVV1TPwTQle2ioCNnKPm7FEB/MAiT+ug==",
164
+ "linux-armv7": "sha512-6OCChowE5lBXXXAZrnGdA9dVktg7UdODEBpE5qTroiAJYZv4yXRMgyDFYajok7du2NTgoklhxGk8d9+4vGv5hg==",
165
+ "linux-x64": "sha512-OTmlmP2r8ozGKdB96X+K5oQE1ojVZanqLqqKlwDpEnfixyIaDGYbVzcjWBNGU3ai/26bvkaCkjynnc2ecYcsuA==",
166
+ "linuxmusl-arm64v8": "sha512-Qh5Wi+bkKTohFYHzSPssfjMhIkD6z6EHbVmnwmWSsgY9zsUBStFp6+mKcNTQfP5YM5Mz06vJOkLHX2OzEr5TzA==",
167
+ "linuxmusl-x64": "sha512-DwB4Fs3+ISw9etaLCANkueZDdk758iOS+wNp4TKZkHdq0al6B/3Pk7OHLR8a9E3H6wYDD328u++dcJzip5tacA==",
168
+ "win32-arm64v8": "sha512-96r3W+O4BtX602B1MtxU5Ru4lKzRRTZqM4OQEBJ//TNL3fiCZdd9agD+RQBjaeR4KFIyBSt3F7IE425ZWmxz+w==",
169
+ "win32-ia32": "sha512-qfN1MsfQGek1QQd1UNW7JT+5K5Ne1suFQ2GpgpYm3JLSpIve/tz2vOGEGzvTVssOBADJvAkTDFt+yIi3PgU9pA==",
170
+ "win32-x64": "sha512-eb3aAmjbVVBVRbiYgebQwoxkAt69WI8nwmKlilSQ3kWqoc0pXfIe322rF2UR8ebbISCGvYRUfzD2r1k92RXISQ=="
171
171
  },
172
172
  "runtime": "napi",
173
173
  "target": 7
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
@@ -26,8 +26,8 @@
26
26
 
27
27
  #if (VIPS_MAJOR_VERSION < 8) || \
28
28
  (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 13) || \
29
- (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 13 && VIPS_MICRO_VERSION < 2)
30
- #error "libvips version 8.13.2+ is required - please see https://sharp.pixelplumbing.com/install"
29
+ (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 13 && VIPS_MICRO_VERSION < 3)
30
+ #error "libvips version 8.13.3+ is required - please see https://sharp.pixelplumbing.com/install"
31
31
  #endif
32
32
 
33
33
  #if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6)))
@@ -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
@@ -81,35 +81,47 @@ class PipelineWorker : public Napi::AsyncWorker {
81
81
  int pageHeight = sharp::GetPageHeight(image);
82
82
 
83
83
  // Calculate angle of rotation
84
- VipsAngle rotation;
85
- bool flip = FALSE;
86
- bool flop = FALSE;
84
+ VipsAngle rotation = VIPS_ANGLE_D0;
85
+ VipsAngle autoRotation = VIPS_ANGLE_D0;
86
+ bool autoFlip = FALSE;
87
+ bool autoFlop = FALSE;
87
88
  if (baton->useExifOrientation) {
88
89
  // Rotate and flip image according to Exif orientation
89
- std::tie(rotation, flip, flop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
90
+ std::tie(autoRotation, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
91
+ image = sharp::RemoveExifOrientation(image);
90
92
  } else {
91
93
  rotation = CalculateAngleRotation(baton->angle);
92
94
  }
93
95
 
94
96
  // Rotate pre-extract
95
97
  bool const shouldRotateBefore = baton->rotateBeforePreExtract &&
96
- (rotation != VIPS_ANGLE_D0 || flip || flop || baton->rotationAngle != 0.0);
98
+ (rotation != VIPS_ANGLE_D0 || autoRotation != VIPS_ANGLE_D0 ||
99
+ autoFlip || baton->flip || autoFlop || baton->flop ||
100
+ baton->rotationAngle != 0.0);
97
101
 
98
102
  if (shouldRotateBefore) {
99
- if (rotation != VIPS_ANGLE_D0) {
100
- image = image.rot(rotation);
103
+ if (autoRotation != VIPS_ANGLE_D0) {
104
+ image = image.rot(autoRotation);
105
+ autoRotation = VIPS_ANGLE_D0;
101
106
  }
102
- if (flip) {
107
+ if (autoFlip) {
108
+ image = image.flip(VIPS_DIRECTION_VERTICAL);
109
+ autoFlip = FALSE;
110
+ } else if (baton->flip) {
103
111
  image = image.flip(VIPS_DIRECTION_VERTICAL);
112
+ baton->flip = FALSE;
104
113
  }
105
- if (flop) {
114
+ if (autoFlop) {
106
115
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
116
+ autoFlop = FALSE;
117
+ } else if (baton->flop) {
118
+ image = image.flip(VIPS_DIRECTION_HORIZONTAL);
119
+ baton->flop = FALSE;
107
120
  }
108
- if (rotation != VIPS_ANGLE_D0 || flip || flop) {
109
- image = sharp::RemoveExifOrientation(image);
121
+ if (rotation != VIPS_ANGLE_D0) {
122
+ image = image.rot(rotation);
123
+ rotation = VIPS_ANGLE_D0;
110
124
  }
111
- flop = FALSE;
112
- flip = FALSE;
113
125
  if (baton->rotationAngle != 0.0) {
114
126
  MultiPageUnsupported(nPages, "Rotate");
115
127
  std::vector<double> background;
@@ -150,7 +162,9 @@ class PipelineWorker : public Napi::AsyncWorker {
150
162
  int targetResizeHeight = baton->height;
151
163
 
152
164
  // Swap input output width and height when rotating by 90 or 270 degrees
153
- bool swap = !baton->rotateBeforePreExtract && (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270);
165
+ bool swap = !baton->rotateBeforePreExtract &&
166
+ (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270 ||
167
+ autoRotation == VIPS_ANGLE_D90 || autoRotation == VIPS_ANGLE_D270);
154
168
 
155
169
  // Shrink to pageHeight, so we work for multi-page images
156
170
  std::tie(hshrink, vshrink) = sharp::ResolveShrink(
@@ -367,30 +381,21 @@ class PipelineWorker : public Napi::AsyncWorker {
367
381
  ->set("kernel", baton->kernel));
368
382
  }
369
383
 
384
+ // Auto-rotate post-extract
385
+ if (autoRotation != VIPS_ANGLE_D0) {
386
+ image = image.rot(autoRotation);
387
+ }
370
388
  // Flip (mirror about Y axis)
371
- if (baton->flip || flip) {
389
+ if (baton->flip || autoFlip) {
372
390
  image = image.flip(VIPS_DIRECTION_VERTICAL);
373
- image = sharp::RemoveExifOrientation(image);
374
391
  }
375
-
376
392
  // Flop (mirror about X axis)
377
- if (baton->flop || flop) {
393
+ if (baton->flop || autoFlop) {
378
394
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
379
- image = sharp::RemoveExifOrientation(image);
380
395
  }
381
-
382
396
  // Rotate post-extract 90-angle
383
- if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
397
+ if (rotation != VIPS_ANGLE_D0) {
384
398
  image = image.rot(rotation);
385
- if (flip) {
386
- image = image.flip(VIPS_DIRECTION_VERTICAL);
387
- flip = FALSE;
388
- }
389
- if (flop) {
390
- image = image.flip(VIPS_DIRECTION_HORIZONTAL);
391
- flop = FALSE;
392
- }
393
- image = sharp::RemoveExifOrientation(image);
394
399
  }
395
400
 
396
401
  // Join additional color channels to the image
@@ -696,24 +701,6 @@ class PipelineWorker : public Napi::AsyncWorker {
696
701
  image = sharp::Tint(image, baton->tintA, baton->tintB);
697
702
  }
698
703
 
699
- // Extract an image channel (aka vips band)
700
- if (baton->extractChannel > -1) {
701
- if (baton->extractChannel >= image.bands()) {
702
- if (baton->extractChannel == 3 && sharp::HasAlpha(image)) {
703
- baton->extractChannel = image.bands() - 1;
704
- } else {
705
- (baton->err).append("Cannot extract channel from image. Too few channels in image.");
706
- return Error();
707
- }
708
- }
709
- VipsInterpretation const interpretation = sharp::Is16Bit(image.interpretation())
710
- ? VIPS_INTERPRETATION_GREY16
711
- : VIPS_INTERPRETATION_B_W;
712
- image = image
713
- .extract_band(baton->extractChannel)
714
- .copy(VImage::option()->set("interpretation", interpretation));
715
- }
716
-
717
704
  // Remove alpha channel, if any
718
705
  if (baton->removeAlpha) {
719
706
  image = sharp::RemoveAlpha(image);
@@ -739,6 +726,26 @@ class PipelineWorker : public Napi::AsyncWorker {
739
726
  }
740
727
  }
741
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
+
742
749
  // Apply output ICC profile
743
750
  if (!baton->withMetadataIcc.empty()) {
744
751
  image = image.icc_transform(
@@ -864,6 +871,8 @@ class PipelineWorker : public Napi::AsyncWorker {
864
871
  ->set("bitdepth", baton->gifBitdepth)
865
872
  ->set("effort", baton->gifEffort)
866
873
  ->set("reoptimise", baton->gifReoptimise)
874
+ ->set("interframe_maxerror", baton->gifInterFrameMaxError)
875
+ ->set("interpalette_maxerror", baton->gifInterPaletteMaxError)
867
876
  ->set("dither", baton->gifDither)));
868
877
  baton->bufferOut = static_cast<char*>(area->data);
869
878
  baton->bufferOutLength = area->length;
@@ -930,6 +939,21 @@ class PipelineWorker : public Napi::AsyncWorker {
930
939
  area->free_fn = nullptr;
931
940
  vips_area_unref(area);
932
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";
933
957
  } else if (baton->formatOut == "raw" ||
934
958
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::RAW)) {
935
959
  // Write raw, uncompressed image data to buffer
@@ -968,6 +992,7 @@ class PipelineWorker : public Napi::AsyncWorker {
968
992
  bool const isTiff = sharp::IsTiff(baton->fileOut);
969
993
  bool const isJp2 = sharp::IsJp2(baton->fileOut);
970
994
  bool const isHeif = sharp::IsHeif(baton->fileOut);
995
+ bool const isJxl = sharp::IsJxl(baton->fileOut);
971
996
  bool const isDz = sharp::IsDz(baton->fileOut);
972
997
  bool const isDzZip = sharp::IsDzZip(baton->fileOut);
973
998
  bool const isV = sharp::IsV(baton->fileOut);
@@ -1085,6 +1110,17 @@ class PipelineWorker : public Napi::AsyncWorker {
1085
1110
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1086
1111
  ->set("lossless", baton->heifLossless));
1087
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";
1088
1124
  } else if (baton->formatOut == "dz" || isDz || isDzZip) {
1089
1125
  // Write DZ to file
1090
1126
  if (isDzZip) {
@@ -1170,8 +1206,8 @@ class PipelineWorker : public Napi::AsyncWorker {
1170
1206
  // Add buffer size to info
1171
1207
  info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));
1172
1208
  // Pass ownership of output data to Buffer instance
1173
- Napi::Buffer<char> data = Napi::Buffer<char>::New(env, static_cast<char*>(baton->bufferOut),
1174
- baton->bufferOutLength, sharp::FreeCallback);
1209
+ Napi::Buffer<char> data = sharp::NewOrCopyBuffer(env, static_cast<char*>(baton->bufferOut),
1210
+ baton->bufferOutLength);
1175
1211
  Callback().MakeCallback(Receiver().Value(), { env.Null(), data, info });
1176
1212
  } else {
1177
1213
  // Add file size to info
@@ -1544,6 +1580,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1544
1580
  baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1545
1581
  baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1546
1582
  baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1583
+ baton->gifInterFrameMaxError = sharp::AttrAsDouble(options, "gifInterFrameMaxError");
1584
+ baton->gifInterPaletteMaxError = sharp::AttrAsDouble(options, "gifInterPaletteMaxError");
1547
1585
  baton->gifReoptimise = sharp::AttrAsBool(options, "gifReoptimise");
1548
1586
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1549
1587
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
@@ -1568,6 +1606,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1568
1606
  options, "heifCompression", VIPS_TYPE_FOREIGN_HEIF_COMPRESSION);
1569
1607
  baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1570
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");
1571
1613
  baton->rawDepth = sharp::AttrAsEnum<VipsBandFormat>(options, "rawDepth", VIPS_TYPE_BAND_FORMAT);
1572
1614
  // Animated output properties
1573
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());