sharp 0.31.2 → 0.32.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.
package/src/operations.cc CHANGED
@@ -1,23 +1,11 @@
1
- // Copyright 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Lovell Fuller and contributors.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
1
+ // Copyright 2013 Lovell Fuller and others.
2
+ // SPDX-License-Identifier: Apache-2.0
14
3
 
15
4
  #include <algorithm>
16
5
  #include <functional>
17
6
  #include <memory>
18
7
  #include <tuple>
19
8
  #include <vector>
20
-
21
9
  #include <vips/vips8>
22
10
 
23
11
  #include "common.h"
@@ -57,7 +45,7 @@ namespace sharp {
57
45
  /*
58
46
  * Stretch luminance to cover full dynamic range.
59
47
  */
60
- VImage Normalise(VImage image) {
48
+ VImage Normalise(VImage image, int const lower, int const upper) {
61
49
  // Get original colourspace
62
50
  VipsInterpretation typeBeforeNormalize = image.interpretation();
63
51
  if (typeBeforeNormalize == VIPS_INTERPRETATION_RGB) {
@@ -67,9 +55,11 @@ namespace sharp {
67
55
  VImage lab = image.colourspace(VIPS_INTERPRETATION_LAB);
68
56
  // Extract luminance
69
57
  VImage luminance = lab[0];
58
+
70
59
  // Find luminance range
71
- int const min = luminance.percent(1);
72
- int const max = luminance.percent(99);
60
+ int const min = lower == 0 ? luminance.min() : luminance.percent(lower);
61
+ int const max = upper == 100 ? luminance.max() : luminance.percent(upper);
62
+
73
63
  if (std::abs(max - min) > 1) {
74
64
  // Extract chroma
75
65
  VImage chroma = lab.extract_band(1, VImage::option()->set("n", 2));
@@ -345,9 +335,9 @@ namespace sharp {
345
335
  if (HasAlpha(image) && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) {
346
336
  // Separate alpha channel
347
337
  VImage alpha = image[bands - 1];
348
- return RemoveAlpha(image).linear(a, b).bandjoin(alpha);
338
+ return RemoveAlpha(image).linear(a, b, VImage::option()->set("uchar", TRUE)).bandjoin(alpha);
349
339
  } else {
350
- return image.linear(a, b);
340
+ return image.linear(a, b, VImage::option()->set("uchar", TRUE));
351
341
  }
352
342
  }
353
343
 
@@ -395,11 +385,11 @@ namespace sharp {
395
385
  * Split into frames, embed each frame, reassemble, and update pageHeight.
396
386
  */
397
387
  VImage EmbedMultiPage(VImage image, int left, int top, int width, int height,
398
- std::vector<double> background, int nPages, int *pageHeight) {
388
+ VipsExtend extendWith, std::vector<double> background, int nPages, int *pageHeight) {
399
389
  if (top == 0 && height == *pageHeight) {
400
390
  // Fast path; no need to adjust the height of the multi-page image
401
391
  return image.embed(left, 0, width, image.height(), VImage::option()
402
- ->set("extend", VIPS_EXTEND_BACKGROUND)
392
+ ->set("extend", extendWith)
403
393
  ->set("background", background));
404
394
  } else if (left == 0 && width == image.width()) {
405
395
  // Fast path; no need to adjust the width of the multi-page image
@@ -411,7 +401,7 @@ namespace sharp {
411
401
 
412
402
  // Do the embed on the wide image
413
403
  image = image.embed(0, top, image.width(), height, VImage::option()
414
- ->set("extend", VIPS_EXTEND_BACKGROUND)
404
+ ->set("extend", extendWith)
415
405
  ->set("background", background));
416
406
 
417
407
  // Split the wide image into frames
@@ -441,7 +431,7 @@ namespace sharp {
441
431
  // Embed each frame in the target size
442
432
  for (int i = 0; i < nPages; i++) {
443
433
  pages[i] = pages[i].embed(left, top, width, height, VImage::option()
444
- ->set("extend", VIPS_EXTEND_BACKGROUND)
434
+ ->set("extend", extendWith)
445
435
  ->set("background", background));
446
436
  }
447
437
 
package/src/operations.h CHANGED
@@ -1,16 +1,5 @@
1
- // Copyright 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Lovell Fuller and contributors.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
1
+ // Copyright 2013 Lovell Fuller and others.
2
+ // SPDX-License-Identifier: Apache-2.0
14
3
 
15
4
  #ifndef SRC_OPERATIONS_H_
16
5
  #define SRC_OPERATIONS_H_
@@ -33,7 +22,7 @@ namespace sharp {
33
22
  /*
34
23
  * Stretch luminance to cover full dynamic range.
35
24
  */
36
- VImage Normalise(VImage image);
25
+ VImage Normalise(VImage image, int const lower, int const upper);
37
26
 
38
27
  /*
39
28
  * Contrast limiting adapative histogram equalization (CLAHE)
@@ -124,7 +113,7 @@ namespace sharp {
124
113
  * Split into frames, embed each frame, reassemble, and update pageHeight.
125
114
  */
126
115
  VImage EmbedMultiPage(VImage image, int left, int top, int width, int height,
127
- std::vector<double> background, int nPages, int *pageHeight);
116
+ VipsExtend extendWith, std::vector<double> background, int nPages, int *pageHeight);
128
117
 
129
118
  } // namespace sharp
130
119
 
package/src/pipeline.cc CHANGED
@@ -1,16 +1,5 @@
1
- // Copyright 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Lovell Fuller and contributors.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
1
+ // Copyright 2013 Lovell Fuller and others.
2
+ // SPDX-License-Identifier: Apache-2.0
14
3
 
15
4
  #include <algorithm>
16
5
  #include <cmath>
@@ -85,6 +74,7 @@ class PipelineWorker : public Napi::AsyncWorker {
85
74
  VipsAngle autoRotation = VIPS_ANGLE_D0;
86
75
  bool autoFlip = FALSE;
87
76
  bool autoFlop = FALSE;
77
+
88
78
  if (baton->useExifOrientation) {
89
79
  // Rotate and flip image according to Exif orientation
90
80
  std::tie(autoRotation, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
@@ -205,7 +195,7 @@ class PipelineWorker : public Napi::AsyncWorker {
205
195
  if (jpegShrinkOnLoad > 1 && static_cast<int>(shrink) == jpegShrinkOnLoad) {
206
196
  jpegShrinkOnLoad /= 2;
207
197
  }
208
- } else if (inputImageType == sharp::ImageType::WEBP && shrink > 1.0) {
198
+ } else if (inputImageType == sharp::ImageType::WEBP && baton->fastShrinkOnLoad && shrink > 1.0) {
209
199
  // Avoid upscaling via webp
210
200
  scale = 1.0 / shrink;
211
201
  } else if (inputImageType == sharp::ImageType::SVG ||
@@ -320,7 +310,8 @@ class PipelineWorker : public Napi::AsyncWorker {
320
310
  if (
321
311
  sharp::HasProfile(image) &&
322
312
  image.interpretation() != VIPS_INTERPRETATION_LABS &&
323
- image.interpretation() != VIPS_INTERPRETATION_GREY16
313
+ image.interpretation() != VIPS_INTERPRETATION_GREY16 &&
314
+ !baton->input->ignoreIcc
324
315
  ) {
325
316
  // Convert to sRGB/P3 using embedded profile
326
317
  try {
@@ -329,7 +320,7 @@ class PipelineWorker : public Napi::AsyncWorker {
329
320
  ->set("depth", image.interpretation() == VIPS_INTERPRETATION_RGB16 ? 16 : 8)
330
321
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
331
322
  } catch(...) {
332
- // Ignore failure of embedded profile
323
+ sharp::VipsWarningCallback(nullptr, G_LOG_LEVEL_WARNING, "Invalid embedded profile", nullptr);
333
324
  }
334
325
  } else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
335
326
  image = image.icc_transform(processingProfile, VImage::option()
@@ -367,11 +358,12 @@ class PipelineWorker : public Napi::AsyncWorker {
367
358
  image = sharp::EnsureAlpha(image, 1);
368
359
  }
369
360
 
361
+ VipsBandFormat premultiplyFormat = image.format();
370
362
  bool const shouldPremultiplyAlpha = sharp::HasAlpha(image) &&
371
363
  (shouldResize || shouldBlur || shouldConv || shouldSharpen);
372
364
 
373
365
  if (shouldPremultiplyAlpha) {
374
- image = image.premultiply();
366
+ image = image.premultiply().cast(premultiplyFormat);
375
367
  }
376
368
 
377
369
  // Resize
@@ -399,7 +391,7 @@ class PipelineWorker : public Napi::AsyncWorker {
399
391
  }
400
392
 
401
393
  // Join additional color channels to the image
402
- if (baton->joinChannelIn.size() > 0) {
394
+ if (!baton->joinChannelIn.empty()) {
403
395
  VImage joinImage;
404
396
  sharp::ImageType joinImageType = sharp::ImageType::UNKNOWN;
405
397
 
@@ -409,6 +401,7 @@ class PipelineWorker : public Napi::AsyncWorker {
409
401
  image = image.bandjoin(joinImage);
410
402
  }
411
403
  image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
404
+ image = sharp::RemoveGifPalette(image);
412
405
  }
413
406
 
414
407
  inputWidth = image.width();
@@ -438,7 +431,7 @@ class PipelineWorker : public Napi::AsyncWorker {
438
431
 
439
432
  image = nPages > 1
440
433
  ? sharp::EmbedMultiPage(image,
441
- left, top, width, height, background, nPages, &targetPageHeight)
434
+ left, top, width, height, VIPS_EXTEND_BACKGROUND, background, nPages, &targetPageHeight)
442
435
  : image.embed(left, top, width, height, VImage::option()
443
436
  ->set("extend", VIPS_EXTEND_BACKGROUND)
444
437
  ->set("background", background));
@@ -455,6 +448,7 @@ class PipelineWorker : public Napi::AsyncWorker {
455
448
  // Gravity-based crop
456
449
  int left;
457
450
  int top;
451
+
458
452
  std::tie(left, top) = sharp::CalculateCrop(
459
453
  inputWidth, inputHeight, baton->width, baton->height, baton->position);
460
454
  int width = std::min(inputWidth, baton->width);
@@ -465,16 +459,25 @@ class PipelineWorker : public Napi::AsyncWorker {
465
459
  left, top, width, height, nPages, &targetPageHeight)
466
460
  : image.extract_area(left, top, width, height);
467
461
  } else {
462
+ int attention_x;
463
+ int attention_y;
464
+
468
465
  // Attention-based or Entropy-based crop
469
466
  MultiPageUnsupported(nPages, "Resize strategy");
470
467
  image = image.tilecache(VImage::option()
471
468
  ->set("access", VIPS_ACCESS_RANDOM)
472
469
  ->set("threaded", TRUE));
470
+
473
471
  image = image.smartcrop(baton->width, baton->height, VImage::option()
474
- ->set("interesting", baton->position == 16 ? VIPS_INTERESTING_ENTROPY : VIPS_INTERESTING_ATTENTION));
472
+ ->set("interesting", baton->position == 16 ? VIPS_INTERESTING_ENTROPY : VIPS_INTERESTING_ATTENTION)
473
+ ->set("attention_x", &attention_x)
474
+ ->set("attention_y", &attention_y));
475
475
  baton->hasCropOffset = true;
476
476
  baton->cropOffsetLeft = static_cast<int>(image.xoffset());
477
477
  baton->cropOffsetTop = static_cast<int>(image.yoffset());
478
+ baton->hasAttentionCenter = true;
479
+ baton->attentionX = static_cast<int>(attention_x * jpegShrinkOnLoad / scale);
480
+ baton->attentionY = static_cast<int>(attention_y * jpegShrinkOnLoad / scale);
478
481
  }
479
482
  }
480
483
  }
@@ -503,7 +506,7 @@ class PipelineWorker : public Napi::AsyncWorker {
503
506
  }
504
507
 
505
508
  // Affine transform
506
- if (baton->affineMatrix.size() > 0) {
509
+ if (!baton->affineMatrix.empty()) {
507
510
  MultiPageUnsupported(nPages, "Affine");
508
511
  std::vector<double> background;
509
512
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
@@ -519,18 +522,29 @@ class PipelineWorker : public Napi::AsyncWorker {
519
522
 
520
523
  // Extend edges
521
524
  if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
522
- std::vector<double> background;
523
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
524
-
525
525
  // Embed
526
526
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
527
527
  baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
528
528
 
529
- image = nPages > 1
530
- ? sharp::EmbedMultiPage(image,
531
- baton->extendLeft, baton->extendTop, baton->width, baton->height, background, nPages, &targetPageHeight)
532
- : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
533
- VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
529
+ if (baton->extendWith == VIPS_EXTEND_BACKGROUND) {
530
+ std::vector<double> background;
531
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
532
+
533
+ image = nPages > 1
534
+ ? sharp::EmbedMultiPage(image,
535
+ baton->extendLeft, baton->extendTop, baton->width, baton->height,
536
+ baton->extendWith, background, nPages, &targetPageHeight)
537
+ : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
538
+ VImage::option()->set("extend", baton->extendWith)->set("background", background));
539
+ } else {
540
+ std::vector<double> ignoredBackground(1);
541
+ image = nPages > 1
542
+ ? sharp::EmbedMultiPage(image,
543
+ baton->extendLeft, baton->extendTop, baton->width, baton->height,
544
+ baton->extendWith, ignoredBackground, nPages, &targetPageHeight)
545
+ : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
546
+ VImage::option()->set("extend", baton->extendWith));
547
+ }
534
548
  }
535
549
  // Median - must happen before blurring, due to the utility of blurring after thresholding
536
550
  if (baton->medianSize > 0) {
@@ -572,13 +586,7 @@ class PipelineWorker : public Napi::AsyncWorker {
572
586
 
573
587
  // Reverse premultiplication after all transformations
574
588
  if (shouldPremultiplyAlpha) {
575
- image = image.unpremultiply();
576
- // Cast pixel values to integer
577
- if (sharp::Is16Bit(image.interpretation())) {
578
- image = image.cast(VIPS_FORMAT_USHORT);
579
- } else {
580
- image = image.cast(VIPS_FORMAT_UCHAR);
581
- }
589
+ image = image.unpremultiply().cast(premultiplyFormat);
582
590
  }
583
591
  baton->premultiplied = shouldPremultiplyAlpha;
584
592
 
@@ -660,6 +668,7 @@ class PipelineWorker : public Napi::AsyncWorker {
660
668
  ys.push_back(top);
661
669
  }
662
670
  image = VImage::composite(images, modes, VImage::option()->set("x", xs)->set("y", ys));
671
+ image = sharp::RemoveGifPalette(image);
663
672
  }
664
673
 
665
674
  // Gamma decoding (brighten)
@@ -674,7 +683,7 @@ class PipelineWorker : public Napi::AsyncWorker {
674
683
 
675
684
  // Apply normalisation - stretch luminance to cover full dynamic range
676
685
  if (baton->normalise) {
677
- image = sharp::Normalise(image);
686
+ image = sharp::Normalise(image, baton->normaliseLower, baton->normaliseUpper);
678
687
  }
679
688
 
680
689
  // Apply contrast limiting adaptive histogram equalization (CLAHE)
@@ -689,6 +698,7 @@ class PipelineWorker : public Napi::AsyncWorker {
689
698
  std::tie(booleanImage, booleanImageType) = sharp::OpenInput(baton->boolean);
690
699
  booleanImage = sharp::EnsureColourspace(booleanImage, baton->colourspaceInput);
691
700
  image = sharp::Boolean(image, booleanImage, baton->booleanOp);
701
+ image = sharp::RemoveGifPalette(image);
692
702
  }
693
703
 
694
704
  // Apply per-channel Bandbool bitwise operations after all other operations
@@ -701,24 +711,6 @@ class PipelineWorker : public Napi::AsyncWorker {
701
711
  image = sharp::Tint(image, baton->tintA, baton->tintB);
702
712
  }
703
713
 
704
- // Extract an image channel (aka vips band)
705
- if (baton->extractChannel > -1) {
706
- if (baton->extractChannel >= image.bands()) {
707
- if (baton->extractChannel == 3 && sharp::HasAlpha(image)) {
708
- baton->extractChannel = image.bands() - 1;
709
- } else {
710
- (baton->err).append("Cannot extract channel from image. Too few channels in image.");
711
- return Error();
712
- }
713
- }
714
- VipsInterpretation const interpretation = sharp::Is16Bit(image.interpretation())
715
- ? VIPS_INTERPRETATION_GREY16
716
- : VIPS_INTERPRETATION_B_W;
717
- image = image
718
- .extract_band(baton->extractChannel)
719
- .copy(VImage::option()->set("interpretation", interpretation));
720
- }
721
-
722
714
  // Remove alpha channel, if any
723
715
  if (baton->removeAlpha) {
724
716
  image = sharp::RemoveAlpha(image);
@@ -744,6 +736,26 @@ class PipelineWorker : public Napi::AsyncWorker {
744
736
  }
745
737
  }
746
738
 
739
+ // Extract channel
740
+ if (baton->extractChannel > -1) {
741
+ if (baton->extractChannel >= image.bands()) {
742
+ if (baton->extractChannel == 3 && sharp::HasAlpha(image)) {
743
+ baton->extractChannel = image.bands() - 1;
744
+ } else {
745
+ (baton->err)
746
+ .append("Cannot extract channel ").append(std::to_string(baton->extractChannel))
747
+ .append(" from image with channels 0-").append(std::to_string(image.bands() - 1));
748
+ return Error();
749
+ }
750
+ }
751
+ VipsInterpretation colourspace = sharp::Is16Bit(image.interpretation())
752
+ ? VIPS_INTERPRETATION_GREY16
753
+ : VIPS_INTERPRETATION_B_W;
754
+ image = image
755
+ .extract_band(baton->extractChannel)
756
+ .copy(VImage::option()->set("interpretation", colourspace));
757
+ }
758
+
747
759
  // Apply output ICC profile
748
760
  if (!baton->withMetadataIcc.empty()) {
749
761
  image = image.icc_transform(
@@ -868,7 +880,10 @@ class PipelineWorker : public Napi::AsyncWorker {
868
880
  ->set("strip", !baton->withMetadata)
869
881
  ->set("bitdepth", baton->gifBitdepth)
870
882
  ->set("effort", baton->gifEffort)
871
- ->set("reoptimise", baton->gifReoptimise)
883
+ ->set("reuse", baton->gifReuse)
884
+ ->set("interlace", baton->gifProgressive)
885
+ ->set("interframe_maxerror", baton->gifInterFrameMaxError)
886
+ ->set("interpalette_maxerror", baton->gifInterPaletteMaxError)
872
887
  ->set("dither", baton->gifDither)));
873
888
  baton->bufferOut = static_cast<char*>(area->data);
874
889
  baton->bufferOutLength = area->length;
@@ -935,6 +950,21 @@ class PipelineWorker : public Napi::AsyncWorker {
935
950
  area->free_fn = nullptr;
936
951
  vips_area_unref(area);
937
952
  baton->formatOut = "dz";
953
+ } else if (baton->formatOut == "jxl" ||
954
+ (baton->formatOut == "input" && inputImageType == sharp::ImageType::JXL)) {
955
+ // Write JXL to buffer
956
+ image = sharp::RemoveAnimationProperties(image);
957
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.jxlsave_buffer(VImage::option()
958
+ ->set("strip", !baton->withMetadata)
959
+ ->set("distance", baton->jxlDistance)
960
+ ->set("tier", baton->jxlDecodingTier)
961
+ ->set("effort", baton->jxlEffort)
962
+ ->set("lossless", baton->jxlLossless)));
963
+ baton->bufferOut = static_cast<char*>(area->data);
964
+ baton->bufferOutLength = area->length;
965
+ area->free_fn = nullptr;
966
+ vips_area_unref(area);
967
+ baton->formatOut = "jxl";
938
968
  } else if (baton->formatOut == "raw" ||
939
969
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::RAW)) {
940
970
  // Write raw, uncompressed image data to buffer
@@ -973,6 +1003,7 @@ class PipelineWorker : public Napi::AsyncWorker {
973
1003
  bool const isTiff = sharp::IsTiff(baton->fileOut);
974
1004
  bool const isJp2 = sharp::IsJp2(baton->fileOut);
975
1005
  bool const isHeif = sharp::IsHeif(baton->fileOut);
1006
+ bool const isJxl = sharp::IsJxl(baton->fileOut);
976
1007
  bool const isDz = sharp::IsDz(baton->fileOut);
977
1008
  bool const isDzZip = sharp::IsDzZip(baton->fileOut);
978
1009
  bool const isV = sharp::IsV(baton->fileOut);
@@ -1048,7 +1079,8 @@ class PipelineWorker : public Napi::AsyncWorker {
1048
1079
  ->set("strip", !baton->withMetadata)
1049
1080
  ->set("bitdepth", baton->gifBitdepth)
1050
1081
  ->set("effort", baton->gifEffort)
1051
- ->set("reoptimise", baton->gifReoptimise)
1082
+ ->set("reuse", baton->gifReuse)
1083
+ ->set("interlace", baton->gifProgressive)
1052
1084
  ->set("dither", baton->gifDither));
1053
1085
  baton->formatOut = "gif";
1054
1086
  } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
@@ -1090,6 +1122,17 @@ class PipelineWorker : public Napi::AsyncWorker {
1090
1122
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1091
1123
  ->set("lossless", baton->heifLossless));
1092
1124
  baton->formatOut = "heif";
1125
+ } else if (baton->formatOut == "jxl" || (mightMatchInput && isJxl) ||
1126
+ (willMatchInput && inputImageType == sharp::ImageType::JXL)) {
1127
+ // Write JXL to file
1128
+ image = sharp::RemoveAnimationProperties(image);
1129
+ image.jxlsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1130
+ ->set("strip", !baton->withMetadata)
1131
+ ->set("distance", baton->jxlDistance)
1132
+ ->set("tier", baton->jxlDecodingTier)
1133
+ ->set("effort", baton->jxlEffort)
1134
+ ->set("lossless", baton->jxlLossless));
1135
+ baton->formatOut = "jxl";
1093
1136
  } else if (baton->formatOut == "dz" || isDz || isDzZip) {
1094
1137
  // Write DZ to file
1095
1138
  if (isDzZip) {
@@ -1133,7 +1176,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1133
1176
  // Handle warnings
1134
1177
  std::string warning = sharp::VipsWarningPop();
1135
1178
  while (!warning.empty()) {
1136
- debuglog.Call({ Napi::String::New(env, warning) });
1179
+ debuglog.MakeCallback(Receiver().Value(), { Napi::String::New(env, warning) });
1137
1180
  warning = sharp::VipsWarningPop();
1138
1181
  }
1139
1182
 
@@ -1162,6 +1205,10 @@ class PipelineWorker : public Napi::AsyncWorker {
1162
1205
  info.Set("cropOffsetLeft", static_cast<int32_t>(baton->cropOffsetLeft));
1163
1206
  info.Set("cropOffsetTop", static_cast<int32_t>(baton->cropOffsetTop));
1164
1207
  }
1208
+ if (baton->hasAttentionCenter) {
1209
+ info.Set("attentionX", static_cast<int32_t>(baton->attentionX));
1210
+ info.Set("attentionY", static_cast<int32_t>(baton->attentionY));
1211
+ }
1165
1212
  if (baton->trimThreshold > 0.0) {
1166
1213
  info.Set("trimOffsetLeft", static_cast<int32_t>(baton->trimOffsetLeft));
1167
1214
  info.Set("trimOffsetTop", static_cast<int32_t>(baton->trimOffsetTop));
@@ -1175,8 +1222,8 @@ class PipelineWorker : public Napi::AsyncWorker {
1175
1222
  // Add buffer size to info
1176
1223
  info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));
1177
1224
  // Pass ownership of output data to Buffer instance
1178
- Napi::Buffer<char> data = Napi::Buffer<char>::New(env, static_cast<char*>(baton->bufferOut),
1179
- baton->bufferOutLength, sharp::FreeCallback);
1225
+ Napi::Buffer<char> data = sharp::NewOrCopyBuffer(env, static_cast<char*>(baton->bufferOut),
1226
+ baton->bufferOutLength);
1180
1227
  Callback().MakeCallback(Receiver().Value(), { env.Null(), data, info });
1181
1228
  } else {
1182
1229
  // Add file size to info
@@ -1187,7 +1234,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1187
1234
  Callback().MakeCallback(Receiver().Value(), { env.Null(), info });
1188
1235
  }
1189
1236
  } else {
1190
- Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, baton->err).Value() });
1237
+ Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1191
1238
  }
1192
1239
 
1193
1240
  // Delete baton
@@ -1205,7 +1252,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1205
1252
  // Decrement processing task counter
1206
1253
  g_atomic_int_dec_and_test(&sharp::counterProcess);
1207
1254
  Napi::Number queueLength = Napi::Number::New(env, static_cast<double>(sharp::counterQueue));
1208
- queueListener.Call(Receiver().Value(), { queueLength });
1255
+ queueListener.MakeCallback(Receiver().Value(), { queueLength });
1209
1256
  }
1210
1257
 
1211
1258
  private:
@@ -1351,7 +1398,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1351
1398
  Napi::Value pipeline(const Napi::CallbackInfo& info) {
1352
1399
  // V8 objects are converted to non-V8 types held in the baton struct
1353
1400
  PipelineBaton *baton = new PipelineBaton;
1354
- Napi::Object options = info[0].As<Napi::Object>();
1401
+ Napi::Object options = info[size_t(0)].As<Napi::Object>();
1355
1402
 
1356
1403
  // Input
1357
1404
  baton->input = sharp::CreateInputDescriptor(options.Get("input").As<Napi::Object>());
@@ -1437,6 +1484,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1437
1484
  baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB");
1438
1485
  baton->greyscale = sharp::AttrAsBool(options, "greyscale");
1439
1486
  baton->normalise = sharp::AttrAsBool(options, "normalise");
1487
+ baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");
1488
+ baton->normaliseUpper = sharp::AttrAsUint32(options, "normaliseUpper");
1440
1489
  baton->tintA = sharp::AttrAsDouble(options, "tintA");
1441
1490
  baton->tintB = sharp::AttrAsDouble(options, "tintB");
1442
1491
  baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
@@ -1454,6 +1503,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1454
1503
  baton->extendLeft = sharp::AttrAsInt32(options, "extendLeft");
1455
1504
  baton->extendRight = sharp::AttrAsInt32(options, "extendRight");
1456
1505
  baton->extendBackground = sharp::AttrAsVectorOfDouble(options, "extendBackground");
1506
+ baton->extendWith = sharp::AttrAsEnum<VipsExtend>(options, "extendWith", VIPS_TYPE_EXTEND);
1457
1507
  baton->extractChannel = sharp::AttrAsInt32(options, "extractChannel");
1458
1508
  baton->affineMatrix = sharp::AttrAsVectorOfDouble(options, "affineMatrix");
1459
1509
  baton->affineBackground = sharp::AttrAsVectorOfDouble(options, "affineBackground");
@@ -1549,7 +1599,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1549
1599
  baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1550
1600
  baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1551
1601
  baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1552
- baton->gifReoptimise = sharp::AttrAsBool(options, "gifReoptimise");
1602
+ baton->gifInterFrameMaxError = sharp::AttrAsDouble(options, "gifInterFrameMaxError");
1603
+ baton->gifInterPaletteMaxError = sharp::AttrAsDouble(options, "gifInterPaletteMaxError");
1604
+ baton->gifReuse = sharp::AttrAsBool(options, "gifReuse");
1605
+ baton->gifProgressive = sharp::AttrAsBool(options, "gifProgressive");
1553
1606
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1554
1607
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
1555
1608
  baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
@@ -1573,6 +1626,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1573
1626
  options, "heifCompression", VIPS_TYPE_FOREIGN_HEIF_COMPRESSION);
1574
1627
  baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1575
1628
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1629
+ baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
1630
+ baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
1631
+ baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");
1632
+ baton->jxlLossless = sharp::AttrAsBool(options, "jxlLossless");
1576
1633
  baton->rawDepth = sharp::AttrAsEnum<VipsBandFormat>(options, "rawDepth", VIPS_TYPE_BAND_FORMAT);
1577
1634
  // Animated output properties
1578
1635
  if (sharp::HasAttr(options, "loop")) {
@@ -1601,9 +1658,12 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1601
1658
  baton->trimThreshold > 0.0 ||
1602
1659
  baton->normalise ||
1603
1660
  baton->position == 16 || baton->position == 17 ||
1604
- baton->angle % 360 != 0 ||
1605
- fmod(baton->rotationAngle, 360.0) != 0.0 ||
1606
- baton->useExifOrientation
1661
+ baton->angle != 0 ||
1662
+ baton->rotationAngle != 0.0 ||
1663
+ baton->tileAngle != 0 ||
1664
+ baton->useExifOrientation ||
1665
+ baton->claheWidth != 0 ||
1666
+ !baton->affineMatrix.empty()
1607
1667
  ) {
1608
1668
  baton->input->access = VIPS_ACCESS_RANDOM;
1609
1669
  }
@@ -1616,7 +1676,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1616
1676
  Napi::Function queueListener = options.Get("queueListener").As<Napi::Function>();
1617
1677
 
1618
1678
  // Join queue for worker thread
1619
- Napi::Function callback = info[1].As<Napi::Function>();
1679
+ Napi::Function callback = info[size_t(1)].As<Napi::Function>();
1620
1680
  PipelineWorker *worker = new PipelineWorker(callback, baton, debuglog, queueListener);
1621
1681
  worker->Receiver().Set("options", options);
1622
1682
  worker->Queue();
@@ -1624,7 +1684,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1624
1684
  // Increment queued task counter
1625
1685
  g_atomic_int_inc(&sharp::counterQueue);
1626
1686
  Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast<double>(sharp::counterQueue));
1627
- queueListener.Call(info.This(), { queueLength });
1687
+ queueListener.MakeCallback(info.This(), { queueLength });
1628
1688
 
1629
1689
  return info.Env().Undefined();
1630
1690
  }