sharp 0.28.2 → 0.29.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/metadata.cc CHANGED
@@ -90,6 +90,9 @@ class MetadataWorker : public Napi::AsyncWorker {
90
90
  baton->subifds = image.get_int(VIPS_META_N_SUBIFDS);
91
91
  }
92
92
  baton->hasProfile = sharp::HasProfile(image);
93
+ if (image.get_typeof("background") == VIPS_TYPE_ARRAY_DOUBLE) {
94
+ baton->background = image.get_array_double("background");
95
+ }
93
96
  // Derived attributes
94
97
  baton->hasAlpha = sharp::HasAlpha(image);
95
98
  baton->orientation = sharp::ExifOrientation(image);
@@ -209,6 +212,17 @@ class MetadataWorker : public Napi::AsyncWorker {
209
212
  if (baton->subifds > 0) {
210
213
  info.Set("subifds", baton->subifds);
211
214
  }
215
+ if (!baton->background.empty()) {
216
+ if (baton->background.size() == 3) {
217
+ Napi::Object background = Napi::Object::New(env);
218
+ background.Set("r", baton->background[0]);
219
+ background.Set("g", baton->background[1]);
220
+ background.Set("b", baton->background[2]);
221
+ info.Set("background", background);
222
+ } else {
223
+ info.Set("background", baton->background[0]);
224
+ }
225
+ }
212
226
  info.Set("hasProfile", baton->hasProfile);
213
227
  info.Set("hasAlpha", baton->hasAlpha);
214
228
  if (baton->orientation > 0) {
package/src/metadata.h CHANGED
@@ -42,6 +42,7 @@ struct MetadataBaton {
42
42
  std::string compression;
43
43
  std::vector<std::pair<int, int>> levels;
44
44
  int subifds;
45
+ std::vector<double> background;
45
46
  bool hasProfile;
46
47
  bool hasAlpha;
47
48
  int orientation;
package/src/operations.cc CHANGED
@@ -92,6 +92,13 @@ namespace sharp {
92
92
  return image;
93
93
  }
94
94
 
95
+ /*
96
+ * Contrast limiting adapative histogram equalization (CLAHE)
97
+ */
98
+ VImage Clahe(VImage image, int const width, int const height, int const maxSlope) {
99
+ return image.hist_local(width, height, VImage::option()->set("max_slope", maxSlope));
100
+ }
101
+
95
102
  /*
96
103
  * Gamma encoding/decoding
97
104
  */
@@ -105,6 +112,19 @@ namespace sharp {
105
112
  }
106
113
  }
107
114
 
115
+ /**
116
+ * Produce the "negative" of the image.
117
+ */
118
+ VImage Negate(VImage image, bool const negateAlpha) {
119
+ if (HasAlpha(image) && !negateAlpha) {
120
+ // Separate alpha channel
121
+ VImage alpha = image[image.bands() - 1];
122
+ return RemoveAlpha(image).invert().bandjoin(alpha);
123
+ } else {
124
+ return image.invert();
125
+ }
126
+ }
127
+
108
128
  /*
109
129
  * Gaussian blur. Use sigma of -1.0 for fast blur.
110
130
  */
@@ -162,7 +182,8 @@ namespace sharp {
162
182
  0.0, 0.0, 0.0, 1.0));
163
183
  }
164
184
 
