@depup/sharp 0.34.5-depup.0 → 0.35.4-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +16 -109
  2. package/changes.json +5 -0
  3. package/{lib/channel.js → dist/channel.cjs} +1 -1
  4. package/dist/channel.mjs +177 -0
  5. package/{lib/colour.js → dist/colour.cjs} +11 -7
  6. package/dist/colour.mjs +199 -0
  7. package/{lib/composite.js → dist/composite.cjs} +8 -7
  8. package/dist/composite.mjs +213 -0
  9. package/{lib/constructor.js → dist/constructor.cjs} +42 -30
  10. package/dist/constructor.mjs +511 -0
  11. package/dist/index.cjs +25 -0
  12. package/dist/index.d.cts +1999 -0
  13. package/dist/index.d.mts +2046 -0
  14. package/dist/index.mjs +25 -0
  15. package/{lib/input.js → dist/input.cjs} +47 -37
  16. package/dist/input.mjs +819 -0
  17. package/{lib/is.js → dist/is.cjs} +1 -1
  18. package/dist/is.mjs +143 -0
  19. package/{lib/libvips.js → dist/libvips.cjs} +35 -30
  20. package/dist/libvips.mjs +212 -0
  21. package/{lib/operation.js → dist/operation.cjs} +37 -51
  22. package/dist/operation.mjs +1002 -0
  23. package/{lib/output.js → dist/output.cjs} +166 -47
  24. package/dist/output.mjs +1785 -0
  25. package/{lib/resize.js → dist/resize.cjs} +54 -32
  26. package/dist/resize.mjs +617 -0
  27. package/dist/sharp.cjs +174 -0
  28. package/dist/sharp.mjs +174 -0
  29. package/{lib/utility.js → dist/utility.cjs} +18 -8
  30. package/dist/utility.mjs +301 -0
  31. package/install/build.js +3 -3
  32. package/lib/index.d.ts +111 -83
  33. package/package.json +95 -54
  34. package/src/binding.gyp +19 -14
  35. package/src/common.cc +77 -21
  36. package/src/common.h +23 -4
  37. package/src/metadata.cc +66 -8
  38. package/src/metadata.h +6 -1
  39. package/src/operations.cc +25 -8
  40. package/src/operations.h +1 -1
  41. package/src/pipeline.cc +203 -69
  42. package/src/pipeline.h +15 -1
  43. package/src/stats.cc +6 -6
  44. package/src/utilities.cc +7 -6
  45. package/install/check.js +0 -14
  46. package/lib/index.js +0 -16
  47. 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,7 @@ 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())
1027
1108
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1028
1109
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1029
1110
  ->set("lossless", baton->heifLossless)));
@@ -1046,6 +1127,9 @@ class PipelineWorker : public Napi::AsyncWorker {
1046
1127
  area->free_fn = nullptr;
1047
1128
  vips_area_unref(area);
1048
1129
  baton->formatOut = "dz";
1130
+ if (baton->tileFormat == "jpeg") {
1131
+ baton->hasAlphaOut = false;
1132
+ }
1049
1133
  } else if (baton->formatOut == "jxl" ||
1050
1134
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::JXL)) {
1051
1135
  // Write JXL to buffer
@@ -1076,20 +1160,19 @@ class PipelineWorker : public Napi::AsyncWorker {
1076
1160
  // Get raw image data
1077
1161
  baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
1078
1162
  if (baton->bufferOut == nullptr) {
1079
- (baton->err).append("Could not allocate enough memory for raw output");
1080
- return Error();
1163
+ throw std::runtime_error("Could not allocate enough memory for raw output");
1081
1164
  }
1082
1165
  baton->formatOut = "raw";
1083
1166
  } else {
1084
1167
  // Unsupported output format
1085
- (baton->err).append("Unsupported output format ");
1168
+ auto unsupported = std::string("Unsupported output format ");
1086
1169
  if (baton->formatOut == "input") {
1087
- (baton->err).append("when trying to match input format of ");
1088
- (baton->err).append(ImageTypeId(inputImageType));
1170
+ unsupported.append("when trying to match input format of ");
1171
+ unsupported.append(ImageTypeId(inputImageType));
1089
1172
  } else {
1090
- (baton->err).append(baton->formatOut);
1173
+ unsupported.append(baton->formatOut);
1091
1174
  }
1092
- return Error();
1175
+ throw std::runtime_error(unsupported);
1093
1176
  }
1094
1177
  } else {
1095
1178
  // File output
@@ -1112,20 +1195,29 @@ class PipelineWorker : public Napi::AsyncWorker {
1112
1195
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
1113
1196
  // Write JPEG to file
1114
1197
  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));
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
+ }
1127
1218
  baton->formatOut = "jpeg";
