@revizly/sharp 0.35.0-revizly4 → 0.35.0-revizly41

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 (46) hide show
  1. package/README.md +12 -18
  2. package/{lib/channel.js → dist/channel.cjs} +1 -1
  3. package/dist/channel.mjs +177 -0
  4. package/{lib/colour.js → dist/colour.cjs} +11 -7
  5. package/dist/colour.mjs +199 -0
  6. package/{lib/composite.js → dist/composite.cjs} +2 -1
  7. package/dist/composite.mjs +213 -0
  8. package/{lib/constructor.js → dist/constructor.cjs} +42 -29
  9. package/dist/constructor.mjs +512 -0
  10. package/dist/index.cjs +25 -0
  11. package/dist/index.d.cts +2001 -0
  12. package/dist/index.d.mts +2048 -0
  13. package/dist/index.mjs +25 -0
  14. package/{lib/input.js → dist/input.cjs} +26 -21
  15. package/dist/input.mjs +814 -0
  16. package/{lib/is.js → dist/is.cjs} +1 -1
  17. package/dist/is.mjs +143 -0
  18. package/{lib/libvips.js → dist/libvips.cjs} +35 -30
  19. package/dist/libvips.mjs +212 -0
  20. package/{lib/operation.js → dist/operation.cjs} +33 -51
  21. package/dist/operation.mjs +998 -0
  22. package/{lib/output.js → dist/output.cjs} +161 -28
  23. package/dist/output.mjs +1799 -0
  24. package/{lib/resize.js → dist/resize.cjs} +46 -24
  25. package/dist/resize.mjs +617 -0
  26. package/dist/sharp.cjs +125 -0
  27. package/dist/sharp.mjs +125 -0
  28. package/{lib/utility.js → dist/utility.cjs} +18 -8
  29. package/dist/utility.mjs +301 -0
  30. package/install/build.js +3 -3
  31. package/lib/index.d.ts +105 -75
  32. package/package.json +46 -27
  33. package/src/binding.gyp +18 -13
  34. package/src/common.cc +70 -17
  35. package/src/common.h +22 -3
  36. package/src/metadata.cc +66 -8
  37. package/src/metadata.h +6 -1
  38. package/src/operations.cc +25 -8
  39. package/src/operations.h +1 -1
  40. package/src/pipeline.cc +206 -69
  41. package/src/pipeline.h +16 -1
  42. package/src/stats.cc +6 -6
  43. package/src/utilities.cc +7 -6
  44. package/install/check.js +0 -14
  45. package/lib/index.js +0 -16
  46. package/lib/sharp.js +0 -121
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);
@@ -554,26 +608,25 @@ class PipelineWorker : public Napi::AsyncWorker {
554
608
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
555
609
  baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
556
610
 
611
+ std::vector<double> background;
557
612
  if (baton->extendWith == VIPS_EXTEND_BACKGROUND) {
558
- std::vector<double> background;
559
613
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
560
-
561
- image = sharp::StaySequential(image, nPages > 1);
562
- image = nPages > 1
563
- ? sharp::EmbedMultiPage(image,
564
- baton->extendLeft, baton->extendTop, baton->width, baton->height,
565
- baton->extendWith, background, nPages, &targetPageHeight)
566
- : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
567
- VImage::option()->set("extend", baton->extendWith)->set("background", background));
568
- } else {
569
- std::vector<double> ignoredBackground(1);
570
- image = sharp::StaySequential(image);
571
- image = nPages > 1
572
- ? sharp::EmbedMultiPage(image,
573
- baton->extendLeft, baton->extendTop, baton->width, baton->height,
574
- baton->extendWith, ignoredBackground, nPages, &targetPageHeight)
575
- : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
576
- 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));
577
630
  }
578
631
  }
579
632
  // Median - must happen before blurring, due to the utility of blurring after thresholding
