sharp 0.34.5 → 0.35.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/pipeline.cc CHANGED
@@ -84,7 +84,7 @@ class PipelineWorker : public Napi::AsyncWorker {
84
84
  if (nPages == -1) {
85
85
  // Resolve the number of pages if we need to render until the end of the document
86
86
  nPages = image.get_typeof(VIPS_META_N_PAGES) != 0
87
- ? image.get_int(VIPS_META_N_PAGES) - baton->input->page
87
+ ? image.get_int(VIPS_META_N_PAGES) - std::max(0, baton->input->page)
88
88
  : 1;
89
89
  }
90
90
 
@@ -153,7 +153,7 @@ class PipelineWorker : public Napi::AsyncWorker {
153
153
  if (baton->trimThreshold >= 0.0) {
154
154
  MultiPageUnsupported(nPages, "Trim");
155
155
  image = sharp::StaySequential(image);
156
- image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt);
156
+ image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold, baton->trimLineArt, baton->trimMargin);
157
157
  baton->trimOffsetLeft = image.xoffset();
158
158
  baton->trimOffsetTop = image.yoffset();
159
159
  }
@@ -203,9 +203,11 @@ class PipelineWorker : public Napi::AsyncWorker {
203
203
  // - the width or height parameters are specified;
204
204
  // - gamma correction doesn't need to be applied;
205
205
  // - trimming or pre-resize extract isn't required;
206
+ // - gain map processing is not required;
206
207
  // - input colourspace is not specified;
207
208
  bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
208
209
  baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold < 0.0 &&
210
+ !baton->keepGainMap && !baton->withGainMap &&
209
211
  baton->colourspacePipeline == VIPS_INTERPRETATION_LAST && !(shouldOrientBefore || shouldRotateBefore);
210
212
 
211
213
  if (shouldPreShrink) {
@@ -274,7 +276,7 @@ class PipelineWorker : public Napi::AsyncWorker {
274
276
  }
275
277
  sharp::SetDensity(image, baton->input->density);
276
278
  if (image.width() > 32767 || image.height() > 32767) {
277
- 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");
278
280
  }
279
281
  } else if (inputImageType == sharp::ImageType::PDF) {
280
282
  if (baton->input->buffer != nullptr) {
@@ -290,12 +292,28 @@ class PipelineWorker : public Napi::AsyncWorker {
290
292
  }
291
293
  } else {
292
294
  if (inputImageType == sharp::ImageType::SVG && (image.width() > 32767 || image.height() > 32767)) {
293
- throw vips::VError("Input SVG image exceeds 32767x32767 pixel limit");
295
+ throw std::runtime_error("Input SVG image exceeds 32767x32767 pixel limit");
294
296
  }
295
297
  }
296
298
  if (baton->input->autoOrient) {
297
299
  image = sharp::RemoveExifOrientation(image);
298
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
+ }
299
317
 
300
318
  // Any pre-shrinking may already have been done
301
319
  inputWidth = image.width();
@@ -335,7 +353,7 @@ class PipelineWorker : public Napi::AsyncWorker {
335
353
  image.interpretation() != VIPS_INTERPRETATION_LABS &&
336
354
  image.interpretation() != VIPS_INTERPRETATION_GREY16 &&
337
355
  baton->colourspacePipeline != VIPS_INTERPRETATION_CMYK &&
338
- !baton->input->ignoreIcc
356
+ !baton->input->ignoreIcc && !baton->withGainMap
339
357
  ) {
340
358
  // Convert to sRGB/P3 using embedded profile
341
359
  try {
@@ -393,6 +411,11 @@ class PipelineWorker : public Napi::AsyncWorker {
393
411
  image = image.resize(1.0 / hshrink, VImage::option()
394
412
  ->set("vscale", 1.0 / vshrink)
395
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
+ }
396
419
  }
397
420
 
398
421
  image = sharp::StaySequential(image,
@@ -405,14 +428,23 @@ class PipelineWorker : public Napi::AsyncWorker {
405
428
  MultiPageUnsupported(nPages, "Rotate");
406
429
  }
407
430
  image = image.rot(autoRotation);
431
+ if (baton->keepGainMap) {
432
+ gainMap = gainMap.rot(autoRotation);
433
+ }
408
434
  }
409
435
  // Mirror vertically (up-down) about the x-axis
410
436
  if (baton->flip) {
411
437
  image = image.flip(VIPS_DIRECTION_VERTICAL);
438
+ if (baton->keepGainMap) {
439
+ gainMap = gainMap.flip(VIPS_DIRECTION_VERTICAL);
440
+ }
412
441
  }
413
442
  // Mirror horizontally (left-right) about the y-axis
414
443
  if (baton->flop != autoFlop) {
415
444
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
445
+ if (baton->keepGainMap) {
446
+ gainMap = gainMap.flip(VIPS_DIRECTION_HORIZONTAL);
447
+ }
416
448
  }
417
449
  // Rotate post-extract 90-angle
418
450
  if (rotation != VIPS_ANGLE_D0) {
@@ -420,6 +452,9 @@ class PipelineWorker : public Napi::AsyncWorker {
420
452
  MultiPageUnsupported(nPages, "Rotate");
421
453
  }
422
454
  image = image.rot(rotation);
455
+ if (baton->keepGainMap) {
456
+ gainMap = gainMap.rot(rotation);
457
+ }
423
458
  }
424
459
 
425
460
  // Join additional color channels to the image
@@ -466,6 +501,12 @@ class PipelineWorker : public Napi::AsyncWorker {
466
501
  : image.embed(left, top, width, height, VImage::option()
467
502
  ->set("extend", VIPS_EXTEND_BACKGROUND)
468
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
+ }
469
510
  } else if (baton->canvas == sharp::Canvas::CROP) {
470
511
  if (baton->width > inputWidth) {
471
512
  baton->width = inputWidth;
@@ -486,12 +527,17 @@ class PipelineWorker : public Napi::AsyncWorker {
486
527
  ? sharp::CropMultiPage(image,
487
528
  left, top, width, height, nPages, &targetPageHeight)
488
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
+ }
489
534
  } else {
490
535
  int attention_x;
491
536
  int attention_y;
492
537
 
493
538
  // Attention-based or Entropy-based crop
494
539
  MultiPageUnsupported(nPages, "Resize strategy");
540
+ KeepGainMapUnsupported(baton->keepGainMap, "Resize strategy");
495
541
  image = sharp::StaySequential(image);
496
542
  image = image.smartcrop(baton->width, baton->height, VImage::option()
497
543
  ->set("interesting", baton->position == 16 ? VIPS_INTERESTING_ENTROPY : VIPS_INTERESTING_ATTENTION)
@@ -515,6 +561,9 @@ class PipelineWorker : public Napi::AsyncWorker {
515
561
  std::vector<double> background;
516
562
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
517
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
+ }
518
567
  }
519
568
 
520
569
  // Post extraction
@@ -523,18 +572,23 @@ class PipelineWorker : public Napi::AsyncWorker {
523
572
  image = sharp::CropMultiPage(image,
524
573
  baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost,
525
574
  nPages, &targetPageHeight);
526
-
527
575
  // heightPost is used in the info object, so update to reflect the number of pages
528
576
  baton->heightPost *= nPages;
529
577
  } else {
530
578
  image = image.extract_area(
531
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
+ }
532
585
  }
533
586
  }
534
587
 
535
588
  // Affine transform
536
589
  if (!baton->affineMatrix.empty()) {
537
590
  MultiPageUnsupported(nPages, "Affine");
591
+ KeepGainMapUnsupported(baton->keepGainMap, "Affine");
538
592
  image = sharp::StaySequential(image);
539
593
  std::vector<double> background;
540
594
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
@@ -565,6 +619,12 @@ class PipelineWorker : public Napi::AsyncWorker {
565
619
  baton->extendWith, background, nPages, &targetPageHeight)
566
620
  : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
567
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
+ }
568
628
  } else {
569
629
  std::vector<double> ignoredBackground(1);
570
630
  image = sharp::StaySequential(image);
@@ -574,6 +634,12 @@ class PipelineWorker : public Napi::AsyncWorker {
574
634
  baton->extendWith, ignoredBackground, nPages, &targetPageHeight)
575
635
  : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
576
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
+ }
577
643
  }
578
644
  }
579
645
  // Median - must happen before blurring, due to the utility of blurring after thresholding
@@ -600,6 +666,9 @@ class PipelineWorker : public Napi::AsyncWorker {
600
666
  // Blur
601
667
  if (shouldBlur) {
602
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
+ }
603
672
  }
604
673
 
605
674
  // Unflatten the image
@@ -609,6 +678,7 @@ class PipelineWorker : public Napi::AsyncWorker {
609
678
 
610
679
  // Convolve
611
680
  if (shouldConv) {
681
+ KeepGainMapUnsupported(baton->keepGainMap, "Convolve");
612
682
  image = sharp::Convolve(image,
613
683
  baton->convKernelWidth, baton->convKernelHeight,
614
684
  baton->convKernelScale, baton->convKernelOffset,
@@ -617,11 +687,13 @@ class PipelineWorker : public Napi::AsyncWorker {
617
687
 
618
688
  // Recomb
619
689
  if (!baton->recombMatrix.empty()) {
690
+ KeepGainMapUnsupported(baton->keepGainMap, "Recomb");
620
691
  image = sharp::Recomb(image, baton->recombMatrix);
621
692
  }
622
693
 
623
694
  // Modulate
624
695
  if (baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0 || baton->lightness != 0.0) {
696
+ KeepGainMapUnsupported(baton->keepGainMap, "Modulate");
625
697
  image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
626
698
  }
627
699
 
@@ -639,6 +711,7 @@ class PipelineWorker : public Napi::AsyncWorker {
639
711
 
640
712
  // Composite
641
713
  if (shouldComposite) {
714
+ KeepGainMapUnsupported(baton->keepGainMap, "Composite");
642
715
  std::vector<VImage> images = { image };
643
716
  std::vector<int> modes, xs, ys;
644
717
  for (Composite *composite : baton->composite) {
@@ -667,7 +740,7 @@ class PipelineWorker : public Napi::AsyncWorker {
667
740
 
668
741
  // Verify within current dimensions
669
742
  if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
670
- 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");
671
744
  }
672
745
  // Check if overlay is tiled
673
746
  if (composite->tile) {
@@ -751,18 +824,21 @@ class PipelineWorker : public Napi::AsyncWorker {
751
824
 
752
825
  // Apply normalisation - stretch luminance to cover full dynamic range
753
826
  if (baton->normalise) {
827
+ KeepGainMapUnsupported(baton->keepGainMap, "Normalise");
754
828
  image = sharp::StaySequential(image);
755
829
  image = sharp::Normalise(image, baton->normaliseLower, baton->normaliseUpper);
756
830
  }
757
831
 
758
832
  // Apply contrast limiting adaptive histogram equalization (CLAHE)
759
833
  if (baton->claheWidth != 0 && baton->claheHeight != 0) {
834
+ KeepGainMapUnsupported(baton->keepGainMap, "Clahe");
760
835
  image = sharp::StaySequential(image);
761
836
  image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope);
762
837
  }
763
838
 
764
839
  // Apply bitwise boolean operation between images
765
840
  if (baton->boolean != nullptr) {
841
+ KeepGainMapUnsupported(baton->keepGainMap, "Boolean");
766
842
  VImage booleanImage;
767
843
  sharp::ImageType booleanImageType = sharp::ImageType::UNKNOWN;
768
844
  baton->boolean->access = access;
@@ -805,6 +881,7 @@ class PipelineWorker : public Napi::AsyncWorker {
805
881
 
806
882
  // Extract channel
807
883
  if (baton->extractChannel > -1) {
884
+ KeepGainMapUnsupported(baton->keepGainMap, "Extract channel");
808
885
  if (baton->extractChannel >= image.bands()) {
809
886
  if (baton->extractChannel == 3 && image.has_alpha()) {
810
887
  baton->extractChannel = image.bands() - 1;
@@ -839,6 +916,9 @@ class PipelineWorker : public Napi::AsyncWorker {
839
916
  // Negate the colours in the image
840
917
  if (baton->negate) {
841
918
  image = sharp::Negate(image, baton->negateAlpha);
919
+ if (baton->keepGainMap) {
920
+ gainMap = sharp::Negate(gainMap, false);
921
+ }
842
922
  }
843
923
 
844
924
  // Override EXIF Orientation tag
@@ -885,18 +965,27 @@ class PipelineWorker : public Napi::AsyncWorker {
885
965
  if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
886
966
  // Write JPEG to buffer
887
967
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
888
- VipsArea *area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option()
889
- ->set("keep", baton->keepMetadata)
890
- ->set("Q", baton->jpegQuality)
891
- ->set("interlace", baton->jpegProgressive)
892
- ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
893
- ? VIPS_FOREIGN_SUBSAMPLE_OFF
894
- : VIPS_FOREIGN_SUBSAMPLE_ON)
895
- ->set("trellis_quant", baton->jpegTrellisQuantisation)
896
- ->set("quant_table", baton->jpegQuantisationTable)
897
- ->set("overshoot_deringing", baton->jpegOvershootDeringing)
898
- ->set("optimize_scans", baton->jpegOptimiseScans)
899
- ->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
+ }
900
989
  baton->bufferOut = static_cast<char*>(area->data);
901
990
  baton->bufferOutLength = area->length;
902
991
  area->free_fn = nullptr;
@@ -957,6 +1046,7 @@ class PipelineWorker : public Napi::AsyncWorker {
957
1046
  ->set("effort", baton->webpEffort)
958
1047
  ->set("min_size", baton->webpMinSize)
959
1048
  ->set("mixed", baton->webpMixed)
1049
+ ->set("exact", baton->webpExact)
960
1050
  ->set("alpha_q", baton->webpAlphaQuality)));
961
1051
  baton->bufferOut = static_cast<char*>(area->data);
962
1052
  baton->bufferOutLength = area->length;
@@ -1024,6 +1114,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1024
1114
  ->set("compression", baton->heifCompression)
1025
1115
  ->set("effort", baton->heifEffort)
1026
1116
  ->set("bitdepth", baton->heifBitdepth)
1117
+ ->set("tune", baton->heifTune.c_str())
1027
1118
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1028
1119
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1029
1120
  ->set("lossless", baton->heifLossless)));
@@ -1076,20 +1167,19 @@ class PipelineWorker : public Napi::AsyncWorker {
1076
1167
  // Get raw image data
1077
1168
  baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
1078
1169
  if (baton->bufferOut == nullptr) {
1079
- (baton->err).append("Could not allocate enough memory for raw output");
1080
- return Error();
1170
+ throw std::runtime_error("Could not allocate enough memory for raw output");
1081
1171
  }
1082
1172
  baton->formatOut = "raw";
1083
1173
  } else {
1084
1174
  // Unsupported output format
1085
- (baton->err).append("Unsupported output format ");
1175
+ auto unsupported = std::string("Unsupported output format ");
1086
1176
  if (baton->formatOut == "input") {
1087
- (baton->err).append("when trying to match input format of ");
1088
- (baton->err).append(ImageTypeId(inputImageType));
1177
+ unsupported.append("when trying to match input format of ");
1178
+ unsupported.append(ImageTypeId(inputImageType));
1089
1179
  } else {
1090
- (baton->err).append(baton->formatOut);
1180
+ unsupported.append(baton->formatOut);
1091
1181
  }
1092
- return Error();
1182
+ throw std::runtime_error(unsupported);
1093
1183
  }
1094
1184
  } else {
1095
1185
  // File output
@@ -1112,18 +1202,26 @@ class PipelineWorker : public Napi::AsyncWorker {
1112
1202
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
1113
1203
  // Write JPEG to file
1114
1204
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
1115
- image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1116
- ->set("keep", baton->keepMetadata)
1117
- ->set("Q", baton->jpegQuality)
1118
- ->set("interlace", baton->jpegProgressive)
1119
- ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
1120
- ? VIPS_FOREIGN_SUBSAMPLE_OFF
1121
- : VIPS_FOREIGN_SUBSAMPLE_ON)
1122
- ->set("trellis_quant", baton->jpegTrellisQuantisation)
1123
- ->set("quant_table", baton->jpegQuantisationTable)
1124
- ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1125
- ->set("optimize_scans", baton->jpegOptimiseScans)
1126
- ->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
+ }
1127
1225
  baton->formatOut = "jpeg";
1128
1226
  baton->channels = std::min(baton->channels, 3);
1129
1227
  } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
@@ -1168,6 +1266,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1168
1266
  ->set("effort", baton->webpEffort)
1169
1267
  ->set("min_size", baton->webpMinSize)
1170
1268
  ->set("mixed", baton->webpMixed)
1269
+ ->set("exact", baton->webpExact)
1171
1270
  ->set("alpha_q", baton->webpAlphaQuality));
1172
1271
  baton->formatOut = "webp";
1173
1272
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
@@ -1223,6 +1322,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1223
1322
  ->set("compression", baton->heifCompression)
1224
1323
  ->set("effort", baton->heifEffort)
1225
1324
  ->set("bitdepth", baton->heifBitdepth)
1325
+ ->set("tune", baton->heifTune.c_str())
1226
1326
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1227
1327
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1228
1328
  ->set("lossless", baton->heifLossless));
@@ -1262,7 +1362,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1262
1362
  return Error();
1263
1363
  }
1264
1364
  }
1265
- } catch (vips::VError const &err) {
1365
+ } catch (std::runtime_error const &err) {
1266
1366
  char const *what = err.what();
1267
1367
  if (what && what[0]) {
1268
1368
  (baton->err).append(what);
@@ -1290,11 +1390,10 @@ class PipelineWorker : public Napi::AsyncWorker {
1290
1390
  if (baton->errUseWarning) {
1291
1391
  (baton->err).append("\n").append(warning);
1292
1392
  } else {
1293
- debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) });
1393
+ debuglog.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { Napi::String::New(env, warning) });
1294
1394
  }
1295
1395
  warning = sharp::VipsWarningPop();
1296
1396
  }
1297
-
1298
1397
  if (baton->err.empty()) {
1299
1398
  int width = baton->width;
1300
1399
  int height = baton->height;
@@ -1337,12 +1436,19 @@ class PipelineWorker : public Napi::AsyncWorker {
1337
1436
  }
1338
1437
 
1339
1438
  if (baton->bufferOutLength > 0) {
1340
- // Add buffer size to info
1341
1439
  info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));
1342
- // Pass ownership of output data to Buffer instance
1343
- Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
1344
- baton->bufferOutLength, sharp::FreeCallback);
1345
- 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
+ }
1346
1452
  } else {
1347
1453
  // Add file size to info
1348
1454
  if (baton->formatOut != "dz" || sharp::IsDzZip(baton->fileOut)) {
@@ -1352,10 +1458,11 @@ class PipelineWorker : public Napi::AsyncWorker {
1352
1458
  info.Set("size", size);
1353
1459
  } catch (...) {}
1354
1460
  }
1355
- Callback().Call(Receiver().Value(), { env.Null(), info });
1461
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), info });
1356
1462
  }
1357
1463
  } else {
1358
- 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() });
1359
1466
  }
1360
1467
 
1361
1468
  // Delete baton
@@ -1376,7 +1483,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1376
1483
  // Decrement processing task counter
1377
1484
  sharp::counterProcess--;
1378
1485
  Napi::Number queueLength = Napi::Number::New(env, static_cast<int>(sharp::counterQueue));
1379
- queueListener.Call(Receiver().Value(), { queueLength });
1486
+ queueListener.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { queueLength });
1380
1487
  }
1381
1488
 
1382
1489
  private:
@@ -1386,7 +1493,13 @@ class PipelineWorker : public Napi::AsyncWorker {
1386
1493
 
1387
1494
  void MultiPageUnsupported(int const pages, std::string op) {
1388
1495
  if (pages > 1) {
1389
- 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");
1390
1503
  }
1391
1504
  }
1392
1505
 
@@ -1469,6 +1582,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1469
1582
  {"preset", vips_enum_nick(VIPS_TYPE_FOREIGN_WEBP_PRESET, baton->webpPreset)},
1470
1583
  {"min_size", baton->webpMinSize ? "true" : "false"},
1471
1584
  {"mixed", baton->webpMixed ? "true" : "false"},
1585
+ {"exact", baton->webpExact ? "true" : "false"},
1472
1586
  {"effort", std::to_string(baton->webpEffort)}
1473
1587
  };
