sharp 0.33.5 → 0.35.0

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.
Files changed (47) hide show
  1. package/README.md +12 -18
  2. package/dist/channel.cjs +177 -0
  3. package/{lib/channel.js → dist/channel.mjs} +11 -8
  4. package/dist/colour.cjs +195 -0
  5. package/{lib/colour.js → dist/colour.mjs} +38 -23
  6. package/dist/composite.cjs +213 -0
  7. package/{lib/composite.js → dist/composite.mjs} +14 -11
  8. package/{lib/constructor.js → dist/constructor.cjs} +102 -43
  9. package/dist/constructor.mjs +511 -0
  10. package/dist/index.cjs +25 -0
  11. package/dist/index.mjs +25 -0
  12. package/dist/input.cjs +814 -0
  13. package/{lib/input.js → dist/input.mjs} +224 -68
  14. package/{lib/is.js → dist/is.cjs} +19 -45
  15. package/dist/is.mjs +143 -0
  16. package/{lib/libvips.js → dist/libvips.cjs} +52 -43
  17. package/dist/libvips.mjs +212 -0
  18. package/dist/operation.cjs +987 -0
  19. package/{lib/operation.js → dist/operation.mjs} +108 -79
  20. package/{lib/output.js → dist/output.cjs} +243 -36
  21. package/dist/output.mjs +1794 -0
  22. package/dist/resize.cjs +613 -0
  23. package/{lib/resize.js → dist/resize.mjs} +38 -12
  24. package/dist/sharp.cjs +164 -0
  25. package/dist/sharp.mjs +164 -0
  26. package/{lib/utility.js → dist/utility.cjs} +23 -24
  27. package/dist/utility.mjs +295 -0
  28. package/install/build.js +38 -0
  29. package/lib/index.d.ts +347 -125
  30. package/package.json +75 -84
  31. package/src/binding.gyp +49 -26
  32. package/src/common.cc +203 -111
  33. package/src/common.h +68 -40
  34. package/src/metadata.cc +106 -22
  35. package/src/metadata.h +15 -5
  36. package/src/operations.cc +62 -21
  37. package/src/operations.h +15 -3
  38. package/src/pipeline.cc +374 -183
  39. package/src/pipeline.h +34 -7
  40. package/src/sharp.cc +11 -8
  41. package/src/stats.cc +16 -13
  42. package/src/stats.h +6 -3
  43. package/src/utilities.cc +32 -12
  44. package/src/utilities.h +5 -2
  45. package/install/check.js +0 -41
  46. package/lib/index.js +0 -16
  47. package/lib/sharp.js +0 -114
package/src/pipeline.cc CHANGED
@@ -1,8 +1,11 @@
1
- // Copyright 2013 Lovell Fuller and others.
2
- // SPDX-License-Identifier: Apache-2.0
1
+ /*!
2
+ Copyright 2013 Lovell Fuller and others.
3
+ SPDX-License-Identifier: Apache-2.0
4
+ */
3
5
 
4
6
  #include <algorithm>
5
7
  #include <cmath>
8
+ #include <filesystem> // NOLINT(build/c++17)
6
9
  #include <map>
7
10
  #include <memory>
8
11
  #include <numeric>
@@ -16,20 +19,9 @@
16
19
  #include <vips/vips8>
17
20
  #include <napi.h>
18
21
 
19
- #include "common.h"
20
- #include "operations.h"
21
- #include "pipeline.h"
22
-
23
- #ifdef _WIN32
24
- #define STAT64_STRUCT __stat64
25
- #define STAT64_FUNCTION _stat64
26
- #elif defined(_LARGEFILE64_SOURCE)
27
- #define STAT64_STRUCT stat64
28
- #define STAT64_FUNCTION stat64
29
- #else
30
- #define STAT64_STRUCT stat
31
- #define STAT64_FUNCTION stat
32
- #endif
22
+ #include "./common.h"
23
+ #include "./operations.h"
24
+ #include "./pipeline.h"
33
25
 