@@ -600,6 +653,9 @@ class PipelineWorker : public Napi::AsyncWorker {
600
653
  // Blur
601
654
  if (shouldBlur) {
602
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
+ }
603
659
  }
604
660
 
605
661
  // Unflatten the image
@@ -609,6 +665,7 @@ class PipelineWorker : public Napi::AsyncWorker {
609
665
 
610
666
  // Convolve
611
667
  if (shouldConv) {
668
+ KeepGainMapUnsupported(baton->keepGainMap, "Convolve");
612
669
  image = sharp::Convolve(image,
613
670
  baton->convKernelWidth, baton->convKernelHeight,
614
671
  baton->convKernelScale, baton->convKernelOffset,
@@ -617,11 +674,13 @@ class PipelineWorker : public Napi::AsyncWorker {
617
674
 
618
675
  // Recomb
619
676
  if (!baton->recombMatrix.empty()) {
677
+ KeepGainMapUnsupported(baton->keepGainMap, "Recomb");
620
678
  image = sharp::Recomb(image, baton->recombMatrix);
621
679
  }
622
680
 
623
681
  // Modulate
624
682
  if (baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0 || baton->lightness != 0.0) {
683
+ KeepGainMapUnsupported(baton->keepGainMap, "Modulate");
625
684
  image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
626
685
  }
627
686
 
@@ -639,6 +698,7 @@ class PipelineWorker : public Napi::AsyncWorker {
639
698
 
640
699
  // Composite
641
700
  if (shouldComposite) {
701
+ KeepGainMapUnsupported(baton->keepGainMap, "Composite");
642
702
  std::vector<VImage> images = { image };
643
703
  std::vector<int> modes, xs, ys;
644
704
  for (Composite *composite : baton->composite) {
@@ -667,7 +727,7 @@ class PipelineWorker : public Napi::AsyncWorker {
667
727
 
668
728
  // Verify within current dimensions
669
729
  if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
670
- 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");
671
731
  }
672
732
  // Check if overlay is tiled
673
733
  if (composite->tile) {
@@ -751,18 +811,21 @@ class PipelineWorker : public Napi::AsyncWorker {
751
811
 
752
812
  // Apply normalisation - stretch luminance to cover full dynamic range
753
813
  if (baton->normalise) {
814
+ KeepGainMapUnsupported(baton->keepGainMap, "Normalise");
754
815
  image = sharp::StaySequential(image);
755
816
  image = sharp::Normalise(image, baton->normaliseLower, baton->normaliseUpper);
756
817
  }
757
818
 
758
819
  // Apply contrast limiting adaptive histogram equalization (CLAHE)
759
820
  if (baton->claheWidth != 0 && baton->claheHeight != 0) {
821
+ KeepGainMapUnsupported(baton->keepGainMap, "Clahe");
760
822
  image = sharp::StaySequential(image);
761
823
  image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope);
762
824
  }
763
825
 
764
826
  // Apply bitwise boolean operation between images
765
827
  if (baton->boolean != nullptr) {
828
+ KeepGainMapUnsupported(baton->keepGainMap, "Boolean");
766
829
  VImage booleanImage;
767
830
  sharp::ImageType booleanImageType = sharp::ImageType::UNKNOWN;
768
831
  baton->boolean->access = access;
@@ -805,6 +868,7 @@ class PipelineWorker : public Napi::AsyncWorker {
805
868
 
806
869
  // Extract channel
807
870
  if (baton->extractChannel > -1) {
871
+ KeepGainMapUnsupported(baton->keepGainMap, "Extract channel");
808
872
  if (baton->extractChannel >= image.bands()) {
809
873
  if (baton->extractChannel == 3 && image.has_alpha()) {
810
874
  baton->extractChannel = image.bands() - 1;
@@ -839,6 +903,9 @@ class PipelineWorker : public Napi::AsyncWorker {
839
903
  // Negate the colours in the image
840
904
  if (baton->negate) {
841
905
  image = sharp::Negate(image, baton->negateAlpha);
906
+ if (baton->keepGainMap) {
907
+ gainMap = sharp::Negate(gainMap, false);
908
+ }
842
909
  }
843
910
 
844
911
  // Override EXIF Orientation tag
@@ -877,6 +944,7 @@ class PipelineWorker : public Napi::AsyncWorker {
877
944
  baton->pageHeightOut = image.get_int(VIPS_META_PAGE_HEIGHT);
878
945
  baton->pagesOut = image.get_int(VIPS_META_N_PAGES);
879
946
  }
947
+ baton->hasAlphaOut = image.has_alpha();
880
948
 
881
949
  // Output
882
950
  sharp::SetTimeout(image, baton->timeoutSeconds);
@@ -885,18 +953,27 @@ class PipelineWorker : public Napi::AsyncWorker {
885
953
  if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
886
954
  // Write JPEG to buffer
887
955
  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)));
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
+ }
900
977
  baton->bufferOut = static_cast<char*>(area->data);
901
978
  baton->bufferOutLength = area->length;
902
979
  area->free_fn = nullptr;
@@ -907,6 +984,7 @@ class PipelineWorker : public Napi::AsyncWorker {
907
984
  } else {
908
985
  baton->channels = std::min(baton->channels, 3);
909
986
  }
987
+ baton->hasAlphaOut = false;
910
988
  } else if (baton->formatOut == "jp2" || (baton->formatOut == "input"
911
989
  && inputImageType == sharp::ImageType::JP2)) {
912
990
  // Write JP2 to Buffer
@@ -957,6 +1035,7 @@ class PipelineWorker : public Napi::AsyncWorker {
957
1035
  ->set("effort", baton->webpEffort)
958
1036
  ->set("min_size", baton->webpMinSize)
959
1037
  ->set("mixed", baton->webpMixed)
1038
+ ->set("exact", baton->webpExact)
960
1039
  ->set("alpha_q", baton->webpAlphaQuality)));
961
1040
  baton->bufferOut = static_cast<char*>(area->data);
962
1041
  baton->bufferOutLength = area->length;
@@ -988,6 +1067,7 @@ class PipelineWorker : public Napi::AsyncWorker {
988
1067
  if (baton->tiffCompression == VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) {
989
1068
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
990
1069
  baton->channels = std::min(baton->channels, 3);
1070
+ baton->hasAlphaOut = false;
991
1071
  }
992
1072
  // Cast pixel values to float, if required
993
1073
  if (baton->tiffPredictor == VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT) {
@@ -1024,6 +1104,8 @@ class PipelineWorker : public Napi::AsyncWorker {
1024
1104
  ->set("compression", baton->heifCompression)
1025
1105
  ->set("effort", baton->heifEffort)
1026
1106
  ->set("bitdepth", baton->heifBitdepth)
1107
+ ->set("tune", baton->heifTune.c_str())
1108
+ ->set("encoder", baton->heifEncoder.c_str())
1027
1109
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1028
1110
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1029
1111
  ->set("lossless", baton->heifLossless)));
@@ -1046,6 +1128,9 @@ class PipelineWorker : public Napi::AsyncWorker {
1046
1128
  area->free_fn = nullptr;
1047
1129
  vips_area_unref(area);
1048
1130
  baton->formatOut = "dz";
1131
+ if (baton->tileFormat == "jpeg") {
1132
+ baton->hasAlphaOut = false;
1133
+ }
1049
1134
  } else if (baton->formatOut == "jxl" ||
1050
1135
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::JXL)) {
1051
1136
  // Write JXL to buffer
@@ -1076,20 +1161,19 @@ class PipelineWorker : public Napi::AsyncWorker {
1076
1161
  // Get raw image data
1077
1162
  baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
1078
1163
  if (baton->bufferOut == nullptr) {
1079
- (baton->err).append("Could not allocate enough memory for raw output");
1080
- return Error();
1164
+ throw std::runtime_error("Could not allocate enough memory for raw output");
1081
1165
  }
1082
1166
  baton->formatOut = "raw";
1083
1167
  } else {
1084
1168
  // Unsupported output format
1085
- (baton->err).append("Unsupported output format ");
1169
+ auto unsupported = std::string("Unsupported output format ");
1086
1170
  if (baton->formatOut == "input") {
1087
- (baton->err).append("when trying to match input format of ");
1088
- (baton->err).append(ImageTypeId(inputImageType));
1171
+ unsupported.append("when trying to match input format of ");
1172
+ unsupported.append(ImageTypeId(inputImageType));
1089
1173
  } else {
1090
- (baton->err).append(baton->formatOut);
1174
+ unsupported.append(baton->formatOut);
1091
1175
  }
1092
- return Error();
1176
+ throw std::runtime_error(unsupported);
1093
1177
  }
1094
1178
  } else {
1095
1179
  // File output
@@ -1112,20 +1196,29 @@ class PipelineWorker : public Napi::AsyncWorker {
1112
1196
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
1113
1197
  // Write JPEG to file
1114
1198
  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));
1199
+ if (baton->keepGainMap) {
1200
+ image = ReattachGainMap(image, gainMap, baton);
1201
+ image.uhdrsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1202
+ ->set("keep", baton->keepMetadata)
1203
+ ->set("Q", baton->jpegQuality)
1204
+ ->set("gainmap_scale_factor", gainMapScaleFactor));
1205
+ } else {
1206
+ image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1207
+ ->set("keep", baton->keepMetadata)
1208
+ ->set("Q", baton->jpegQuality)
1209
+ ->set("interlace", baton->jpegProgressive)
1210
+ ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
1211
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
1212
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
1213
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
1214
+ ->set("quant_table", baton->jpegQuantisationTable)
1215
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1216
+ ->set("optimize_scans", baton->jpegOptimiseScans)
1217
+ ->set("optimize_coding", baton->jpegOptimiseCoding));
1218
+ }
1127
1219
  baton->formatOut = "jpeg";
1128
1220
  baton->channels = std::min(baton->channels, 3);
1221
+ baton->hasAlphaOut = false;
1129
1222
  } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
1130
1223
  (willMatchInput && (inputImageType == sharp::ImageType::JP2))) {
1131
1224
  // Write JP2 to file
@@ -1168,6 +1261,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1168
1261
  ->set("effort", baton->webpEffort)
1169
1262
  ->set("min_size", baton->webpMinSize)
1170
1263
  ->set("mixed", baton->webpMixed)
1264
+ ->set("exact", baton->webpExact)
1171
1265
  ->set("alpha_q", baton->webpAlphaQuality));
1172
1266
  baton->formatOut = "webp";
1173
1267
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
@@ -1191,6 +1285,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1191
1285
  if (baton->tiffCompression == VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) {
1192
1286
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
1193
1287
  baton->channels = std::min(baton->channels, 3);
1288
+ baton->hasAlphaOut = false;
1194
1289
  }
1195
1290
  // Cast pixel values to float, if required
1196
1291
  if (baton->tiffPredictor == VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT) {
@@ -1223,6 +1318,8 @@ class PipelineWorker : public Napi::AsyncWorker {
1223
1318
  ->set("compression", baton->heifCompression)
1224
1319
  ->set("effort", baton->heifEffort)
1225
1320
  ->set("bitdepth", baton->heifBitdepth)
1321
+ ->set("tune", baton->heifTune.c_str())
1322
+ ->set("encoder", baton->heifEncoder.c_str())
1226
1323
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1227
1324
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1228
1325
  ->set("lossless", baton->heifLossless));
@@ -1250,6 +1347,9 @@ class PipelineWorker : public Napi::AsyncWorker {
1250
1347
  vips::VOption *options = BuildOptionsDZ(baton);
1251
1348
  image.dzsave(const_cast<char*>(baton->fileOut.data()), options);
1252
1349
  baton->formatOut = "dz";
1350
+ if (baton->tileFormat == "jpeg") {
1351
+ baton->hasAlphaOut = false;
1352
+ }
1253
1353
  } else if (baton->formatOut == "v" || (mightMatchInput && isV) ||
1254
1354
  (willMatchInput && inputImageType == sharp::ImageType::VIPS)) {
1255
1355
  // Write V to file
@@ -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;
@@ -1335,14 +1434,22 @@ class PipelineWorker : public Napi::AsyncWorker {
1335
1434
  info.Set("pageHeight", static_cast<int32_t>(baton->pageHeightOut));
1336
1435
  info.Set("pages", static_cast<int32_t>(baton->pagesOut));
1337
1436
  }
1437
+ info.Set("hasAlpha", baton->hasAlphaOut);
1338
1438
 
1339
1439
  if (baton->bufferOutLength > 0) {
1340
- // Add buffer size to info
1341
1440
  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 });
1441
+ if (baton->typedArrayOut) {
1442
+ // ECMAScript ArrayBuffer with Uint8Array view
1443
+ Napi::TypedArrayOf<uint8_t> data = Napi::Buffer<char>::Copy(env,
1444
+ static_cast<char*>(baton->bufferOut), baton->bufferOutLength);
1445
+ sharp::FreeCallback(static_cast<char*>(baton->bufferOut), nullptr);
1446
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), data, info });
1447
+ } else {
1448
+ // Node.js Buffer
1449
+ Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
1450
+ baton->bufferOutLength, sharp::FreeCallback);
1451
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), data, info });
1452
+ }
1346
1453
  } else {
1347
1454
  // Add file size to info
1348
1455
  if (baton->formatOut != "dz" || sharp::IsDzZip(baton->fileOut)) {
@@ -1352,10 +1459,11 @@ class PipelineWorker : public Napi::AsyncWorker {
1352
1459
  info.Set("size", size);
1353
1460
  } catch (...) {}
1354
1461
  }
1355
- Callback().Call(Receiver().Value(), { env.Null(), info });
1462
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), info });
1356
1463
  }