1474
1588
  suffix = AssembleSuffixString(".webp", options);
@@ -1507,6 +1621,21 @@ class PipelineWorker : public Napi::AsyncWorker {
1507
1621
  return options;
1508
1622
  }
1509
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
+
1510
1639
  /*
1511
1640
  Clear all thread-local data.
1512
1641
  */
@@ -1615,6 +1744,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1615
1744
  baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
1616
1745
  baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
1617
1746
  baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
1747
+ baton->trimMargin = sharp::AttrAsUint32(options, "trimMargin");
1618
1748
  baton->gamma = sharp::AttrAsDouble(options, "gamma");
1619
1749
  baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
1620
1750
  baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
@@ -1692,6 +1822,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1692
1822
  // Output
1693
1823
  baton->formatOut = sharp::AttrAsStr(options, "formatOut");
1694
1824
  baton->fileOut = sharp::AttrAsStr(options, "fileOut");
1825
+ baton->typedArrayOut = sharp::AttrAsBool(options, "typedArrayOut");
1695
1826
  baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata");
1696
1827
  baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
1697
1828
  baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");
@@ -1706,6 +1837,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1706
1837
  }
1707
1838
  baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge");
1708
1839
  baton->withXmp = sharp::AttrAsStr(options, "withXmp");
