@revizly/sharp 0.35.0-revizly22 → 0.35.0-revizly24

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.
@@ -315,6 +315,7 @@ const Sharp = function (input, options) {
315
315
  withExif: {},
316
316
  withExifMerge: true,
317
317
  withXmp: '',
318
+ keepGainMap: false,
318
319
  withGainMap: false,
319
320
  resolveWithObject: false,
320
321
  loop: -1,
package/lib/libvips.js CHANGED
@@ -37,13 +37,13 @@ const log = (item) => {
37
37
  }
38
38
  };
39
39
 
40
- /* node:coverage ignore next */
40
+ /* node:coverage disable */
41
+
41
42
  const runtimeLibc = () => detectLibc.isNonGlibcLinuxSync() ? detectLibc.familySync() : '';
42
43
 
43
44
  const runtimePlatformArch = () => `${process.platform}${runtimeLibc()}-${process.arch}`;
44
45
 
45
46
  const buildPlatformArch = () => {
46
- /* node:coverage ignore next 3 */
47
47
  if (isEmscripten()) {
48
48
  return 'wasm32';
49
49
  }
@@ -56,7 +56,6 @@ const buildSharpLibvipsIncludeDir = () => {
56
56
  try {
57
57
  return require(`@revizly/sharp-libvips-dev-${buildPlatformArch()}/include`);
58
58
  } catch {
59
- /* node:coverage ignore next 5 */
60
59
  try {
61
60
  return require('@revizly/sharp-libvips-dev/include');
62
61
  } catch {}
@@ -65,7 +64,6 @@ const buildSharpLibvipsIncludeDir = () => {
65
64
  };
66
65
 
67
66
  const buildSharpLibvipsCPlusPlusDir = () => {
68
- /* node:coverage ignore next 4 */
69
67
  try {
70
68
  return require('@revizly/sharp-libvips-dev/cplusplus');
71
69
  } catch {}
@@ -76,7 +74,6 @@ const buildSharpLibvipsLibDir = () => {
76
74
  try {
77
75
  return require(`@revizly/sharp-libvips-dev-${buildPlatformArch()}/lib`);
78
76
  } catch {
79
- /* node:coverage ignore next 5 */
80
77
  try {
81
78
  return require(`@revizly/sharp-libvips-${buildPlatformArch()}/lib`);
82
79
  } catch {}
@@ -84,8 +81,6 @@ const buildSharpLibvipsLibDir = () => {
84
81
  return '';
85
82
  };
86
83
 
87
- /* node:coverage disable */
88
-
89
84
  const isUnsupportedNodeRuntime = () => {
90
85
  if (process.release?.name === 'node' && process.versions) {
91
86
  if (!semverSatisfies(process.versions.node, engines.node)) {
@@ -151,9 +146,17 @@ const getBrewPkgConfigPath = () => {
151
146
  if (brewPrefix) {
152
147
  return `${brewPrefix}/lib/pkgconfig`;
153
148
  }
154
- } catch (_err) {
155
- // ignore
156
- }
149
+ } catch (_err) {}
150
+ return undefined;
151
+ };
152
+
153
+ const getPkgConfigPath = () => {
154
+ try {
155
+ const pkgConfigPath = (spawnSync('pkg-config', ['--variable', 'pc_path', 'pkg-config'], { encoding: 'utf8' }).stdout || '').trim();
156
+ if (pkgConfigPath) {
157
+ return pkgConfigPath;
158
+ }
159
+ } catch (_err) {}
157
160
  return undefined;
158
161
  };
159
162
 
@@ -163,11 +166,8 @@ const pkgConfigPath = () => {
163
166
  if (process.platform !== 'win32') {
164
167
  return [
165
168
  getBrewPkgConfigPath(),
166
- process.env.PKG_CONFIG_PATH,
167
- '/usr/local/lib/pkgconfig',
168
- '/usr/lib/pkgconfig',
169
- '/usr/local/libdata/pkgconfig',
170
- '/usr/libdata/pkgconfig'
169
+ getPkgConfigPath(),
170
+ process.env.PKG_CONFIG_PATH
171
171
  ].filter(Boolean).join(':');
172
172
  } else {
173
173
  return '';
package/lib/output.js CHANGED
@@ -377,6 +377,35 @@ function withIccProfile (icc, options) {
377
377
  return this;
378
378
  }
379
379
 
380
+ /**
381
+ * If the input contains gain map metadata, attempt to process the image and gain map separately,
382
+ * recombining them into a single output image.
383
+ *
384
+ * This approach is faster and should produce better results than {@link #withgainmap withGainMap},
385
+ * however not all operations are supported.
386
+ *
387
+ * Only JPEG input and output are supported.
388
+ * JPEG output options other than `quality` are ignored.
389
+ *
390
+ * This feature is experimental and the API may change.
391
+ *
392
+ * @since 0.35.0
393
+ *
394
+ * @example
395
+ * const outputWithResizedGainMap = await sharp(inputWithGainMap)
396
+ * .keepGainMap()
397
+ * .resize({ width: 64 })
398
+ * .toBuffer();
399
+ *
400
+ * @returns {Sharp}
401
+ */
402
+ function keepGainMap() {
403
+ this.options.keepGainMap = true;
404
+ this.options.withGainMap = false;
405
+ this.options.keepMetadata |= 0b100000;
406
+ return this;
407
+ }
408
+
380
409
  /**
381
410
  * If the input contains gain map metadata, use it to convert the main image to HDR (High Dynamic Range) before further processing.
382
411
  * The input gain map is discarded.
@@ -389,7 +418,7 @@ function withIccProfile (icc, options) {
389
418
  * @since 0.35.0
390
419
  *
391
420
  * @example
392
- * const outputWithGainMap = await sharp(inputWithGainMap)
421
+ * const outputWithRegeneratedGainMap = await sharp(inputWithGainMap)
393
422
  * .withGainMap()
394
423
  * .toBuffer();
395
424
  *
@@ -397,6 +426,7 @@ function withIccProfile (icc, options) {
397
426
  */
398
427
  function withGainMap() {
399
428
  this.options.withGainMap = true;
429
+ this.options.keepGainMap = false;
400
430
  this.options.colourspace = 'scrgb';
401
431
  return this;
402
432
  }
@@ -1741,6 +1771,7 @@ module.exports = (Sharp) => {
1741
1771
  withExifMerge,
1742
1772
  keepIccProfile,
1743
1773
  withIccProfile,
1774
+ keepGainMap,
1744
1775
  withGainMap,
1745
1776
  keepXmp,
1746
1777
  withXmp,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revizly/sharp",
3
3
  "description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, GIF, AVIF and TIFF images",
4
- "version": "0.35.0-revizly22",
4
+ "version": "0.35.0-revizly24",
5
5
  "author": "Lovell Fuller <npm@lovell.info>",
6
6
  "homepage": "https://sharp.pixelplumbing.com",
7
7
  "contributors": [
@@ -145,24 +145,24 @@
145
145
  "semver": "^7.7.4"
146
146
  },
147
147
  "optionalDependencies": {
148
- "@revizly/sharp-libvips-linux-arm64": "1.0.28",
149
- "@revizly/sharp-libvips-linux-x64": "1.0.28",
150
- "@revizly/sharp-linux-arm64": "0.35.0-revizly16",
151
- "@revizly/sharp-linux-x64": "0.35.0-revizly16"
148
+ "@revizly/sharp-libvips-linux-arm64": "1.0.29",
149
+ "@revizly/sharp-libvips-linux-x64": "1.0.29",
150
+ "@revizly/sharp-linux-arm64": "0.35.0-revizly22",
151
+ "@revizly/sharp-linux-x64": "0.35.0-revizly22"
152
152
  },
153
153
  "devDependencies": {
154
- "@biomejs/biome": "^2.4.4",
154
+ "@biomejs/biome": "^2.4.8",
155
155
  "@cpplint/cli": "^0.1.0",
156
- "@emnapi/runtime": "^1.8.1",
157
- "@revizly/sharp-libvips-dev": "1.0.28",
156
+ "@emnapi/runtime": "^1.9.1",
157
+ "@revizly/sharp-libvips-dev": "1.0.29",
158
158
  "@types/node": "*",
159
- "emnapi": "^1.8.1",
159
+ "emnapi": "^1.9.1",
160
160
  "exif-reader": "^2.0.3",
161
161
  "extract-zip": "^2.0.1",
162
162
  "icc": "^3.0.0",
163
- "node-addon-api": "^8.5.0",
163
+ "node-addon-api": "^8.6.0",
164
164
  "node-gyp": "^12.2.0",
165
- "tar-fs": "^3.1.1",
165
+ "tar-fs": "^3.1.2",
166
166
  "tsd": "^0.33.0"
167
167
  },
168
168
  "license": "Apache-2.0",
@@ -170,7 +170,7 @@
170
170
  "node": ">=20.9.0"
171
171
  },
172
172
  "config": {
173
- "libvips": ">=8.18.0"
173
+ "libvips": ">=8.18.1"
174
174
  },
175
175
  "funding": {
176
176
  "url": "https://opencollective.com/libvips"
package/src/binding.gyp CHANGED
@@ -212,7 +212,6 @@
212
212
  '-Oz',
213
213
  '-sALLOW_MEMORY_GROWTH',
214
214
  '-sENVIRONMENT=node',
215
- '-sEXPORTED_FUNCTIONS=emnapiInit,_vips_shutdown,_uv_library_shutdown',
216
215
  '-sNODERAWFS',
217
216
  '-sWASM_ASYNC_COMPILATION=0'
218
217
  ],
package/src/common.cc CHANGED
@@ -1164,7 +1164,8 @@ namespace sharp {
1164
1164
  Does this image have a gain map?
1165
1165
  */
1166
1166
  bool HasGainMap(VImage image) {
1167
- return image.get_typeof("gainmap-data") == VIPS_TYPE_BLOB;
1167
+ return image.get_typeof("gainmap") == VIPS_TYPE_BLOB ||
1168
+ image.get_typeof("gainmap-data") == VIPS_TYPE_BLOB;
1168
1169
  }
1169
1170
 
1170
1171
  /*
@@ -1172,6 +1173,7 @@ namespace sharp {
1172
1173
  */
1173
1174
  VImage RemoveGainMap(VImage image) {
1174
1175
  VImage copy = image.copy();
1176
+ copy.remove("gainmap");
1175
1177
  copy.remove("gainmap-data");
1176
1178
  return copy;
1177
1179
  }
package/src/common.h CHANGED
@@ -19,8 +19,8 @@
19
19
 
20
20
  #if (VIPS_MAJOR_VERSION < 8) || \
21
21
  (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 18) || \
22
- (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 18 && VIPS_MICRO_VERSION < 0)
23
- #error "libvips version 8.18.0+ is required - please see https://sharp.pixelplumbing.com/install"
22
+ (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION == 18 && VIPS_MICRO_VERSION < 1)
23
+ #error "libvips version 8.18.1+ is required - please see https://sharp.pixelplumbing.com/install"
24
24
  #endif
25
25
 
26
26
  #if defined(__has_include)
@@ -29,6 +29,12 @@
29
29
  #endif
30
30
  #endif
31
31
 
32
+ #ifdef __EMSCRIPTEN__
33
+ #define SHARP_CALLBACK_FN_NAME Call
34
+ #else
35
+ #define SHARP_CALLBACK_FN_NAME MakeCallback
36
+ #endif
37
+
32
38
  using vips::VImage;
33
39
 
34
40
  namespace sharp {
package/src/metadata.cc CHANGED
@@ -209,7 +209,7 @@ class MetadataWorker : public Napi::AsyncWorker {
209
209
  // Handle warnings
210
210
  std::string warning = sharp::VipsWarningPop();
211
211
  while (!warning.empty()) {
212
- debuglog.MakeCallback(Receiver().Value(), { Napi::String::New(env, warning) });
212
+ debuglog.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { Napi::String::New(env, warning) });
213
213
  warning = sharp::VipsWarningPop();
214
214
  }
215
215
 
@@ -344,9 +344,10 @@ class MetadataWorker : public Napi::AsyncWorker {
344
344
  }
345
345
  info.Set("comments", comments);
346
346
  }
347
- Callback().MakeCallback(Receiver().Value(), { env.Null(), info });
347
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), info });
348
348
  } else {
349
- Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
349
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(),
350
+ { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
350
351
  }
351
352
 
352
353
  delete baton->input;
package/src/pipeline.cc CHANGED
@@ -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) {
@@ -296,12 +298,20 @@ class PipelineWorker : public Napi::AsyncWorker {
296
298
  if (baton->input->autoOrient) {
297
299
  image = sharp::RemoveExifOrientation(image);
298
300
  }
301
+ VImage gainMap;
302
+ int gainMapScaleFactor = 1;
299
303
  if (sharp::HasGainMap(image)) {
300
- if (baton->withGainMap) {
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) {
301
310
  image = image.uhdr2scRGB();
302
311
  }
303
312
  image = sharp::RemoveGainMap(image);
304
313
  } else {
314
+ baton->keepGainMap = false;
305
315
  baton->withGainMap = false;
306
316
  }
307
317
 
@@ -401,6 +411,11 @@ class PipelineWorker : public Napi::AsyncWorker {
401
411
  image = image.resize(1.0 / hshrink, VImage::option()
402
412
  ->set("vscale", 1.0 / vshrink)
403
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
+ }
404
419
  }
405
420
 
406
421
  image = sharp::StaySequential(image,
@@ -413,14 +428,23 @@ class PipelineWorker : public Napi::AsyncWorker {
413
428
  MultiPageUnsupported(nPages, "Rotate");
414
429
  }
415
430
  image = image.rot(autoRotation);
431
+ if (baton->keepGainMap) {
432
+ gainMap = gainMap.rot(autoRotation);
433
+ }
416
434
  }
417
435
  // Mirror vertically (up-down) about the x-axis
418
436
  if (baton->flip) {
419
437
  image = image.flip(VIPS_DIRECTION_VERTICAL);
438
+ if (baton->keepGainMap) {
439
+ gainMap = gainMap.flip(VIPS_DIRECTION_VERTICAL);
440
+ }
420
441
  }
421
442
  // Mirror horizontally (left-right) about the y-axis
422
443
  if (baton->flop != autoFlop) {
423
444
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
445
+ if (baton->keepGainMap) {
446
+ gainMap = gainMap.flip(VIPS_DIRECTION_HORIZONTAL);
447
+ }
424
448
  }
425
449
  // Rotate post-extract 90-angle
426
450
  if (rotation != VIPS_ANGLE_D0) {
@@ -428,6 +452,9 @@ class PipelineWorker : public Napi::AsyncWorker {
428
452
  MultiPageUnsupported(nPages, "Rotate");
429
453
  }
430
454
  image = image.rot(rotation);
455
+ if (baton->keepGainMap) {
456
+ gainMap = gainMap.rot(rotation);
457
+ }
431
458
  }
432
459
 
433
460
  // Join additional color channels to the image
@@ -474,6 +501,12 @@ class PipelineWorker : public Napi::AsyncWorker {
474
501
  : image.embed(left, top, width, height, VImage::option()
475
502
  ->set("extend", VIPS_EXTEND_BACKGROUND)
476
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
+ }
477
510
  } else if (baton->canvas == sharp::Canvas::CROP) {
478
511
  if (baton->width > inputWidth) {
479
512
  baton->width = inputWidth;
@@ -494,12 +527,17 @@ class PipelineWorker : public Napi::AsyncWorker {
494
527
  ? sharp::CropMultiPage(image,
495
528
  left, top, width, height, nPages, &targetPageHeight)
496
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
+ }
497
534
  } else {
498
535
  int attention_x;
499
536
  int attention_y;
500
537
 
501
538
  // Attention-based or Entropy-based crop
502
539
  MultiPageUnsupported(nPages, "Resize strategy");
540
+ KeepGainMapUnsupported(baton->keepGainMap, "Resize strategy");
503
541
  image = sharp::StaySequential(image);
504
542
  image = image.smartcrop(baton->width, baton->height, VImage::option()
505
543
  ->set("interesting", baton->position == 16 ? VIPS_INTERESTING_ENTROPY : VIPS_INTERESTING_ATTENTION)
@@ -523,6 +561,9 @@ class PipelineWorker : public Napi::AsyncWorker {
523
561
  std::vector<double> background;
524
562
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
525
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
+ }
526
567
  }
527
568
 
528
569
  // Post extraction
@@ -531,18 +572,23 @@ class PipelineWorker : public Napi::AsyncWorker {
531
572
  image = sharp::CropMultiPage(image,
532
573
  baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost,
533
574
  nPages, &targetPageHeight);
534
-
535
575
  // heightPost is used in the info object, so update to reflect the number of pages
536
576
  baton->heightPost *= nPages;
537
577
  } else {
538
578
  image = image.extract_area(
539
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
+ }
540
585
  }
541
586
  }
542
587
 
543
588
  // Affine transform
544
589
  if (!baton->affineMatrix.empty()) {
545
590
  MultiPageUnsupported(nPages, "Affine");
591
+ KeepGainMapUnsupported(baton->keepGainMap, "Affine");
546
592
  image = sharp::StaySequential(image);
547
593
  std::vector<double> background;
548
594
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
@@ -573,6 +619,12 @@ class PipelineWorker : public Napi::AsyncWorker {
573
619
  baton->extendWith, background, nPages, &targetPageHeight)
574
620
  : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
575
621
  VImage::option()->set("extend", baton->extendWith)->set("background", background));
622
+ if (baton->keepGainMap) {
623
+ gainMap = gainMap.embed(baton->extendLeft / gainMapScaleFactor, baton->extendTop / gainMapScaleFactor,
624
+ baton->width / gainMapScaleFactor, baton->height / gainMapScaleFactor, VImage::option()
625
+ ->set("extend", baton->extendWith)
626
+ ->set("background", 0));
627
+ }
576
628
  } else {
577
629
  std::vector<double> ignoredBackground(1);
578
630
  image = sharp::StaySequential(image);
@@ -582,6 +634,12 @@ class PipelineWorker : public Napi::AsyncWorker {
582
634
  baton->extendWith, ignoredBackground, nPages, &targetPageHeight)
583
635
  : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
584
636
  VImage::option()->set("extend", baton->extendWith));
637
+ if (baton->keepGainMap) {
638
+ gainMap = gainMap.embed(baton->extendLeft / gainMapScaleFactor, baton->extendTop / gainMapScaleFactor,
639
+ baton->width / gainMapScaleFactor, baton->height / gainMapScaleFactor, VImage::option()
640
+ ->set("extend", baton->extendWith)
641
+ ->set("background", 0));
642
+ }
585
643
  }
586
644
  }
587
645
  // Median - must happen before blurring, due to the utility of blurring after thresholding
@@ -608,6 +666,9 @@ class PipelineWorker : public Napi::AsyncWorker {
608
666
  // Blur
609
667
  if (shouldBlur) {
610
668
  image = sharp::Blur(image, baton->blurSigma, baton->precision, baton->minAmpl);
669
+ if (baton->keepGainMap) {
670
+ gainMap = sharp::Blur(gainMap, baton->blurSigma, baton->precision, baton->minAmpl);
671
+ }
611
672
  }
612
673
 
613
674
  // Unflatten the image
@@ -617,6 +678,7 @@ class PipelineWorker : public Napi::AsyncWorker {
617
678
 
618
679
  // Convolve
619
680
  if (shouldConv) {
681
+ KeepGainMapUnsupported(baton->keepGainMap, "Convolve");
620
682
  image = sharp::Convolve(image,
621
683
  baton->convKernelWidth, baton->convKernelHeight,
622
684
  baton->convKernelScale, baton->convKernelOffset,
@@ -625,11 +687,13 @@ class PipelineWorker : public Napi::AsyncWorker {
625
687
 
626
688
  // Recomb
627
689
  if (!baton->recombMatrix.empty()) {
690
+ KeepGainMapUnsupported(baton->keepGainMap, "Recomb");
628
691
  image = sharp::Recomb(image, baton->recombMatrix);
629
692
  }
630
693
 
631
694
  // Modulate
632
695
  if (baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0 || baton->lightness != 0.0) {
696
+ KeepGainMapUnsupported(baton->keepGainMap, "Modulate");
633
697
  image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
634
698
  }
635
699
 
@@ -647,6 +711,7 @@ class PipelineWorker : public Napi::AsyncWorker {
647
711
 
648
712
  // Composite
649
713
  if (shouldComposite) {
714
+ KeepGainMapUnsupported(baton->keepGainMap, "Composite");
650
715
  std::vector<VImage> images = { image };
651
716
  std::vector<int> modes, xs, ys;
652
717
  for (Composite *composite : baton->composite) {
@@ -759,18 +824,21 @@ class PipelineWorker : public Napi::AsyncWorker {
759
824
 
760
825
  // Apply normalisation - stretch luminance to cover full dynamic range
761
826
  if (baton->normalise) {
827
+ KeepGainMapUnsupported(baton->keepGainMap, "Normalise");
762
828
  image = sharp::StaySequential(image);
763
829
  image = sharp::Normalise(image, baton->normaliseLower, baton->normaliseUpper);
764
830
  }
765
831
 
766
832
  // Apply contrast limiting adaptive histogram equalization (CLAHE)
767
833
  if (baton->claheWidth != 0 && baton->claheHeight != 0) {
834
+ KeepGainMapUnsupported(baton->keepGainMap, "Clahe");
768
835
  image = sharp::StaySequential(image);
769
836
  image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope);
770
837
  }
771
838
 
772
839
  // Apply bitwise boolean operation between images
773
840
  if (baton->boolean != nullptr) {
841
+ KeepGainMapUnsupported(baton->keepGainMap, "Boolean");
774
842
  VImage booleanImage;
775
843
  sharp::ImageType booleanImageType = sharp::ImageType::UNKNOWN;
776
844
  baton->boolean->access = access;
@@ -813,6 +881,7 @@ class PipelineWorker : public Napi::AsyncWorker {
813
881
 
814
882
  // Extract channel
815
883
  if (baton->extractChannel > -1) {
884
+ KeepGainMapUnsupported(baton->keepGainMap, "Extract channel");
816
885
  if (baton->extractChannel >= image.bands()) {
817
886
  if (baton->extractChannel == 3 && image.has_alpha()) {
818
887
  baton->extractChannel = image.bands() - 1;
@@ -847,6 +916,9 @@ class PipelineWorker : public Napi::AsyncWorker {
847
916
  // Negate the colours in the image
848
917
  if (baton->negate) {
849
918
  image = sharp::Negate(image, baton->negateAlpha);
919
+ if (baton->keepGainMap) {
920
+ gainMap = sharp::Negate(gainMap, false);
921
+ }
850
922
  }
851
923
 
852
924
  // Override EXIF Orientation tag
@@ -893,18 +965,27 @@ class PipelineWorker : public Napi::AsyncWorker {
893
965
  if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
894
966
  // Write JPEG to buffer
895
967
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
896
- VipsArea *area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option()
897
- ->set("keep", baton->keepMetadata)
898
- ->set("Q", baton->jpegQuality)
899
- ->set("interlace", baton->jpegProgressive)
900
- ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
901
- ? VIPS_FOREIGN_SUBSAMPLE_OFF
902
- : VIPS_FOREIGN_SUBSAMPLE_ON)
903
- ->set("trellis_quant", baton->jpegTrellisQuantisation)
904
- ->set("quant_table", baton->jpegQuantisationTable)
905
- ->set("overshoot_deringing", baton->jpegOvershootDeringing)
906
- ->set("optimize_scans", baton->jpegOptimiseScans)
907
- ->set("optimize_coding", baton->jpegOptimiseCoding)));
968
+ VipsArea *area;
969
+ if (baton->keepGainMap) {
970
+ image = ReattachGainMap(image, gainMap, baton);
971
+ area = reinterpret_cast<VipsArea*>(image.uhdrsave_buffer(VImage::option()
972
+ ->set("keep", baton->keepMetadata)
973
+ ->set("Q", baton->jpegQuality)
974
+ ->set("gainmap_scale_factor", gainMapScaleFactor)));
975
+ } else {
976
+ area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option()
977
+ ->set("keep", baton->keepMetadata)
978
+ ->set("Q", baton->jpegQuality)
979
+ ->set("interlace", baton->jpegProgressive)
980
+ ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
981
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
982
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
983
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
984
+ ->set("quant_table", baton->jpegQuantisationTable)
985
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
986
+ ->set("optimize_scans", baton->jpegOptimiseScans)
987
+ ->set("optimize_coding", baton->jpegOptimiseCoding)));
988
+ }
908
989
  baton->bufferOut = static_cast<char*>(area->data);
909
990
  baton->bufferOutLength = area->length;
910
991
  area->free_fn = nullptr;
@@ -1121,18 +1202,26 @@ class PipelineWorker : public Napi::AsyncWorker {
1121
1202
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
1122
1203
  // Write JPEG to file
1123
1204
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
1124
- image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1125
- ->set("keep", baton->keepMetadata)
1126
- ->set("Q", baton->jpegQuality)
1127
- ->set("interlace", baton->jpegProgressive)
1128
- ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
1129
- ? VIPS_FOREIGN_SUBSAMPLE_OFF
1130
- : VIPS_FOREIGN_SUBSAMPLE_ON)
1131
- ->set("trellis_quant", baton->jpegTrellisQuantisation)
1132
- ->set("quant_table", baton->jpegQuantisationTable)
1133
- ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1134
- ->set("optimize_scans", baton->jpegOptimiseScans)
1135
- ->set("optimize_coding", baton->jpegOptimiseCoding));
1205
+ if (baton->keepGainMap) {
1206
+ image = ReattachGainMap(image, gainMap, baton);
1207
+ image.uhdrsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1208
+ ->set("keep", baton->keepMetadata)
1209
+ ->set("Q", baton->jpegQuality)
1210
+ ->set("gainmap_scale_factor", gainMapScaleFactor));
1211
+ } else {
1212
+ image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1213
+ ->set("keep", baton->keepMetadata)
1214
+ ->set("Q", baton->jpegQuality)
1215
+ ->set("interlace", baton->jpegProgressive)
1216
+ ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
1217
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
1218
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
1219
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
1220
+ ->set("quant_table", baton->jpegQuantisationTable)
1221
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1222
+ ->set("optimize_scans", baton->jpegOptimiseScans)
1223
+ ->set("optimize_coding", baton->jpegOptimiseCoding));
1224
+ }
1136
1225
  baton->formatOut = "jpeg";
1137
1226
  baton->channels = std::min(baton->channels, 3);
1138
1227
  } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
@@ -1301,7 +1390,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1301
1390
  if (baton->errUseWarning) {
1302
1391
  (baton->err).append("\n").append(warning);
1303
1392
  } else {
1304
- debuglog.MakeCallback(Receiver().Value(), { Napi::String::New(env, warning) });
1393
+ debuglog.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { Napi::String::New(env, warning) });
1305
1394
  }
1306
1395
  warning = sharp::VipsWarningPop();
1307
1396
  }
@@ -1350,17 +1439,15 @@ class PipelineWorker : public Napi::AsyncWorker {
1350
1439
  info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));
1351
1440
  if (baton->typedArrayOut) {
1352
1441
  // ECMAScript ArrayBuffer with Uint8Array view
1353
- Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, baton->bufferOutLength);
1354
- memcpy(ab.Data(), baton->bufferOut, baton->bufferOutLength);
1442
+ Napi::TypedArrayOf<uint8_t> data = Napi::Buffer<char>::Copy(env,
1443
+ static_cast<char*>(baton->bufferOut), baton->bufferOutLength);
1355
1444
  sharp::FreeCallback(static_cast<char*>(baton->bufferOut), nullptr);
1356
- Napi::TypedArrayOf<uint8_t> data = Napi::TypedArrayOf<uint8_t>::New(env,
1357
- baton->bufferOutLength, ab, 0, napi_uint8_array);
1358
- Callback().MakeCallback(Receiver().Value(), { env.Null(), data, info });
1445
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), data, info });
1359
1446
  } else {
1360
1447
  // Node.js Buffer
1361
1448
  Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
1362
1449
  baton->bufferOutLength, sharp::FreeCallback);
1363
- Callback().MakeCallback(Receiver().Value(), { env.Null(), data, info });
1450
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), data, info });
1364
1451
  }
1365
1452
  } else {
1366
1453
  // Add file size to info
@@ -1371,10 +1458,11 @@ class PipelineWorker : public Napi::AsyncWorker {
1371
1458
  info.Set("size", size);
1372
1459
  } catch (...) {}
1373
1460
  }