1128
1219
  baton->channels = std::min(baton->channels, 3);
1220
+ baton->hasAlphaOut = false;
1129
1221
  } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
1130
1222
  (willMatchInput && (inputImageType == sharp::ImageType::JP2))) {
1131
1223
  // Write JP2 to file
@@ -1168,6 +1260,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1168
1260
  ->set("effort", baton->webpEffort)
1169
1261
  ->set("min_size", baton->webpMinSize)
1170
1262
  ->set("mixed", baton->webpMixed)
1263
+ ->set("exact", baton->webpExact)
1171
1264
  ->set("alpha_q", baton->webpAlphaQuality));
1172
1265
  baton->formatOut = "webp";
1173
1266
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
@@ -1191,6 +1284,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1191
1284
  if (baton->tiffCompression == VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) {
1192
1285
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
1193
1286
  baton->channels = std::min(baton->channels, 3);
1287
+ baton->hasAlphaOut = false;
1194
1288
  }
1195
1289
  // Cast pixel values to float, if required
1196
1290
  if (baton->tiffPredictor == VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT) {
@@ -1223,6 +1317,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1223
1317
  ->set("compression", baton->heifCompression)
1224
1318
  ->set("effort", baton->heifEffort)
1225
1319
  ->set("bitdepth", baton->heifBitdepth)
1320
+ ->set("tune", baton->heifTune.c_str())
1226
1321
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1227
1322
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1228
1323
  ->set("lossless", baton->heifLossless));
@@ -1250,6 +1345,9 @@ class PipelineWorker : public Napi::AsyncWorker {
1250
1345
  vips::VOption *options = BuildOptionsDZ(baton);
1251
1346
  image.dzsave(const_cast<char*>(baton->fileOut.data()), options);
1252
1347
  baton->formatOut = "dz";
1348
+ if (baton->tileFormat == "jpeg") {
1349
+ baton->hasAlphaOut = false;
1350
+ }
1253
1351
  } else if (baton->formatOut == "v" || (mightMatchInput && isV) ||
1254
1352
  (willMatchInput && inputImageType == sharp::ImageType::VIPS)) {
1255
1353
  // Write V to file
@@ -1262,7 +1360,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1262
1360
  return Error();
1263
1361
  }
1264
1362
  }
1265
- } catch (vips::VError const &err) {
1363
+ } catch (std::runtime_error const &err) {
1266
1364
  char const *what = err.what();
1267
1365
  if (what && what[0]) {
1268
1366
  (baton->err).append(what);
@@ -1290,11 +1388,10 @@ class PipelineWorker : public Napi::AsyncWorker {
1290
1388
  if (baton->errUseWarning) {
1291
1389
  (baton->err).append("\n").append(warning);
1292
1390
  } else {
1293
- debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) });
1391
+ debuglog.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { Napi::String::New(env, warning) });
1294
1392
  }
1295
1393
  warning = sharp::VipsWarningPop();
1296
1394
  }
1297
-
1298
1395
  if (baton->err.empty()) {
1299
1396
  int width = baton->width;
1300
1397
  int height = baton->height;
@@ -1335,14 +1432,22 @@ class PipelineWorker : public Napi::AsyncWorker {
1335
1432
  info.Set("pageHeight", static_cast<int32_t>(baton->pageHeightOut));
1336
1433
  info.Set("pages", static_cast<int32_t>(baton->pagesOut));
1337
1434
  }
1435
+ info.Set("hasAlpha", baton->hasAlphaOut);
1338
1436
 
1339
1437
  if (baton->bufferOutLength > 0) {
1340
- // Add buffer size to info
1341
1438
  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 });
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
+ }
1346
1451
  } else {
1347
1452
  // Add file size to info
1348
1453
  if (baton->formatOut != "dz" || sharp::IsDzZip(baton->fileOut)) {
@@ -1352,10 +1457,11 @@ class PipelineWorker : public Napi::AsyncWorker {
1352
1457
  info.Set("size", size);
1353
1458
  } catch (...) {}
1354
1459
  }
1355
- Callback().Call(Receiver().Value(), { env.Null(), info });
1460
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), info });
1356
1461
  }