1357
1464
  } else {
1358
- Callback().Call(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1465
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(),
1466
+ { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1359
1467
  }
1360
1468
 
1361
1469
  // Delete baton
@@ -1376,7 +1484,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1376
1484
  // Decrement processing task counter
1377
1485
  sharp::counterProcess--;
1378
1486
  Napi::Number queueLength = Napi::Number::New(env, static_cast<int>(sharp::counterQueue));
1379
- queueListener.Call(Receiver().Value(), { queueLength });
1487
+ queueListener.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { queueLength });
1380
1488
  }
1381
1489
 
1382
1490
  private:
@@ -1386,7 +1494,13 @@ class PipelineWorker : public Napi::AsyncWorker {
1386
1494
 
1387
1495
  void MultiPageUnsupported(int const pages, std::string op) {
1388
1496
  if (pages > 1) {
1389
- throw vips::VError(op + " is not supported for multi-page images");
1497
+ throw std::runtime_error(op + " is not supported for multi-page images");
1498
+ }
1499
+ }
1500
+
1501
+ void KeepGainMapUnsupported(bool const keepGainMap, std::string op) {
1502
+ if (keepGainMap) {
1503
+ throw std::runtime_error(op + " is not supported when keeping gain maps");
1390
1504
  }
1391
1505
  }
1392
1506
 
@@ -1469,6 +1583,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1469
1583
  {"preset", vips_enum_nick(VIPS_TYPE_FOREIGN_WEBP_PRESET, baton->webpPreset)},
1470
1584
  {"min_size", baton->webpMinSize ? "true" : "false"},
1471
1585
  {"mixed", baton->webpMixed ? "true" : "false"},
1586
+ {"exact", baton->webpExact ? "true" : "false"},
1472
1587
  {"effort", std::to_string(baton->webpEffort)}
1473
1588
  };