1374
- Callback().MakeCallback(Receiver().Value(), { env.Null(), info });
1461
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), info });
1375
1462
  }
1376
1463
  } else {
1377
- Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1464
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(),
1465
+ { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1378
1466
  }
1379
1467
 
1380
1468
  // Delete baton
@@ -1395,7 +1483,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1395
1483
  // Decrement processing task counter
1396
1484
  sharp::counterProcess--;
1397
1485
  Napi::Number queueLength = Napi::Number::New(env, static_cast<int>(sharp::counterQueue));
1398
- queueListener.MakeCallback(Receiver().Value(), { queueLength });
1486
+ queueListener.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { queueLength });
1399
1487
  }
1400
1488
 
1401
1489
  private:
@@ -1409,6 +1497,12 @@ class PipelineWorker : public Napi::AsyncWorker {
1409
1497
  }
1410
1498
  }
1411
1499
 
1500
+ void KeepGainMapUnsupported(bool const keepGainMap, std::string op) {
1501
+ if (keepGainMap) {
1502
+ throw std::runtime_error(op + " is not supported when keeping gain maps");
1503
+ }
1504
+ }
1505
+
1412
1506
  /*
1413
1507
  Calculate the angle of rotation and need-to-flip for the given Exif orientation
1414
1508
  By default, returns zero, i.e. no rotation.
@@ -1527,6 +1621,21 @@ class PipelineWorker : public Napi::AsyncWorker {
1527
1621
  return options;
1528
1622
  }
1529
1623
 
1624
+ VImage ReattachGainMap(VImage image, VImage gainMap, PipelineBaton *baton) {
1625
+ VipsArea *gainMapJpeg = reinterpret_cast<VipsArea*>(gainMap.jpegsave_buffer(VImage::option()
1626
+ ->set("keep", FALSE)
1627
+ ->set("Q", baton->jpegQuality)
1628
+ ->set("subsample_mode", VIPS_FOREIGN_SUBSAMPLE_OFF)
1629
+ ->set("trellis_quant", baton->jpegTrellisQuantisation)
1630
+ ->set("quant_table", baton->jpegQuantisationTable)
1631
+ ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1632
+ ->set("optimize_coding", baton->jpegOptimiseCoding)));
1633
+ image = image.copy();
1634
+ image.set("gainmap-data", reinterpret_cast<VipsCallbackFn>(vips_area_free_cb),
1635
+ gainMapJpeg->data, gainMapJpeg->length);
1636
+ return image;
1637
+ }
1638
+
1530
1639
  /*
1531
1640
  Clear all thread-local data.
1532
1641
  */
