sharp 0.33.5 → 0.35.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.
Files changed (49) 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 +199 -0
  5. package/{lib/colour.js → dist/colour.mjs} +42 -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.d.cts +1999 -0
  12. package/dist/index.d.mts +2046 -0
  13. package/dist/index.mjs +25 -0
  14. package/dist/input.cjs +814 -0
  15. package/{lib/input.js → dist/input.mjs} +232 -76
  16. package/{lib/is.js → dist/is.cjs} +19 -45
  17. package/dist/is.mjs +143 -0
  18. package/{lib/libvips.js → dist/libvips.cjs} +52 -43
  19. package/dist/libvips.mjs +212 -0
  20. package/dist/operation.cjs +998 -0
  21. package/{lib/operation.js → dist/operation.mjs} +131 -91
  22. package/{lib/output.js → dist/output.cjs} +262 -61
  23. package/dist/output.mjs +1788 -0
  24. package/dist/resize.cjs +617 -0
  25. package/{lib/resize.js → dist/resize.mjs} +65 -35
  26. package/dist/sharp.cjs +174 -0
  27. package/dist/sharp.mjs +174 -0
  28. package/{lib/utility.js → dist/utility.cjs} +30 -25
  29. package/dist/utility.mjs +301 -0
  30. package/install/build.js +38 -0
  31. package/lib/index.d.ts +387 -142
  32. package/package.json +90 -85
  33. package/src/binding.gyp +49 -26
  34. package/src/common.cc +203 -111
  35. package/src/common.h +68 -40
  36. package/src/metadata.cc +106 -22
  37. package/src/metadata.h +15 -5
  38. package/src/operations.cc +62 -21
  39. package/src/operations.h +15 -3
  40. package/src/pipeline.cc +391 -201
  41. package/src/pipeline.h +36 -7
  42. package/src/sharp.cc +11 -8
  43. package/src/stats.cc +16 -13
  44. package/src/stats.h +6 -3
  45. package/src/utilities.cc +35 -15
  46. package/src/utilities.h +5 -2
  47. package/install/check.js +0 -41
  48. package/lib/index.js +0 -16
  49. 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,13 +288,31 @@ 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");
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();
295
311
  }
312
+ image = sharp::RemoveGainMap(image);
313
+ } else {
314
+ baton->keepGainMap = false;
315
+ baton->withGainMap = false;
296
316
  }
297
317
 
298
318
  // Any pre-shrinking may already have been done
@@ -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);
@@ -558,26 +608,25 @@ class PipelineWorker : public Napi::AsyncWorker {
558
608
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
559
609
  baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
560
610
 
611
+ std::vector<double> background;
561
612
  if (baton->extendWith == VIPS_EXTEND_BACKGROUND) {
562
- std::vector<double> background;
563
613
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
564
-
565
- image = sharp::StaySequential(image, nPages > 1);
566
- image = nPages > 1
567
- ? sharp::EmbedMultiPage(image,
568
- baton->extendLeft, baton->extendTop, baton->width, baton->height,
569
- baton->extendWith, background, nPages, &targetPageHeight)
570
- : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
571
- VImage::option()->set("extend", baton->extendWith)->set("background", background));
572
- } else {
573
- std::vector<double> ignoredBackground(1);
574
- image = sharp::StaySequential(image);
575
- image = nPages > 1
576
- ? sharp::EmbedMultiPage(image,
577
- baton->extendLeft, baton->extendTop, baton->width, baton->height,
578
- baton->extendWith, ignoredBackground, nPages, &targetPageHeight)
579
- : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
580
- VImage::option()->set("extend", baton->extendWith));
614
+ }
615
+ image = sharp::StaySequential(image, nPages > 1 || baton->extendWith != VIPS_EXTEND_BACKGROUND);
616
+ auto options = VImage::option()->set("extend", baton->extendWith);
617
+ if (baton->extendWith == VIPS_EXTEND_BACKGROUND) {
618
+ options->set("background", background);
619
+ }
620
+ image = nPages > 1
621
+ ? sharp::EmbedMultiPage(image,
622
+ baton->extendLeft, baton->extendTop, baton->width, baton->height,
623
+ baton->extendWith, background, nPages, &targetPageHeight)
624
+ : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height, options);
625
+ if (baton->keepGainMap) {
626
+ gainMap = gainMap.embed(baton->extendLeft / gainMapScaleFactor, baton->extendTop / gainMapScaleFactor,
627
+ baton->width / gainMapScaleFactor, baton->height / gainMapScaleFactor, VImage::option()
628
+ ->set("extend", baton->extendWith)
629
+ ->set("background", 0));
581
630
  }
582
631
  }
583
632
  // Median - must happen before blurring, due to the utility of blurring after thresholding
@@ -591,9 +640,22 @@ class PipelineWorker : public Napi::AsyncWorker {
591
640
  image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale);
592
641
  }
593
642
 
643
+ // Dilate - must happen before blurring, due to the utility of dilating after thresholding
644
+ if (baton->dilateWidth != 0) {
645
+ image = sharp::Dilate(image, baton->dilateWidth);
646
+ }
647
+
648
+ // Erode - must happen before blurring, due to the utility of eroding after thresholding
649
+ if (baton->erodeWidth != 0) {
650
+ image = sharp::Erode(image, baton->erodeWidth);
651
+ }
652
+
594
653
  // Blur