34
26
  class PipelineWorker : public Napi::AsyncWorker {
35
27
  public:
@@ -52,7 +44,39 @@ class PipelineWorker : public Napi::AsyncWorker {
52
44
  // Open input
53
45
  vips::VImage image;
54
46
  sharp::ImageType inputImageType;
55
- std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
47
+ if (baton->join.empty()) {
48
+ std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
49
+ } else {
50
+ std::vector<VImage> images;
51
+ bool hasAlpha = false;
52
+ for (auto &join : baton->join) {
53
+ std::tie(image, inputImageType) = sharp::OpenInput(join);
54
+ image = sharp::EnsureColourspace(image, baton->colourspacePipeline);
55
+ images.push_back(image);
56
+ hasAlpha |= image.has_alpha();
57
+ }
58
+ if (hasAlpha) {
59
+ for (auto &image : images) {
60
+ if (!image.has_alpha()) {
61
+ image = sharp::EnsureAlpha(image, 1);
62
+ }
63
+ }
64
+ } else {
65
+ baton->input->joinBackground.pop_back();
66
+ }
67
+ inputImageType = sharp::ImageType::PNG;
68
+ image = VImage::arrayjoin(images, VImage::option()
69
+ ->set("across", baton->input->joinAcross)
70
+ ->set("shim", baton->input->joinShim)
71
+ ->set("background", baton->input->joinBackground)
72
+ ->set("halign", baton->input->joinHalign)
73
+ ->set("valign", baton->input->joinValign));
74
+ if (baton->input->joinAnimated) {
75
+ image = image.copy();
76
+ image.set(VIPS_META_N_PAGES, static_cast<int>(images.size()));
77
+ image.set(VIPS_META_PAGE_HEIGHT, static_cast<int>(image.height() / images.size()));
78
+ }
79
+ }
56
80
  VipsAccess access = baton->input->access;
57
81
  image = sharp::EnsureColourspace(image, baton->colourspacePipeline);
58
82
 
@@ -60,7 +84,7 @@ class PipelineWorker : public Napi::AsyncWorker {
60
84
  if (nPages == -1) {
61
85
  // Resolve the number of pages if we need to render until the end of the document
62
86
  nPages = image.get_typeof(VIPS_META_N_PAGES) != 0
63
- ? image.get_int(VIPS_META_N_PAGES) - baton->input->page
87
+ ? image.get_int(VIPS_META_N_PAGES) - std::max(0, baton->input->page)
64
88
  : 1;
65
89
  }
66
90
 
@@ -70,31 +94,22 @@ class PipelineWorker : public Napi::AsyncWorker {
70
94
  // Calculate angle of rotation
71
95
  VipsAngle rotation = VIPS_ANGLE_D0;
72
96
  VipsAngle autoRotation = VIPS_ANGLE_D0;
73
- bool autoFlip = false;
74
97
  bool autoFlop = false;
75
98
 
76
- if (baton->useExifOrientation) {
99
+ if (baton->input->autoOrient) {
77
100
  // Rotate and flip image according to Exif orientation
78
- std::tie(autoRotation, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
79
- image = sharp::RemoveExifOrientation(image);
80
- } else {
81
- rotation = CalculateAngleRotation(baton->angle);
101
+ std::tie(autoRotation, autoFlop) = CalculateExifRotationAndFlop(sharp::ExifOrientation(image));
82
102
  }
83
103
 
84
- // Rotate pre-extract
85
- bool const shouldRotateBefore = baton->rotateBeforePreExtract &&
86
- (rotation != VIPS_ANGLE_D0 || autoRotation != VIPS_ANGLE_D0 ||
87
- autoFlip || baton->flip || autoFlop || baton->flop ||
88
- baton->rotationAngle != 0.0);
104
+ rotation = CalculateAngleRotation(baton->angle);
89
105
 
90
- if (shouldRotateBefore) {
91
- image = sharp::StaySequential(image,
92
- rotation != VIPS_ANGLE_D0 ||
93
- autoRotation != VIPS_ANGLE_D0 ||
94
- autoFlip ||
95
- baton->flip ||
96
- baton->rotationAngle != 0.0);
106
+ bool const shouldRotateBefore = baton->rotateBefore &&
107
+ (rotation != VIPS_ANGLE_D0 || baton->flip || baton->flop || baton->rotationAngle != 0.0);
108
+ bool const shouldOrientBefore = (shouldRotateBefore || baton->orientBefore) &&
109
+ (autoRotation != VIPS_ANGLE_D0 || autoFlop);
97
110
 
111
+ if (shouldOrientBefore) {
112
+ image = sharp::StaySequential(image, autoRotation != VIPS_ANGLE_D0);
98
113
  if (autoRotation != VIPS_ANGLE_D0) {
99
114
  if (autoRotation != VIPS_ANGLE_D180) {
100
115
  MultiPageUnsupported(nPages, "Rotate");
@@ -102,17 +117,19 @@ class PipelineWorker : public Napi::AsyncWorker {
102
117
  image = image.rot(autoRotation);
103
118
  autoRotation = VIPS_ANGLE_D0;
104
119
  }
105
- if (autoFlip) {
106
- image = image.flip(VIPS_DIRECTION_VERTICAL);
107
- autoFlip = false;
108
- } else if (baton->flip) {
109
- image = image.flip(VIPS_DIRECTION_VERTICAL);
110
- baton->flip = false;
111
- }
112
120
  if (autoFlop) {
113
121
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
114
122
  autoFlop = false;
115
- } else if (baton->flop) {
123
+ }
124
+ }
125
+
126
+ if (shouldRotateBefore) {
127
+ image = sharp::StaySequential(image, rotation != VIPS_ANGLE_D0 || baton->flip || baton->rotationAngle != 0.0);
128
+ if (baton->flip) {
129
+ image = image.flip(VIPS_DIRECTION_VERTICAL);
130
+ baton->flip = false;
131
+ }
132
+ if (baton->flop) {
116
133
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
117
134
  baton->flop = false;
118
135
  }
@@ -128,6 +145,7 @@ class PipelineWorker : public Napi::AsyncWorker {
128
145
  std::vector<double> background;
129
146
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, false);
130
147
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background)).copy_memory();
148
+ baton->rotationAngle = 0.0;
131
149
  }
132
150
  }
133
151
 
@@ -135,7 +153,7 @@ class PipelineWorker : public Napi::AsyncWorker {
135
153
  if (baton->trimThreshold >= 0.0) {
136
154
  MultiPageUnsupported(nPages, "Trim");
137
155
  image = sharp::StaySequential(image);
138
- image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt);
156
+ image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt, baton->trimMargin);
139
157
  baton->trimOffsetLeft = image.xoffset();
140
158
  baton->trimOffsetTop = image.yoffset();
141
159
  }
@@ -166,8 +184,7 @@ class PipelineWorker : public Napi::AsyncWorker {
166
184
  // When auto-rotating by 90 or 270 degrees, swap the target width and
167
185
  // height to ensure the behavior aligns with how it would have been if
168
186
  // the rotation had taken place *before* resizing.
169
- if (!baton->rotateBeforePreExtract &&
170
- (autoRotation == VIPS_ANGLE_D90 || autoRotation == VIPS_ANGLE_D270)) {
187
+ if (autoRotation == VIPS_ANGLE_D90 || autoRotation == VIPS_ANGLE_D270) {
171
188
  std::swap(targetResizeWidth, targetResizeHeight);
172
189
  }
173
190
 
@@ -186,10 +203,12 @@ class PipelineWorker : public Napi::AsyncWorker {
186
203
  // - the width or height parameters are specified;
187
204
  // - gamma correction doesn't need to be applied;
188
205
  // - trimming or pre-resize extract isn't required;
206
+ // - gain map processing is not required;
189
207
  // - input colourspace is not specified;
190
208
  bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
191
209
  baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold < 0.0 &&
192
- baton->colourspacePipeline == VIPS_INTERPRETATION_LAST && !shouldRotateBefore;
210
+ !baton->keepGainMap && !baton->withGainMap &&
211
+ baton->colourspacePipeline == VIPS_INTERPRETATION_LAST && !(shouldOrientBefore || shouldRotateBefore);
193
212
 
194
213
  if (shouldPreShrink) {
195
214
  // The common part of the shrink: the bit by which both axes must be shrunk
@@ -223,11 +242,7 @@ class PipelineWorker : public Napi::AsyncWorker {
223
242
  // factor for jpegload*, a double scale factor for webpload*,
224
243
  // pdfload* and svgload*
225
244
  if (jpegShrinkOnLoad > 1) {
226
- vips::VOption *option = VImage::option()
227
- ->set("access", access)
228
- ->set("shrink", jpegShrinkOnLoad)
229
- ->set("unlimited", baton->input->unlimited)
230
- ->set("fail_on", baton->input->failOn);
245
+ vips::VOption *option = GetOptionsForImageType(inputImageType, baton->input)->set("shrink", jpegShrinkOnLoad);
231
246
  if (baton->input->buffer != nullptr) {
232
247
  // Reload JPEG buffer
233
248
  VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
@@ -238,14 +253,8 @@ class PipelineWorker : public Napi::AsyncWorker {
238
253
  image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option);
239
254
  }
240
255
  } else if (scale != 1.0) {
241
- vips::VOption *option = VImage::option()
242
- ->set("access", access)
243
- ->set("scale", scale)
244
- ->set("fail_on", baton->input->failOn);
256
+ vips::VOption *option = GetOptionsForImageType(inputImageType, baton->input)->set("scale", scale);
245
257
  if (inputImageType == sharp::ImageType::WEBP) {
246
- option->set("n", baton->input->pages);
247
- option->set("page", baton->input->page);
248
-
249
258
  if (baton->input->buffer != nullptr) {
250
259
  // Reload WebP buffer
251
260
  VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
@@ -256,9 +265,6 @@ class PipelineWorker : public Napi::AsyncWorker {
256
265
  image = VImage::webpload(const_cast<char*>(baton->input->file.data()), option);
257
266
  }
258
267
  } else if (inputImageType == sharp::ImageType::SVG) {
259
- option->set("unlimited", baton->input->unlimited);
260
- option->set("dpi", baton->input->density);
261
-
262
268
  if (baton->input->buffer != nullptr) {
263
269
  // Reload SVG buffer
264
270
  VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
@@ -270,13 +276,9 @@ class PipelineWorker : public Napi::AsyncWorker {
270
276
  }
271
277
  sharp::SetDensity(image, baton->input->density);
272
278
  if (image.width() > 32767 || image.height() > 32767) {
273
- throw vips::VError("Input SVG image will exceed 32767x32767 pixel limit when scaled");
279
+ throw std::runtime_error("Input SVG image will exceed 32767x32767 pixel limit when scaled");
274
280
  }
275
281
  } else if (inputImageType == sharp::ImageType::PDF) {
276
- option->set("n", baton->input->pages);
277
- option->set("page", baton->input->page);
278
- option->set("dpi", baton->input->density);
279
-
280
282
  if (baton->input->buffer != nullptr) {
281
283
  // Reload PDF buffer
282
284
  VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
@@ -286,14 +288,32 @@ class PipelineWorker : public Napi::AsyncWorker {
286
288
  // Reload PDF file
287
289
  image = VImage::pdfload(const_cast<char*>(baton->input->file.data()), option);
288
290
  }
289
-
290
291
  sharp::SetDensity(image, baton->input->density);
291
292
  }
292
293
  } else {
293
294
  if (inputImageType == sharp::ImageType::SVG && (image.width() > 32767 || image.height() > 32767)) {
294
- throw vips::VError("Input SVG image exceeds 32767x32767 pixel limit");
295
+ throw std::runtime_error("Input SVG image exceeds 32767x32767 pixel limit");
295
296
  }
296
297
  }
298
+ if (baton->input->autoOrient) {
299
+ image = sharp::RemoveExifOrientation(image);
300
+ }
301
+ VImage gainMap;
302
+ int gainMapScaleFactor = 1;
303
+ if (sharp::HasGainMap(image)) {
304
+ if (baton->keepGainMap) {
305
+ gainMap = image.gainmap();
306
+ if (image.get_typeof("gainmap-scale-factor") == G_TYPE_INT) {
307
+ gainMapScaleFactor = image.get_int("gainmap-scale-factor");
308
+ }
309
+ } else if (baton->withGainMap) {
310
+ image = image.uhdr2scRGB();
311
+ }
312
+ image = sharp::RemoveGainMap(image);
313
+ } else {
314
+ baton->keepGainMap = false;
315
+ baton->withGainMap = false;
316
+ }
297
317
 
298
318
  // Any pre-shrinking may already have been done
299
319
  inputWidth = image.width();
@@ -333,7 +353,7 @@ class PipelineWorker : public Napi::AsyncWorker {
333
353
  image.interpretation() != VIPS_INTERPRETATION_LABS &&
334
354
  image.interpretation() != VIPS_INTERPRETATION_GREY16 &&
335
355
  baton->colourspacePipeline != VIPS_INTERPRETATION_CMYK &&
336
- !baton->input->ignoreIcc
356
+ !baton->input->ignoreIcc && !baton->withGainMap
337
357
  ) {
338
358
  // Convert to sRGB/P3 using embedded profile
339
359
  try {
@@ -354,7 +374,7 @@ class PipelineWorker : public Napi::AsyncWorker {
354
374
  }
355
375
 
356
376
  // Flatten image to remove alpha channel
357
- if (baton->flatten && sharp::HasAlpha(image)) {
377
+ if (baton->flatten && image.has_alpha()) {
358
378
  image = sharp::Flatten(image, baton->flattenBackground);
359
379
  }
360
380
 
@@ -374,12 +394,12 @@ class PipelineWorker : public Napi::AsyncWorker {
374
394
  bool const shouldSharpen = baton->sharpenSigma != 0.0;
375
395
  bool const shouldComposite = !baton->composite.empty();
376
396
 
377
- if (shouldComposite && !sharp::HasAlpha(image)) {
397
+ if (shouldComposite && !image.has_alpha()) {
378
398
  image = sharp::EnsureAlpha(image, 1);
379
399
  }
380
400
 
381
401
  VipsBandFormat premultiplyFormat = image.format();
382
- bool const shouldPremultiplyAlpha = sharp::HasAlpha(image) &&
402
+ bool const shouldPremultiplyAlpha = image.has_alpha() &&
383
403
  (shouldResize || shouldBlur || shouldConv || shouldSharpen);
384
404
 
385
405
  if (shouldPremultiplyAlpha) {
@@ -391,12 +411,16 @@ class PipelineWorker : public Napi::AsyncWorker {
391
411
  image = image.resize(1.0 / hshrink, VImage::option()
392
412
  ->set("vscale", 1.0 / vshrink)
393
413
  ->set("kernel", baton->kernel));
414
+ if (baton->keepGainMap) {
415
+ gainMap = gainMap.resize(1.0 / hshrink, VImage::option()
416
+ ->set("vscale", 1.0 / vshrink)
417
+ ->set("kernel", baton->kernel));
418
+ }
394
419
  }
395
420
 
396
421
  image = sharp::StaySequential(image,
397
422
  autoRotation != VIPS_ANGLE_D0 ||
398
423
  baton->flip ||
399
- autoFlip ||
400
424
  rotation != VIPS_ANGLE_D0);
401
425
  // Auto-rotate post-extract
402
426
  if (autoRotation != VIPS_ANGLE_D0) {
@@ -404,14 +428,23 @@ class PipelineWorker : public Napi::AsyncWorker {
404
428
  MultiPageUnsupported(nPages, "Rotate");
405
429
  }
406
430
  image = image.rot(autoRotation);
431
+ if (baton->keepGainMap) {
432
+ gainMap = gainMap.rot(autoRotation);
433
+ }
407
434
  }
408
435
  // Mirror vertically (up-down) about the x-axis
409
- if (baton->flip || autoFlip) {
436
+ if (baton->flip) {
410
437
  image = image.flip(VIPS_DIRECTION_VERTICAL);
438
+ if (baton->keepGainMap) {
439
+ gainMap = gainMap.flip(VIPS_DIRECTION_VERTICAL);
440
+ }
411
441
  }
412
442
  // Mirror horizontally (left-right) about the y-axis
413
- if (baton->flop || autoFlop) {
443
+ if (baton->flop != autoFlop) {
414
444
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
445
+ if (baton->keepGainMap) {
446
+ gainMap = gainMap.flip(VIPS_DIRECTION_HORIZONTAL);
447
+ }
415
448
  }
416
449
  // Rotate post-extract 90-angle
417
450
  if (rotation != VIPS_ANGLE_D0) {
@@ -419,6 +452,9 @@ class PipelineWorker : public Napi::AsyncWorker {
419
452
  MultiPageUnsupported(nPages, "Rotate");
420
453
  }
421
454
  image = image.rot(rotation);
455
+ if (baton->keepGainMap) {
456
+ gainMap = gainMap.rot(rotation);
457
+ }
422
458
  }
423
459
 
424
460
  // Join additional color channels to the image
@@ -454,12 +490,10 @@ class PipelineWorker : public Napi::AsyncWorker {
454
490
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
455
491
 
456
492
  // Embed
457
- int left;
458
- int top;
459
- std::tie(left, top) = sharp::CalculateEmbedPosition(
493
+ const auto& [left, top] = sharp::CalculateEmbedPosition(
460
494
  inputWidth, inputHeight, baton->width, baton->height, baton->position);
461
- int width = std::max(inputWidth, baton->width);
462
- int height = std::max(inputHeight, baton->height);
495
+ const int width = std::max(inputWidth, baton->width);
496
+ const int height = std::max(inputHeight, baton->height);
463
497
 
464
498
  image = nPages > 1
465
499
  ? sharp::EmbedMultiPage(image,
@@ -467,6 +501,12 @@ class PipelineWorker : public Napi::AsyncWorker {
467
501
  : image.embed(left, top, width, height, VImage::option()
468
502
  ->set("extend", VIPS_EXTEND_BACKGROUND)
469
503
  ->set("background", background));
504
+ if (baton->keepGainMap) {
505
+ gainMap = gainMap.embed(left / gainMapScaleFactor, top / gainMapScaleFactor,
506
+ width / gainMapScaleFactor, height / gainMapScaleFactor, VImage::option()
507
+ ->set("extend", VIPS_EXTEND_BACKGROUND)
508
+ ->set("background", 0));
509
+ }
470
510
  } else if (baton->canvas == sharp::Canvas::CROP) {
471
511
  if (baton->width > inputWidth) {
472
512
  baton->width = inputWidth;
@@ -478,24 +518,26 @@ class PipelineWorker : public Napi::AsyncWorker {
478
518
  // Crop
479
519
  if (baton->position < 9) {
480
520
  // Gravity-based crop
481
- int left;
482
- int top;
483
-
484
- std::tie(left, top) = sharp::CalculateCrop(
521
+ const auto& [left, top] = sharp::CalculateCrop(
485
522
  inputWidth, inputHeight, baton->width, baton->height, baton->position);
486
- int width = std::min(inputWidth, baton->width);
487
- int height = std::min(inputHeight, baton->height);
523
+ const int width = std::min(inputWidth, baton->width);
524
+ const int height = std::min(inputHeight, baton->height);
488
525
 
489
526
  image = nPages > 1
490
527
  ? sharp::CropMultiPage(image,
491
528
  left, top, width, height, nPages, &targetPageHeight)
492
529
  : image.extract_area(left, top, width, height);
530
+ if (baton->keepGainMap) {
531
+ gainMap = gainMap.extract_area(left / gainMapScaleFactor, top / gainMapScaleFactor,
532
+ width / gainMapScaleFactor, height / gainMapScaleFactor);
533
+ }
493
534
  } else {
494
535
  int attention_x;
495
536
  int attention_y;
496
537
 
497
538
  // Attention-based or Entropy-based crop
498
539
  MultiPageUnsupported(nPages, "Resize strategy");
540
+ KeepGainMapUnsupported(baton->keepGainMap, "Resize strategy");
499
541
  image = sharp::StaySequential(image);
500
542
  image = image.smartcrop(baton->width, baton->height, VImage::option()
501
543
  ->set("interesting", baton->position == 16 ? VIPS_INTERESTING_ENTROPY : VIPS_INTERESTING_ATTENTION)
@@ -513,12 +555,15 @@ class PipelineWorker : public Napi::AsyncWorker {
513
555
  }
514
556
 
515
557
  // Rotate post-extract non-90 angle
516
- if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
558
+ if (!baton->rotateBefore && baton->rotationAngle != 0.0) {
517
559
  MultiPageUnsupported(nPages, "Rotate");
518
560
  image = sharp::StaySequential(image);
519
561
  std::vector<double> background;
520
562
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
521
563
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
564
+ if (baton->keepGainMap) {
565
+ gainMap = gainMap.rotate(baton->rotationAngle, VImage::option()->set("background", 0));
566
+ }
522
567
  }
523
568
 
524
569
  // Post extraction
@@ -527,18 +572,23 @@ class PipelineWorker : public Napi::AsyncWorker {
527
572
  image = sharp::CropMultiPage(image,
528
573
  baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost,
529
574
  nPages, &targetPageHeight);
530
-
531
575
  // heightPost is used in the info object, so update to reflect the number of pages
532
576
  baton->heightPost *= nPages;
533
577
  } else {
534
578
  image = image.extract_area(
535
579
  baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
580
+ if (baton->keepGainMap) {
581
+ gainMap = gainMap.extract_area(baton->leftOffsetPost / gainMapScaleFactor,
582
+ baton->topOffsetPost / gainMapScaleFactor, baton->widthPost / gainMapScaleFactor,
583
+ baton->heightPost / gainMapScaleFactor);
584
+ }
536
585
  }
537
586
  }
538
587
 
539
588
  // Affine transform
540
589
  if (!baton->affineMatrix.empty()) {
541
590
  MultiPageUnsupported(nPages, "Affine");
591
+ KeepGainMapUnsupported(baton->keepGainMap, "Affine");
542
592
  image = sharp::StaySequential(image);
543
593
  std::vector<double> background;
544
594
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
@@ -569,6 +619,12 @@ class PipelineWorker : public Napi::AsyncWorker {
569
619
  baton->extendWith, background, nPages, &targetPageHeight)
570
620
  : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
571
621
  VImage::option()->set("extend", baton->extendWith)->set("background", background));
622
+ if (baton->keepGainMap) {
623
+ gainMap = gainMap.embed(baton->extendLeft / gainMapScaleFactor, baton->extendTop / gainMapScaleFactor,
624
+ baton->width / gainMapScaleFactor, baton->height / gainMapScaleFactor, VImage::option()
625
+ ->set("extend", baton->extendWith)
626
+ ->set("background", 0));
627
+ }
572
628
  } else {
573
629
  std::vector<double> ignoredBackground(1);
574
630
  image = sharp::StaySequential(image);
@@ -578,6 +634,12 @@ class PipelineWorker : public Napi::AsyncWorker {
578
634
  baton->extendWith, ignoredBackground, nPages, &targetPageHeight)
579
635
  : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
580
636
  VImage::option()->set("extend", baton->extendWith));
637
+ if (baton->keepGainMap) {
638
+ gainMap = gainMap.embed(baton->extendLeft / gainMapScaleFactor, baton->extendTop / gainMapScaleFactor,
639
+ baton->width / gainMapScaleFactor, baton->height / gainMapScaleFactor, VImage::option()
640
+ ->set("extend", baton->extendWith)
641
+ ->set("background", 0));
642
+ }
581
643
  }
582
644
  }
583
645
  // Median - must happen before blurring, due to the utility of blurring after thresholding
@@ -591,9 +653,22 @@ class PipelineWorker : public Napi::AsyncWorker {
591
653
  image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale);
592
654
  }
593
655
 
656
+ // Dilate - must happen before blurring, due to the utility of dilating after thresholding
657
+ if (baton->dilateWidth != 0) {
658
+ image = sharp::Dilate(image, baton->dilateWidth);
659
+ }
660
+
661
+ // Erode - must happen before blurring, due to the utility of eroding after thresholding
662
+ if (baton->erodeWidth != 0) {
663
+ image = sharp::Erode(image, baton->erodeWidth);
664
+ }
665
+
594
666
  // Blur
595
667
  if (shouldBlur) {
596
668
  image = sharp::Blur(image, baton->blurSigma, baton->precision, baton->minAmpl);
669
+ if (baton->keepGainMap) {
670
+ gainMap = sharp::Blur(gainMap, baton->blurSigma, baton->precision, baton->minAmpl);
671
+ }
597
672
  }
598
673
 
599
674
  // Unflatten the image
@@ -603,6 +678,7 @@ class PipelineWorker : public Napi::AsyncWorker {
603
678
 
604
679
  // Convolve
605
680
  if (shouldConv) {
681
+ KeepGainMapUnsupported(baton->keepGainMap, "Convolve");
606
682
  image = sharp::Convolve(image,
607
683
  baton->convKernelWidth, baton->convKernelHeight,
608
684
  baton->convKernelScale, baton->convKernelOffset,
@@ -611,11 +687,13 @@ class PipelineWorker : public Napi::AsyncWorker {
611
687
 
612
688
  // Recomb
613
689
  if (!baton->recombMatrix.empty()) {
690
+ KeepGainMapUnsupported(baton->keepGainMap, "Recomb");
614
691
  image = sharp::Recomb(image, baton->recombMatrix);
615
692
  }
616
693
 
617
694
  // Modulate
618
695
  if (baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0 || baton->lightness != 0.0) {
696
+ KeepGainMapUnsupported(baton->keepGainMap, "Modulate");
619
697
  image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
620
698
  }
621
699
 
@@ -633,6 +711,7 @@ class PipelineWorker : public Napi::AsyncWorker {
633
711
 
634
712
  // Composite
635
713
  if (shouldComposite) {
714
+ KeepGainMapUnsupported(baton->keepGainMap, "Composite");
636
715
  std::vector<VImage> images = { image };
637
716
  std::vector<int> modes, xs, ys;
638
717
  for (Composite *composite : baton->composite) {
@@ -640,10 +719,28 @@ class PipelineWorker : public Napi::AsyncWorker {
640
719
  sharp::ImageType compositeImageType = sharp::ImageType::UNKNOWN;
641
720
  composite->input->access = access;
642
721
  std::tie(compositeImage, compositeImageType) = sharp::OpenInput(composite->input);
643
- compositeImage = sharp::EnsureColourspace(compositeImage, baton->colourspacePipeline);
722
+
723
+ if (composite->input->autoOrient) {
724
+ // Respect EXIF Orientation
725
+ VipsAngle compositeAutoRotation = VIPS_ANGLE_D0;
726
+ bool compositeAutoFlop = false;
727
+ std::tie(compositeAutoRotation, compositeAutoFlop) =
728
+ CalculateExifRotationAndFlop(sharp::ExifOrientation(compositeImage));
729
+
730
+ compositeImage = sharp::RemoveExifOrientation(compositeImage);
731
+ compositeImage = sharp::StaySequential(compositeImage, compositeAutoRotation != VIPS_ANGLE_D0);
732
+
733
+ if (compositeAutoRotation != VIPS_ANGLE_D0) {
734
+ compositeImage = compositeImage.rot(compositeAutoRotation);
735
+ }
736
+ if (compositeAutoFlop) {
737
+ compositeImage = compositeImage.flip(VIPS_DIRECTION_HORIZONTAL);
738
+ }
739
+ }
740
+
644
741
  // Verify within current dimensions
645
742
  if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
646
- throw vips::VError("Image to composite must have same dimensions or smaller");
743
+ throw std::runtime_error("Image to composite must have same dimensions or smaller");
647
744
  }
648
745
  // Check if overlay is tiled
649
746
  if (composite->tile) {
@@ -681,11 +778,8 @@ class PipelineWorker : public Napi::AsyncWorker {
681
778
  // gravity was used for extract_area, set it back to its default value of 0
682
779
  composite->gravity = 0;
683
780
  }
684
- // Ensure image to composite is sRGB with unpremultiplied alpha
685
- compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB);
686
- if (!sharp::HasAlpha(compositeImage)) {
687
- compositeImage = sharp::EnsureAlpha(compositeImage, 1);
688
- }
781
+ // Ensure image to composite is with unpremultiplied alpha
782
+ compositeImage = sharp::EnsureAlpha(compositeImage, 1);
689
783
  if (composite->premultiplied) compositeImage = compositeImage.unpremultiply();
690
784
  // Calculate position
691
785
  int left;
@@ -709,7 +803,12 @@ class PipelineWorker : public Napi::AsyncWorker {
709
803
  xs.push_back(left);
710
804
  ys.push_back(top);
711
805
  }
712
- image = VImage::composite(images, modes, VImage::option()->set("x", xs)->set("y", ys));
806
+ image = VImage::composite(images, modes, VImage::option()
807
+ ->set("compositing_space", baton->colourspacePipeline == VIPS_INTERPRETATION_LAST
808
+ ? VIPS_INTERPRETATION_sRGB
809
+ : baton->colourspacePipeline)
810
+ ->set("x", xs)
811
+ ->set("y", ys));
713
812
  image = sharp::RemoveGifPalette(image);
714
813
  }
715
814
 
@@ -725,18 +824,21 @@ class PipelineWorker : public Napi::AsyncWorker {
725
824
 
726
825
  // Apply normalisation - stretch luminance to cover full dynamic range
727
826
  if (baton->normalise) {
827
+ KeepGainMapUnsupported(baton->keepGainMap, "Normalise");
728
828
  image = sharp::StaySequential(image);
729
829
  image = sharp::Normalise(image, baton->normaliseLower, baton->normaliseUpper);
730
830
  }
731
831
 
732
832
  // Apply contrast limiting adaptive histogram equalization (CLAHE)
733
833
  if (baton->claheWidth != 0 && baton->claheHeight != 0) {
834
+ KeepGainMapUnsupported(baton->keepGainMap, "Clahe");
734
835
  image = sharp::StaySequential(image);
735
836
  image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope);
736
837
  }
737
838
 
738
839
  // Apply bitwise boolean operation between images
739
840
  if (baton->boolean != nullptr) {
841
+ KeepGainMapUnsupported(baton->keepGainMap, "Boolean");
740
842
  VImage booleanImage;
741
843
  sharp::ImageType booleanImageType = sharp::ImageType::UNKNOWN;
742
844
  baton->boolean->access = access;
@@ -766,27 +868,22 @@ class PipelineWorker : public Napi::AsyncWorker {
766
868
  image = sharp::EnsureAlpha(image, baton->ensureAlpha);
767
869
  }
768
870
 
769
- // Convert image to sRGB, if not already
871
+ // Ensure output colour space
770
872
  if (sharp::Is16Bit(image.interpretation())) {
771
873
  image = image.cast(VIPS_FORMAT_USHORT);
772
874
  }
773
875
  if (image.interpretation() != baton->colourspace) {
774
- // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess
775
876
  image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
776
- // Transform colours from embedded profile to output profile
777
- if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) && baton->colourspacePipeline != VIPS_INTERPRETATION_CMYK &&
778
- baton->withIccProfile.empty() && sharp::HasProfile(image)) {
779
- image = image.icc_transform(processingProfile, VImage::option()
780
- ->set("embedded", true)
781
- ->set("depth", sharp::Is16Bit(image.interpretation()) ? 16 : 8)
782
- ->set("intent", VIPS_INTENT_PERCEPTUAL));
877
+ if (inputProfile.first != nullptr && baton->withIccProfile.empty()) {
878
+ image = sharp::SetProfile(image, inputProfile);
783
879
  }
784
880
  }
785
881
 
786
882
  // Extract channel
787
883
  if (baton->extractChannel > -1) {
884
+ KeepGainMapUnsupported(baton->keepGainMap, "Extract channel");
788
885
  if (baton->extractChannel >= image.bands()) {
789
- if (baton->extractChannel == 3 && sharp::HasAlpha(image)) {
886
+ if (baton->extractChannel == 3 && image.has_alpha()) {
790
887
  baton->extractChannel = image.bands() - 1;
791
888
  } else {
792
889
  (baton->err)
@@ -814,13 +911,14 @@ class PipelineWorker : public Napi::AsyncWorker {
814
911
  } catch(...) {
815
912
  sharp::VipsWarningCallback(nullptr, G_LOG_LEVEL_WARNING, "Invalid profile", nullptr);
816
913
  }
817
- } else if (baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) {
818
- image = sharp::SetProfile(image, inputProfile);
819
914
  }
820
915
 
821
916
  // Negate the colours in the image
822
917
  if (baton->negate) {
823
918
  image = sharp::Negate(image, baton->negateAlpha);
919
+ if (baton->keepGainMap) {
920
+ gainMap = sharp::Negate(gainMap, false);
921
+ }
824
922
  }
825
923
 
826
924
  // Override EXIF Orientation tag
@@ -837,11 +935,16 @@ class PipelineWorker : public Napi::AsyncWorker {
837
935
  if (!baton->withExifMerge) {
838
936
  image = sharp::RemoveExif(image);
839
937
  }
840
- for (const auto& s : baton->withExif) {
841
- image.set(s.first.data(), s.second.data());
938
+ for (const auto& [key, value] : baton->withExif) {
939
+ image.set(key.c_str(), value.c_str());
842
940
  }
843
941
  }
844
-
942
+ // XMP buffer
943
+ if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_XMP) && !baton->withXmp.empty()) {
944
+ image = image.copy();
945
+ image.set(VIPS_META_XMP_NAME, nullptr,
946
+ const_cast<void*>(static_cast<void const*>(baton->withXmp.c_str())), baton->withXmp.size());
947
+ }
845
948
  // Number of channels used in output image
846
949
  baton->channels = image.bands();
847
950
  baton->width = image.width();
@@ -862,18 +965,27 @@ class PipelineWorker : public Napi::AsyncWorker {
862
965
  if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
863
966
  // Write JPEG to buffer
864
967
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
865
- VipsArea *area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option()
866
- ->set("keep", baton->keepMetadata)
867
- ->set("Q", baton->jpegQuality)
868
- ->set("interlace", baton->jpegProgressive)
869
- ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
870
- ? VIPS_FOREIGN_SUBSAMPLE_OFF
871
- : VIPS_FOREIGN_SUBSAMPLE_ON)
872
- ->set("trellis_quant", baton->jpegTrellisQuantisation)
873
- ->set("quant_table", baton->jpegQuantisationTable)
874
- ->set("overshoot_deringing", baton->jpegOvershootDeringing)
875
- ->set("optimize_scans", baton->jpegOptimiseScans)
876
- ->set("optimize_coding", baton->jpegOptimiseCoding)));
968
+ VipsArea *area;
969
+ if (baton->keepGainMap) {
970
+ image = ReattachGainMap(image, gainMap, baton);
971
+ area = reinterpret_cast<VipsArea*>(image.uhdrsave_buffer(VImage::option()
972
+ ->set("keep", baton->keepMetadata)
973
+ ->set("Q", baton->jpegQuality)
974
+ ->set("gainmap_scale_factor", gainMapScaleFactor)));
975
+ } else {
976
+ area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option()
977
+ ->set("keep", baton->keepMetadata)
978
+ ->set("Q", baton->jpegQuality)
979
+ ->set("interlace", baton->jpegProgressive)
980
+ ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
981
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
982
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
983
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
984
+ ->set("quant_table", baton->jpegQuantisationTable)
985
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
986
+ ->set("optimize_scans", baton->jpegOptimiseScans)
987
+ ->set("optimize_coding", baton->jpegOptimiseCoding)));
988
+ }
877
989
  baton->bufferOut = static_cast<char*>(area->data);
878
990
  baton->bufferOutLength = area->length;
879
991
  area->free_fn = nullptr;
@@ -929,10 +1041,12 @@ class PipelineWorker : public Napi::AsyncWorker {
929
1041
  ->set("lossless", baton->webpLossless)
930
1042
  ->set("near_lossless", baton->webpNearLossless)
931
1043
  ->set("smart_subsample", baton->webpSmartSubsample)
1044
+ ->set("smart_deblock", baton->webpSmartDeblock)
932
1045
  ->set("preset", baton->webpPreset)
933
1046
  ->set("effort", baton->webpEffort)
934
1047
  ->set("min_size", baton->webpMinSize)
935
1048
  ->set("mixed", baton->webpMixed)
1049
+ ->set("exact", baton->webpExact)
936
1050
  ->set("alpha_q", baton->webpAlphaQuality)));
937
1051
  baton->bufferOut = static_cast<char*>(area->data);
938
1052
  baton->bufferOutLength = area->length;
@@ -951,6 +1065,7 @@ class PipelineWorker : public Napi::AsyncWorker {
951
1065
  ->set("interlace", baton->gifProgressive)
952
1066
  ->set("interframe_maxerror", baton->gifInterFrameMaxError)
953
1067
  ->set("interpalette_maxerror", baton->gifInterPaletteMaxError)
1068
+ ->set("keep_duplicate_frames", baton->gifKeepDuplicateFrames)
954
1069
  ->set("dither", baton->gifDither)));
955
1070
  baton->bufferOut = static_cast<char*>(area->data);
956
1071
  baton->bufferOutLength = area->length;
@@ -973,6 +1088,7 @@ class PipelineWorker : public Napi::AsyncWorker {
973
1088
  ->set("Q", baton->tiffQuality)
974
1089
  ->set("bitdepth", baton->tiffBitdepth)
975
1090
  ->set("compression", baton->tiffCompression)
1091
+ ->set("bigtiff", baton->tiffBigtiff)
976
1092
  ->set("miniswhite", baton->tiffMiniswhite)
977
1093
  ->set("predictor", baton->tiffPredictor)
978
1094
  ->set("pyramid", baton->tiffPyramid)
@@ -991,13 +1107,14 @@ class PipelineWorker : public Napi::AsyncWorker {
991
1107
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::HEIF)) {
992
1108
  // Write HEIF to buffer
993
1109
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF);
994
- image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
1110
+ image = sharp::RemoveAnimationProperties(image);
995
1111
  VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option()
996
1112
  ->set("keep", baton->keepMetadata)
997
1113
  ->set("Q", baton->heifQuality)
998
1114
  ->set("compression", baton->heifCompression)
999
1115
  ->set("effort", baton->heifEffort)
1000
1116
  ->set("bitdepth", baton->heifBitdepth)
1117
+ ->set("tune", baton->heifTune.c_str())
1001
1118
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1002
1119
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1003
1120
  ->set("lossless", baton->heifLossless)));
@@ -1009,7 +1126,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1009
1126
  } else if (baton->formatOut == "dz") {
1010
1127
  // Write DZ to buffer
1011
1128
  baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
1012
- if (!sharp::HasAlpha(image)) {
1129
+ if (!image.has_alpha()) {
1013
1130
  baton->tileBackground.pop_back();
1014
1131
  }
1015
1132
  image = sharp::StaySequential(image, baton->tileAngle != 0);
@@ -1050,19 +1167,19 @@ class PipelineWorker : public Napi::AsyncWorker {
1050
1167
  // Get raw image data
1051
1168
  baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
1052
1169
  if (baton->bufferOut == nullptr) {
1053
- (baton->err).append("Could not allocate enough memory for raw output");
1054
- return Error();
1170
+ throw std::runtime_error("Could not allocate enough memory for raw output");
1055
1171
  }
1056
1172
  baton->formatOut = "raw";
1057
1173
  } else {
1058
1174
  // Unsupported output format
1059
- (baton->err).append("Unsupported output format ");
1175
+ auto unsupported = std::string("Unsupported output format ");
1060
1176
  if (baton->formatOut == "input") {
1061
- (baton->err).append(ImageTypeId(inputImageType));
1177
+ unsupported.append("when trying to match input format of ");
1178
+ unsupported.append(ImageTypeId(inputImageType));
1062
1179
  } else {
1063
- (baton->err).append(baton->formatOut);
1180
+ unsupported.append(baton->formatOut);
1064
1181
  }
1065
- return Error();
1182
+ throw std::runtime_error(unsupported);
1066
1183
  }
1067
1184
  } else {
1068
1185
  // File output
@@ -1085,18 +1202,26 @@ class PipelineWorker : public Napi::AsyncWorker {
1085
1202
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
1086
1203
  // Write JPEG to file
1087
1204
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
1088
- image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1089
- ->set("keep", baton->keepMetadata)
1090
- ->set("Q", baton->jpegQuality)
1091
- ->set("interlace", baton->jpegProgressive)
1092
- ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
1093
- ? VIPS_FOREIGN_SUBSAMPLE_OFF
1094
- : VIPS_FOREIGN_SUBSAMPLE_ON)
1095
- ->set("trellis_quant", baton->jpegTrellisQuantisation)
1096
- ->set("quant_table", baton->jpegQuantisationTable)
1097
- ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1098
- ->set("optimize_scans", baton->jpegOptimiseScans)
1099
- ->set("optimize_coding", baton->jpegOptimiseCoding));
1205
+ if (baton->keepGainMap) {
1206
+ image = ReattachGainMap(image, gainMap, baton);
1207
+ image.uhdrsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1208
+ ->set("keep", baton->keepMetadata)
1209
+ ->set("Q", baton->jpegQuality)
1210
+ ->set("gainmap_scale_factor", gainMapScaleFactor));
1211
+ } else {
1212
+ image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1213
+ ->set("keep", baton->keepMetadata)
1214
+ ->set("Q", baton->jpegQuality)
1215
+ ->set("interlace", baton->jpegProgressive)
1216
+ ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
1217
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
1218
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
1219
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
1220
+ ->set("quant_table", baton->jpegQuantisationTable)
1221
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1222
+ ->set("optimize_scans", baton->jpegOptimiseScans)
1223
+ ->set("optimize_coding", baton->jpegOptimiseCoding));
1224
+ }
1100
1225
  baton->formatOut = "jpeg";
1101
1226
  baton->channels = std::min(baton->channels, 3);
1102
1227
  } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
@@ -1136,10 +1261,12 @@ class PipelineWorker : public Napi::AsyncWorker {
1136
1261
  ->set("lossless", baton->webpLossless)
1137
1262
  ->set("near_lossless", baton->webpNearLossless)
1138
1263
  ->set("smart_subsample", baton->webpSmartSubsample)
1264
+ ->set("smart_deblock", baton->webpSmartDeblock)
1139
1265
  ->set("preset", baton->webpPreset)
1140
1266
  ->set("effort", baton->webpEffort)
1141
1267
  ->set("min_size", baton->webpMinSize)
1142
1268
  ->set("mixed", baton->webpMixed)
1269
+ ->set("exact", baton->webpExact)
1143
1270
  ->set("alpha_q", baton->webpAlphaQuality));
1144
1271
  baton->formatOut = "webp";
1145
1272
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
@@ -1152,6 +1279,9 @@ class PipelineWorker : public Napi::AsyncWorker {
1152
1279
  ->set("effort", baton->gifEffort)
1153
1280
  ->set("reuse", baton->gifReuse)
1154
1281
  ->set("interlace", baton->gifProgressive)
1282
+ ->set("interframe_maxerror", baton->gifInterFrameMaxError)
1283
+ ->set("interpalette_maxerror", baton->gifInterPaletteMaxError)
1284
+ ->set("keep_duplicate_frames", baton->gifKeepDuplicateFrames)
1155
1285
  ->set("dither", baton->gifDither));
1156
1286
  baton->formatOut = "gif";
1157
1287
  } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
@@ -1170,6 +1300,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1170
1300
  ->set("Q", baton->tiffQuality)
1171
1301
  ->set("bitdepth", baton->tiffBitdepth)
1172
1302
  ->set("compression", baton->tiffCompression)
1303
+ ->set("bigtiff", baton->tiffBigtiff)
1173
1304
  ->set("miniswhite", baton->tiffMiniswhite)
1174
1305
  ->set("predictor", baton->tiffPredictor)
1175
1306
  ->set("pyramid", baton->tiffPyramid)
@@ -1184,13 +1315,14 @@ class PipelineWorker : public Napi::AsyncWorker {
1184
1315
  (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
1185
1316
  // Write HEIF to file
1186
1317
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF);
1187
- image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
1318
+ image = sharp::RemoveAnimationProperties(image);
1188
1319
  image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1189
1320
  ->set("keep", baton->keepMetadata)
1190
1321
  ->set("Q", baton->heifQuality)
1191
1322
  ->set("compression", baton->heifCompression)
1192
1323
  ->set("effort", baton->heifEffort)
1193
1324
  ->set("bitdepth", baton->heifBitdepth)
1325
+ ->set("tune", baton->heifTune.c_str())
1194
1326
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1195
1327
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1196
1328
  ->set("lossless", baton->heifLossless));
@@ -1211,7 +1343,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1211
1343
  if (isDzZip) {
1212
1344
  baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
1213
1345
  }
1214
- if (!sharp::HasAlpha(image)) {
1346
+ if (!image.has_alpha()) {
1215
1347
  baton->tileBackground.pop_back();
1216
1348
  }
1217
1349
  image = sharp::StaySequential(image, baton->tileAngle != 0);
@@ -1230,12 +1362,17 @@ class PipelineWorker : public Napi::AsyncWorker {
1230
1362
  return Error();
1231
1363
  }
1232
1364
  }
1233
- } catch (vips::VError const &err) {
1365
+ } catch (std::runtime_error const &err) {
1234
1366
  char const *what = err.what();
1235
1367
  if (what && what[0]) {
1236
1368
  (baton->err).append(what);
1237
1369
  } else {
1238
- (baton->err).append("Unknown error");
1370
+ if (baton->input->failOn == VIPS_FAIL_ON_WARNING) {
1371
+ (baton->err).append("Warning treated as error due to failOn setting");
1372
+ baton->errUseWarning = true;
1373
+ } else {
1374
+ (baton->err).append("Unknown error");
1375
+ }
1239
1376
  }
1240
1377
  }
1241
1378
  // Clean up libvips' per-request data and threads
@@ -1250,10 +1387,13 @@ class PipelineWorker : public Napi::AsyncWorker {
1250
1387
  // Handle warnings
1251
1388
  std::string warning = sharp::VipsWarningPop();
1252
1389
  while (!warning.empty()) {
1253
- debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) });
1390
+ if (baton->errUseWarning) {
1391
+ (baton->err).append("\n").append(warning);
1392
+ } else {
1393
+ debuglog.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { Napi::String::New(env, warning) });
1394
+ }
1254
1395
  warning = sharp::VipsWarningPop();
1255
1396
  }
1256
-
1257
1397
  if (baton->err.empty()) {
1258
1398
  int width = baton->width;
1259
1399
  int height = baton->height;
@@ -1296,22 +1436,33 @@ class PipelineWorker : public Napi::AsyncWorker {
1296
1436
  }
1297
1437
 
1298
1438
  if (baton->bufferOutLength > 0) {
1299
- // Add buffer size to info
1300
1439
  info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));
1301
- // Pass ownership of output data to Buffer instance
1302
- Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
1303
- baton->bufferOutLength, sharp::FreeCallback);
1304
- Callback().Call(Receiver().Value(), { env.Null(), data, info });
1440
+ if (baton->typedArrayOut) {
1441
+ // ECMAScript ArrayBuffer with Uint8Array view
1442
+ Napi::TypedArrayOf<uint8_t> data = Napi::Buffer<char>::Copy(env,
1443
+ static_cast<char*>(baton->bufferOut), baton->bufferOutLength);
1444
+ sharp::FreeCallback(static_cast<char*>(baton->bufferOut), nullptr);
1445
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), data, info });
1446
+ } else {
1447
+ // Node.js Buffer
1448
+ Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
1449
+ baton->bufferOutLength, sharp::FreeCallback);
1450
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), data, info });
1451
+ }
1305
1452
  } else {
1306
1453
  // Add file size to info
1307
- struct STAT64_STRUCT st;
1308
- if (STAT64_FUNCTION(baton->fileOut.data(), &st) == 0) {
1309
- info.Set("size", static_cast<uint32_t>(st.st_size));
1454
+ if (baton->formatOut != "dz" || sharp::IsDzZip(baton->fileOut)) {
1455
+ try {
1456
+ uint32_t const size = static_cast<uint32_t>(
1457
+ std::filesystem::file_size(std::filesystem::u8path(baton->fileOut)));
1458
+ info.Set("size", size);
1459
+ } catch (...) {}
1310
1460
  }
1311
- Callback().Call(Receiver().Value(), { env.Null(), info });
1461
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), info });
1312
1462
  }
1313
1463
  } else {
1314
- Callback().Call(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1464
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(),
1465
+ { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1315
1466
  }
1316
1467
 
1317
1468
  // Delete baton
@@ -1324,12 +1475,15 @@ class PipelineWorker : public Napi::AsyncWorker {
1324
1475
  for (sharp::InputDescriptor *input : baton->joinChannelIn) {
1325
1476
  delete input;
1326
1477
  }
1478
+ for (sharp::InputDescriptor *input : baton->join) {
1479
+ delete input;
1480
+ }
1327
1481
  delete baton;
1328
1482
 
1329
1483
  // Decrement processing task counter
1330
1484
  sharp::counterProcess--;
1331
1485
  Napi::Number queueLength = Napi::Number::New(env, static_cast<int>(sharp::counterQueue));
1332
- queueListener.Call(Receiver().Value(), { queueLength });
1486
+ queueListener.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { queueLength });
1333
1487
  }
1334
1488
 
1335
1489
  private:
@@ -1339,7 +1493,13 @@ class PipelineWorker : public Napi::AsyncWorker {
1339
1493
 
1340
1494
  void MultiPageUnsupported(int const pages, std::string op) {
1341
1495
  if (pages > 1) {
1342
- throw vips::VError(op + " is not supported for multi-page images");
1496
+ throw std::runtime_error(op + " is not supported for multi-page images");
1497
+ }
1498
+ }
1499
+
1500
+ void KeepGainMapUnsupported(bool const keepGainMap, std::string op) {
1501
+ if (keepGainMap) {
1502
+ throw std::runtime_error(op + " is not supported when keeping gain maps");
1343
1503
  }
1344
1504
  }
1345
1505
 
@@ -1347,21 +1507,20 @@ class PipelineWorker : public Napi::AsyncWorker {
1347
1507
  Calculate the angle of rotation and need-to-flip for the given Exif orientation
1348
1508
  By default, returns zero, i.e. no rotation.
1349
1509
  */
1350
- std::tuple<VipsAngle, bool, bool>
1351
- CalculateExifRotationAndFlip(int const exifOrientation) {
1510
+ std::tuple<VipsAngle, bool>
1511
+ CalculateExifRotationAndFlop(int const exifOrientation) {
1352
1512
  VipsAngle rotate = VIPS_ANGLE_D0;
1353
- bool flip = false;
1354
1513
  bool flop = false;
1355
1514
  switch (exifOrientation) {
1356
1515
  case 6: rotate = VIPS_ANGLE_D90; break;
1357
1516
  case 3: rotate = VIPS_ANGLE_D180; break;
1358
1517
  case 8: rotate = VIPS_ANGLE_D270; break;
1359
- case 2: flop = true; break; // flop 1
1360
- case 7: flip = true; rotate = VIPS_ANGLE_D90; break; // flip 6
1361
- case 4: flop = true; rotate = VIPS_ANGLE_D180; break; // flop 3
1362
- case 5: flip = true; rotate = VIPS_ANGLE_D270; break; // flip 8
1518
+ case 2: flop = true; break;
1519
+ case 7: flop = true; rotate = VIPS_ANGLE_D270; break;
1520
+ case 4: flop = true; rotate = VIPS_ANGLE_D180; break;
1521
+ case 5: flop = true; rotate = VIPS_ANGLE_D90; break;
1363
1522
  }
1364
- return std::make_tuple(rotate, flip, flop);
1523
+ return std::make_tuple(rotate, flop);
1365
1524
  }
1366
1525
 
1367
1526
  /*
@@ -1389,11 +1548,11 @@ class PipelineWorker : public Napi::AsyncWorker {
1389
1548
  std::string
1390
1549
  AssembleSuffixString(std::string extname, std::vector<std::pair<std::string, std::string>> options) {
1391
1550
  std::string argument;
1392
- for (auto const &option : options) {
1551
+ for (const auto& [key, value] : options) {
1393
1552
  if (!argument.empty()) {
1394
1553
  argument += ",";
1395
1554
  }
1396
- argument += option.first + "=" + option.second;
1555
+ argument += key + "=" + value;
1397
1556
  }
1398
1557
  return extname + "[" + argument + "]";
1399
1558
  }
@@ -1419,9 +1578,11 @@ class PipelineWorker : public Napi::AsyncWorker {
1419
1578
  {"lossless", baton->webpLossless ? "true" : "false"},
1420
1579
  {"near_lossless", baton->webpNearLossless ? "true" : "false"},
1421
1580
  {"smart_subsample", baton->webpSmartSubsample ? "true" : "false"},
1581
+ {"smart_deblock", baton->webpSmartDeblock ? "true" : "false"},
1422
1582
  {"preset", vips_enum_nick(VIPS_TYPE_FOREIGN_WEBP_PRESET, baton->webpPreset)},
1423
1583
  {"min_size", baton->webpMinSize ? "true" : "false"},
1424
1584
  {"mixed", baton->webpMixed ? "true" : "false"},
1585
+ {"exact", baton->webpExact ? "true" : "false"},
1425
1586
  {"effort", std::to_string(baton->webpEffort)}
1426
1587
  };
1427
1588
  suffix = AssembleSuffixString(".webp", options);
@@ -1460,6 +1621,21 @@ class PipelineWorker : public Napi::AsyncWorker {
1460
1621
  return options;
1461
1622
  }
1462
1623
 
1624
+ VImage ReattachGainMap(VImage image, VImage gainMap, PipelineBaton *baton) {
1625
+ VipsArea *gainMapJpeg = reinterpret_cast<VipsArea*>(gainMap.jpegsave_buffer(VImage::option()
1626
+ ->set("keep", FALSE)
1627
+ ->set("Q", baton->jpegQuality)
1628
+ ->set("subsample_mode", VIPS_FOREIGN_SUBSAMPLE_OFF)
1629
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
1630
+ ->set("quant_table", baton->jpegQuantisationTable)
1631
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1632
+ ->set("optimize_coding", baton->jpegOptimiseCoding)));
1633
+ image = image.copy();
1634
+ image.set("gainmap-data", reinterpret_cast<VipsCallbackFn>(vips_area_free_cb),
1635
+ gainMapJpeg->data, gainMapJpeg->length);
1636
+ return image;
1637
+ }
1638
+
1463
1639
  /*
1464
1640
  Clear all thread-local data.
1465
1641
  */
@@ -1480,6 +1656,14 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1480
1656
 
1481
1657
  // Input
1482
1658
  baton->input = sharp::CreateInputDescriptor(options.Get("input").As<Napi::Object>());
1659
+ // Join images together
1660
+ if (sharp::HasAttr(options, "join")) {
1661
+ Napi::Array join = options.Get("join").As<Napi::Array>();
1662
+ for (unsigned int i = 0; i < join.Length(); i++) {
1663
+ baton->join.push_back(
1664
+ sharp::CreateInputDescriptor(join.Get(i).As<Napi::Object>()));
1665
+ }
1666
+ }
1483
1667
  // Extract image options
1484
1668
  baton->topOffsetPre = sharp::AttrAsInt32(options, "topOffsetPre");
1485
1669
  baton->leftOffsetPre = sharp::AttrAsInt32(options, "leftOffsetPre");
@@ -1560,10 +1744,13 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1560
1744
  baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
1561
1745
  baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
1562
1746
  baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
1747
+ baton->trimMargin = sharp::AttrAsUint32(options, "trimMargin");
1563
1748
  baton->gamma = sharp::AttrAsDouble(options, "gamma");
1564
1749
  baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
1565
1750
  baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
1566
1751
  baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB");
1752
+ baton->dilateWidth = sharp::AttrAsUint32(options, "dilateWidth");
1753
+ baton->erodeWidth = sharp::AttrAsUint32(options, "erodeWidth");
1567
1754
  baton->greyscale = sharp::AttrAsBool(options, "greyscale");
1568
1755
  baton->normalise = sharp::AttrAsBool(options, "normalise");
1569
1756
  baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");
@@ -1572,11 +1759,11 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1572
1759
  baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
1573
1760
  baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
1574
1761
  baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
1575
- baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation");
1576
1762
  baton->angle = sharp::AttrAsInt32(options, "angle");
1577
1763
  baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
1578
1764
  baton->rotationBackground = sharp::AttrAsVectorOfDouble(options, "rotationBackground");
1579
- baton->rotateBeforePreExtract = sharp::AttrAsBool(options, "rotateBeforePreExtract");
1765
+ baton->rotateBefore = sharp::AttrAsBool(options, "rotateBefore");
1766
+ baton->orientBefore = sharp::AttrAsBool(options, "orientBefore");
1580
1767
  baton->flip = sharp::AttrAsBool(options, "flip");
1581
1768
  baton->flop = sharp::AttrAsBool(options, "flop");
1582
1769
  baton->extendTop = sharp::AttrAsInt32(options, "extendTop");
@@ -1635,6 +1822,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1635
1822
  // Output
1636
1823
  baton->formatOut = sharp::AttrAsStr(options, "formatOut");
1637
1824
  baton->fileOut = sharp::AttrAsStr(options, "fileOut");
1825
+ baton->typedArrayOut = sharp::AttrAsBool(options, "typedArrayOut");
1638
1826
  baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata");
1639
1827
  baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
1640
1828
  baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");
@@ -1648,7 +1836,12 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1648
1836
  }
1649
1837
  }
1650
1838
  baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge");
1839
+ baton->withXmp = sharp::AttrAsStr(options, "withXmp");
1840
+ baton->keepGainMap = sharp::AttrAsBool(options, "keepGainMap");
1841
+ baton->withGainMap = sharp::AttrAsBool(options, "withGainMap");
1651
1842
  baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1843
+ baton->loop = sharp::AttrAsUint32(options, "loop");
1844
+ baton->delay = sharp::AttrAsInt32Vector(options, "delay");
1652
1845
  // Format-specific
1653
1846
  baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
1654
1847
  baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");
@@ -1676,18 +1869,22 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1676
1869
  baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
1677
1870
  baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
1678
1871
  baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
1872
+ baton->webpSmartDeblock = sharp::AttrAsBool(options, "webpSmartDeblock");
1679
1873
  baton->webpPreset = sharp::AttrAsEnum<VipsForeignWebpPreset>(options, "webpPreset", VIPS_TYPE_FOREIGN_WEBP_PRESET);
1680
1874
  baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1681
1875
  baton->webpMinSize = sharp::AttrAsBool(options, "webpMinSize");
1682
1876
  baton->webpMixed = sharp::AttrAsBool(options, "webpMixed");
1877
+ baton->webpExact = sharp::AttrAsBool(options, "webpExact");
1683
1878
  baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1684
1879
  baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1685
1880
  baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1686
1881
  baton->gifInterFrameMaxError = sharp::AttrAsDouble(options, "gifInterFrameMaxError");
1687
1882
  baton->gifInterPaletteMaxError = sharp::AttrAsDouble(options, "gifInterPaletteMaxError");
1883
+ baton->gifKeepDuplicateFrames = sharp::AttrAsBool(options, "gifKeepDuplicateFrames");
1688
1884
  baton->gifReuse = sharp::AttrAsBool(options, "gifReuse");
1689
1885
  baton->gifProgressive = sharp::AttrAsBool(options, "gifProgressive");
1690
1886
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1887
+ baton->tiffBigtiff = sharp::AttrAsBool(options, "tiffBigtiff");
1691
1888
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
1692
1889
  baton->tiffMiniswhite = sharp::AttrAsBool(options, "tiffMiniswhite");
1693
1890
  baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
@@ -1712,18 +1909,12 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1712
1909
  baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1713
1910
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1714
1911
  baton->heifBitdepth = sharp::AttrAsUint32(options, "heifBitdepth");
1912
+ baton->heifTune = sharp::AttrAsStr(options, "heifTune");
1715
1913
  baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
1716
1914
  baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
1717
1915
  baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");
1718
1916
  baton->jxlLossless = sharp::AttrAsBool(options, "jxlLossless");
1719
1917
  baton->rawDepth = sharp::AttrAsEnum<VipsBandFormat>(options, "rawDepth", VIPS_TYPE_BAND_FORMAT);
1720
- // Animated output properties
1721
- if (sharp::HasAttr(options, "loop")) {
1722
- baton->loop = sharp::AttrAsUint32(options, "loop");
1723
- }
1724
- if (sharp::HasAttr(options, "delay")) {
1725
- baton->delay = sharp::AttrAsInt32Vector(options, "delay");
1726
- }
1727
1918
  baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
1728
1919
  baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");
1729
1920
  baton->tileAngle = sharp::AttrAsInt32(options, "tileAngle");
@@ -1752,7 +1943,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1752
1943
 
1753
1944
  // Increment queued task counter
1754
1945
  Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast<int>(++sharp::counterQueue));
1755
- queueListener.Call(info.This(), { queueLength });
1946
+ queueListener.SHARP_CALLBACK_FN_NAME(info.This(), { queueLength });
1756
1947
 
1757
1948
  return info.Env().Undefined();
1758
1949
  }