@@ -1728,6 +1837,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1728
1837
  }
1729
1838
  baton->withExifMerge = sharp::AttrAsBool(options, "withExifMerge");
1730
1839
  baton->withXmp = sharp::AttrAsStr(options, "withXmp");
1840
+ baton->keepGainMap = sharp::AttrAsBool(options, "keepGainMap");
1731
1841
  baton->withGainMap = sharp::AttrAsBool(options, "withGainMap");
1732
1842
  baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1733
1843
  baton->loop = sharp::AttrAsUint32(options, "loop");
@@ -1833,7 +1943,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1833
1943
 
1834
1944
  // Increment queued task counter
1835
1945
  Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast<int>(++sharp::counterQueue));
1836
- queueListener.MakeCallback(info.This(), { queueLength });
1946
+ queueListener.SHARP_CALLBACK_FN_NAME(info.This(), { queueLength });
1837
1947
 
1838
1948
  return info.Env().Undefined();
1839
1949
  }
package/src/pipeline.h CHANGED
@@ -213,6 +213,7 @@ struct PipelineBaton {
213
213
  bool withExifMerge;
214
214
  std::string withXmp;
215
215
  bool withGainMap;
216
+ bool keepGainMap;
216
217
  int timeoutSeconds;
217
218
  std::vector<double> convKernel;
218
219
  int convKernelWidth;
@@ -391,6 +392,7 @@ struct PipelineBaton {
391
392
  withMetadataDensity(0.0),
392
393
  withExifMerge(true),
393
394
  withGainMap(false),
395
+ keepGainMap(false),
394
396
  timeoutSeconds(0),
395
397
  convKernelWidth(0),
396
398
  convKernelHeight(0),
package/src/stats.cc CHANGED
@@ -109,7 +109,7 @@ class StatsWorker : public Napi::AsyncWorker {
109
109
  // Handle warnings
110
110
  std::string warning = sharp::VipsWarningPop();
111
111
  while (!warning.empty()) {
112
- debuglog.MakeCallback(Receiver().Value(), { Napi::String::New(env, warning) });
112
+ debuglog.SHARP_CALLBACK_FN_NAME(Receiver().Value(), { Napi::String::New(env, warning) });
113
113
  warning = sharp::VipsWarningPop();
114
114
  }
115
115
  if (baton->err.empty()) {
@@ -143,9 +143,10 @@ class StatsWorker : public Napi::AsyncWorker {
143
143
  dominant.Set("g", baton->dominantGreen);
144
144
  dominant.Set("b", baton->dominantBlue);
145
145
  info.Set("dominant", dominant);
146
- Callback().MakeCallback(Receiver().Value(), { env.Null(), info });
146
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(), { env.Null(), info });
147
147
  } else {
148
- Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
148
+ Callback().SHARP_CALLBACK_FN_NAME(Receiver().Value(),
149
+ { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
149
150
  }
150
151
 
151
152
  delete baton->input;