1474
1589
  suffix = AssembleSuffixString(".webp", options);
@@ -1507,6 +1622,21 @@ class PipelineWorker : public Napi::AsyncWorker {
1507
1622
  return options;
1508
1623
  }
1509
1624
 
1625
+ VImage ReattachGainMap(VImage image, VImage gainMap, PipelineBaton *baton) {
1626
+ VipsArea *gainMapJpeg = reinterpret_cast<VipsArea*>(gainMap.jpegsave_buffer(VImage::option()
1627
+ ->set("keep", FALSE)
1628
+ ->set("Q", baton->jpegQuality)
1629
+ ->set("subsample_mode", VIPS_FOREIGN_SUBSAMPLE_OFF)
1630
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
1631
+ ->set("quant_table", baton->jpegQuantisationTable)
1632
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1633
+ ->set("optimize_coding", baton->jpegOptimiseCoding)));
1634
+ image = image.copy();
1635
+ image.set("gainmap-data", reinterpret_cast<VipsCallbackFn>(vips_area_free_cb),
1636
+ gainMapJpeg->data, gainMapJpeg->length);
1637
+ return image;
1638
+ }
1639
+
1510
1640
  /*
1511
1641
  Clear all thread-local data.
1512
1642
  */
@@ -1615,6 +1745,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1615
1745
  baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