595
654
  if (shouldBlur) {
596
655
  image = sharp::Blur(image, baton->blurSigma, baton->precision, baton->minAmpl);
656
+ if (baton->keepGainMap) {
657
+ gainMap = sharp::Blur(gainMap, baton->blurSigma, baton->precision, baton->minAmpl);
658
+ }
597
659
  }
598
660
 
599
661
  // Unflatten the image
@@ -603,6 +665,7 @@ class PipelineWorker : public Napi::AsyncWorker {
603
665
 
604
666
  // Convolve
605
667
  if (shouldConv) {
668
+ KeepGainMapUnsupported(baton->keepGainMap, "Convolve");
606
669
  image = sharp::Convolve(image,
607
670
  baton->convKernelWidth, baton->convKernelHeight,
608
671
  baton->convKernelScale, baton->convKernelOffset,
@@ -611,11 +674,13 @@ class PipelineWorker : public Napi::AsyncWorker {
611
674
 
612
675
  // Recomb
613
676
  if (!baton->recombMatrix.empty()) {
677
+ KeepGainMapUnsupported(baton->keepGainMap, "Recomb");
614
678
  image = sharp::Recomb(image, baton->recombMatrix);
615
679
  }
616
680
 
617
681
  // Modulate
618
682
  if (baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0 || baton->lightness != 0.0) {
683
+ KeepGainMapUnsupported(baton->keepGainMap, "Modulate");
619
684
  image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
620
685
  }
621
686
 
@@ -633,6 +698,7 @@ class PipelineWorker : public Napi::AsyncWorker {
633
698
 
634
699
  // Composite
635
700
  if (shouldComposite) {
701
+ KeepGainMapUnsupported(baton->keepGainMap, "Composite");
636
702
  std::vector<VImage> images = { image };
637
703
  std::vector<int> modes, xs, ys;
638
704
  for (Composite *composite : baton->composite) {
@@ -640,10 +706,28 @@ class PipelineWorker : public Napi::AsyncWorker {
640
706
  sharp::ImageType compositeImageType = sharp::ImageType::UNKNOWN;
641
707
  composite->input->access = access;
642
708
  std::tie(compositeImage, compositeImageType) = sharp::OpenInput(composite->input);
643
- compositeImage = sharp::EnsureColourspace(compositeImage, baton->colourspacePipeline);
709
+
710
+ if (composite->input->autoOrient) {
711
+ // Respect EXIF Orientation
712
+ VipsAngle compositeAutoRotation = VIPS_ANGLE_D0;
713
+ bool compositeAutoFlop = false;
714
+ std::tie(compositeAutoRotation, compositeAutoFlop) =
715
+ CalculateExifRotationAndFlop(sharp::ExifOrientation(compositeImage));
716
+
717
+ compositeImage = sharp::RemoveExifOrientation(compositeImage);
718
+ compositeImage = sharp::StaySequential(compositeImage, compositeAutoRotation != VIPS_ANGLE_D0);
719
+
720
+ if (compositeAutoRotation != VIPS_ANGLE_D0) {
721
+ compositeImage = compositeImage.rot(compositeAutoRotation);
722
+ }
723
+ if (compositeAutoFlop) {
724
+ compositeImage = compositeImage.flip(VIPS_DIRECTION_HORIZONTAL);
725
+ }
726
+ }
727
+
644
728
  // Verify within current dimensions
645
729
  if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
646
- throw vips::VError("Image to composite must have same dimensions or smaller");
730
+ throw std::runtime_error("Image to composite must have same dimensions or smaller");
647
731
  }
648
732
  // Check if overlay is tiled
649
733
  if (composite->tile) {
@@ -681,11 +765,8 @@ class PipelineWorker : public Napi::AsyncWorker {
681
765
  // gravity was used for extract_area, set it back to its default value of 0
682
766
  composite->gravity = 0;
683
767
  }
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
- }
768
+ // Ensure image to composite is with unpremultiplied alpha
769
+ compositeImage = sharp::EnsureAlpha(compositeImage, 1);
689
770
  if (composite->premultiplied) compositeImage = compositeImage.unpremultiply();
690
771
  // Calculate position
691
772
  int left;
@@ -709,7 +790,12 @@ class PipelineWorker : public Napi::AsyncWorker {
709
790
  xs.push_back(left);
710
791
  ys.push_back(top);
711
792
  }
712
- image = VImage::composite(images, modes, VImage::option()->set("x", xs)->set("y", ys));
793
+ image = VImage::composite(images, modes, VImage::option()
794
+ ->set("compositing_space", baton->colourspacePipeline == VIPS_INTERPRETATION_LAST
795
+ ? VIPS_INTERPRETATION_sRGB
796
+ : baton->colourspacePipeline)
797
+ ->set("x", xs)
798
+ ->set("y", ys));
713
799
  image = sharp::RemoveGifPalette(image);
714
800
  }
715
801
 
@@ -725,18 +811,21 @@ class PipelineWorker : public Napi::AsyncWorker {
725
811
 
726
812
  // Apply normalisation - stretch luminance to cover full dynamic range
727
813
  if (baton->normalise) {
814
+ KeepGainMapUnsupported(baton->keepGainMap, "Normalise");
728
815
  image = sharp::StaySequential(image);
729
816
  image = sharp::Normalise(image, baton->normaliseLower, baton->normaliseUpper);
730
817
  }
731
818
 
732
819
  // Apply contrast limiting adaptive histogram equalization (CLAHE)
733
820
  if (baton->claheWidth != 0 && baton->claheHeight != 0) {
821
+ KeepGainMapUnsupported(baton->keepGainMap, "Clahe");
734
822
  image = sharp::StaySequential(image);
735
823
  image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope);
736
824
  }
737
825
 
738
826
  // Apply bitwise boolean operation between images
739
827
  if (baton->boolean != nullptr) {
828
+ KeepGainMapUnsupported(baton->keepGainMap, "Boolean");
740
829
  VImage booleanImage;
741
830
  sharp::ImageType booleanImageType = sharp::ImageType::UNKNOWN;
742
831
  baton->boolean->access = access;
@@ -766,27 +855,22 @@ class PipelineWorker : public Napi::AsyncWorker {
766
855
  image = sharp::EnsureAlpha(image, baton->ensureAlpha);
767
856
  }
768
857
 
769
- // Convert image to sRGB, if not already
858
+ // Ensure output colour space
770
859
  if (sharp::Is16Bit(image.interpretation())) {
771
860
  image = image.cast(VIPS_FORMAT_USHORT);
772
861
  }
773
862
  if (image.interpretation() != baton->colourspace) {
774
- // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess
775
863
  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));
864
+ if (inputProfile.first != nullptr && baton->withIccProfile.empty()) {
865
+ image = sharp::SetProfile(image, inputProfile);
783
866
  }
784
867
  }
785
868
 
786
869
  // Extract channel
787
870
  if (baton->extractChannel > -1) {
871
+ KeepGainMapUnsupported(baton->keepGainMap, "Extract channel");
788
872
  if (baton->extractChannel >= image.bands()) {
789
- if (baton->extractChannel == 3 && sharp::HasAlpha(image)) {
873
+ if (baton->extractChannel == 3 && image.has_alpha()) {
790
874
  baton->extractChannel = image.bands() - 1;
791
875
  } else {
792
876
  (baton->err)
@@ -814,13 +898,14 @@ class PipelineWorker : public Napi::AsyncWorker {
814
898
  } catch(...) {
815
899
  sharp::VipsWarningCallback(nullptr, G_LOG_LEVEL_WARNING, "Invalid profile", nullptr);
816
900
  }
817
- } else if (baton->keepMetadata & VIPS_FOREIGN_KEEP_ICC) {
818
- image = sharp::SetProfile(image, inputProfile);
819
901
  }
820
902
 
821
903
  // Negate the colours in the image
822
904
  if (baton->negate) {
823
905
  image = sharp::Negate(image, baton->negateAlpha);
906
+ if (baton->keepGainMap) {
907
+ gainMap = sharp::Negate(gainMap, false);
908
+ }
824
909
  }
825
910
 
826
911
  // Override EXIF Orientation tag
@@ -837,11 +922,16 @@ class PipelineWorker : public Napi::AsyncWorker {
837
922
  if (!baton->withExifMerge) {
838
923
  image = sharp::RemoveExif(image);
839
924
  }
840
- for (const auto& s : baton->withExif) {
841
- image.set(s.first.data(), s.second.data());
925
+ for (const auto& [key, value] : baton->withExif) {
926
+ image.set(key.c_str(), value.c_str());
842
927
  }
843
928
  }
844
-
929
+ // XMP buffer
930
+ if ((baton->keepMetadata & VIPS_FOREIGN_KEEP_XMP) && !baton->withXmp.empty()) {
931
+ image = image.copy();
932
+ image.set(VIPS_META_XMP_NAME, nullptr,
933
+ const_cast<void*>(static_cast<void const*>(baton->withXmp.c_str())), baton->withXmp.size());
934
+ }
845
935
  // Number of channels used in output image
846
936
  baton->channels = image.bands();
847
937
  baton->width = image.width();
@@ -854,6 +944,7 @@ class PipelineWorker : public Napi::AsyncWorker {
854
944
  baton->pageHeightOut = image.get_int(VIPS_META_PAGE_HEIGHT);
855
945
  baton->pagesOut = image.get_int(VIPS_META_N_PAGES);
856
946
  }
947
+ baton->hasAlphaOut = image.has_alpha();
857
948
 
858
949
  // Output
859
950
  sharp::SetTimeout(image, baton->timeoutSeconds);
@@ -862,18 +953,27 @@ class PipelineWorker : public Napi::AsyncWorker {
862
953
  if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
863
954
  // Write JPEG to buffer
864
955
  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)));
956
+ VipsArea *area;
957
+ if (baton->keepGainMap) {
958
+ image = ReattachGainMap(image, gainMap, baton);
959
+ area = reinterpret_cast<VipsArea*>(image.uhdrsave_buffer(VImage::option()
960
+ ->set("keep", baton->keepMetadata)
961
+ ->set("Q", baton->jpegQuality)
962
+ ->set("gainmap_scale_factor", gainMapScaleFactor)));
963
+ } else {
964
+ area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option()
965
+ ->set("keep", baton->keepMetadata)
966
+ ->set("Q", baton->jpegQuality)
967
+ ->set("interlace", baton->jpegProgressive)
968
+ ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
969
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
970
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
971
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
972
+ ->set("quant_table", baton->jpegQuantisationTable)
973
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
974
+ ->set("optimize_scans", baton->jpegOptimiseScans)
975
+ ->set("optimize_coding", baton->jpegOptimiseCoding)));
976
+ }
877
977
  baton->bufferOut = static_cast<char*>(area->data);
878
978
  baton->bufferOutLength = area->length;
879
979
  area->free_fn = nullptr;
@@ -884,6 +984,7 @@ class PipelineWorker : public Napi::AsyncWorker {
884
984
  } else {
885
985
  baton->channels = std::min(baton->channels, 3);
886
986
  }
987
+ baton->hasAlphaOut = false;
887
988
  } else if (baton->formatOut == "jp2" || (baton->formatOut == "input"
888
989
  && inputImageType == sharp::ImageType::JP2)) {
889
990
  // Write JP2 to Buffer
@@ -929,10 +1030,12 @@ class PipelineWorker : public Napi::AsyncWorker {
929
1030
  ->set("lossless", baton->webpLossless)
930
1031
  ->set("near_lossless", baton->webpNearLossless)
931
1032
  ->set("smart_subsample", baton->webpSmartSubsample)
1033
+ ->set("smart_deblock", baton->webpSmartDeblock)
932
1034
  ->set("preset", baton->webpPreset)
933
1035
  ->set("effort", baton->webpEffort)
934
1036
  ->set("min_size", baton->webpMinSize)
935
1037
  ->set("mixed", baton->webpMixed)
1038
+ ->set("exact", baton->webpExact)
936
1039
  ->set("alpha_q", baton->webpAlphaQuality)));
937
1040
  baton->bufferOut = static_cast<char*>(area->data);
938
1041
  baton->bufferOutLength = area->length;
@@ -951,6 +1054,7 @@ class PipelineWorker : public Napi::AsyncWorker {
951
1054
  ->set("interlace", baton->gifProgressive)
952
1055
  ->set("interframe_maxerror", baton->gifInterFrameMaxError)
953
1056
  ->set("interpalette_maxerror", baton->gifInterPaletteMaxError)
1057
+ ->set("keep_duplicate_frames", baton->gifKeepDuplicateFrames)
954
1058
  ->set("dither", baton->gifDither)));
955
1059
  baton->bufferOut = static_cast<char*>(area->data);
956
1060
  baton->bufferOutLength = area->length;
@@ -963,6 +1067,7 @@ class PipelineWorker : public Napi::AsyncWorker {
963
1067
  if (baton->tiffCompression == VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) {
964
1068
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
965
1069
  baton->channels = std::min(baton->channels, 3);
1070
+ baton->hasAlphaOut = false;
966
1071
  }
967
1072
  // Cast pixel values to float, if required
968
1073
  if (baton->tiffPredictor == VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT) {
@@ -973,6 +1078,7 @@ class PipelineWorker : public Napi::AsyncWorker {
973
1078
  ->set("Q", baton->tiffQuality)
974
1079
  ->set("bitdepth", baton->tiffBitdepth)
975
1080
  ->set("compression", baton->tiffCompression)
1081
+ ->set("bigtiff", baton->tiffBigtiff)
976
1082
  ->set("miniswhite", baton->tiffMiniswhite)
977
1083
  ->set("predictor", baton->tiffPredictor)
978
1084
  ->set("pyramid", baton->tiffPyramid)
@@ -991,13 +1097,14 @@ class PipelineWorker : public Napi::AsyncWorker {
991
1097
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::HEIF)) {
992
1098
  // Write HEIF to buffer
993
1099
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF);
994
- image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
1100
+ image = sharp::RemoveAnimationProperties(image);
995
1101
  VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option()
996
1102
  ->set("keep", baton->keepMetadata)
997
1103
  ->set("Q", baton->heifQuality)
998
1104
  ->set("compression", baton->heifCompression)
999
1105
  ->set("effort", baton->heifEffort)
1000
1106
  ->set("bitdepth", baton->heifBitdepth)
1107
+ ->set("tune", baton->heifTune.c_str())
1001
1108
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1002
1109
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1003
1110
  ->set("lossless", baton->heifLossless)));
@@ -1009,7 +1116,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1009
1116
  } else if (baton->formatOut == "dz") {
1010
1117
  // Write DZ to buffer
1011
1118
  baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
1012
- if (!sharp::HasAlpha(image)) {
1119
+ if (!image.has_alpha()) {
1013
1120
  baton->tileBackground.pop_back();
1014
1121
  }
1015
1122
  image = sharp::StaySequential(image, baton->tileAngle != 0);
@@ -1020,6 +1127,9 @@ class PipelineWorker : public Napi::AsyncWorker {
1020
1127
  area->free_fn = nullptr;
1021
1128
  vips_area_unref(area);
1022
1129
  baton->formatOut = "dz";
1130
+ if (baton->tileFormat == "jpeg") {
1131
+ baton->hasAlphaOut = false;
1132
+ }
1023
1133
  } else if (baton->formatOut == "jxl" ||
1024
1134
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::JXL)) {
1025
1135
  // Write JXL to buffer
@@ -1050,19 +1160,19 @@ class PipelineWorker : public Napi::AsyncWorker {
1050
1160
  // Get raw image data
1051
1161
  baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
1052
1162
  if (baton->bufferOut == nullptr) {
1053
- (baton->err).append("Could not allocate enough memory for raw output");
1054
- return Error();
1163
+ throw std::runtime_error("Could not allocate enough memory for raw output");
1055
1164
  }
1056
1165
  baton->formatOut = "raw";
1057
1166
  } else {
1058
1167
  // Unsupported output format
1059
- (baton->err).append("Unsupported output format ");
1168
+ auto unsupported = std::string("Unsupported output format ");
1060
1169
  if (baton->formatOut == "input") {
1061
- (baton->err).append(ImageTypeId(inputImageType));
1170
+ unsupported.append("when trying to match input format of ");
1171
+ unsupported.append(ImageTypeId(inputImageType));
1062
1172
  } else {
1063
- (baton->err).append(baton->formatOut);
1173
+ unsupported.append(baton->formatOut);
1064
1174
  }
1065
- return Error();
1175
+ throw std::runtime_error(unsupported);
1066
1176
  }
1067
1177
  } else {
1068
1178
  // File output
@@ -1085,20 +1195,29 @@ class PipelineWorker : public Napi::AsyncWorker {
1085
1195
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
1086
1196
  // Write JPEG to file
1087
1197
  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));
1198
+ if (baton->keepGainMap) {
1199
+ image = ReattachGainMap(image, gainMap, baton);
1200
+ image.uhdrsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1201
+ ->set("keep", baton->keepMetadata)
1202
+ ->set("Q", baton->jpegQuality)
1203
+ ->set("gainmap_scale_factor", gainMapScaleFactor));
1204
+ } else {
1205
+ image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1206
+ ->set("keep", baton->keepMetadata)
1207
+ ->set("Q", baton->jpegQuality)
1208
+ ->set("interlace", baton->jpegProgressive)
1209
+ ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
1210
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
1211
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
1212
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
1213
+ ->set("quant_table", baton->jpegQuantisationTable)
1214
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1215
+ ->set("optimize_scans", baton->jpegOptimiseScans)
1216
+ ->set("optimize_coding", baton->jpegOptimiseCoding));
1217
+ }
1100
1218
  baton->formatOut = "jpeg";
1101
1219
  baton->channels = std::min(baton->channels, 3);
1220
+ baton->hasAlphaOut = false;
1102
1221
  } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