1840
+ baton->keepGainMap = sharp::AttrAsBool(options, "keepGainMap");
1841
+ baton->withGainMap = sharp::AttrAsBool(options, "withGainMap");
1709
1842
  baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1710
1843
  baton->loop = sharp::AttrAsUint32(options, "loop");
1711
1844
  baton->delay = sharp::AttrAsInt32Vector(options, "delay");
@@ -1741,6 +1874,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1741
1874
  baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1742
1875
  baton->webpMinSize = sharp::AttrAsBool(options, "webpMinSize");
1743
1876
  baton->webpMixed = sharp::AttrAsBool(options, "webpMixed");
1877
+ baton->webpExact = sharp::AttrAsBool(options, "webpExact");
1744
1878
  baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1745
1879
  baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1746
1880
  baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
@@ -1775,6 +1909,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1775
1909
  baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1776
1910
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1777
1911
  baton->heifBitdepth = sharp::AttrAsUint32(options, "heifBitdepth");
1912
+ baton->heifTune = sharp::AttrAsStr(options, "heifTune");
1778
1913
  baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
1779
1914
  baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
1780
1915
  baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");
@@ -1808,7 +1943,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1808
1943
 
1809
1944
  // Increment queued task counter
1810
1945
  Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast<int>(++sharp::counterQueue));
1811
- queueListener.Call(info.This(), { queueLength });
1946
+ queueListener.SHARP_CALLBACK_FN_NAME(info.This(), { queueLength });
1812
1947
 
1813
1948
  return info.Env().Undefined();
1814
1949
  }