1616
1746
  baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
1617
1747
  baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
1748
+ baton->trimMargin = sharp::AttrAsUint32(options, "trimMargin");
1618
1749
  baton->gamma = sharp::AttrAsDouble(options, "gamma");
1619
1750
  baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
1620
1751
  baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
@@ -1692,6 +1823,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1692
1823
  // Output
1693
1824
  baton->formatOut = sharp::AttrAsStr(options, "formatOut");
1694
1825
  baton->fileOut = sharp::AttrAsStr(options, "fileOut");
1826
+ baton->typedArrayOut = sharp::AttrAsBool(options, "typedArrayOut");
1695
1827
  baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata");
1696
1828
  baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
1697
1829
  baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");
@@ -1706,6 +1838,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1706
1838
  }
1707
1839
  baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge");
1708
1840
  baton->withXmp = sharp::AttrAsStr(options, "withXmp");
1841
+ baton->keepGainMap = sharp::AttrAsBool(options, "keepGainMap");
1842
+ baton->withGainMap = sharp::AttrAsBool(options, "withGainMap");
1709
1843
  baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1710
1844
  baton->loop = sharp::AttrAsUint32(options, "loop");
1711
1845
  baton->delay = sharp::AttrAsInt32Vector(options, "delay");