1103
1222
  (willMatchInput && (inputImageType == sharp::ImageType::JP2))) {
1104
1223
  // Write JP2 to file
@@ -1136,10 +1255,12 @@ class PipelineWorker : public Napi::AsyncWorker {
1136
1255
  ->set("lossless", baton->webpLossless)
1137
1256
  ->set("near_lossless", baton->webpNearLossless)
1138
1257
  ->set("smart_subsample", baton->webpSmartSubsample)
1258
+ ->set("smart_deblock", baton->webpSmartDeblock)
1139
1259
  ->set("preset", baton->webpPreset)
1140
1260
  ->set("effort", baton->webpEffort)
1141
1261
  ->set("min_size", baton->webpMinSize)
1142
1262
  ->set("mixed", baton->webpMixed)
1263
+ ->set("exact", baton->webpExact)
1143
1264
  ->set("alpha_q", baton->webpAlphaQuality));
1144
1265
  baton->formatOut = "webp";
1145
1266
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
@@ -1152,6 +1273,9 @@ class PipelineWorker : public Napi::AsyncWorker {
1152
1273
  ->set("effort", baton->gifEffort)
1153
1274
  ->set("reuse", baton->gifReuse)
1154
1275
  ->set("interlace", baton->gifProgressive)
1276
+ ->set("interframe_maxerror", baton->gifInterFrameMaxError)
1277
+ ->set("interpalette_maxerror", baton->gifInterPaletteMaxError)
1278
+ ->set("keep_duplicate_frames", baton->gifKeepDuplicateFrames)
1155
1279
  ->set("dither", baton->gifDither));
1156
1280
  baton->formatOut = "gif";
1157
1281
  } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