165
- VImage Modulate(VImage image, double const brightness, double const saturation, int const hue) {
185
+ VImage Modulate(VImage image, double const brightness, double const saturation,
186
+ int const hue, double const lightness) {
166
187
  if (HasAlpha(image)) {
167
188
  // Separate alpha channel
168
189
  VImage alpha = image[image.bands() - 1];
@@ -170,7 +191,7 @@ namespace sharp {
170
191
  .colourspace(VIPS_INTERPRETATION_LCH)
171
192
  .linear(
172
193
  { brightness, saturation, 1},
173
- { 0.0, 0.0, static_cast<double>(hue) }
194
+ { lightness, 0.0, static_cast<double>(hue) }
174
195
  )
175
196
  .colourspace(VIPS_INTERPRETATION_sRGB)
176
197
  .bandjoin(alpha);
@@ -179,7 +200,7 @@ namespace sharp {
179
200
  .colourspace(VIPS_INTERPRETATION_LCH)
180
201
  .linear(
181
202
  { brightness, saturation, 1 },
182
- { 0.0, 0.0, static_cast<double>(hue) }
203
+ { lightness, 0.0, static_cast<double>(hue) }
183
204
  )
184
205
  .colourspace(VIPS_INTERPRETATION_sRGB);
185
206
  }
@@ -275,4 +296,16 @@ namespace sharp {
275
296
  return image.linear(a, b);
276
297
  }
277
298
  }
299
+
300
+ /*
301
+ * Ensure the image is in a given colourspace
302
+ */
303
+ VImage EnsureColourspace(VImage image, VipsInterpretation colourspace) {
304
+ if (colourspace != VIPS_INTERPRETATION_LAST && image.interpretation() != colourspace) {
305
+ image = image.colourspace(colourspace,
306
+ VImage::option()->set("source_space", image.interpretation()));
307
+ }
308
+ return image;
309
+ }
310
+
278
311
  } // namespace sharp
package/src/operations.h CHANGED
@@ -35,11 +35,21 @@ namespace sharp {
35
35
  */
36
36
  VImage Normalise(VImage image);
37
37
 
38
+ /*
39
+ * Contrast limiting adapative histogram equalization (CLAHE)
40
+ */
41
+ VImage Clahe(VImage image, int const width, int const height, int const maxSlope);
42
+
38
43
  /*
39
44
  * Gamma encoding/decoding
40
45
  */
41
46
  VImage Gamma(VImage image, double const exponent);
42
47
 
48
+ /*
49
+ * Produce the "negative" of the image.
50
+ */
51
+ VImage Negate(VImage image, bool const negateAlpha);
52
+
43
53
  /*
44
54
  * Gaussian blur. Use sigma of -1.0 for fast blur.
45
55
  */
@@ -88,9 +98,15 @@ namespace sharp {
88
98
  VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix);
89
99
 
90
100
  /*
91
- * Modulate brightness, saturation and hue
101
+ * Modulate brightness, saturation, hue and lightness
102
+ */
103
+ VImage Modulate(VImage image, double const brightness, double const saturation,
104
+ int const hue, double const lightness);
105
+
106
+ /*
107
+ * Ensure the image is in a given colourspace
92
108
  */
93
- VImage Modulate(VImage image, double const brightness, double const saturation, int const hue);
109
+ VImage EnsureColourspace(VImage image, VipsInterpretation colourspace);
94
110
 
95
111
  } // namespace sharp
96
112
 
package/src/pipeline.cc CHANGED
@@ -67,6 +67,7 @@ class PipelineWorker : public Napi::AsyncWorker {
67
67
  vips::VImage image;
68
68
  sharp::ImageType inputImageType;
69
69
  std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
70
+ image = sharp::EnsureColourspace(image, baton->colourspaceInput);
70
71
 
71
72
  // Calculate angle of rotation
72
73
  VipsAngle rotation;
@@ -89,7 +90,7 @@ class PipelineWorker : public Napi::AsyncWorker {
89
90
  }
90
91
  if (baton->rotationAngle != 0.0) {
91
92
  std::vector<double> background;
92
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
93
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, FALSE);
93
94
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
94
95
  }
95
96
  }
@@ -214,7 +215,7 @@ class PipelineWorker : public Napi::AsyncWorker {
214
215
  double yresidual = static_cast<double>(yshrink) / yfactor;
215
216
 
216
217
  // If integral x and y shrink are equal, try to use shrink-on-load for JPEG and WebP,
217
- // but not when applying gamma correction, pre-resize extract or trim
218
+ // but not when applying gamma correction, pre-resize extract, trim or input colourspace
218
219
  int shrink_on_load = 1;
219
220
 
220
221
  int shrink_on_load_factor = 1;
@@ -227,7 +228,8 @@ class PipelineWorker : public Napi::AsyncWorker {
227
228
  xshrink == yshrink && xshrink >= 2 * shrink_on_load_factor &&
228
229
  (inputImageType == sharp::ImageType::JPEG || inputImageType == sharp::ImageType::WEBP) &&
229
230
  baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0 &&
230
- image.width() > 3 && image.height() > 3
231
+ baton->colourspaceInput == VIPS_INTERPRETATION_LAST &&
232
+ image.width() > 3 && image.height() > 3 && baton->input->pages == 1
231
233
  ) {
232
234
  if (xshrink >= 8 * shrink_on_load_factor) {
233
235
  xfactor = xfactor / 8;
@@ -287,16 +289,21 @@ class PipelineWorker : public Napi::AsyncWorker {
287
289
  yfactor = static_cast<double>(shrunkOnLoadHeight) / static_cast<double>(targetResizeHeight);
288
290
  }
289
291
  }
292
+ // Remove animation properties from single page images
293
+ if (baton->input->pages == 1) {
294
+ image = sharp::RemoveAnimationProperties(image);
295
+ }
290
296
 
291
297
  // Ensure we're using a device-independent colour space
298
+ char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb";
292
299
  if (
293
300
  sharp::HasProfile(image) &&
294
301
  image.interpretation() != VIPS_INTERPRETATION_LABS &&
295
302
  image.interpretation() != VIPS_INTERPRETATION_GREY16
296
303
  ) {
297
- // Convert to sRGB using embedded profile
304
+ // Convert to sRGB/P3 using embedded profile
298
305
  try {
299
- image = image.icc_transform("srgb", VImage::option()
306
+ image = image.icc_transform(processingProfile, VImage::option()
300
307
  ->set("embedded", TRUE)
301
308
  ->set("depth", image.interpretation() == VIPS_INTERPRETATION_RGB16 ? 16 : 8)
302
309
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
@@ -304,7 +311,7 @@ class PipelineWorker : public Napi::AsyncWorker {
304
311
  // Ignore failure of embedded profile
305
312
  }
306
313
  } else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
307
- image = image.icc_transform("srgb", VImage::option()
314
+ image = image.icc_transform(processingProfile, VImage::option()
308
315
  ->set("input_profile", "cmyk")
309
316
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
310
317
  }
@@ -325,7 +332,7 @@ class PipelineWorker : public Napi::AsyncWorker {
325
332
 
326
333
  // Negate the colours in the image
327
334
  if (baton->negate) {
328
- image = image.invert();
335
+ image = sharp::Negate(image, baton->negateAlpha);
329
336
  }
330
337
 
331
338
  // Gamma encoding (darken)
@@ -344,7 +351,9 @@ class PipelineWorker : public Napi::AsyncWorker {
344
351
  bool const shouldSharpen = baton->sharpenSigma != 0.0;
345
352
  bool const shouldApplyMedian = baton->medianSize > 0;
346
353
  bool const shouldComposite = !baton->composite.empty();
347
- bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0;
354
+ bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 ||
355
+ baton->hue != 0.0 || baton->lightness != 0.0;
356
+ bool const shouldApplyClahe = baton->claheWidth != 0 && baton->claheHeight != 0;
348
357
 
349
358
  if (shouldComposite && !sharp::HasAlpha(image)) {
350
359
  image = sharp::EnsureAlpha(image, 1);
@@ -410,6 +419,7 @@ class PipelineWorker : public Napi::AsyncWorker {
410
419
 
411
420
  for (unsigned int i = 0; i < baton->joinChannelIn.size(); i++) {
412
421
  std::tie(joinImage, joinImageType) = sharp::OpenInput(baton->joinChannelIn[i]);
422
+ joinImage = sharp::EnsureColourspace(joinImage, baton->colourspaceInput);
413
423
  image = image.bandjoin(joinImage);
414
424
  }
415
425
  image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
@@ -419,7 +429,7 @@ class PipelineWorker : public Napi::AsyncWorker {
419
429
  if (image.width() != baton->width || image.height() != baton->height) {
420
430
  if (baton->canvas == Canvas::EMBED) {
421
431
  std::vector<double> background;
422
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground);
432
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
423
433
 
424
434
  // Embed
425
435
 
@@ -476,7 +486,7 @@ class PipelineWorker : public Napi::AsyncWorker {
476
486
  // Rotate post-extract non-90 angle
477
487
  if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
478
488
  std::vector<double> background;
479
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
489
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
480
490
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
481
491
  }
482
492
 
@@ -489,7 +499,7 @@ class PipelineWorker : public Napi::AsyncWorker {
489
499
  // Affine transform
490
500
  if (baton->affineMatrix.size() > 0) {
491
501
  std::vector<double> background;
492
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground);
502
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
493
503
  image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
494
504
  ->set("idx", baton->affineIdx)
495
505
  ->set("idy", baton->affineIdy)
@@ -501,7 +511,7 @@ class PipelineWorker : public Napi::AsyncWorker {
501
511
  // Extend edges
502
512
  if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
503
513
  std::vector<double> background;
504
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground);
514
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
505
515
 
506
516
  // Embed
507
517
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
@@ -538,7 +548,7 @@ class PipelineWorker : public Napi::AsyncWorker {
538
548
  }
539
549
 
540
550
  if (shouldModulate) {
541
- image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue);
551
+ image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
542
552
  }
543
553
 
544
554
  // Sharpen
@@ -551,7 +561,8 @@ class PipelineWorker : public Napi::AsyncWorker {
551
561
  for (Composite *composite : baton->composite) {
552
562
  VImage compositeImage;
553
563
  sharp::ImageType compositeImageType = sharp::ImageType::UNKNOWN;
554
- std::tie(compositeImage, compositeImageType) = OpenInput(composite->input);
564
+ std::tie(compositeImage, compositeImageType) = sharp::OpenInput(composite->input);
565
+ compositeImage = sharp::EnsureColourspace(compositeImage, baton->colourspaceInput);
555
566
  // Verify within current dimensions
556
567
  if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
557
568
  throw vips::VError("Image to composite must have same dimensions or smaller");
@@ -650,11 +661,17 @@ class PipelineWorker : public Napi::AsyncWorker {
650
661
  image = sharp::Normalise(image);
651
662
  }
652
663
 
664
+ // Apply contrast limiting adaptive histogram equalization (CLAHE)
665
+ if (shouldApplyClahe) {
666
+ image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope);
667
+ }
668
+
653
669
  // Apply bitwise boolean operation between images
654
670
  if (baton->boolean != nullptr) {
655
671
  VImage booleanImage;
656
672
  sharp::ImageType booleanImageType = sharp::ImageType::UNKNOWN;
657
673
  std::tie(booleanImage, booleanImageType) = sharp::OpenInput(baton->boolean);
674
+ booleanImage = sharp::EnsureColourspace(booleanImage, baton->colourspaceInput);
658
675
  image = sharp::Boolean(image, booleanImage, baton->booleanOp);
659
676
  }
660
677
 
@@ -704,9 +721,10 @@ class PipelineWorker : public Napi::AsyncWorker {
704
721
  // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess
705
722
  image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
706
723
  // Transform colours from embedded profile to output profile
707
- if (baton->withMetadata && sharp::HasProfile(image)) {
708
- image = image.icc_transform(vips_enum_nick(VIPS_TYPE_INTERPRETATION, baton->colourspace),
709
- VImage::option()->set("embedded", TRUE));
724
+ if (baton->withMetadata && sharp::HasProfile(image) && baton->withMetadataIcc.empty()) {
725
+ image = image.icc_transform("srgb", VImage::option()
726
+ ->set("embedded", TRUE)
727
+ ->set("intent", VIPS_INTENT_PERCEPTUAL));
710
728
  }
711
729
  }
712
730
 
@@ -715,7 +733,8 @@ class PipelineWorker : public Napi::AsyncWorker {
715
733
  image = image.icc_transform(
716
734
  const_cast<char*>(baton->withMetadataIcc.data()),
717
735
  VImage::option()
718
- ->set("input_profile", "srgb")
736
+ ->set("input_profile", processingProfile)
737
+ ->set("embedded", TRUE)
719
738
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
720
739
  }
721
740
  // Override EXIF Orientation tag
@@ -749,6 +768,7 @@ class PipelineWorker : public Napi::AsyncWorker {
749
768
  baton->loop);
750
769
 
751
770
  // Output
771
+ sharp::SetTimeout(image, baton->timeoutSeconds);
752
772
  if (baton->fileOut.empty()) {
753
773
  // Buffer output
754
774
  if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
@@ -759,8 +779,8 @@ class PipelineWorker : public Napi::AsyncWorker {
759
779
  ->set("Q", baton->jpegQuality)
760
780
  ->set("interlace", baton->jpegProgressive)
761
781
  ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
762
- ? VIPS_FOREIGN_JPEG_SUBSAMPLE_OFF
763
- : VIPS_FOREIGN_JPEG_SUBSAMPLE_ON)
782
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
783
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
764
784
  ->set("trellis_quant", baton->jpegTrellisQuantisation)
765
785
  ->set("quant_table", baton->jpegQuantisationTable)
766
786
  ->set("overshoot_deringing", baton->jpegOvershootDeringing)
@@ -776,6 +796,22 @@ class PipelineWorker : public Napi::AsyncWorker {
776
796
  } else {
777
797
  baton->channels = std::min(baton->channels, 3);
778
798
  }
799
+ } else if (baton->formatOut == "jp2" || (baton->formatOut == "input"
800
+ && inputImageType == sharp::ImageType::JP2)) {
801
+ // Write JP2 to Buffer
802
+ sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
803
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.jp2ksave_buffer(VImage::option()
804
+ ->set("Q", baton->jp2Quality)
805
+ ->set("lossless", baton->jp2Lossless)
806
+ ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
807
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
808
+ ->set("tile_height", baton->jp2TileHeight)
809
+ ->set("tile_width", baton->jp2TileWidth)));
810
+ baton->bufferOut = static_cast<char*>(area->data);
811
+ baton->bufferOutLength = area->length;
812
+ area->free_fn = nullptr;
813
+ vips_area_unref(area);
814
+ baton->formatOut = "jp2";
779
815
  } else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
780
816
  (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
781
817
  inputImageType == sharp::ImageType::SVG))) {
@@ -788,7 +824,7 @@ class PipelineWorker : public Napi::AsyncWorker {
788
824
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
789
825
  ->set("palette", baton->pngPalette)
790
826
  ->set("Q", baton->pngQuality)
791
- ->set("colours", baton->pngColours)
827
+ ->set("bitdepth", baton->pngBitdepth)
792
828
  ->set("dither", baton->pngDither)));
793
829
  baton->bufferOut = static_cast<char*>(area->data);
794
830
  baton->bufferOutLength = area->length;
@@ -857,15 +893,14 @@ class PipelineWorker : public Napi::AsyncWorker {
857
893
  } else if (baton->formatOut == "heif" ||
858
894
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::HEIF)) {
859
895
  // Write HEIF to buffer
896
+ image = sharp::RemoveAnimationProperties(image);
860
897
  VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option()
861
898
  ->set("strip", !baton->withMetadata)
862
- ->set("compression", baton->heifCompression)
863
899
  ->set("Q", baton->heifQuality)
900
+ ->set("compression", baton->heifCompression)
864
901
  ->set("speed", baton->heifSpeed)
865
- #if defined(VIPS_TYPE_FOREIGN_SUBSAMPLE)
866
902
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
867
903
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
868
- #endif
869
904
  ->set("lossless", baton->heifLossless)));
870
905
  baton->bufferOut = static_cast<char*>(area->data);
871
906
  baton->bufferOutLength = area->length;
@@ -880,9 +915,9 @@ class PipelineWorker : public Napi::AsyncWorker {
880
915
  image = image[0];
881
916
  baton->channels = 1;
882
917
  }
883
- if (image.format() != VIPS_FORMAT_UCHAR) {
884
- // Cast pixels to uint8 (unsigned char)
885
- image = image.cast(VIPS_FORMAT_UCHAR);
918
+ if (image.format() != baton->rawDepth) {
919
+ // Cast pixels to requested format
920
+ image = image.cast(baton->rawDepth);
886
921
  }
887
922
  // Get raw image data
888
923
  baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
@@ -908,13 +943,14 @@ class PipelineWorker : public Napi::AsyncWorker {
908
943
  bool const isWebp = sharp::IsWebp(baton->fileOut);
909
944
  bool const isGif = sharp::IsGif(baton->fileOut);
910
945
  bool const isTiff = sharp::IsTiff(baton->fileOut);
946
+ bool const isJp2 = sharp::IsJp2(baton->fileOut);
911
947
  bool const isHeif = sharp::IsHeif(baton->fileOut);
912
948
  bool const isDz = sharp::IsDz(baton->fileOut);
913
949
  bool const isDzZip = sharp::IsDzZip(baton->fileOut);
914
950
  bool const isV = sharp::IsV(baton->fileOut);
915
951
  bool const mightMatchInput = baton->formatOut == "input";
916
952
  bool const willMatchInput = mightMatchInput &&
917
- !(isJpeg || isPng || isWebp || isGif || isTiff || isHeif || isDz || isDzZip || isV);
953
+ !(isJpeg || isPng || isWebp || isGif || isTiff || isJp2 || isHeif || isDz || isDzZip || isV);
918
954
 
919
955
  if (baton->formatOut == "jpeg" || (mightMatchInput && isJpeg) ||
920
956
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
@@ -925,8 +961,8 @@ class PipelineWorker : public Napi::AsyncWorker {
925
961
  ->set("Q", baton->jpegQuality)
926
962
  ->set("interlace", baton->jpegProgressive)
927
963
  ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
928
- ? VIPS_FOREIGN_JPEG_SUBSAMPLE_OFF
929
- : VIPS_FOREIGN_JPEG_SUBSAMPLE_ON)
964
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
965
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
930
966
  ->set("trellis_quant", baton->jpegTrellisQuantisation)
931
967
  ->set("quant_table", baton->jpegQuantisationTable)
932
968
  ->set("overshoot_deringing", baton->jpegOvershootDeringing)
@@ -934,6 +970,18 @@ class PipelineWorker : public Napi::AsyncWorker {
934
970
  ->set("optimize_coding", baton->jpegOptimiseCoding));
935
971
  baton->formatOut = "jpeg";
936
972
  baton->channels = std::min(baton->channels, 3);
973
+ } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
974
+ (willMatchInput && (inputImageType == sharp::ImageType::JP2))) {
975
+ // Write JP2 to file
976
+ sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
977
+ image.jp2ksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
978
+ ->set("Q", baton->jp2Quality)
979
+ ->set("lossless", baton->jp2Lossless)
980
+ ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
981
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
982
+ ->set("tile_height", baton->jp2TileHeight)
983
+ ->set("tile_width", baton->jp2TileWidth));
984
+ baton->formatOut = "jp2";
937
985
  } else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
938
986
  (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
939
987
  inputImageType == sharp::ImageType::SVG))) {
@@ -946,7 +994,7 @@ class PipelineWorker : public Napi::AsyncWorker {
946
994
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
947
995
  ->set("palette", baton->pngPalette)
948
996
  ->set("Q", baton->pngQuality)
949
- ->set("colours", baton->pngColours)
997
+ ->set("bitdepth", baton->pngBitdepth)
950
998
  ->set("dither", baton->pngDither));
951
999
  baton->formatOut = "png";
952
1000
  } else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) ||
@@ -999,15 +1047,14 @@ class PipelineWorker : public Napi::AsyncWorker {
999
1047
  } else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
1000
1048
  (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
1001
1049
  // Write HEIF to file
1050
+ image = sharp::RemoveAnimationProperties(image);
1002
1051
  image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1003
1052
  ->set("strip", !baton->withMetadata)
1004
1053
  ->set("Q", baton->heifQuality)
1005
1054
  ->set("compression", baton->heifCompression)
1006
1055
  ->set("speed", baton->heifSpeed)
1007
- #if defined(VIPS_TYPE_FOREIGN_SUBSAMPLE)
1008
1056
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1009
1057
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1010
- #endif
1011
1058
  ->set("lossless", baton->heifLossless));
1012
1059
  baton->formatOut = "heif";
1013
1060
  } else if (baton->formatOut == "dz" || isDz || isDzZip) {
@@ -1124,6 +1171,9 @@ class PipelineWorker : public Napi::AsyncWorker {
1124
1171
  info.Set("width", static_cast<uint32_t>(width));
1125
1172
  info.Set("height", static_cast<uint32_t>(height));
1126
1173
  info.Set("channels", static_cast<uint32_t>(baton->channels));
1174
+ if (baton->formatOut == "raw") {
1175
+ info.Set("depth", vips_enum_nick(VIPS_TYPE_BAND_FORMAT, baton->rawDepth));
1176
+ }
1127
1177
  info.Set("premultiplied", baton->premultiplied);
1128
1178
  if (baton->hasCropOffset) {
1129
1179
  info.Set("cropOffsetLeft", static_cast<int32_t>(baton->cropOffsetLeft));
@@ -1313,10 +1363,12 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1313
1363
  baton->flatten = sharp::AttrAsBool(options, "flatten");
1314
1364
  baton->flattenBackground = sharp::AttrAsVectorOfDouble(options, "flattenBackground");
1315
1365
  baton->negate = sharp::AttrAsBool(options, "negate");
1366
+ baton->negateAlpha = sharp::AttrAsBool(options, "negateAlpha");
1316
1367
  baton->blurSigma = sharp::AttrAsDouble(options, "blurSigma");
1317
1368
  baton->brightness = sharp::AttrAsDouble(options, "brightness");
1318
1369
  baton->saturation = sharp::AttrAsDouble(options, "saturation");
1319
1370
  baton->hue = sharp::AttrAsInt32(options, "hue");
1371
+ baton->lightness = sharp::AttrAsDouble(options, "lightness");
1320
1372
  baton->medianSize = sharp::AttrAsUint32(options, "medianSize");
1321
1373
  baton->sharpenSigma = sharp::AttrAsDouble(options, "sharpenSigma");
1322
1374
  baton->sharpenFlat = sharp::AttrAsDouble(options, "sharpenFlat");
@@ -1330,6 +1382,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1330
1382
  baton->linearB = sharp::AttrAsDouble(options, "linearB");
1331
1383
  baton->greyscale = sharp::AttrAsBool(options, "greyscale");
1332
1384
  baton->normalise = sharp::AttrAsBool(options, "normalise");
1385
+ baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
1386
+ baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
1387
+ baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
1333
1388
  baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation");
1334
1389
  baton->angle = sharp::AttrAsInt32(options, "angle");
1335
1390
  baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
@@ -1380,6 +1435,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1380
1435
  baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i);
1381
1436
  }
1382
1437
  }
1438
+ baton->colourspaceInput = sharp::GetInterpretation(sharp::AttrAsStr(options, "colourspaceInput"));
1439
+ if (baton->colourspaceInput == VIPS_INTERPRETATION_ERROR) {
1440
+ baton->colourspaceInput = VIPS_INTERPRETATION_LAST;
1441
+ }
1383
1442
  baton->colourspace = sharp::GetInterpretation(sharp::AttrAsStr(options, "colourspace"));
1384
1443
  if (baton->colourspace == VIPS_INTERPRETATION_ERROR) {
1385
1444
  baton->colourspace = VIPS_INTERPRETATION_sRGB;
@@ -1397,6 +1456,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1397
1456
  std::string k = sharp::AttrAsStr(mdStrKeys, i);
1398
1457
  baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k)));
1399
1458
  }
1459
+ baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1400
1460
  // Format-specific
1401
1461
  baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
1402
1462
  baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");
@@ -1411,8 +1471,13 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1411
1471
  baton->pngAdaptiveFiltering = sharp::AttrAsBool(options, "pngAdaptiveFiltering");
1412
1472
  baton->pngPalette = sharp::AttrAsBool(options, "pngPalette");
1413
1473
  baton->pngQuality = sharp::AttrAsUint32(options, "pngQuality");
1414
- baton->pngColours = sharp::AttrAsUint32(options, "pngColours");
1474
+ baton->pngBitdepth = sharp::AttrAsUint32(options, "pngBitdepth");
1415
1475
  baton->pngDither = sharp::AttrAsDouble(options, "pngDither");
1476
+ baton->jp2Quality = sharp::AttrAsUint32(options, "jp2Quality");
1477
+ baton->jp2Lossless = sharp::AttrAsBool(options, "jp2Lossless");
1478
+ baton->jp2TileHeight = sharp::AttrAsUint32(options, "jp2TileHeight");
1479
+ baton->jp2TileWidth = sharp::AttrAsUint32(options, "jp2TileWidth");
1480
+ baton->jp2ChromaSubsampling = sharp::AttrAsStr(options, "jp2ChromaSubsampling");
1416
1481
  baton->webpQuality = sharp::AttrAsUint32(options, "webpQuality");
1417
1482
  baton->webpAlphaQuality = sharp::AttrAsUint32(options, "webpAlphaQuality");
1418
1483
  baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
@@ -1442,6 +1507,11 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1442
1507
  baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed");
1443
1508
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1444
1509
 
1510
+ // Raw output
1511
+ baton->rawDepth = static_cast<VipsBandFormat>(
1512
+ vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT,
1513
+ sharp::AttrAsStr(options, "rawDepth").data()));
1514
+
1445
1515
  // Animated output
1446
1516
  if (sharp::HasAttr(options, "pageHeight")) {
1447
1517
  baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight");