sharp 0.29.3 → 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,9 +95,18 @@ 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
111
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, FALSE);
94
112
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
@@ -97,6 +115,7 @@ class PipelineWorker : public Napi::AsyncWorker {
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,194 +123,182 @@ 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
- }
291
270
  }
292
- // Remove animation properties from single page images
293
- if (baton->input->pages == 1) {
294
- image = sharp::RemoveAnimationProperties(image);
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;
295
302
  }
296
303
 
297
304
  // Ensure we're using a device-independent colour space
@@ -345,7 +352,7 @@ class PipelineWorker : public Napi::AsyncWorker {
345
352
  image = image.colourspace(VIPS_INTERPRETATION_B_W);
346
353
  }
347
354
 
348
- bool const shouldResize = xfactor != 1.0 || yfactor != 1.0;
355
+ bool const shouldResize = hshrink != 1.0 || vshrink != 1.0;
349
356
  bool const shouldBlur = baton->blurSigma != 0.0;
350
357
  bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
351
358
  bool const shouldSharpen = baton->sharpenSigma != 0.0;
@@ -379,39 +386,33 @@ class PipelineWorker : public Napi::AsyncWorker {
379
386
  ) {
380
387
  throw vips::VError("Unknown kernel");
381
388
  }
382
- // Ensure shortest edge is at least 1 pixel
383
- if (image.width() / xfactor < 0.5) {
384
- xfactor = 2 * image.width();
385
- if (baton->canvas != Canvas::EMBED) {
386
- baton->width = 1;
387
- }
388
- }
389
- if (image.height() / yfactor < 0.5) {
390
- yfactor = 2 * image.height();
391
- if (baton->canvas != Canvas::EMBED) {
392
- baton->height = 1;
393
- }
394
- }
395
- image = image.resize(1.0 / xfactor, VImage::option()
396
- ->set("vscale", 1.0 / yfactor)
389
+ image = image.resize(1.0 / hshrink, VImage::option()
390
+ ->set("vscale", 1.0 / vshrink)
397
391
  ->set("kernel", kernel));
398
392
  }
399
393
 
400
394
  // Rotate post-extract 90-angle
401
- if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
402
- image = image.rot(rotation);
403
- 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);
404
406
  }
405
407
 
406
-
407
408
  // Flip (mirror about Y axis)
408
- if (baton->flip) {
409
+ if (baton->flip || flip) {
409
410
  image = image.flip(VIPS_DIRECTION_VERTICAL);
410
411
  image = sharp::RemoveExifOrientation(image);
411
412
  }
412
413
 
413
414
  // Flop (mirror about X axis)
414
- if (baton->flop) {
415
+ if (baton->flop || flop) {
415
416
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
416
417
  image = sharp::RemoveExifOrientation(image);
417
418
  }
@@ -429,52 +430,68 @@ class PipelineWorker : public Napi::AsyncWorker {
429
430
  image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
430
431
  }
431
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
+
432
444
  // Crop/embed
433
- if (image.width() != baton->width || image.height() != baton->height) {
434
- if (baton->canvas == Canvas::EMBED) {
445
+ if (inputWidth != baton->width || inputHeight != baton->height) {
446
+ if (baton->canvas == sharp::Canvas::EMBED) {
435
447
  std::vector<double> background;
436
448
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
437
449
 
438
450
  // Embed
439
451
 
440
- // 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.
441
453
  int left;
442
454
  int top;
443
455
 
444
- left = static_cast<int>(round((baton->width - image.width()) / 2));
445
- 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));
446
458
 
447
- int width = std::max(image.width(), baton->width);
448
- int height = std::max(image.height(), baton->height);
459
+ int width = std::max(inputWidth, baton->width);
460
+ int height = std::max(inputHeight, baton->height);
449
461
  std::tie(left, top) = sharp::CalculateEmbedPosition(
450
- image.width(), image.height(), baton->width, baton->height, baton->position);
451
-
452
- image = image.embed(left, top, width, height, VImage::option()
453
- ->set("extend", VIPS_EXTEND_BACKGROUND)
454
- ->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
+ }
455
477
 
456
- } else if (
457
- baton->canvas != Canvas::IGNORE_ASPECT &&
458
- (image.width() > baton->width || image.height() > baton->height)
459
- ) {
460
- // Crop/max/min
478
+ // Crop
461
479
  if (baton->position < 9) {
462
480
  // Gravity-based crop
463
481
  int left;
464
482
  int top;
465
483
  std::tie(left, top) = sharp::CalculateCrop(
466
- image.width(), image.height(), baton->width, baton->height, baton->position);
467
- int width = std::min(image.width(), baton->width);
468
- int height = std::min(image.height(), baton->height);
469
- 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);
470
492
  } else {
471
493
  // Attention-based or Entropy-based crop
472
- if (baton->width > image.width()) {
473
- baton->width = image.width();
474
- }
475
- if (baton->height > image.height()) {
476
- baton->height = image.height();
477
- }
494
+ MultiPageUnsupported(nPages, "Resize strategy");
478
495
  image = image.tilecache(VImage::option()
479
496
  ->set("access", VIPS_ACCESS_RANDOM)
480
497
  ->set("threaded", TRUE));
@@ -489,6 +506,7 @@ class PipelineWorker : public Napi::AsyncWorker {
489
506
 
490
507
  // Rotate post-extract non-90 angle
491
508
  if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
509
+ MultiPageUnsupported(nPages, "Rotate");
492
510
  std::vector<double> background;
493
511
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
494
512
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
@@ -496,12 +514,22 @@ class PipelineWorker : public Napi::AsyncWorker {
496
514
 
497
515
  // Post extraction
498
516
  if (baton->topOffsetPost != -1) {
499
- image = image.extract_area(
500
- 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
+ }
501
528
  }
502
529
 
503
530
  // Affine transform
504
531
  if (baton->affineMatrix.size() > 0) {
532
+ MultiPageUnsupported(nPages, "Affine");
505
533
  std::vector<double> background;
506
534
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
507
535
  image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
@@ -519,10 +547,13 @@ class PipelineWorker : public Napi::AsyncWorker {
519
547
 
520
548
  // Embed
521
549
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
522
- baton->height = image.height() + baton->extendTop + baton->extendBottom;
550
+ baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
523
551
 
524
- image = image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
525
- 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));
526
557
  }
527
558
  // Median - must happen before blurring, due to the utility of blurring after thresholding
528
559
  if (shouldApplyMedian) {
@@ -762,14 +793,8 @@ class PipelineWorker : public Napi::AsyncWorker {
762
793
  baton->width = image.width();
763
794
  baton->height = image.height();
764
795
 
765
- bool const supportsGifOutput = vips_type_find("VipsOperation", "magicksave") != 0 &&
766
- vips_type_find("VipsOperation", "magicksave_buffer") != 0;
767
-
768
796
  image = sharp::SetAnimationProperties(
769
- image,
770
- baton->pageHeight,
771
- baton->delay,
772
- baton->loop);
797
+ image, nPages, targetPageHeight, baton->delay, baton->loop);
773
798
 
774
799
  // Output
775
800
  sharp::SetTimeout(image, baton->timeoutSeconds);
@@ -817,8 +842,7 @@ class PipelineWorker : public Napi::AsyncWorker {
817
842
  vips_area_unref(area);
818
843
  baton->formatOut = "jp2";
819
844
  } else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
820
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
821
- inputImageType == sharp::ImageType::SVG))) {
845
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
822
846
  // Write PNG to buffer
823
847
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
824
848
  VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option()
@@ -828,7 +852,8 @@ class PipelineWorker : public Napi::AsyncWorker {
828
852
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
829
853
  ->set("palette", baton->pngPalette)
830
854
  ->set("Q", baton->pngQuality)
831
- ->set("bitdepth", baton->pngBitdepth)
855
+ ->set("effort", baton->pngEffort)
856
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
832
857
  ->set("dither", baton->pngDither)));
833
858
  baton->bufferOut = static_cast<char*>(area->data);
834
859
  baton->bufferOutLength = area->length;
@@ -845,7 +870,7 @@ class PipelineWorker : public Napi::AsyncWorker {
845
870
  ->set("lossless", baton->webpLossless)
846
871
  ->set("near_lossless", baton->webpNearLossless)
847
872
  ->set("smart_subsample", baton->webpSmartSubsample)
848
- ->set("reduction_effort", baton->webpReductionEffort)
873
+ ->set("effort", baton->webpEffort)
849
874
  ->set("alpha_q", baton->webpAlphaQuality)));
850
875
  baton->bufferOut = static_cast<char*>(area->data);
851
876
  baton->bufferOutLength = area->length;
@@ -853,14 +878,14 @@ class PipelineWorker : public Napi::AsyncWorker {
853
878
  vips_area_unref(area);
854
879
  baton->formatOut = "webp";
855
880
  } else if (baton->formatOut == "gif" ||
856
- (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
881
+ (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF)) {
857
882
  // Write GIF to buffer
858
883
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
859
- VipsArea *area = reinterpret_cast<VipsArea*>(image.magicksave_buffer(VImage::option()
884
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option()
860
885
  ->set("strip", !baton->withMetadata)
861
- ->set("optimize_gif_frames", TRUE)
862
- ->set("optimize_gif_transparency", TRUE)
863
- ->set("format", "gif")));
886
+ ->set("bitdepth", baton->gifBitdepth)
887
+ ->set("effort", baton->gifEffort)
888
+ ->set("dither", baton->gifDither)));
864
889
  baton->bufferOut = static_cast<char*>(area->data);
865
890
  baton->bufferOutLength = area->length;
866
891
  area->free_fn = nullptr;
@@ -888,7 +913,8 @@ class PipelineWorker : public Napi::AsyncWorker {
888
913
  ->set("tile_height", baton->tiffTileHeight)
889
914
  ->set("tile_width", baton->tiffTileWidth)
890
915
  ->set("xres", baton->tiffXres)
891
- ->set("yres", baton->tiffYres)));
916
+ ->set("yres", baton->tiffYres)
917
+ ->set("resunit", baton->tiffResolutionUnit)));
892
918
  baton->bufferOut = static_cast<char*>(area->data);
893
919
  baton->bufferOutLength = area->length;
894
920
  area->free_fn = nullptr;
@@ -902,7 +928,7 @@ class PipelineWorker : public Napi::AsyncWorker {
902
928
  ->set("strip", !baton->withMetadata)
903
929
  ->set("Q", baton->heifQuality)
904
930
  ->set("compression", baton->heifCompression)
905
- ->set("speed", baton->heifSpeed)
931
+ ->set("effort", baton->heifEffort)
906
932
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
907
933
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
908
934
  ->set("lossless", baton->heifLossless)));
@@ -987,8 +1013,7 @@ class PipelineWorker : public Napi::AsyncWorker {
987
1013
  ->set("tile_width", baton->jp2TileWidth));
988
1014
  baton->formatOut = "jp2";
989
1015
  } else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
990
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
991
- inputImageType == sharp::ImageType::SVG))) {
1016
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
992
1017
  // Write PNG to file
993
1018
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
994
1019
  image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
@@ -998,7 +1023,8 @@ class PipelineWorker : public Napi::AsyncWorker {
998
1023
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
999
1024
  ->set("palette", baton->pngPalette)
1000
1025
  ->set("Q", baton->pngQuality)
1001
- ->set("bitdepth", baton->pngBitdepth)
1026
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
1027
+ ->set("effort", baton->pngEffort)
1002
1028
  ->set("dither", baton->pngDither));
1003
1029
  baton->formatOut = "png";
1004
1030
  } else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) ||
@@ -1011,18 +1037,18 @@ class PipelineWorker : public Napi::AsyncWorker {
1011
1037
  ->set("lossless", baton->webpLossless)
1012
1038
  ->set("near_lossless", baton->webpNearLossless)
1013
1039
  ->set("smart_subsample", baton->webpSmartSubsample)
1014
- ->set("reduction_effort", baton->webpReductionEffort)
1040
+ ->set("effort", baton->webpEffort)
1015
1041
  ->set("alpha_q", baton->webpAlphaQuality));
1016
1042
  baton->formatOut = "webp";
1017
1043
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
1018
- (willMatchInput && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
1044
+ (willMatchInput && inputImageType == sharp::ImageType::GIF)) {
1019
1045
  // Write GIF to file
1020
1046
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
1021
- image.magicksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1047
+ image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1022
1048
  ->set("strip", !baton->withMetadata)
1023
- ->set("optimize_gif_frames", TRUE)
1024
- ->set("optimize_gif_transparency", TRUE)
1025
- ->set("format", "gif"));
1049
+ ->set("bitdepth", baton->gifBitdepth)
1050
+ ->set("effort", baton->gifEffort)
1051
+ ->set("dither", baton->gifDither));
1026
1052
  baton->formatOut = "gif";
1027
1053
  } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
1028
1054
  (willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
@@ -1046,7 +1072,8 @@ class PipelineWorker : public Napi::AsyncWorker {
1046
1072
  ->set("tile_height", baton->tiffTileHeight)
1047
1073
  ->set("tile_width", baton->tiffTileWidth)
1048
1074
  ->set("xres", baton->tiffXres)
1049
- ->set("yres", baton->tiffYres));
1075
+ ->set("yres", baton->tiffYres)
1076
+ ->set("resunit", baton->tiffResolutionUnit));
1050
1077
  baton->formatOut = "tiff";
1051
1078
  } else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
1052
1079
  (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
@@ -1056,7 +1083,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1056
1083
  ->set("strip", !baton->withMetadata)
1057
1084
  ->set("Q", baton->heifQuality)
1058
1085
  ->set("compression", baton->heifCompression)
1059
- ->set("speed", baton->heifSpeed)
1086
+ ->set("effort", baton->heifEffort)
1060
1087
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1061
1088
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1062
1089
  ->set("lossless", baton->heifLossless));
@@ -1081,7 +1108,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1081
1108
  {"lossless", baton->webpLossless ? "TRUE" : "FALSE"},
1082
1109
  {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"},
1083
1110
  {"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"},
1084
- {"reduction_effort", std::to_string(baton->webpReductionEffort)}
1111
+ {"effort", std::to_string(baton->webpEffort)}
1085
1112
  };
1086
1113
  suffix = AssembleSuffixString(".webp", options);