@@ -1160,6 +1284,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1160
1284
  if (baton->tiffCompression == VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) {
1161
1285
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
1162
1286
  baton->channels = std::min(baton->channels, 3);
1287
+ baton->hasAlphaOut = false;
1163
1288
  }
1164
1289
  // Cast pixel values to float, if required
1165
1290
  if (baton->tiffPredictor == VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT) {
@@ -1170,6 +1295,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1170
1295
  ->set("Q", baton->tiffQuality)
1171
1296
  ->set("bitdepth", baton->tiffBitdepth)
1172
1297
  ->set("compression", baton->tiffCompression)
1298
+ ->set("bigtiff", baton->tiffBigtiff)
1173
1299
  ->set("miniswhite", baton->tiffMiniswhite)
1174
1300
  ->set("predictor", baton->tiffPredictor)
1175
1301
  ->set("pyramid", baton->tiffPyramid)
@@ -1184,13 +1310,14 @@ class PipelineWorker : public Napi::AsyncWorker {
1184
1310
  (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
1185
1311
  // Write HEIF to file
1186
1312
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::HEIF);
1187
- image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
1313
+ image = sharp::RemoveAnimationProperties(image);
1188
1314
  image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1189
1315
  ->set("keep", baton->keepMetadata)
1190
1316
  ->set("Q", baton->heifQuality)
1191
1317
  ->set("compression", baton->heifCompression)
1192
1318
  ->set("effort", baton->heifEffort)
1193
1319
  ->set("bitdepth", baton->heifBitdepth)
1320
+ ->set("tune", baton->heifTune.c_str())
1194
1321
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1195
1322
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1196
1323
  ->set("lossless", baton->heifLossless));
@@ -1211,13 +1338,16 @@ class PipelineWorker : public Napi::AsyncWorker {
1211
1338
  if (isDzZip) {
1212
1339
  baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
1213
1340
  }
1214
- if (!sharp::HasAlpha(image)) {
1341
+ if (!image.has_alpha()) {
1215
1342
  baton->tileBackground.pop_back();
1216
1343
  }
1217
1344
  image = sharp::StaySequential(image, baton->tileAngle != 0);
1218
1345
  vips::VOption *options = BuildOptionsDZ(baton);
1219
1346
  image.dzsave(const_cast<char*>(baton->fileOut.data()), options);
1220
1347
  baton->formatOut = "dz";
1348
+ if (baton->tileFormat == "jpeg") {
1349
+ baton->hasAlphaOut = false;
1350
+ }
1221
1351
  } else if (baton->formatOut == "v" || (mightMatchInput && isV) ||
1222
1352
  (willMatchInput && inputImageType == sharp::ImageType::VIPS)) {
1223
1353
  // Write V to file
@@ -1230,12 +1360,17 @@ class PipelineWorker : public Napi::AsyncWorker {
1230
1360
  return Error();
1231
1361
  }
1232
1362
  }
1233
- } catch (vips::VError const &err) {
1363
+ } catch (std::runtime_error const &err) {
1234
1364
  char const *what = err.what();
1235
1365
  if (what && what[0]) {
1236
1366
  (baton->err).append(what);
1237
1367
  } else {
1238
- (baton->err).append("Unknown error");
1368
+ if (baton->input->failOn == VIPS_FAIL_ON_WARNING) {
1369
+ (baton->err).append("Warning treated as error due to failOn setting");
1370
+ baton->errUseWarning = true;
1371
+ } else {
1372
+ (baton->err).append("Unknown error");
1373
+ }
1239
1374
  }
1240
1375
  }
1241
1376
  // Clean up libvips' per-request data and threads
@@ -1250,10 +1385,13 @@ class PipelineWorker : public Napi::AsyncWorker {
1250
1385
  // Handle warnings
1251
1386
  std::string warning = sharp::VipsWarningPop();
1252
1387
  while (!warning.empty()) {
1253
- debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) });
1388
+ if (baton->errUseWarning) {
1389
+ (baton->err).append("\n").append(warning);
1390
+ } else {
1391
+ debuglog.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { Napi::String::New(env, warning) });
1392
+ }
1254
1393
  warning = sharp::VipsWarningPop();
1255
1394
  }
1256
-
1257
1395
  if (baton->err.empty()) {
1258
1396
  int width = baton->width;
1259
1397
  int height = baton->height;
@@ -1294,24 +1432,36 @@ class PipelineWorker : public Napi::AsyncWorker {
1294
1432
  info.Set("pageHeight", static_cast<int32_t>(baton->pageHeightOut));
1295
1433
  info.Set("pages", static_cast<int32_t>(baton->pagesOut));
1296
1434
  }
1435
+ info.Set("hasAlpha", baton->hasAlphaOut);
1297
1436
 
1298
1437
  if (baton->bufferOutLength > 0) {
1299
- // Add buffer size to info
1300
1438
  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 });
1439
+ if (baton->typedArrayOut) {
1440
+ // ECMAScript ArrayBuffer with Uint8Array view
1441
+ Napi::TypedArrayOf<uint8_t> data = Napi::Buffer<char>::Copy(env,
1442
+ static_cast<char*>(baton->bufferOut), baton->bufferOutLength);
1443
+ sharp::FreeCallback(static_cast<char*>(baton->bufferOut), nullptr);
1444
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), data, info });
1445
+ } else {
1446
+ // Node.js Buffer
1447
+ Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
1448
+ baton->bufferOutLength, sharp::FreeCallback);
1449
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), data, info });
1450
+ }
1305
1451
  } else {
1306
1452
  // 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));
1453
+ if (baton->formatOut != "dz" || sharp::IsDzZip(baton->fileOut)) {
1454
+ try {
1455
+ uint32_t const size = static_cast<uint32_t>(
1456
+ std::filesystem::file_size(std::filesystem::u8path(baton->fileOut)));
1457
+ info.Set("size", size);
1458
+ } catch (...) {}
1310
1459
  }
1311
- Callback().Call(Receiver().Value(), { env.Null(), info });
1460
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), info });
1312
1461
  }
1313
1462
  } else {
1314
- Callback().Call(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1463
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(),
1464
+ { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1315
1465
  }
1316
1466
 
1317
1467
  // Delete baton
@@ -1324,12 +1474,15 @@ class PipelineWorker : public Napi::AsyncWorker {
1324
1474
  for (sharp::InputDescriptor *input : baton->joinChannelIn) {
1325
1475
  delete input;
1326
1476
  }
1477
+ for (sharp::InputDescriptor *input : baton->join) {
1478
+ delete input;
1479
+ }
1327
1480
  delete baton;
1328
1481
 
1329
1482
  // Decrement processing task counter
1330
1483
  sharp::counterProcess--;
1331
1484
  Napi::Number queueLength = Napi::Number::New(env, static_cast<int>(sharp::counterQueue));
1332
- queueListener.Call(Receiver().Value(), { queueLength });
1485
+ queueListener.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { queueLength });
1333
1486
  }
1334
1487
 
1335
1488
  private:
@@ -1339,7 +1492,13 @@ class PipelineWorker : public Napi::AsyncWorker {
1339
1492
 
1340
1493
  void MultiPageUnsupported(int const pages, std::string op) {
1341
1494
  if (pages > 1) {
1342
- throw vips::VError(op + " is not supported for multi-page images");
1495
+ throw std::runtime_error(op + " is not supported for multi-page images");
1496
+ }
1497
+ }
1498
+
1499
+ void KeepGainMapUnsupported(bool const keepGainMap, std::string op) {
1500
+ if (keepGainMap) {
1501
+ throw std::runtime_error(op + " is not supported when keeping gain maps");
1343
1502
  }
1344
1503
  }
1345
1504
 
@@ -1347,21 +1506,20 @@ class PipelineWorker : public Napi::AsyncWorker {
1347
1506
  Calculate the angle of rotation and need-to-flip for the given Exif orientation
1348
1507
  By default, returns zero, i.e. no rotation.
1349
1508
  */
1350
- std::tuple<VipsAngle, bool, bool>
1351
- CalculateExifRotationAndFlip(int const exifOrientation) {
1509
+ std::tuple<VipsAngle, bool>
1510
+ CalculateExifRotationAndFlop(int const exifOrientation) {
1352
1511
  VipsAngle rotate = VIPS_ANGLE_D0;
1353
- bool flip = false;
1354
1512
  bool flop = false;
1355
1513
  switch (exifOrientation) {
1356
1514
  case 6: rotate = VIPS_ANGLE_D90; break;
1357
1515
  case 3: rotate = VIPS_ANGLE_D180; break;
1358
1516
  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
1517
+ case 2: flop = true; break;
1518
+ case 7: flop = true; rotate = VIPS_ANGLE_D270; break;
1519
+ case 4: flop = true; rotate = VIPS_ANGLE_D180; break;
1520
+ case 5: flop = true; rotate = VIPS_ANGLE_D90; break;
1363
1521
  }
1364
- return std::make_tuple(rotate, flip, flop);
1522
+ return std::make_tuple(rotate, flop);
1365
1523
  }
1366
1524
 
1367
1525
  /*
@@ -1389,11 +1547,11 @@ class PipelineWorker : public Napi::AsyncWorker {
1389
1547
  std::string
1390
1548
  AssembleSuffixString(std::string extname, std::vector<std::pair<std::string, std::string>> options) {
1391
1549
  std::string argument;
1392
- for (auto const &option : options) {
1550
+ for (const auto& [key, value] : options) {
1393
1551
  if (!argument.empty()) {
1394
1552
  argument += ",";
1395
1553
  }
1396
- argument += option.first + "=" + option.second;
1554
+ argument += key + "=" + value;
1397
1555
  }
1398
1556
  return extname + "[" + argument + "]";
1399
1557
  }
@@ -1419,9 +1577,11 @@ class PipelineWorker : public Napi::AsyncWorker {
1419
1577
  {"lossless", baton->webpLossless ? "true" : "false"},
1420
1578
  {"near_lossless", baton->webpNearLossless ? "true" : "false"},
1421
1579
  {"smart_subsample", baton->webpSmartSubsample ? "true" : "false"},
1580
+ {"smart_deblock", baton->webpSmartDeblock ? "true" : "false"},
1422
1581
  {"preset", vips_enum_nick(VIPS_TYPE_FOREIGN_WEBP_PRESET, baton->webpPreset)},
1423
1582
  {"min_size", baton->webpMinSize ? "true" : "false"},
1424
1583
  {"mixed", baton->webpMixed ? "true" : "false"},
1584
+ {"exact", baton->webpExact ? "true" : "false"},
1425
1585
  {"effort", std::to_string(baton->webpEffort)}
1426
1586
  };
1427
1587
  suffix = AssembleSuffixString(".webp", options);
@@ -1460,6 +1620,21 @@ class PipelineWorker : public Napi::AsyncWorker {
1460
1620
  return options;
1461
1621
  }
1462
1622
 
1623
+ VImage ReattachGainMap(VImage image, VImage gainMap, PipelineBaton *baton) {
1624
+ VipsArea *gainMapJpeg = reinterpret_cast<VipsArea*>(gainMap.jpegsave_buffer(VImage::option()
1625
+ ->set("keep", FALSE)
1626
+ ->set("Q", baton->jpegQuality)
1627
+ ->set("subsample_mode", VIPS_FOREIGN_SUBSAMPLE_OFF)
1628
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
1629
+ ->set("quant_table", baton->jpegQuantisationTable)
1630
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1631
+ ->set("optimize_coding", baton->jpegOptimiseCoding)));
1632
+ image = image.copy();
1633
+ image.set("gainmap-data", reinterpret_cast<VipsCallbackFn>(vips_area_free_cb),
1634
+ gainMapJpeg->data, gainMapJpeg->length);
1635
+ return image;
1636
+ }
1637
+
1463
1638
  /*
1464
1639
  Clear all thread-local data.
1465
1640
  */
@@ -1480,6 +1655,14 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1480
1655
 
1481
1656
  // Input
1482
1657
  baton->input = sharp::CreateInputDescriptor(options.Get("input").As<Napi::Object>());
1658
+ // Join images together
1659
+ if (sharp::HasAttr(options, "join")) {
1660
+ Napi::Array join = options.Get("join").As<Napi::Array>();
1661
+ for (unsigned int i = 0; i < join.Length(); i++) {
1662
+ baton->join.push_back(
1663
+ sharp::CreateInputDescriptor(join.Get(i).As<Napi::Object>()));
1664
+ }
1665
+ }
1483
1666
  // Extract image options
1484
1667
  baton->topOffsetPre = sharp::AttrAsInt32(options, "topOffsetPre");
1485
1668
  baton->leftOffsetPre = sharp::AttrAsInt32(options, "leftOffsetPre");
@@ -1560,10 +1743,13 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1560
1743
  baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
1561
1744
  baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
1562
1745
  baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
1746
+ baton->trimMargin = sharp::AttrAsUint32(options, "trimMargin");
1563
1747
  baton->gamma = sharp::AttrAsDouble(options, "gamma");
1564
1748
  baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
1565
1749
  baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
1566
1750
  baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB");
1751
+ baton->dilateWidth = sharp::AttrAsUint32(options, "dilateWidth");
1752
+ baton->erodeWidth = sharp::AttrAsUint32(options, "erodeWidth");
1567
1753
  baton->greyscale = sharp::AttrAsBool(options, "greyscale");
1568
1754
  baton->normalise = sharp::AttrAsBool(options, "normalise");
1569
1755
  baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");
@@ -1572,11 +1758,11 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1572
1758
  baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
1573
1759
  baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
1574
1760
  baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
1575
- baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation");
1576
1761
  baton->angle = sharp::AttrAsInt32(options, "angle");
1577
1762
  baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
1578
1763
  baton->rotationBackground = sharp::AttrAsVectorOfDouble(options, "rotationBackground");
1579
- baton->rotateBeforePreExtract = sharp::AttrAsBool(options, "rotateBeforePreExtract");
1764
+ baton->rotateBefore = sharp::AttrAsBool(options, "rotateBefore");
1765
+ baton->orientBefore = sharp::AttrAsBool(options, "orientBefore");
1580
1766
  baton->flip = sharp::AttrAsBool(options, "flip");
1581
1767
  baton->flop = sharp::AttrAsBool(options, "flop");
1582
1768
  baton->extendTop = sharp::AttrAsInt32(options, "extendTop");
@@ -1635,6 +1821,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1635
1821
  // Output
1636
1822
  baton->formatOut = sharp::AttrAsStr(options, "formatOut");
1637
1823
  baton->fileOut = sharp::AttrAsStr(options, "fileOut");
1824
+ baton->typedArrayOut = sharp::AttrAsBool(options, "typedArrayOut");
1638
1825
  baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata");
1639
1826
  baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
1640
1827
  baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");
@@ -1648,7 +1835,12 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1648
1835
  }
1649
1836
  }
1650
1837
  baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge");
1838
+ baton->withXmp = sharp::AttrAsStr(options, "withXmp");
1839
+ baton->keepGainMap = sharp::AttrAsBool(options, "keepGainMap");
1840
+ baton->withGainMap = sharp::AttrAsBool(options, "withGainMap");
1651
1841
  baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1842
+ baton->loop = sharp::AttrAsUint32(options, "loop");
1843
+ baton->delay = sharp::AttrAsInt32Vector(options, "delay");
1652
1844
  // Format-specific
1653
1845
  baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
1654
1846
  baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");
@@ -1676,18 +1868,22 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1676
1868
  baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
1677
1869
  baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
1678
1870
  baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
1871
+ baton->webpSmartDeblock = sharp::AttrAsBool(options, "webpSmartDeblock");
1679
1872
  baton->webpPreset = sharp::AttrAsEnum<VipsForeignWebpPreset>(options, "webpPreset", VIPS_TYPE_FOREIGN_WEBP_PRESET);
1680
1873
  baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1681
1874
  baton->webpMinSize = sharp::AttrAsBool(options, "webpMinSize");
1682
1875
  baton->webpMixed = sharp::AttrAsBool(options, "webpMixed");
1876
+ baton->webpExact = sharp::AttrAsBool(options, "webpExact");
1683
1877
  baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1684
1878
  baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1685
1879
  baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1686
1880
  baton->gifInterFrameMaxError = sharp::AttrAsDouble(options, "gifInterFrameMaxError");
1687
1881
  baton->gifInterPaletteMaxError = sharp::AttrAsDouble(options, "gifInterPaletteMaxError");
1882
+ baton->gifKeepDuplicateFrames = sharp::AttrAsBool(options, "gifKeepDuplicateFrames");
1688
1883
  baton->gifReuse = sharp::AttrAsBool(options, "gifReuse");
1689
1884
  baton->gifProgressive = sharp::AttrAsBool(options, "gifProgressive");
1690
1885
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1886
+ baton->tiffBigtiff = sharp::AttrAsBool(options, "tiffBigtiff");
1691
1887
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
1692
1888
  baton->tiffMiniswhite = sharp::AttrAsBool(options, "tiffMiniswhite");
1693
1889
  baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
@@ -1712,18 +1908,12 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1712
1908
  baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1713
1909
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1714
1910
  baton->heifBitdepth = sharp::AttrAsUint32(options, "heifBitdepth");
1911
+ baton->heifTune = sharp::AttrAsStr(options, "heifTune");
1715
1912
  baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
1716
1913
  baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
1717
1914
  baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");
1718
1915
  baton->jxlLossless = sharp::AttrAsBool(options, "jxlLossless");
1719
1916
  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
1917
  baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
1728
1918
  baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");
1729
1919
  baton->tileAngle = sharp::AttrAsInt32(options, "tileAngle");
@@ -1752,7 +1942,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1752
1942
 
1753
1943
  // Increment queued task counter
1754
1944
  Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast<int>(++sharp::counterQueue));
1755
- queueListener.Call(info.This(), { queueLength });
1945
+ queueListener.SHARP_CALLBACK_FN_NAME(info.This(), { queueLength });
1756
1946
 
1757
1947
  return info.Env().Undefined();
1758
1948
  }