1357
1462
  } else {
1358
- 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() });
1359
1465
  }
1360
1466
 
1361
1467
  // Delete baton
@@ -1376,7 +1482,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1376
1482
  // Decrement processing task counter
1377
1483
  sharp::counterProcess--;
1378
1484
  Napi::Number queueLength = Napi::Number::New(env, static_cast<int>(sharp::counterQueue));
1379
- queueListener.Call(Receiver().Value(), { queueLength });
1485
+ queueListener.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { queueLength });
1380
1486
  }
1381
1487
 
1382
1488
  private:
@@ -1386,7 +1492,13 @@ class PipelineWorker : public Napi::AsyncWorker {
1386
1492
 
1387
1493
  void MultiPageUnsupported(int const pages, std::string op) {
1388
1494
  if (pages > 1) {
1389
- 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");
1390
1502
  }
1391
1503
  }
1392
1504
 
@@ -1469,6 +1581,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1469
1581
  {"preset", vips_enum_nick(VIPS_TYPE_FOREIGN_WEBP_PRESET, baton->webpPreset)},
1470
1582
  {"min_size", baton->webpMinSize ? "true" : "false"},
1471
1583
  {"mixed", baton->webpMixed ? "true" : "false"},
1584
+ {"exact", baton->webpExact ? "true" : "false"},
1472
1585
  {"effort", std::to_string(baton->webpEffort)}
1473
1586
  };
1474
1587
  suffix = AssembleSuffixString(".webp", options);
@@ -1507,6 +1620,21 @@ class PipelineWorker : public Napi::AsyncWorker {
1507
1620
  return options;
1508
1621
  }
1509
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
+
1510
1638
  /*
1511
1639
  Clear all thread-local data.
1512
1640
  */
@@ -1615,6 +1743,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1615
1743
  baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
1616
1744
  baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
1617
1745
  baton->trimLineArt = sharp::AttrAsBool(options, "trimLineArt");
1746
+ baton->trimMargin = sharp::AttrAsUint32(options, "trimMargin");
1618
1747
  baton->gamma = sharp::AttrAsDouble(options, "gamma");
1619
1748
  baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
1620
1749
  baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
@@ -1692,6 +1821,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1692
1821
  // Output
1693
1822
  baton->formatOut = sharp::AttrAsStr(options, "formatOut");
1694
1823
  baton->fileOut = sharp::AttrAsStr(options, "fileOut");
1824
+ baton->typedArrayOut = sharp::AttrAsBool(options, "typedArrayOut");
1695
1825
  baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata");
1696
1826
  baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
1697
1827
  baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");
@@ -1706,6 +1836,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1706
1836
  }
1707
1837
  baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge");
1708
1838
  baton->withXmp = sharp::AttrAsStr(options, "withXmp");
1839
+ baton->keepGainMap = sharp::AttrAsBool(options, "keepGainMap");
1840
+ baton->withGainMap = sharp::AttrAsBool(options, "withGainMap");
1709
1841
  baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1710
1842
  baton->loop = sharp::AttrAsUint32(options, "loop");
1711
1843
  baton->delay = sharp::AttrAsInt32Vector(options, "delay");
@@ -1741,6 +1873,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1741
1873
  baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1742
1874
  baton->webpMinSize = sharp::AttrAsBool(options, "webpMinSize");
1743
1875
  baton->webpMixed = sharp::AttrAsBool(options, "webpMixed");
1876
+ baton->webpExact = sharp::AttrAsBool(options, "webpExact");
1744
1877
  baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1745
1878
  baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1746
1879
  baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
@@ -1775,6 +1908,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1775
1908
  baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1776
1909
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1777
1910
  baton->heifBitdepth = sharp::AttrAsUint32(options, "heifBitdepth");
1911
+ baton->heifTune = sharp::AttrAsStr(options, "heifTune");
1778
1912
  baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
1779
1913
  baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
1780
1914
  baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");
@@ -1808,7 +1942,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1808
1942
 
1809
1943
  // Increment queued task counter
1810
1944
  Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast<int>(++sharp::counterQueue));
1811
- queueListener.Call(info.This(), { queueLength });
1945
+ queueListener.SHARP_CALLBACK_FN_NAME(info.This(), { queueLength });
1812
1946
 
1813
1947
  return info.Env().Undefined();
1814
1948
  }