1087
1114
  } else {
@@ -1230,6 +1257,12 @@ class PipelineWorker : public Napi::AsyncWorker {
1230
1257
  Napi::FunctionReference debuglog;
1231
1258
  Napi::FunctionReference queueListener;
1232
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
+
1233
1266
  /*
1234
1267
  Calculate the angle of rotation and need-to-flip for the given Exif orientation
1235
1268
  By default, returns zero, i.e. no rotation.
@@ -1320,15 +1353,15 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1320
1353
  // Canvas option
1321
1354
  std::string canvas = sharp::AttrAsStr(options, "canvas");
1322
1355
  if (canvas == "crop") {
1323
- baton->canvas = Canvas::CROP;
1356
+ baton->canvas = sharp::Canvas::CROP;
1324
1357
  } else if (canvas == "embed") {
1325
- baton->canvas = Canvas::EMBED;
1358
+ baton->canvas = sharp::Canvas::EMBED;
1326
1359
  } else if (canvas == "max") {
1327
- baton->canvas = Canvas::MAX;
1360
+ baton->canvas = sharp::Canvas::MAX;
1328
1361
  } else if (canvas == "min") {
1329
- baton->canvas = Canvas::MIN;
1362
+ baton->canvas = sharp::Canvas::MIN;
1330
1363
  } else if (canvas == "ignore_aspect") {
1331
- baton->canvas = Canvas::IGNORE_ASPECT;
1364
+ baton->canvas = sharp::Canvas::IGNORE_ASPECT;
1332
1365
  }
1333
1366
  // Tint chroma
1334
1367
  baton->tintA = sharp::AttrAsDouble(options, "tintA");
@@ -1351,6 +1384,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1351
1384
  }
1352
1385
  // Resize options
1353
1386
  baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement");
1387
+ baton->withoutReduction = sharp::AttrAsBool(options, "withoutReduction");
1354
1388
  baton->position = sharp::AttrAsInt32(options, "position");
1355
1389
  baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground");
1356
1390
  baton->kernel = sharp::AttrAsStr(options, "kernel");
@@ -1409,7 +1443,6 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1409
1443
  baton->affineOdx = sharp::AttrAsDouble(options, "affineOdx");
1410
1444
  baton->affineOdy = sharp::AttrAsDouble(options, "affineOdy");
1411
1445
  baton->affineInterpolator = vips::VInterpolate::new_from_name(sharp::AttrAsStr(options, "affineInterpolator").data());
1412
-
1413
1446
  baton->removeAlpha = sharp::AttrAsBool(options, "removeAlpha");
1414
1447
  baton->ensureAlpha = sharp::AttrAsDouble(options, "ensureAlpha");
1415
1448
  if (options.Has("boolean")) {
@@ -1475,6 +1508,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1475
1508
  baton->pngAdaptiveFiltering = sharp::AttrAsBool(options, "pngAdaptiveFiltering");
1476
1509
  baton->pngPalette = sharp::AttrAsBool(options, "pngPalette");
1477
1510
  baton->pngQuality = sharp::AttrAsUint32(options, "pngQuality");
1511
+ baton->pngEffort = sharp::AttrAsUint32(options, "pngEffort");
1478
1512
  baton->pngBitdepth = sharp::AttrAsUint32(options, "pngBitdepth");
1479
1513
  baton->pngDither = sharp::AttrAsDouble(options, "pngDither");
1480
1514
  baton->jp2Quality = sharp::AttrAsUint32(options, "jp2Quality");
@@ -1487,7 +1521,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1487
1521
  baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
1488
1522
  baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
1489
1523
  baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
1490
- 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");
1491
1528
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1492
1529
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
1493
1530
  baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
@@ -1506,30 +1543,28 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1506
1543
  baton->tiffPredictor = static_cast<VipsForeignTiffPredictor>(
1507
1544
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR,
1508
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
+
1509
1550
  baton->heifQuality = sharp::AttrAsUint32(options, "heifQuality");
1510
1551
  baton->heifLossless = sharp::AttrAsBool(options, "heifLossless");
1511
1552
  baton->heifCompression = static_cast<VipsForeignHeifCompression>(
1512
1553
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
1513
1554
  sharp::AttrAsStr(options, "heifCompression").data()));
1514
- baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed");
1555
+ baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1515
1556
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1516
-
1517
1557
  // Raw output
1518
1558
  baton->rawDepth = static_cast<VipsBandFormat>(
1519
1559
  vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT,
1520
1560
  sharp::AttrAsStr(options, "rawDepth").data()));
1521
-
1522
- // Animated output
1523
- if (sharp::HasAttr(options, "pageHeight")) {
1524
- baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight");
1525
- }
1561
+ // Animated output properties
1526
1562
  if (sharp::HasAttr(options, "loop")) {
1527
1563
  baton->loop = sharp::AttrAsUint32(options, "loop");
1528
1564
  }
1529
1565
  if (sharp::HasAttr(options, "delay")) {
1530
1566
  baton->delay = sharp::AttrAsInt32Vector(options, "delay");
1531
1567
  }
1532
-
1533
1568
  // Tile output
1534
1569
  baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
1535
1570
  baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");