@@ -1741,6 +1875,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1741
1875
  baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1742
1876
  baton->webpMinSize = sharp::AttrAsBool(options, "webpMinSize");
1743
1877
  baton->webpMixed = sharp::AttrAsBool(options, "webpMixed");
1878
+ baton->webpExact = sharp::AttrAsBool(options, "webpExact");
1744
1879
  baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1745
1880
  baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1746
1881
  baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
@@ -1775,6 +1910,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1775
1910
  baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1776
1911
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1777
1912
  baton->heifBitdepth = sharp::AttrAsUint32(options, "heifBitdepth");
1913
+ baton->heifTune = sharp::AttrAsStr(options, "heifTune");
1914
+ baton->heifEncoder = sharp::AttrAsStr(options, "heifEncoder");
1778
1915
  baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
1779
1916
  baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
1780
1917
  baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");
@@ -1808,7 +1945,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1808
1945
 
1809
1946
  // Increment queued task counter
1810
1947
  Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast<int>(++sharp::counterQueue));
1811
- queueListener.Call(info.This(), { queueLength });
1948
+ queueListener.SHARP_CALLBACK_FN_NAME(info.This(), { queueLength });
1812
1949
 
1813
1950
  return info.Env().Undefined();
1814
1951
  }