sharp 0.29.0 → 0.30.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/pipeline.cc CHANGED
@@ -69,15 +69,24 @@ class PipelineWorker : public Napi::AsyncWorker {
69
69
  std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
70
70
  image = sharp::EnsureColourspace(image, baton->colourspaceInput);
71
71
 
72
+ int nPages = baton->input->pages;
73
+ if (nPages == -1) {
74
+ // Resolve the number of pages if we need to render until the end of the document
75
+ nPages = image.get_typeof(VIPS_META_N_PAGES) != 0
76
+ ? image.get_int(VIPS_META_N_PAGES) - baton->input->page
77
+ : 1;
78
+ }
79
+
80
+ // Get pre-resize page height
81
+ int pageHeight = sharp::GetPageHeight(image);
82
+
72
83
  // Calculate angle of rotation
73
84
  VipsAngle rotation;
85
+ bool flip = FALSE;
86
+ bool flop = FALSE;
74
87
  if (baton->useExifOrientation) {
75
88
  // Rotate and flip image according to Exif orientation
76
- bool flip;
77
- bool flop;
78
89
  std::tie(rotation, flip, flop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
79
- baton->flip = baton->flip || flip;
80
- baton->flop = baton->flop || flop;
81
90
  } else {
82
91
  rotation = CalculateAngleRotation(baton->angle);
83
92
  }
@@ -86,17 +95,27 @@ class PipelineWorker : public Napi::AsyncWorker {
86
95
  if (baton->rotateBeforePreExtract) {
87
96
  if (rotation != VIPS_ANGLE_D0) {
88
97
  image = image.rot(rotation);
98
+ if (flip) {
99
+ image = image.flip(VIPS_DIRECTION_VERTICAL);
100
+ flip = FALSE;
101
+ }
102
+ if (flop) {
103
+ image = image.flip(VIPS_DIRECTION_HORIZONTAL);
104
+ flop = FALSE;
105
+ }
89
106
  image = sharp::RemoveExifOrientation(image);
90
107
  }
91
108
  if (baton->rotationAngle != 0.0) {
109
+ MultiPageUnsupported(nPages, "Rotate");
92
110
  std::vector<double> background;
93
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
111
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, FALSE);
94
112
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
95
113
  }
96
114
  }
97
115
 
98
116
  // Trim
99
117
  if (baton->trimThreshold > 0.0) {
118
+ MultiPageUnsupported(nPages, "Trim");
100
119
  image = sharp::Trim(image, baton->trimThreshold);
101
120
  baton->trimOffsetLeft = image.xoffset();
102
121
  baton->trimOffsetTop = image.yoffset();
@@ -104,201 +123,194 @@ class PipelineWorker : public Napi::AsyncWorker {
104
123
 
105
124
  // Pre extraction
106
125
  if (baton->topOffsetPre != -1) {
107
- image = image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre);
126
+ image = nPages > 1
127
+ ? sharp::CropMultiPage(image,
128
+ baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre, nPages, &pageHeight)
129
+ : image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre);
108
130
  }
109
131
 
110
132
  // Get pre-resize image width and height
111
133
  int inputWidth = image.width();
112
134
  int inputHeight = image.height();
113
- if (!baton->rotateBeforePreExtract &&
114
- (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) {
115
- // Swap input output width and height when rotating by 90 or 270 degrees
116
- std::swap(inputWidth, inputHeight);
135
+
136
+ // Is there just one page? Shrink to inputHeight instead
137
+ if (nPages == 1) {
138
+ pageHeight = inputHeight;
117
139
  }
118
140
 
119
- // If withoutEnlargement is specified,
120
- // Override target width and height if exceeds respective value from input file
121
- if (baton->withoutEnlargement) {
122
- if (baton->width > inputWidth) {
123
- baton->width = inputWidth;
124
- }
125
- if (baton->height > inputHeight) {
126
- baton->height = inputHeight;
127
- }
141
+ // If withoutReduction is specified,
142
+ // Override target width and height if less than respective value from input file
143
+ if (baton->withoutReduction) {
144
+ if (baton->width < inputWidth) {
145
+ baton->width = inputWidth;
146
+ }
147
+ if (baton->height < inputHeight) {
148
+ baton->height = inputHeight;
149
+ }
128
150
  }
129
151
 
130
152
  // Scaling calculations
131
- double xfactor = 1.0;
132
- double yfactor = 1.0;
153
+ double hshrink;
154
+ double vshrink;
133
155
  int targetResizeWidth = baton->width;
134
156
  int targetResizeHeight = baton->height;
135
- if (baton->width > 0 && baton->height > 0) {
136
- // Fixed width and height
137
- xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
138
- yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
139
- switch (baton->canvas) {
140
- case Canvas::CROP:
141
- if (xfactor < yfactor) {
142
- targetResizeHeight = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
143
- yfactor = xfactor;
144
- } else {
145
- targetResizeWidth = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
146
- xfactor = yfactor;
147
- }
148
- break;
149
- case Canvas::EMBED:
150
- if (xfactor > yfactor) {
151
- targetResizeHeight = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
152
- yfactor = xfactor;
153
- } else {
154
- targetResizeWidth = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
155
- xfactor = yfactor;
156
- }
157
- break;
158
- case Canvas::MAX:
159
- if (xfactor > yfactor) {
160
- targetResizeHeight = baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
161
- yfactor = xfactor;
162
- } else {
163
- targetResizeWidth = baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
164
- xfactor = yfactor;
165
- }
166
- break;
167
- case Canvas::MIN:
168
- if (xfactor < yfactor) {
169
- targetResizeHeight = baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
170
- yfactor = xfactor;
171
- } else {
172
- targetResizeWidth = baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
173
- xfactor = yfactor;
174
- }
175
- break;
176
- case Canvas::IGNORE_ASPECT:
177
- if (!baton->rotateBeforePreExtract &&
178
- (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) {
179
- std::swap(xfactor, yfactor);
180
- }
181
- break;
182
- }
183
- } else if (baton->width > 0) {
184
- // Fixed width
185
- xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
186
- if (baton->canvas == Canvas::IGNORE_ASPECT) {
187
- targetResizeHeight = baton->height = inputHeight;
188
- } else {
189
- // Auto height
190
- yfactor = xfactor;
191
- targetResizeHeight = baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / yfactor));
192
- }
193
- } else if (baton->height > 0) {
194
- // Fixed height
195
- yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
196
- if (baton->canvas == Canvas::IGNORE_ASPECT) {
197
- targetResizeWidth = baton->width = inputWidth;
198
- } else {
199
- // Auto width
200
- xfactor = yfactor;
201
- targetResizeWidth = baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / xfactor));
202
- }
203
- } else {
204
- // Identity transform
205
- baton->width = inputWidth;
206
- baton->height = inputHeight;
207
- }
208
157
 
209
- // Calculate integral box shrink
210
- int xshrink = std::max(1, static_cast<int>(floor(xfactor)));
211
- int yshrink = std::max(1, static_cast<int>(floor(yfactor)));
158
+ // Swap input output width and height when rotating by 90 or 270 degrees
159
+ bool swap = !baton->rotateBeforePreExtract && (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270);
212
160
 
213
- // Calculate residual float affine transformation
214
- double xresidual = static_cast<double>(xshrink) / xfactor;
215
- double yresidual = static_cast<double>(yshrink) / yfactor;
161
+ // Shrink to pageHeight, so we work for multi-page images
162
+ std::tie(hshrink, vshrink) = sharp::ResolveShrink(
163
+ inputWidth, pageHeight, targetResizeWidth, targetResizeHeight,
164
+ baton->canvas, swap, baton->withoutEnlargement);
216
165
 
217
- // If integral x and y shrink are equal, try to use shrink-on-load for JPEG and WebP,
218
- // but not when applying gamma correction, pre-resize extract, trim or input colourspace
219
- int shrink_on_load = 1;
166
+ // The jpeg preload shrink.
167
+ int jpegShrinkOnLoad = 1;
220
168
 
221
- int shrink_on_load_factor = 1;
222
- // Leave at least a factor of two for the final resize step, when fastShrinkOnLoad: false
223
- // for more consistent results and avoid occasional small image shifting
224
- if (!baton->fastShrinkOnLoad) {
225
- shrink_on_load_factor = 2;
226
- }
227
- if (
228
- xshrink == yshrink && xshrink >= 2 * shrink_on_load_factor &&
229
- (inputImageType == sharp::ImageType::JPEG || inputImageType == sharp::ImageType::WEBP) &&
169
+ // WebP, PDF, SVG scale
170
+ double scale = 1.0;
171
+
172
+ // Try to reload input using shrink-on-load for JPEG, WebP, SVG and PDF, when:
173
+ // - the width or height parameters are specified;
174
+ // - gamma correction doesn't need to be applied;
175
+ // - trimming or pre-resize extract isn't required;
176
+ // - input colourspace is not specified;
177
+ bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
230
178
  baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0 &&
231
- baton->colourspaceInput == VIPS_INTERPRETATION_LAST &&
232
- image.width() > 3 && image.height() > 3 && baton->input->pages == 1
233
- ) {
234
- if (xshrink >= 8 * shrink_on_load_factor) {
235
- xfactor = xfactor / 8;
236
- yfactor = yfactor / 8;
237
- shrink_on_load = 8;
238
- } else if (xshrink >= 4 * shrink_on_load_factor) {
239
- xfactor = xfactor / 4;
240
- yfactor = yfactor / 4;
241
- shrink_on_load = 4;
242
- } else if (xshrink >= 2 * shrink_on_load_factor) {
243
- xfactor = xfactor / 2;
244
- yfactor = yfactor / 2;
245
- shrink_on_load = 2;
179
+ baton->colourspaceInput == VIPS_INTERPRETATION_LAST;
180
+
181
+ if (shouldPreShrink) {
182
+ // The common part of the shrink: the bit by which both axes must be shrunk
183
+ double shrink = std::min(hshrink, vshrink);
184
+
185
+ if (inputImageType == sharp::ImageType::JPEG) {
186
+ // Leave at least a factor of two for the final resize step, when fastShrinkOnLoad: false
187
+ // for more consistent results and avoid occasional small image shifting
188
+ int factor = baton->fastShrinkOnLoad ? 1 : 2;
189
+ if (shrink >= 8 * factor) {
190
+ jpegShrinkOnLoad = 8;
191
+ } else if (shrink >= 4 * factor) {
192
+ jpegShrinkOnLoad = 4;
193
+ } else if (shrink >= 2 * factor) {
194
+ jpegShrinkOnLoad = 2;
195
+ }
196
+ } else if (inputImageType == sharp::ImageType::WEBP ||
197
+ inputImageType == sharp::ImageType::SVG ||
198
+ inputImageType == sharp::ImageType::PDF) {
199
+ scale = 1.0 / shrink;
246
200
  }
247
201
  }
248
- // Help ensure a final kernel-based reduction to prevent shrink aliasing
249
- if (shrink_on_load > 1 && (xresidual == 1.0 || yresidual == 1.0)) {
250
- shrink_on_load = shrink_on_load / 2;
251
- xfactor = xfactor * 2;
252
- yfactor = yfactor * 2;
253
- }
254
- if (shrink_on_load > 1) {
255
- // Reload input using shrink-on-load
202
+
203
+ // Reload input using shrink-on-load, it'll be an integer shrink
204
+ // factor for jpegload*, a double scale factor for webpload*,
205
+ // pdfload* and svgload*
206
+ if (jpegShrinkOnLoad > 1) {
256
207
  vips::VOption *option = VImage::option()
257
208
  ->set("access", baton->input->access)
258
- ->set("shrink", shrink_on_load)
209
+ ->set("shrink", jpegShrinkOnLoad)
259
210
  ->set("fail", baton->input->failOnError);
260
211
  if (baton->input->buffer != nullptr) {
212
+ // Reload JPEG buffer
261
213
  VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
262
- if (inputImageType == sharp::ImageType::JPEG) {
263
- // Reload JPEG buffer
264
- image = VImage::jpegload_buffer(blob, option);
265
- } else {
266
- // Reload WebP buffer
267
- image = VImage::webpload_buffer(blob, option);
268
- }
214
+ image = VImage::jpegload_buffer(blob, option);
269
215
  vips_area_unref(reinterpret_cast<VipsArea*>(blob));
270
216
  } else {
271
- if (inputImageType == sharp::ImageType::JPEG) {
272
- // Reload JPEG file
273
- image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option);
217
+ // Reload JPEG file
218
+ image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option);
219
+ }
220
+ } else if (scale != 1.0) {
221
+ vips::VOption *option = VImage::option()
222
+ ->set("access", baton->input->access)
223
+ ->set("scale", scale)
224
+ ->set("fail", baton->input->failOnError);
225
+ if (inputImageType == sharp::ImageType::WEBP) {
226
+ option->set("n", baton->input->pages);
227
+ option->set("page", baton->input->page);
228
+
229
+ if (baton->input->buffer != nullptr) {
230
+ // Reload WebP buffer
231
+ VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
232
+ image = VImage::webpload_buffer(blob, option);
233
+ vips_area_unref(reinterpret_cast<VipsArea*>(blob));
274
234
  } else {
275
235
  // Reload WebP file
276
236
  image = VImage::webpload(const_cast<char*>(baton->input->file.data()), option);
277
237
  }
238
+ } else if (inputImageType == sharp::ImageType::SVG) {
239
+ option->set("unlimited", baton->input->unlimited);
240
+ option->set("dpi", baton->input->density);
241
+
242
+ if (baton->input->buffer != nullptr) {
243
+ // Reload SVG buffer
244
+ VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
245
+ image = VImage::svgload_buffer(blob, option);
246
+ vips_area_unref(reinterpret_cast<VipsArea*>(blob));
247
+ } else {
248
+ // Reload SVG file
249
+ image = VImage::svgload(const_cast<char*>(baton->input->file.data()), option);
250
+ }
251
+
252
+ sharp::SetDensity(image, baton->input->density);
253
+ } else if (inputImageType == sharp::ImageType::PDF) {
254
+ option->set("n", baton->input->pages);
255
+ option->set("page", baton->input->page);
256
+ option->set("dpi", baton->input->density);
257
+
258
+ if (baton->input->buffer != nullptr) {
259
+ // Reload PDF buffer
260
+ VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
261
+ image = VImage::pdfload_buffer(blob, option);
262
+ vips_area_unref(reinterpret_cast<VipsArea*>(blob));
263
+ } else {
264
+ // Reload PDF file
265
+ image = VImage::pdfload(const_cast<char*>(baton->input->file.data()), option);
266
+ }
267
+
268
+ sharp::SetDensity(image, baton->input->density);
278
269
  }
279
- // Recalculate integral shrink and double residual
280
- int const shrunkOnLoadWidth = image.width();
281
- int const shrunkOnLoadHeight = image.height();
282
- if (!baton->rotateBeforePreExtract &&
283
- (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) {
284
- // Swap when rotating by 90 or 270 degrees
285
- xfactor = static_cast<double>(shrunkOnLoadWidth) / static_cast<double>(targetResizeHeight);
286
- yfactor = static_cast<double>(shrunkOnLoadHeight) / static_cast<double>(targetResizeWidth);
287
- } else {
288
- xfactor = static_cast<double>(shrunkOnLoadWidth) / static_cast<double>(targetResizeWidth);
289
- yfactor = static_cast<double>(shrunkOnLoadHeight) / static_cast<double>(targetResizeHeight);
290
- }
270
+ }
271
+
272
+ // Any pre-shrinking may already have been done
273
+ int thumbWidth = image.width();
274
+ int thumbHeight = image.height();
275
+
276
+ // After pre-shrink, but before the main shrink stage
277
+ // Reuse the initial pageHeight if we didn't pre-shrink
278
+ int preshrunkPageHeight = shouldPreShrink ? sharp::GetPageHeight(image) : pageHeight;
279
+
280
+ if (baton->fastShrinkOnLoad && jpegShrinkOnLoad > 1) {
281
+ // JPEG shrink-on-load rounds the output dimensions down, which
282
+ // may cause incorrect dimensions when fastShrinkOnLoad is enabled
283
+ // Just recalculate vshrink / hshrink on the main image instead of
284
+ // the pre-shrunk image when this is the case
285
+ hshrink = static_cast<double>(thumbWidth) / (static_cast<double>(inputWidth) / hshrink);
286
+ vshrink = static_cast<double>(preshrunkPageHeight) / (static_cast<double>(pageHeight) / vshrink);
287
+ } else {
288
+ // Shrink to preshrunkPageHeight, so we work for multi-page images
289
+ std::tie(hshrink, vshrink) = sharp::ResolveShrink(
290
+ thumbWidth, preshrunkPageHeight, targetResizeWidth, targetResizeHeight,
291
+ baton->canvas, swap, baton->withoutEnlargement);
292
+ }
293
+
294
+ int targetHeight = static_cast<int>(std::rint(static_cast<double>(preshrunkPageHeight) / vshrink));
295
+ int targetPageHeight = targetHeight;
296
+
297
+ // In toilet-roll mode, we must adjust vshrink so that we exactly hit
298
+ // preshrunkPageHeight or we'll have pixels straddling pixel boundaries
299
+ if (thumbHeight > preshrunkPageHeight) {
300
+ targetHeight *= nPages;
301
+ vshrink = static_cast<double>(thumbHeight) / targetHeight;
291
302
  }
292
303
 
293
304
  // Ensure we're using a device-independent colour space
305
+ char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb";
294
306
  if (
295
307
  sharp::HasProfile(image) &&
296
308
  image.interpretation() != VIPS_INTERPRETATION_LABS &&
297
309
  image.interpretation() != VIPS_INTERPRETATION_GREY16
298
310
  ) {
299
- // Convert to sRGB using embedded profile
311
+ // Convert to sRGB/P3 using embedded profile
300
312
  try {
301
- image = image.icc_transform("srgb", VImage::option()
313
+ image = image.icc_transform(processingProfile, VImage::option()
302
314
  ->set("embedded", TRUE)
303
315
  ->set("depth", image.interpretation() == VIPS_INTERPRETATION_RGB16 ? 16 : 8)
304
316
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
@@ -306,7 +318,7 @@ class PipelineWorker : public Napi::AsyncWorker {
306
318
  // Ignore failure of embedded profile
307
319
  }
308
320
  } else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
309
- image = image.icc_transform("srgb", VImage::option()
321
+ image = image.icc_transform(processingProfile, VImage::option()
310
322
  ->set("input_profile", "cmyk")
311
323
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
312
324
  }
@@ -340,13 +352,14 @@ class PipelineWorker : public Napi::AsyncWorker {
340
352
  image = image.colourspace(VIPS_INTERPRETATION_B_W);
341
353
  }
342
354
 
343
- bool const shouldResize = xfactor != 1.0 || yfactor != 1.0;
355
+ bool const shouldResize = hshrink != 1.0 || vshrink != 1.0;
344
356
  bool const shouldBlur = baton->blurSigma != 0.0;
345
357
  bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
346
358
  bool const shouldSharpen = baton->sharpenSigma != 0.0;
347
359
  bool const shouldApplyMedian = baton->medianSize > 0;
348
360
  bool const shouldComposite = !baton->composite.empty();
349
- bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0;
361
+ bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 ||
362
+ baton->hue != 0.0 || baton->lightness != 0.0;
350
363
  bool const shouldApplyClahe = baton->claheWidth != 0 && baton->claheHeight != 0;
351
364
 
352
365
  if (shouldComposite && !sharp::HasAlpha(image)) {
@@ -373,35 +386,33 @@ class PipelineWorker : public Napi::AsyncWorker {
373
386
  ) {
374
387
  throw vips::VError("Unknown kernel");
375
388
  }
376
- // Ensure shortest edge is at least 1 pixel
377
- if (image.width() / xfactor < 0.5) {
378
- xfactor = 2 * image.width();
379
- baton->width = 1;
380
- }
381
- if (image.height() / yfactor < 0.5) {
382
- yfactor = 2 * image.height();
383
- baton->height = 1;
384
- }
385
- image = image.resize(1.0 / xfactor, VImage::option()
386
- ->set("vscale", 1.0 / yfactor)
389
+ image = image.resize(1.0 / hshrink, VImage::option()
390
+ ->set("vscale", 1.0 / vshrink)
387
391
  ->set("kernel", kernel));
388
392
  }
389
393
 
390
394
  // Rotate post-extract 90-angle
391
- if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
392
- image = image.rot(rotation);
393
- image = sharp::RemoveExifOrientation(image);
395
+ if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
396
+ image = image.rot(rotation);
397
+ if (flip) {
398
+ image = image.flip(VIPS_DIRECTION_VERTICAL);
399
+ flip = FALSE;
400
+ }
401
+ if (flop) {
402
+ image = image.flip(VIPS_DIRECTION_HORIZONTAL);
403
+ flop = FALSE;
404
+ }
405
+ image = sharp::RemoveExifOrientation(image);
394
406
  }
395
407
 
396
-
397
408
  // Flip (mirror about Y axis)
398
- if (baton->flip) {
409
+ if (baton->flip || flip) {
399
410
  image = image.flip(VIPS_DIRECTION_VERTICAL);
400
411
  image = sharp::RemoveExifOrientation(image);
401
412
  }
402
413
 
403
414
  // Flop (mirror about X axis)
404
- if (baton->flop) {
415
+ if (baton->flop || flop) {
405
416
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
406
417
  image = sharp::RemoveExifOrientation(image);
407
418
  }
@@ -419,52 +430,68 @@ class PipelineWorker : public Napi::AsyncWorker {
419
430
  image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
420
431
  }
421
432
 
433
+ inputWidth = image.width();
434
+ inputHeight = nPages > 1 ? targetPageHeight : image.height();
435
+
436
+ // Resolve dimensions
437
+ if (baton->width <= 0) {
438
+ baton->width = inputWidth;
439
+ }
440
+ if (baton->height <= 0) {
441
+ baton->height = inputHeight;
442
+ }
443
+
422
444
  // Crop/embed
423
- if (image.width() != baton->width || image.height() != baton->height) {
424
- if (baton->canvas == Canvas::EMBED) {
445
+ if (inputWidth != baton->width || inputHeight != baton->height) {
446
+ if (baton->canvas == sharp::Canvas::EMBED) {
425
447
  std::vector<double> background;
426
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground);
448
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
427
449
 
428
450
  // Embed
429
451
 
430
- // Calculate where to position the embeded image if gravity specified, else center.
452
+ // Calculate where to position the embedded image if gravity specified, else center.
431
453
  int left;
432
454
  int top;
433
455
 
434
- left = static_cast<int>(round((baton->width - image.width()) / 2));
435
- top = static_cast<int>(round((baton->height - image.height()) / 2));
456
+ left = static_cast<int>(round((baton->width - inputWidth) / 2));
457
+ top = static_cast<int>(round((baton->height - inputHeight) / 2));
436
458
 
437
- int width = std::max(image.width(), baton->width);
438
- int height = std::max(image.height(), baton->height);
459
+ int width = std::max(inputWidth, baton->width);
460
+ int height = std::max(inputHeight, baton->height);
439
461
  std::tie(left, top) = sharp::CalculateEmbedPosition(
440
- image.width(), image.height(), baton->width, baton->height, baton->position);
441
-
442
- image = image.embed(left, top, width, height, VImage::option()
443
- ->set("extend", VIPS_EXTEND_BACKGROUND)
444
- ->set("background", background));
462
+ inputWidth, inputHeight, baton->width, baton->height, baton->position);
463
+
464
+ image = nPages > 1
465
+ ? sharp::EmbedMultiPage(image,
466
+ left, top, width, height, background, nPages, &targetPageHeight)
467
+ : image.embed(left, top, width, height, VImage::option()
468
+ ->set("extend", VIPS_EXTEND_BACKGROUND)
469
+ ->set("background", background));
470
+ } else if (baton->canvas == sharp::Canvas::CROP) {
471
+ if (baton->width > inputWidth) {
472
+ baton->width = inputWidth;
473
+ }
474
+ if (baton->height > inputHeight) {
475
+ baton->height = inputHeight;
476
+ }
445
477
 
446
- } else if (
447
- baton->canvas != Canvas::IGNORE_ASPECT &&
448
- (image.width() > baton->width || image.height() > baton->height)
449
- ) {
450
- // Crop/max/min
478
+ // Crop
451
479
  if (baton->position < 9) {
452
480
  // Gravity-based crop
453
481
  int left;
454
482
  int top;
455
483
  std::tie(left, top) = sharp::CalculateCrop(
456
- image.width(), image.height(), baton->width, baton->height, baton->position);
457
- int width = std::min(image.width(), baton->width);
458
- int height = std::min(image.height(), baton->height);
459
- image = image.extract_area(left, top, width, height);
484
+ inputWidth, inputHeight, baton->width, baton->height, baton->position);
485
+ int width = std::min(inputWidth, baton->width);
486
+ int height = std::min(inputHeight, baton->height);
487
+
488
+ image = nPages > 1
489
+ ? sharp::CropMultiPage(image,
490
+ left, top, width, height, nPages, &targetPageHeight)
491
+ : image.extract_area(left, top, width, height);
460
492
  } else {
461
493
  // Attention-based or Entropy-based crop
462
- if (baton->width > image.width()) {
463
- baton->width = image.width();
464
- }
465
- if (baton->height > image.height()) {
466
- baton->height = image.height();
467
- }
494
+ MultiPageUnsupported(nPages, "Resize strategy");
468
495
  image = image.tilecache(VImage::option()
469
496
  ->set("access", VIPS_ACCESS_RANDOM)
470
497
  ->set("threaded", TRUE));
@@ -479,21 +506,32 @@ class PipelineWorker : public Napi::AsyncWorker {
479
506
 
480
507
  // Rotate post-extract non-90 angle
481
508
  if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
509
+ MultiPageUnsupported(nPages, "Rotate");
482
510
  std::vector<double> background;
483
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
511
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
484
512
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
485
513
  }
486
514
 
487
515
  // Post extraction
488
516
  if (baton->topOffsetPost != -1) {
489
- image = image.extract_area(
490
- baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
517
+ if (nPages > 1) {
518
+ image = sharp::CropMultiPage(image,
519
+ baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost,
520
+ nPages, &targetPageHeight);
521
+
522
+ // heightPost is used in the info object, so update to reflect the number of pages
523
+ baton->heightPost *= nPages;
524
+ } else {
525
+ image = image.extract_area(
526
+ baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
527
+ }
491
528
  }
492
529
 
493
530
  // Affine transform
494
531
  if (baton->affineMatrix.size() > 0) {
532
+ MultiPageUnsupported(nPages, "Affine");
495
533
  std::vector<double> background;
496
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground);
534
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
497
535
  image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
498
536
  ->set("idx", baton->affineIdx)
499
537
  ->set("idy", baton->affineIdy)
@@ -505,14 +543,17 @@ class PipelineWorker : public Napi::AsyncWorker {
505
543
  // Extend edges
506
544
  if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
507
545
  std::vector<double> background;
508
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground);
546
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
509
547
 
510
548
  // Embed
511
549
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
512
- baton->height = image.height() + baton->extendTop + baton->extendBottom;
550
+ baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
513
551
 
514
- image = image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
515
- VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
552
+ image = nPages > 1
553
+ ? sharp::EmbedMultiPage(image,
554
+ baton->extendLeft, baton->extendTop, baton->width, baton->height, background, nPages, &targetPageHeight)
555
+ : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
556
+ VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
516
557
  }
517
558
  // Median - must happen before blurring, due to the utility of blurring after thresholding
518
559
  if (shouldApplyMedian) {
@@ -542,7 +583,7 @@ class PipelineWorker : public Napi::AsyncWorker {
542
583
  }
543
584
 
544
585
  if (shouldModulate) {
545
- image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue);
586
+ image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
546
587
  }
547
588
 
548
589
  // Sharpen
@@ -715,9 +756,10 @@ class PipelineWorker : public Napi::AsyncWorker {
715
756
  // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess
716
757
  image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
717
758
  // 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));
759
+ if (baton->withMetadata && sharp::HasProfile(image) && baton->withMetadataIcc.empty()) {
760
+ image = image.icc_transform("srgb", VImage::option()
761
+ ->set("embedded", TRUE)
762
+ ->set("intent", VIPS_INTENT_PERCEPTUAL));
721
763
  }
722
764
  }
723
765
 
@@ -726,7 +768,8 @@ class PipelineWorker : public Napi::AsyncWorker {
726
768
  image = image.icc_transform(
727
769
  const_cast<char*>(baton->withMetadataIcc.data()),
728
770
  VImage::option()
729
- ->set("input_profile", "srgb")
771
+ ->set("input_profile", processingProfile)
772
+ ->set("embedded", TRUE)
730
773
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
731
774
  }
732
775
  // Override EXIF Orientation tag
@@ -750,16 +793,11 @@ class PipelineWorker : public Napi::AsyncWorker {
750
793
  baton->width = image.width();
751
794
  baton->height = image.height();
752
795
 
753
- bool const supportsGifOutput = vips_type_find("VipsOperation", "magicksave") != 0 &&
754
- vips_type_find("VipsOperation", "magicksave_buffer") != 0;
755
-
756
796
  image = sharp::SetAnimationProperties(
757
- image,
758
- baton->pageHeight,
759
- baton->delay,
760
- baton->loop);
797
+ image, nPages, targetPageHeight, baton->delay, baton->loop);
761
798
 
762
799
  // Output
800
+ sharp::SetTimeout(image, baton->timeoutSeconds);
763
801
  if (baton->fileOut.empty()) {
764
802
  // Buffer output
765
803
  if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
@@ -787,9 +825,24 @@ class PipelineWorker : public Napi::AsyncWorker {
787
825
  } else {
788
826
  baton->channels = std::min(baton->channels, 3);
789
827
  }
828
+ } else if (baton->formatOut == "jp2" || (baton->formatOut == "input"
829
+ && inputImageType == sharp::ImageType::JP2)) {
830
+ // Write JP2 to Buffer
831
+ sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
832
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.jp2ksave_buffer(VImage::option()
833
+ ->set("Q", baton->jp2Quality)
834
+ ->set("lossless", baton->jp2Lossless)
835
+ ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
836
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
837
+ ->set("tile_height", baton->jp2TileHeight)
838
+ ->set("tile_width", baton->jp2TileWidth)));
839
+ baton->bufferOut = static_cast<char*>(area->data);
840
+ baton->bufferOutLength = area->length;
841
+ area->free_fn = nullptr;
842
+ vips_area_unref(area);
843
+ baton->formatOut = "jp2";
790
844
  } else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
791
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
792
- inputImageType == sharp::ImageType::SVG))) {
845
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
793
846
  // Write PNG to buffer
794
847
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
795
848
  VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option()
@@ -799,7 +852,8 @@ class PipelineWorker : public Napi::AsyncWorker {
799
852
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
800
853
  ->set("palette", baton->pngPalette)
801
854
  ->set("Q", baton->pngQuality)
802
- ->set("colours", baton->pngColours)
855
+ ->set("effort", baton->pngEffort)
856
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
803
857
  ->set("dither", baton->pngDither)));
804
858
  baton->bufferOut = static_cast<char*>(area->data);
805
859
  baton->bufferOutLength = area->length;
@@ -816,7 +870,7 @@ class PipelineWorker : public Napi::AsyncWorker {
816
870
  ->set("lossless", baton->webpLossless)
817
871
  ->set("near_lossless", baton->webpNearLossless)
818
872
  ->set("smart_subsample", baton->webpSmartSubsample)
819
- ->set("reduction_effort", baton->webpReductionEffort)
873
+ ->set("effort", baton->webpEffort)
820
874
  ->set("alpha_q", baton->webpAlphaQuality)));
821
875
  baton->bufferOut = static_cast<char*>(area->data);
822
876
  baton->bufferOutLength = area->length;
@@ -824,14 +878,14 @@ class PipelineWorker : public Napi::AsyncWorker {
824
878
  vips_area_unref(area);
825
879
  baton->formatOut = "webp";
826
880
  } else if (baton->formatOut == "gif" ||
827
- (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
881
+ (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF)) {
828
882
  // Write GIF to buffer
829
883
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
830
- VipsArea *area = reinterpret_cast<VipsArea*>(image.magicksave_buffer(VImage::option()
884
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option()
831
885
  ->set("strip", !baton->withMetadata)
832
- ->set("optimize_gif_frames", TRUE)
833
- ->set("optimize_gif_transparency", TRUE)
834
- ->set("format", "gif")));
886
+ ->set("bitdepth", baton->gifBitdepth)
887
+ ->set("effort", baton->gifEffort)
888
+ ->set("dither", baton->gifDither)));
835
889
  baton->bufferOut = static_cast<char*>(area->data);
836
890
  baton->bufferOutLength = area->length;
837
891
  area->free_fn = nullptr;
@@ -859,7 +913,8 @@ class PipelineWorker : public Napi::AsyncWorker {
859
913
  ->set("tile_height", baton->tiffTileHeight)
860
914
  ->set("tile_width", baton->tiffTileWidth)
861
915
  ->set("xres", baton->tiffXres)
862
- ->set("yres", baton->tiffYres)));
916
+ ->set("yres", baton->tiffYres)
917
+ ->set("resunit", baton->tiffResolutionUnit)));
863
918
  baton->bufferOut = static_cast<char*>(area->data);
864
919
  baton->bufferOutLength = area->length;
865
920
  area->free_fn = nullptr;
@@ -868,11 +923,12 @@ class PipelineWorker : public Napi::AsyncWorker {
868
923
  } else if (baton->formatOut == "heif" ||
869
924
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::HEIF)) {
870
925
  // Write HEIF to buffer
926
+ image = sharp::RemoveAnimationProperties(image);
871
927
  VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option()
872
928
  ->set("strip", !baton->withMetadata)
873
929
  ->set("Q", baton->heifQuality)
874
930
  ->set("compression", baton->heifCompression)
875
- ->set("speed", baton->heifSpeed)
931
+ ->set("effort", baton->heifEffort)
876
932
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
877
933
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
878
934
  ->set("lossless", baton->heifLossless)));
@@ -917,13 +973,14 @@ class PipelineWorker : public Napi::AsyncWorker {
917
973
  bool const isWebp = sharp::IsWebp(baton->fileOut);
918
974
  bool const isGif = sharp::IsGif(baton->fileOut);
919
975
  bool const isTiff = sharp::IsTiff(baton->fileOut);
976
+ bool const isJp2 = sharp::IsJp2(baton->fileOut);
920
977
  bool const isHeif = sharp::IsHeif(baton->fileOut);
921
978
  bool const isDz = sharp::IsDz(baton->fileOut);
922
979
  bool const isDzZip = sharp::IsDzZip(baton->fileOut);
923
980
  bool const isV = sharp::IsV(baton->fileOut);
924
981
  bool const mightMatchInput = baton->formatOut == "input";
925
982
  bool const willMatchInput = mightMatchInput &&
926
- !(isJpeg || isPng || isWebp || isGif || isTiff || isHeif || isDz || isDzZip || isV);
983
+ !(isJpeg || isPng || isWebp || isGif || isTiff || isJp2 || isHeif || isDz || isDzZip || isV);
927
984
 
928
985
  if (baton->formatOut == "jpeg" || (mightMatchInput && isJpeg) ||
929
986
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
@@ -943,9 +1000,20 @@ class PipelineWorker : public Napi::AsyncWorker {
943
1000
  ->set("optimize_coding", baton->jpegOptimiseCoding));
944
1001
  baton->formatOut = "jpeg";
945
1002
  baton->channels = std::min(baton->channels, 3);
1003
+ } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
1004
+ (willMatchInput && (inputImageType == sharp::ImageType::JP2))) {
1005
+ // Write JP2 to file
1006
+ sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
1007
+ image.jp2ksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1008
+ ->set("Q", baton->jp2Quality)
1009
+ ->set("lossless", baton->jp2Lossless)
1010
+ ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
1011
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1012
+ ->set("tile_height", baton->jp2TileHeight)
1013
+ ->set("tile_width", baton->jp2TileWidth));
1014
+ baton->formatOut = "jp2";
946
1015
  } else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
947
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
948
- inputImageType == sharp::ImageType::SVG))) {
1016
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
949
1017
  // Write PNG to file
950
1018
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
951
1019
  image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
@@ -955,7 +1023,8 @@ class PipelineWorker : public Napi::AsyncWorker {
955
1023
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
956
1024
  ->set("palette", baton->pngPalette)
957
1025
  ->set("Q", baton->pngQuality)
958
- ->set("colours", baton->pngColours)
1026
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
1027
+ ->set("effort", baton->pngEffort)
959
1028
  ->set("dither", baton->pngDither));
960
1029
  baton->formatOut = "png";
961
1030
  } else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) ||
@@ -968,18 +1037,18 @@ class PipelineWorker : public Napi::AsyncWorker {
968
1037
  ->set("lossless", baton->webpLossless)
969
1038
  ->set("near_lossless", baton->webpNearLossless)
970
1039
  ->set("smart_subsample", baton->webpSmartSubsample)
971
- ->set("reduction_effort", baton->webpReductionEffort)
1040
+ ->set("effort", baton->webpEffort)
972
1041
  ->set("alpha_q", baton->webpAlphaQuality));
973
1042
  baton->formatOut = "webp";
974
1043
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
975
- (willMatchInput && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
1044
+ (willMatchInput && inputImageType == sharp::ImageType::GIF)) {
976
1045
  // Write GIF to file
977
1046
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
978
- image.magicksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1047
+ image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
979
1048
  ->set("strip", !baton->withMetadata)
980
- ->set("optimize_gif_frames", TRUE)
981
- ->set("optimize_gif_transparency", TRUE)
982
- ->set("format", "gif"));
1049
+ ->set("bitdepth", baton->gifBitdepth)
1050
+ ->set("effort", baton->gifEffort)
1051
+ ->set("dither", baton->gifDither));
983
1052
  baton->formatOut = "gif";
984
1053
  } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
985
1054
  (willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
@@ -1003,16 +1072,18 @@ class PipelineWorker : public Napi::AsyncWorker {
1003
1072
  ->set("tile_height", baton->tiffTileHeight)
1004
1073
  ->set("tile_width", baton->tiffTileWidth)
1005
1074
  ->set("xres", baton->tiffXres)
1006
- ->set("yres", baton->tiffYres));
1075
+ ->set("yres", baton->tiffYres)
1076
+ ->set("resunit", baton->tiffResolutionUnit));
1007
1077
  baton->formatOut = "tiff";
1008
1078
  } else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
1009
1079
  (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
1010
1080
  // Write HEIF to file
1081
+ image = sharp::RemoveAnimationProperties(image);
1011
1082
  image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1012
1083
  ->set("strip", !baton->withMetadata)
1013
1084
  ->set("Q", baton->heifQuality)
1014
1085
  ->set("compression", baton->heifCompression)
1015
- ->set("speed", baton->heifSpeed)
1086
+ ->set("effort", baton->heifEffort)
1016
1087
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1017
1088
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1018
1089
  ->set("lossless", baton->heifLossless));
@@ -1037,7 +1108,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1037
1108
  {"lossless", baton->webpLossless ? "TRUE" : "FALSE"},
1038
1109
  {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"},
1039
1110
  {"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"},
1040
- {"reduction_effort", std::to_string(baton->webpReductionEffort)}
1111
+ {"effort", std::to_string(baton->webpEffort)}
1041
1112
  };
1042
1113
  suffix = AssembleSuffixString(".webp", options);
1043
1114
  } else {
@@ -1186,6 +1257,12 @@ class PipelineWorker : public Napi::AsyncWorker {
1186
1257
  Napi::FunctionReference debuglog;
1187
1258
  Napi::FunctionReference queueListener;
1188
1259
 
1260
+ void MultiPageUnsupported(int const pages, std::string op) {
1261
+ if (pages > 1) {
1262
+ throw vips::VError(op + " is not supported for multi-page images");
1263
+ }
1264
+ }
1265
+
1189
1266
  /*
1190
1267
  Calculate the angle of rotation and need-to-flip for the given Exif orientation
1191
1268
  By default, returns zero, i.e. no rotation.
@@ -1276,15 +1353,15 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1276
1353
  // Canvas option
1277
1354
  std::string canvas = sharp::AttrAsStr(options, "canvas");
1278
1355
  if (canvas == "crop") {
1279
- baton->canvas = Canvas::CROP;
1356
+ baton->canvas = sharp::Canvas::CROP;
1280
1357
  } else if (canvas == "embed") {
1281
- baton->canvas = Canvas::EMBED;
1358
+ baton->canvas = sharp::Canvas::EMBED;
1282
1359
  } else if (canvas == "max") {
1283
- baton->canvas = Canvas::MAX;
1360
+ baton->canvas = sharp::Canvas::MAX;
1284
1361
  } else if (canvas == "min") {
1285
- baton->canvas = Canvas::MIN;
1362
+ baton->canvas = sharp::Canvas::MIN;
1286
1363
  } else if (canvas == "ignore_aspect") {
1287
- baton->canvas = Canvas::IGNORE_ASPECT;
1364
+ baton->canvas = sharp::Canvas::IGNORE_ASPECT;
1288
1365
  }
1289
1366
  // Tint chroma
1290
1367
  baton->tintA = sharp::AttrAsDouble(options, "tintA");
@@ -1307,6 +1384,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1307
1384
  }
1308
1385
  // Resize options
1309
1386
  baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement");
1387
+ baton->withoutReduction = sharp::AttrAsBool(options, "withoutReduction");
1310
1388
  baton->position = sharp::AttrAsInt32(options, "position");
1311
1389
  baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground");
1312
1390
  baton->kernel = sharp::AttrAsStr(options, "kernel");
@@ -1328,6 +1406,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1328
1406
  baton->brightness = sharp::AttrAsDouble(options, "brightness");
1329
1407
  baton->saturation = sharp::AttrAsDouble(options, "saturation");
1330
1408
  baton->hue = sharp::AttrAsInt32(options, "hue");
1409
+ baton->lightness = sharp::AttrAsDouble(options, "lightness");
1331
1410
  baton->medianSize = sharp::AttrAsUint32(options, "medianSize");
1332
1411
  baton->sharpenSigma = sharp::AttrAsDouble(options, "sharpenSigma");
1333
1412
  baton->sharpenFlat = sharp::AttrAsDouble(options, "sharpenFlat");
@@ -1364,7 +1443,6 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1364
1443
  baton->affineOdx = sharp::AttrAsDouble(options, "affineOdx");
1365
1444
  baton->affineOdy = sharp::AttrAsDouble(options, "affineOdy");
1366
1445
  baton->affineInterpolator = vips::VInterpolate::new_from_name(sharp::AttrAsStr(options, "affineInterpolator").data());
1367
-
1368
1446
  baton->removeAlpha = sharp::AttrAsBool(options, "removeAlpha");
1369
1447
  baton->ensureAlpha = sharp::AttrAsDouble(options, "ensureAlpha");
1370
1448
  if (options.Has("boolean")) {
@@ -1415,6 +1493,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1415
1493
  std::string k = sharp::AttrAsStr(mdStrKeys, i);
1416
1494
  baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k)));
1417
1495
  }
1496
+ baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1418
1497
  // Format-specific
1419
1498
  baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
1420
1499
  baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");
@@ -1429,14 +1508,23 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1429
1508
  baton->pngAdaptiveFiltering = sharp::AttrAsBool(options, "pngAdaptiveFiltering");
1430
1509
  baton->pngPalette = sharp::AttrAsBool(options, "pngPalette");
1431
1510
  baton->pngQuality = sharp::AttrAsUint32(options, "pngQuality");
1432
- baton->pngColours = sharp::AttrAsUint32(options, "pngColours");
1511
+ baton->pngEffort = sharp::AttrAsUint32(options, "pngEffort");
1512
+ baton->pngBitdepth = sharp::AttrAsUint32(options, "pngBitdepth");
1433
1513
  baton->pngDither = sharp::AttrAsDouble(options, "pngDither");
1514
+ baton->jp2Quality = sharp::AttrAsUint32(options, "jp2Quality");
1515
+ baton->jp2Lossless = sharp::AttrAsBool(options, "jp2Lossless");
1516
+ baton->jp2TileHeight = sharp::AttrAsUint32(options, "jp2TileHeight");
1517
+ baton->jp2TileWidth = sharp::AttrAsUint32(options, "jp2TileWidth");
1518
+ baton->jp2ChromaSubsampling = sharp::AttrAsStr(options, "jp2ChromaSubsampling");
1434
1519
  baton->webpQuality = sharp::AttrAsUint32(options, "webpQuality");
1435
1520
  baton->webpAlphaQuality = sharp::AttrAsUint32(options, "webpAlphaQuality");
1436
1521
  baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
1437
1522
  baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
1438
1523
  baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
1439
- baton->webpReductionEffort = sharp::AttrAsUint32(options, "webpReductionEffort");
1524
+ baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1525
+ baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1526
+ baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1527
+ baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1440
1528
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1441
1529
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
1442
1530
  baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
@@ -1445,6 +1533,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1445
1533
  baton->tiffTileHeight = sharp::AttrAsUint32(options, "tiffTileHeight");
1446
1534
  baton->tiffXres = sharp::AttrAsDouble(options, "tiffXres");
1447
1535
  baton->tiffYres = sharp::AttrAsDouble(options, "tiffYres");
1536
+ if (baton->tiffXres == 1.0 && baton->tiffYres == 1.0 && baton->withMetadataDensity > 0) {
1537
+ baton->tiffXres = baton->tiffYres = baton->withMetadataDensity / 25.4;
1538
+ }
1448
1539
  // tiff compression options
1449
1540
  baton->tiffCompression = static_cast<VipsForeignTiffCompression>(
1450
1541
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_COMPRESSION,
@@ -1452,30 +1543,28 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1452
1543
  baton->tiffPredictor = static_cast<VipsForeignTiffPredictor>(
1453
1544
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR,
1454
1545
  sharp::AttrAsStr(options, "tiffPredictor").data()));
1546
+ baton->tiffResolutionUnit = static_cast<VipsForeignTiffResunit>(
1547
+ vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_RESUNIT,
1548
+ sharp::AttrAsStr(options, "tiffResolutionUnit").data()));
1549
+
1455
1550
  baton->heifQuality = sharp::AttrAsUint32(options, "heifQuality");
1456
1551
  baton->heifLossless = sharp::AttrAsBool(options, "heifLossless");
1457
1552
  baton->heifCompression = static_cast<VipsForeignHeifCompression>(
1458
1553
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
1459
1554
  sharp::AttrAsStr(options, "heifCompression").data()));
1460
- baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed");
1555
+ baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1461
1556
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1462
-
1463
1557
  // Raw output
1464
1558
  baton->rawDepth = static_cast<VipsBandFormat>(
1465
1559
  vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT,
1466
1560
  sharp::AttrAsStr(options, "rawDepth").data()));
1467
-
1468
- // Animated output
1469
- if (sharp::HasAttr(options, "pageHeight")) {
1470
- baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight");
1471
- }
1561
+ // Animated output properties
1472
1562
  if (sharp::HasAttr(options, "loop")) {
1473
1563
  baton->loop = sharp::AttrAsUint32(options, "loop");
1474
1564
  }
1475
1565
  if (sharp::HasAttr(options, "delay")) {
1476
1566
  baton->delay = sharp::AttrAsInt32Vector(options, "delay");
1477
1567
  }
1478
-
1479
1568
  // Tile output
1480
1569
  baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
1481
1570
  baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");