sharp 0.29.0 → 0.29.1

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.
@@ -199,6 +199,7 @@ const Sharp = function (input, options) {
199
199
  brightness: 1,
200
200
  saturation: 1,
201
201
  hue: 0,
202
+ lightness: 0,
202
203
  booleanBufferIn: null,
203
204
  booleanFileIn: '',
204
205
  joinChannelIn: [],
@@ -232,8 +233,13 @@ const Sharp = function (input, options) {
232
233
  pngAdaptiveFiltering: false,
233
234
  pngPalette: false,
234
235
  pngQuality: 100,
235
- pngColours: 256,
236
+ pngBitdepth: 8,
236
237
  pngDither: 1,
238
+ jp2Quality: 80,
239
+ jp2TileHeight: 512,
240
+ jp2TileWidth: 512,
241
+ jp2Lossless: false,
242
+ jp2ChromaSubsampling: '4:4:4',
237
243
  webpQuality: 80,
238
244
  webpAlphaQuality: 100,
239
245
  webpLossless: false,
package/lib/operation.js CHANGED
@@ -570,14 +570,16 @@ function recomb (inputMatrix) {
570
570
  }
571
571
 
572
572
  /**
573
- * Transforms the image using brightness, saturation and hue rotation.
573
+ * Transforms the image using brightness, saturation, hue rotation, and lightness.
574
+ * Brightness and lightness both operate on luminance, with the difference being that
575
+ * brightness is multiplicative whereas lightness is additive.
574
576
  *
575
577
  * @since 0.22.1
576
578
  *
577
579
  * @example
578
580
  * sharp(input)
579
581
  * .modulate({
580
- * brightness: 2 // increase lightness by a factor of 2
582
+ * brightness: 2 // increase brightness by a factor of 2
581
583
  * });
582
584
  *
583
585
  * sharp(input)
@@ -585,6 +587,11 @@ function recomb (inputMatrix) {
585
587
  * hue: 180 // hue-rotate by 180 degrees
586
588
  * });
587
589
  *
590
+ * sharp(input)
591
+ * .modulate({
592
+ * lightness: 50 // increase lightness by +50
593
+ * });
594
+ *
588
595
  * // decreate brightness and saturation while also hue-rotating by 90 degrees
589
596
  * sharp(input)
590
597
  * .modulate({
@@ -597,6 +604,7 @@ function recomb (inputMatrix) {
597
604
  * @param {number} [options.brightness] Brightness multiplier
598
605
  * @param {number} [options.saturation] Saturation multiplier
599
606
  * @param {number} [options.hue] Degrees for hue rotation
607
+ * @param {number} [options.lightness] Lightness addend
600
608
  * @returns {Sharp}
601
609
  */
602
610
  function modulate (options) {
@@ -624,6 +632,13 @@ function modulate (options) {
624
632
  throw is.invalidParameterError('hue', 'number', options.hue);
625
633
  }
626
634
  }
635
+ if ('lightness' in options) {
636
+ if (is.number(options.lightness)) {
637
+ this.options.lightness = options.lightness;
638
+ } else {
639
+ throw is.invalidParameterError('lightness', 'number', options.lightness);
640
+ }
641
+ }
627
642
  return this;
628
643
  }
629
644
 
package/lib/output.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('path');
3
4
  const is = require('./is');
4
5
  const sharp = require('./sharp');
5
6
 
@@ -13,10 +14,15 @@ const formats = new Map([
13
14
  ['raw', 'raw'],
14
15
  ['tiff', 'tiff'],
15
16
  ['webp', 'webp'],
16
- ['gif', 'gif']
17
+ ['gif', 'gif'],
18
+ ['jp2', 'jp2'],
19
+ ['jpx', 'jp2'],
20
+ ['j2k', 'jp2'],
21
+ ['j2c', 'jp2']
17
22
  ]);
18
23
 
19
24
  const errMagickSave = new Error('GIF output requires libvips with support for ImageMagick');
25
+ const errJp2Save = new Error('JP2 output requires libvips with support for OpenJPEG');
20
26
 
21
27
  /**
22
28
  * Write output image data to a file.
@@ -28,6 +34,8 @@ const errMagickSave = new Error('GIF output requires libvips with support for Im
28
34
  * By default all metadata will be removed, which includes EXIF-based orientation.
29
35
  * See {@link withMetadata} for control over this.
30
36
  *
37
+ * The caller is responsible for ensuring directory structures and permissions exist.
38
+ *
31
39
  * A `Promise` is returned when `callback` is not provided.
32
40
  *
33
41
  * @example
@@ -52,7 +60,7 @@ function toFile (fileOut, callback) {
52
60
  let err;
53
61
  if (!is.string(fileOut)) {
54
62
  err = new Error('Missing output file path');
55
- } else if (this.options.input.file === fileOut) {
63
+ } else if (is.string(this.options.input.file) && path.resolve(this.options.input.file) === path.resolve(fileOut)) {
56
64
  err = new Error('Cannot use same file for input and output');
57
65
  } else if (this.options.formatOut === 'input' && fileOut.toLowerCase().endsWith('.gif') && !this.constructor.format.magick.output.file) {
58
66
  err = errMagickSave;
@@ -403,7 +411,7 @@ function png (options) {
403
411
  const colours = options.colours || options.colors;
404
412
  if (is.defined(colours)) {
405
413
  if (is.integer(colours) && is.inRange(colours, 2, 256)) {
406
- this.options.pngColours = colours;
414
+ this.options.pngBitdepth = 1 << 31 - Math.clz32(Math.ceil(Math.log2(colours)));
407
415
  } else {
408
416
  throw is.invalidParameterError('colours', 'integer between 2 and 256', colours);
409
417
  }
@@ -509,6 +517,84 @@ function gif (options) {
509
517
  return this._updateFormatOut('gif', options);
510
518
  }
511
519
 
520
+ /**
521
+ * Use these JP2 options for output image.
522
+ *
523
+ * Requires libvips compiled with support for OpenJPEG.
524
+ * The prebuilt binaries do not include this - see
525
+ * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}.
526
+ *
527
+ * @example
528
+ * // Convert any input to lossless JP2 output
529
+ * const data = await sharp(input)
530
+ * .jp2({ lossless: true })
531
+ * .toBuffer();
532
+ *
533
+ * @example
534
+ * // Convert any input to very high quality JP2 output
535
+ * const data = await sharp(input)
536
+ * .jp2({
537
+ * quality: 100,
538
+ * chromaSubsampling: '4:4:4'
539
+ * })
540
+ * .toBuffer();
541
+ *
542
+ * @since 0.29.1
543
+ *
544
+ * @param {Object} [options] - output options
545
+ * @param {number} [options.quality=80] - quality, integer 1-100
546
+ * @param {boolean} [options.lossless=false] - use lossless compression mode
547
+ * @param {number} [options.tileWidth=512] - horizontal tile size
548
+ * @param {number} [options.tileHeight=512] - vertical tile size
549
+ * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
550
+ * @returns {Sharp}
551
+ * @throws {Error} Invalid options
552
+ */
553
+ /* istanbul ignore next */
554
+ function jp2 (options) {
555
+ if (!this.constructor.format.jp2k.output.buffer) {
556
+ throw errJp2Save;
557
+ }
558
+ if (is.object(options)) {
559
+ if (is.defined(options.quality)) {
560
+ if (is.integer(options.quality) && is.inRange(options.quality, 1, 100)) {
561
+ this.options.jp2Quality = options.quality;
562
+ } else {
563
+ throw is.invalidParameterError('quality', 'integer between 1 and 100', options.quality);
564
+ }
565
+ }
566
+ if (is.defined(options.lossless)) {
567
+ if (is.bool(options.lossless)) {
568
+ this.options.jp2Lossless = options.lossless;
569
+ } else {
570
+ throw is.invalidParameterError('lossless', 'boolean', options.lossless);
571
+ }
572
+ }
573
+ if (is.defined(options.tileWidth)) {
574
+ if (is.integer(options.tileWidth) && is.inRange(options.tileWidth, 1, 32768)) {
575
+ this.options.jp2TileWidth = options.tileWidth;
576
+ } else {
577
+ throw is.invalidParameterError('tileWidth', 'integer between 1 and 32768', options.tileWidth);
578
+ }
579
+ }
580
+ if (is.defined(options.tileHeight)) {
581
+ if (is.integer(options.tileHeight) && is.inRange(options.tileHeight, 1, 32768)) {
582
+ this.options.jp2TileHeight = options.tileHeight;
583
+ } else {
584
+ throw is.invalidParameterError('tileHeight', 'integer between 1 and 32768', options.tileHeight);
585
+ }
586
+ }
587
+ if (is.defined(options.chromaSubsampling)) {
588
+ if (is.string(options.chromaSubsampling) && is.inArray(options.chromaSubsampling, ['4:2:0', '4:4:4'])) {
589
+ this.options.heifChromaSubsampling = options.chromaSubsampling;
590
+ } else {
591
+ throw is.invalidParameterError('chromaSubsampling', 'one of: 4:2:0, 4:4:4', options.chromaSubsampling);
592
+ }
593
+ }
594
+ }
595
+ return this._updateFormatOut('jp2', options);
596
+ }
597
+
512
598
  /**
513
599
  * Set animation options if available.
514
600
  * @private
@@ -654,12 +740,14 @@ function tiff (options) {
654
740
  * Whilst it is possible to create AVIF images smaller than 16x16 pixels,
655
741
  * most web browsers do not display these properly.
656
742
  *
743
+ * AVIF image sequences are not supported.
744
+ *
657
745
  * @since 0.27.0
658
746
  *
659
747
  * @param {Object} [options] - output options
660
748
  * @param {number} [options.quality=50] - quality, integer 1-100
661
749
  * @param {boolean} [options.lossless=false] - use lossless compression
662
- * @param {number} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest)
750
+ * @param {number} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 9 (fastest/largest)
663
751
  * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
664
752
  * @returns {Sharp}
665
753
  * @throws {Error} Invalid options
@@ -680,7 +768,7 @@ function avif (options) {
680
768
  * @param {number} [options.quality=50] - quality, integer 1-100
681
769
  * @param {string} [options.compression='av1'] - compression format: av1, hevc
682
770
  * @param {boolean} [options.lossless=false] - use lossless compression
683
- * @param {number} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 8 (fastest/largest)
771
+ * @param {number} [options.speed=5] - CPU effort vs file size, 0 (slowest/smallest) to 9 (fastest/largest)
684
772
  * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
685
773
  * @returns {Sharp}
686
774
  * @throws {Error} Invalid options
@@ -709,10 +797,10 @@ function heif (options) {
709
797
  }
710
798
  }
711
799
  if (is.defined(options.speed)) {
712
- if (is.integer(options.speed) && is.inRange(options.speed, 0, 8)) {
800
+ if (is.integer(options.speed) && is.inRange(options.speed, 0, 9)) {
713
801
  this.options.heifSpeed = options.speed;
714
802
  } else {
715
- throw is.invalidParameterError('speed', 'integer between 0 and 8', options.speed);
803
+ throw is.invalidParameterError('speed', 'integer between 0 and 9', options.speed);
716
804
  }
717
805
  }
718
806
  if (is.defined(options.chromaSubsampling)) {
@@ -1031,6 +1119,7 @@ module.exports = function (Sharp) {
1031
1119
  withMetadata,
1032
1120
  toFormat,
1033
1121
  jpeg,
1122
+ jp2,
1034
1123
  png,
1035
1124
  webp,
1036
1125
  tiff,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sharp",
3
3
  "description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images",
4
- "version": "0.29.0",
4
+ "version": "0.29.1",
5
5
  "author": "Lovell Fuller <npm@lovell.info>",
6
6
  "homepage": "https://github.com/lovell/sharp",
7
7
  "contributors": [
@@ -78,7 +78,8 @@
78
78
  "Jacob Smith <jacob@frende.me>",
79
79
  "Michael Nutt <michael@nutt.im>",
80
80
  "Brad Parham <baparham@gmail.com>",
81
- "Taneli Vatanen <taneli.vatanen@gmail.com>"
81
+ "Taneli Vatanen <taneli.vatanen@gmail.com>",
82
+ "Joris Dugué <zaruike10@gmail.com>"
82
83
  ],
83
84
  "scripts": {
84
85
  "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)",
@@ -112,6 +113,7 @@
112
113
  "tiff",
113
114
  "gif",
114
115
  "svg",
116
+ "jp2",
115
117
  "dzi",
116
118
  "image",
117
119
  "resize",
@@ -124,7 +126,7 @@
124
126
  "dependencies": {
125
127
  "color": "^4.0.1",
126
128
  "detect-libc": "^1.0.3",
127
- "node-addon-api": "^4.0.0",
129
+ "node-addon-api": "^4.1.0",
128
130
  "prebuild-install": "^6.1.4",
129
131
  "semver": "^7.3.5",
130
132
  "simple-get": "^3.1.0",
@@ -139,7 +141,7 @@
139
141
  "exif-reader": "^1.0.3",
140
142
  "icc": "^2.0.0",
141
143
  "license-checker": "^25.0.1",
142
- "mocha": "^9.0.3",
144
+ "mocha": "^9.1.1",
143
145
  "mock-fs": "^5.0.0",
144
146
  "nyc": "^15.1.0",
145
147
  "prebuild": "^10.0.1",
package/src/common.cc CHANGED
@@ -157,6 +157,10 @@ namespace sharp {
157
157
  bool IsGif(std::string const &str) {
158
158
  return EndsWith(str, ".gif") || EndsWith(str, ".GIF");
159
159
  }
160
+ bool IsJp2(std::string const &str) {
161
+ return EndsWith(str, ".jp2") || EndsWith(str, ".jpx") || EndsWith(str, ".j2k") || EndsWith(str, ".j2c")
162
+ || EndsWith(str, ".JP2") || EndsWith(str, ".JPX") || EndsWith(str, ".J2K") || EndsWith(str, ".J2C");
163
+ }
160
164
  bool IsTiff(std::string const &str) {
161
165
  return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF");
162
166
  }
@@ -190,6 +194,7 @@ namespace sharp {
190
194
  case ImageType::WEBP: id = "webp"; break;
191
195
  case ImageType::TIFF: id = "tiff"; break;
192
196
  case ImageType::GIF: id = "gif"; break;
197
+ case ImageType::JP2: id = "jp2"; break;
193
198
  case ImageType::SVG: id = "svg"; break;
194
199
  case ImageType::HEIF: id = "heif"; break;
195
200
  case ImageType::PDF: id = "pdf"; break;
@@ -226,6 +231,8 @@ namespace sharp {
226
231
  { "VipsForeignLoadGifBuffer", ImageType::GIF },
227
232
  { "VipsForeignLoadNsgifFile", ImageType::GIF },
228
233
  { "VipsForeignLoadNsgifBuffer", ImageType::GIF },
234
+ { "VipsForeignLoadJp2kBuffer", ImageType::JP2 },
235
+ { "VipsForeignLoadJp2kFile", ImageType::JP2 },
229
236
  { "VipsForeignLoadSvgFile", ImageType::SVG },
230
237
  { "VipsForeignLoadSvgBuffer", ImageType::SVG },
231
238
  { "VipsForeignLoadHeifFile", ImageType::HEIF },
@@ -234,6 +241,8 @@ namespace sharp {
234
241
  { "VipsForeignLoadPdfBuffer", ImageType::PDF },
235
242
  { "VipsForeignLoadMagickFile", ImageType::MAGICK },
236
243
  { "VipsForeignLoadMagickBuffer", ImageType::MAGICK },
244
+ { "VipsForeignLoadMagick7File", ImageType::MAGICK },
245
+ { "VipsForeignLoadMagick7Buffer", ImageType::MAGICK },
237
246
  { "VipsForeignLoadOpenslide", ImageType::OPENSLIDE },
238
247
  { "VipsForeignLoadPpmFile", ImageType::PPM },
239
248
  { "VipsForeignLoadFits", ImageType::FITS },
@@ -285,6 +294,7 @@ namespace sharp {
285
294
  imageType == ImageType::WEBP ||
286
295
  imageType == ImageType::MAGICK ||
287
296
  imageType == ImageType::GIF ||
297
+ imageType == ImageType::JP2 ||
288
298
  imageType == ImageType::TIFF ||
289
299
  imageType == ImageType::HEIF ||
290
300
  imageType == ImageType::PDF;
@@ -508,6 +518,17 @@ namespace sharp {
508
518
  return copy;
509
519
  }
510
520
 
521
+ /*
522
+ Remove animation properties from image.
523
+ */
524
+ VImage RemoveAnimationProperties(VImage image) {
525
+ VImage copy = image.copy();
526
+ copy.remove(VIPS_META_PAGE_HEIGHT);
527
+ copy.remove("delay");
528
+ copy.remove("loop");
529
+ return copy;
530
+ }
531
+
511
532
  /*
512
533
  Does this image have a non-default density?
513
534
  */
@@ -757,23 +778,27 @@ namespace sharp {
757
778
  /*
758
779
  Convert RGBA value to another colourspace
759
780
  */
760
- std::vector<double> GetRgbaAsColourspace(std::vector<double> const rgba, VipsInterpretation const interpretation) {
781
+ std::vector<double> GetRgbaAsColourspace(std::vector<double> const rgba,
782
+ VipsInterpretation const interpretation, bool premultiply) {
761
783
  int const bands = static_cast<int>(rgba.size());
762
- if (bands < 3 || interpretation == VIPS_INTERPRETATION_sRGB || interpretation == VIPS_INTERPRETATION_RGB) {
784
+ if (bands < 3) {
763
785
  return rgba;
764
- } else {
765
- VImage pixel = VImage::new_matrix(1, 1);
766
- pixel.set("bands", bands);
767
- pixel = pixel.new_from_image(rgba);
768
- pixel = pixel.colourspace(interpretation, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB));
769
- return pixel(0, 0);
770
786
  }
787
+ VImage pixel = VImage::new_matrix(1, 1);
788
+ pixel.set("bands", bands);
789
+ pixel = pixel
790
+ .new_from_image(rgba)
791
+ .colourspace(interpretation, VImage::option()->set("source_space", VIPS_INTERPRETATION_sRGB));
792
+ if (premultiply) {
793
+ pixel = pixel.premultiply();
794
+ }
795
+ return pixel(0, 0);
771
796
  }
772
797
 
773
798
  /*
774
799
  Apply the alpha channel to a given colour
775
800
  */
776
- std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour) {
801
+ std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour, bool premultiply) {
777
802
  // Scale up 8-bit values to match 16-bit input image
778
803
  double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0;
779
804
  // Create alphaColour colour
@@ -797,7 +822,7 @@ namespace sharp {
797
822
  alphaColour.push_back(colour[3] * multiplier);
798
823
  }
799
824
  // Ensure alphaColour colour uses correct colourspace
800
- alphaColour = sharp::GetRgbaAsColourspace(alphaColour, image.interpretation());
825
+ alphaColour = sharp::GetRgbaAsColourspace(alphaColour, image.interpretation(), premultiply);
801
826
  // Add non-transparent alpha channel, if required
802
827
  if (colour[3] < 255.0 && !HasAlpha(image)) {
803
828
  image = image.bandjoin(
@@ -827,4 +852,5 @@ namespace sharp {
827
852
  }
828
853
  return image;
829
854
  }
855
+
830
856
  } // namespace sharp
package/src/common.h CHANGED
@@ -116,6 +116,7 @@ namespace sharp {
116
116
  JPEG,
117
117
  PNG,
118
118
  WEBP,
119
+ JP2,
119
120
  TIFF,
120
121
  GIF,
121
122
  SVG,
@@ -142,6 +143,7 @@ namespace sharp {
142
143
  bool IsJpeg(std::string const &str);
143
144
  bool IsPng(std::string const &str);
144
145
  bool IsWebp(std::string const &str);
146
+ bool IsJp2(std::string const &str);
145
147
  bool IsGif(std::string const &str);
146
148
  bool IsTiff(std::string const &str);
147
149
  bool IsHeic(std::string const &str);
@@ -208,6 +210,11 @@ namespace sharp {
208
210
  */
209
211
  VImage SetAnimationProperties(VImage image, int pageHeight, std::vector<int> delay, int loop);
210
212
 
213
+ /*
214
+ Remove animation properties from image.
215
+ */
216
+ VImage RemoveAnimationProperties(VImage image);
217
+
211
218
  /*
212
219
  Does this image have a non-default density?
213
220
  */
@@ -288,12 +295,13 @@ namespace sharp {
288
295
  /*
289
296
  Convert RGBA value to another colourspace
290
297
  */
291
- std::vector<double> GetRgbaAsColourspace(std::vector<double> const rgba, VipsInterpretation const interpretation);
298
+ std::vector<double> GetRgbaAsColourspace(std::vector<double> const rgba,
299
+ VipsInterpretation const interpretation, bool premultiply);
292
300
 
293
301
  /*
294
302
  Apply the alpha channel to a given colour
295
303
  */
296
- std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour);
304
+ std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour, bool premultiply);
297
305
 
298
306
  /*
299
307
  Removes alpha channel, if any.
package/src/operations.cc CHANGED
@@ -182,7 +182,8 @@ namespace sharp {
182
182
  0.0, 0.0, 0.0, 1.0));
183
183
  }
184
184
 
185
- 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) {
186
187
  if (HasAlpha(image)) {
187
188
  // Separate alpha channel
188
189
  VImage alpha = image[image.bands() - 1];
@@ -190,7 +191,7 @@ namespace sharp {
190
191
  .colourspace(VIPS_INTERPRETATION_LCH)
191
192
  .linear(
192
193
  { brightness, saturation, 1},
193
- { 0.0, 0.0, static_cast<double>(hue) }
194
+ { lightness, 0.0, static_cast<double>(hue) }
194
195
  )
195
196
  .colourspace(VIPS_INTERPRETATION_sRGB)
196
197
  .bandjoin(alpha);
@@ -199,7 +200,7 @@ namespace sharp {
199
200
  .colourspace(VIPS_INTERPRETATION_LCH)
200
201
  .linear(
201
202
  { brightness, saturation, 1 },
202
- { 0.0, 0.0, static_cast<double>(hue) }
203
+ { lightness, 0.0, static_cast<double>(hue) }
203
204
  )
204
205
  .colourspace(VIPS_INTERPRETATION_sRGB);
205
206
  }
package/src/operations.h CHANGED
@@ -98,9 +98,10 @@ namespace sharp {
98
98
  VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix);
99
99
 
100
100
  /*
101
- * Modulate brightness, saturation and hue
101
+ * Modulate brightness, saturation, hue and lightness
102
102
  */
103
- VImage Modulate(VImage image, double const brightness, double const saturation, int const hue);
103
+ VImage Modulate(VImage image, double const brightness, double const saturation,
104
+ int const hue, double const lightness);
104
105
 
105
106
  /*
106
107
  * Ensure the image is in a given colourspace
package/src/pipeline.cc CHANGED
@@ -90,7 +90,7 @@ class PipelineWorker : public Napi::AsyncWorker {
90
90
  }
91
91
  if (baton->rotationAngle != 0.0) {
92
92
  std::vector<double> background;
93
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
93
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, FALSE);
94
94
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
95
95
  }
96
96
  }
@@ -291,14 +291,15 @@ class PipelineWorker : public Napi::AsyncWorker {
291
291
  }
292
292
 
293
293
  // Ensure we're using a device-independent colour space
294
+ char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb";
294
295
  if (
295
296
  sharp::HasProfile(image) &&
296
297
  image.interpretation() != VIPS_INTERPRETATION_LABS &&
297
298
  image.interpretation() != VIPS_INTERPRETATION_GREY16
298
299
  ) {
299
- // Convert to sRGB using embedded profile
300
+ // Convert to sRGB/P3 using embedded profile
300
301
  try {
301
- image = image.icc_transform("srgb", VImage::option()
302
+ image = image.icc_transform(processingProfile, VImage::option()
302
303
  ->set("embedded", TRUE)
303
304
  ->set("depth", image.interpretation() == VIPS_INTERPRETATION_RGB16 ? 16 : 8)
304
305
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
@@ -306,7 +307,7 @@ class PipelineWorker : public Napi::AsyncWorker {
306
307
  // Ignore failure of embedded profile
307
308
  }
308
309
  } else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
309
- image = image.icc_transform("srgb", VImage::option()
310
+ image = image.icc_transform(processingProfile, VImage::option()
310
311
  ->set("input_profile", "cmyk")
311
312
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
312
313
  }
@@ -346,7 +347,8 @@ class PipelineWorker : public Napi::AsyncWorker {
346
347
  bool const shouldSharpen = baton->sharpenSigma != 0.0;
347
348
  bool const shouldApplyMedian = baton->medianSize > 0;
348
349
  bool const shouldComposite = !baton->composite.empty();
349
- bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0;
350
+ bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 ||
351
+ baton->hue != 0.0 || baton->lightness != 0.0;
350
352
  bool const shouldApplyClahe = baton->claheWidth != 0 && baton->claheHeight != 0;
351
353
 
352
354
  if (shouldComposite && !sharp::HasAlpha(image)) {
@@ -423,7 +425,7 @@ class PipelineWorker : public Napi::AsyncWorker {
423
425
  if (image.width() != baton->width || image.height() != baton->height) {
424
426
  if (baton->canvas == Canvas::EMBED) {
425
427
  std::vector<double> background;
426
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground);
428
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
427
429
 
428
430
  // Embed
429
431
 
@@ -480,7 +482,7 @@ class PipelineWorker : public Napi::AsyncWorker {
480
482
  // Rotate post-extract non-90 angle
481
483
  if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
482
484
  std::vector<double> background;
483
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
485
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
484
486
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
485
487
  }
486
488
 
@@ -493,7 +495,7 @@ class PipelineWorker : public Napi::AsyncWorker {
493
495
  // Affine transform
494
496
  if (baton->affineMatrix.size() > 0) {
495
497
  std::vector<double> background;
496
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground);
498
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
497
499
  image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
498
500
  ->set("idx", baton->affineIdx)
499
501
  ->set("idy", baton->affineIdy)
@@ -505,7 +507,7 @@ class PipelineWorker : public Napi::AsyncWorker {
505
507
  // Extend edges
506
508
  if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
507
509
  std::vector<double> background;
508
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground);
510
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
509
511
 
510
512
  // Embed
511
513
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
@@ -542,7 +544,7 @@ class PipelineWorker : public Napi::AsyncWorker {
542
544
  }
543
545
 
544
546
  if (shouldModulate) {
545
- image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue);
547
+ image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
546
548
  }
547
549
 
548
550
  // Sharpen
@@ -715,9 +717,10 @@ class PipelineWorker : public Napi::AsyncWorker {
715
717
  // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess
716
718
  image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
717
719
  // Transform colours from embedded profile to output profile
718
- if (baton->withMetadata && sharp::HasProfile(image)) {
719
- image = image.icc_transform(vips_enum_nick(VIPS_TYPE_INTERPRETATION, baton->colourspace),
720
- VImage::option()->set("embedded", TRUE));
720
+ if (baton->withMetadata && sharp::HasProfile(image) && baton->withMetadataIcc.empty()) {
721
+ image = image.icc_transform("srgb", VImage::option()
722
+ ->set("embedded", TRUE)
723
+ ->set("intent", VIPS_INTENT_PERCEPTUAL));
721
724
  }
722
725
  }
723
726
 
@@ -726,7 +729,8 @@ class PipelineWorker : public Napi::AsyncWorker {
726
729
  image = image.icc_transform(
727
730
  const_cast<char*>(baton->withMetadataIcc.data()),
728
731
  VImage::option()
729
- ->set("input_profile", "srgb")
732
+ ->set("input_profile", processingProfile)
733
+ ->set("embedded", TRUE)
730
734
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
731
735
  }
732
736
  // Override EXIF Orientation tag
@@ -787,6 +791,22 @@ class PipelineWorker : public Napi::AsyncWorker {
787
791
  } else {
788
792
  baton->channels = std::min(baton->channels, 3);
789
793
  }
794
+ } else if (baton->formatOut == "jp2" || (baton->formatOut == "input"
795
+ && inputImageType == sharp::ImageType::JP2)) {
796
+ // Write JP2 to Buffer
797
+ sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
798
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.jp2ksave_buffer(VImage::option()
799
+ ->set("Q", baton->jp2Quality)
800
+ ->set("lossless", baton->jp2Lossless)
801
+ ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
802
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
803
+ ->set("tile_height", baton->jp2TileHeight)
804
+ ->set("tile_width", baton->jp2TileWidth)));
805
+ baton->bufferOut = static_cast<char*>(area->data);
806
+ baton->bufferOutLength = area->length;
807
+ area->free_fn = nullptr;
808
+ vips_area_unref(area);
809
+ baton->formatOut = "jp2";
790
810
  } else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
791
811
  (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
792
812
  inputImageType == sharp::ImageType::SVG))) {
@@ -799,7 +819,7 @@ class PipelineWorker : public Napi::AsyncWorker {
799
819
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
800
820
  ->set("palette", baton->pngPalette)
801
821
  ->set("Q", baton->pngQuality)
802
- ->set("colours", baton->pngColours)
822
+ ->set("bitdepth", baton->pngBitdepth)
803
823
  ->set("dither", baton->pngDither)));
804
824
  baton->bufferOut = static_cast<char*>(area->data);
805
825
  baton->bufferOutLength = area->length;
@@ -868,6 +888,7 @@ class PipelineWorker : public Napi::AsyncWorker {
868
888
  } else if (baton->formatOut == "heif" ||
869
889
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::HEIF)) {
870
890
  // Write HEIF to buffer
891
+ image = sharp::RemoveAnimationProperties(image);
871
892
  VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option()
872
893
  ->set("strip", !baton->withMetadata)
873
894
  ->set("Q", baton->heifQuality)
@@ -917,13 +938,14 @@ class PipelineWorker : public Napi::AsyncWorker {
917
938
  bool const isWebp = sharp::IsWebp(baton->fileOut);
918
939
  bool const isGif = sharp::IsGif(baton->fileOut);
919
940
  bool const isTiff = sharp::IsTiff(baton->fileOut);
941
+ bool const isJp2 = sharp::IsJp2(baton->fileOut);
920
942
  bool const isHeif = sharp::IsHeif(baton->fileOut);
921
943
  bool const isDz = sharp::IsDz(baton->fileOut);
922
944
  bool const isDzZip = sharp::IsDzZip(baton->fileOut);
923
945
  bool const isV = sharp::IsV(baton->fileOut);
924
946
  bool const mightMatchInput = baton->formatOut == "input";
925
947
  bool const willMatchInput = mightMatchInput &&
926
- !(isJpeg || isPng || isWebp || isGif || isTiff || isHeif || isDz || isDzZip || isV);
948
+ !(isJpeg || isPng || isWebp || isGif || isTiff || isJp2 || isHeif || isDz || isDzZip || isV);
927
949
 
928
950
  if (baton->formatOut == "jpeg" || (mightMatchInput && isJpeg) ||
929
951
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
@@ -943,6 +965,18 @@ class PipelineWorker : public Napi::AsyncWorker {
943
965
  ->set("optimize_coding", baton->jpegOptimiseCoding));
944
966
  baton->formatOut = "jpeg";
945
967
  baton->channels = std::min(baton->channels, 3);
968
+ } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
969
+ (willMatchInput && (inputImageType == sharp::ImageType::JP2))) {
970
+ // Write JP2 to file
971
+ sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
972
+ image.jp2ksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
973
+ ->set("Q", baton->jp2Quality)
974
+ ->set("lossless", baton->jp2Lossless)
975
+ ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
976
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
977
+ ->set("tile_height", baton->jp2TileHeight)
978
+ ->set("tile_width", baton->jp2TileWidth));
979
+ baton->formatOut = "jp2";
946
980
  } else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
947
981
  (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
948
982
  inputImageType == sharp::ImageType::SVG))) {
@@ -955,7 +989,7 @@ class PipelineWorker : public Napi::AsyncWorker {
955
989
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
956
990
  ->set("palette", baton->pngPalette)
957
991
  ->set("Q", baton->pngQuality)
958
- ->set("colours", baton->pngColours)
992
+ ->set("bitdepth", baton->pngBitdepth)
959
993
  ->set("dither", baton->pngDither));
960
994
  baton->formatOut = "png";
961
995
  } else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) ||
@@ -1008,6 +1042,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1008
1042
  } else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
1009
1043
  (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
1010
1044
  // Write HEIF to file
1045
+ image = sharp::RemoveAnimationProperties(image);
1011
1046
  image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1012
1047
  ->set("strip", !baton->withMetadata)
1013
1048
  ->set("Q", baton->heifQuality)
@@ -1328,6 +1363,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1328
1363
  baton->brightness = sharp::AttrAsDouble(options, "brightness");
1329
1364
  baton->saturation = sharp::AttrAsDouble(options, "saturation");
1330
1365
  baton->hue = sharp::AttrAsInt32(options, "hue");
1366
+ baton->lightness = sharp::AttrAsDouble(options, "lightness");
1331
1367
  baton->medianSize = sharp::AttrAsUint32(options, "medianSize");
1332
1368
  baton->sharpenSigma = sharp::AttrAsDouble(options, "sharpenSigma");
1333
1369
  baton->sharpenFlat = sharp::AttrAsDouble(options, "sharpenFlat");
@@ -1429,8 +1465,13 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1429
1465
  baton->pngAdaptiveFiltering = sharp::AttrAsBool(options, "pngAdaptiveFiltering");
1430
1466
  baton->pngPalette = sharp::AttrAsBool(options, "pngPalette");
1431
1467
  baton->pngQuality = sharp::AttrAsUint32(options, "pngQuality");
1432
- baton->pngColours = sharp::AttrAsUint32(options, "pngColours");
1468
+ baton->pngBitdepth = sharp::AttrAsUint32(options, "pngBitdepth");
1433
1469
  baton->pngDither = sharp::AttrAsDouble(options, "pngDither");
1470
+ baton->jp2Quality = sharp::AttrAsUint32(options, "jp2Quality");
1471
+ baton->jp2Lossless = sharp::AttrAsBool(options, "jp2Lossless");
1472
+ baton->jp2TileHeight = sharp::AttrAsUint32(options, "jp2TileHeight");
1473
+ baton->jp2TileWidth = sharp::AttrAsUint32(options, "jp2TileWidth");
1474
+ baton->jp2ChromaSubsampling = sharp::AttrAsStr(options, "jp2ChromaSubsampling");
1434
1475
  baton->webpQuality = sharp::AttrAsUint32(options, "webpQuality");
1435
1476
  baton->webpAlphaQuality = sharp::AttrAsUint32(options, "webpAlphaQuality");
1436
1477
  baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
package/src/pipeline.h CHANGED
@@ -95,6 +95,7 @@ struct PipelineBaton {
95
95
  double brightness;
96
96
  double saturation;
97
97
  int hue;
98
+ double lightness;
98
99
  int medianSize;
99
100
  double sharpenSigma;
100
101
  double sharpenFlat;
@@ -146,8 +147,13 @@ struct PipelineBaton {
146
147
  bool pngAdaptiveFiltering;
147
148
  bool pngPalette;
148
149
  int pngQuality;
149
- int pngColours;
150
+ int pngBitdepth;
150
151
  double pngDither;
152
+ int jp2Quality;
153
+ bool jp2Lossless;
154
+ int jp2TileHeight;
155
+ int jp2TileWidth;
156
+ std::string jp2ChromaSubsampling;
151
157
  int webpQuality;
152
158
  int webpAlphaQuality;
153
159
  bool webpNearLossless;
@@ -227,6 +233,7 @@ struct PipelineBaton {
227
233
  brightness(1.0),
228
234
  saturation(1.0),
229
235
  hue(0),
236
+ lightness(0),
230
237
  medianSize(0),
231
238
  sharpenSigma(0.0),
232
239
  sharpenFlat(1.0),
@@ -276,8 +283,13 @@ struct PipelineBaton {
276
283
  pngAdaptiveFiltering(false),
277
284
  pngPalette(false),
278
285
  pngQuality(100),
279
- pngColours(256),
286
+ pngBitdepth(8),
280
287
  pngDither(1.0),
288
+ jp2Quality(80),
289
+ jp2Lossless(false),
290
+ jp2TileHeight(512),
291
+ jp2TileWidth(512),
292
+ jp2ChromaSubsampling("4:4:4"),
281
293
  webpQuality(80),
282
294
  webpAlphaQuality(100),
283
295
  webpNearLossless(false),
package/src/utilities.cc CHANGED
@@ -115,7 +115,7 @@ Napi::Value format(const Napi::CallbackInfo& info) {
115
115
  Napi::Object format = Napi::Object::New(env);
116
116
  for (std::string const f : {
117
117
  "jpeg", "png", "webp", "tiff", "magick", "openslide", "dz",
118
- "ppm", "fits", "gif", "svg", "heif", "pdf", "vips"
118
+ "ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k"
119
119
  }) {
120
120
  // Input
121
121
  Napi::Boolean hasInputFile =