sharp 0.29.3 → 0.30.2

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,168 @@ 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);
117
- }
118
135
 
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
- }
136
+ // Is there just one page? Shrink to inputHeight instead
137
+ if (nPages == 1) {
138
+ pageHeight = inputHeight;
128
139
  }
129
140
 
130
141
  // Scaling calculations
131
- double xfactor = 1.0;
132
- double yfactor = 1.0;
142
+ double hshrink;
143
+ double vshrink;
133
144
  int targetResizeWidth = baton->width;
134
145
  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
146
 
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)));
147
+ // Swap input output width and height when rotating by 90 or 270 degrees
148
+ bool swap = !baton->rotateBeforePreExtract && (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270);
212
149
 
213
- // Calculate residual float affine transformation
214
- double xresidual = static_cast<double>(xshrink) / xfactor;
215
- double yresidual = static_cast<double>(yshrink) / yfactor;
150
+ // Shrink to pageHeight, so we work for multi-page images
151
+ std::tie(hshrink, vshrink) = sharp::ResolveShrink(
152
+ inputWidth, pageHeight, targetResizeWidth, targetResizeHeight,
153
+ baton->canvas, swap, baton->withoutEnlargement, baton->withoutReduction);
216
154
 
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;
155
+ // The jpeg preload shrink.
156
+ int jpegShrinkOnLoad = 1;
220
157
 
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) &&
158
+ // WebP, PDF, SVG scale
159
+ double scale = 1.0;
160
+
161
+ // Try to reload input using shrink-on-load for JPEG, WebP, SVG and PDF, when:
162
+ // - the width or height parameters are specified;
163
+ // - gamma correction doesn't need to be applied;
164
+ // - trimming or pre-resize extract isn't required;
165
+ // - input colourspace is not specified;
166
+ bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
230
167
  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;
168
+ baton->colourspaceInput == VIPS_INTERPRETATION_LAST;
169
+
170
+ if (shouldPreShrink) {
171
+ // The common part of the shrink: the bit by which both axes must be shrunk
172
+ double shrink = std::min(hshrink, vshrink);
173
+
174
+ if (inputImageType == sharp::ImageType::JPEG) {
175
+ // Leave at least a factor of two for the final resize step, when fastShrinkOnLoad: false
176
+ // for more consistent results and to avoid extra sharpness to the image
177
+ int factor = baton->fastShrinkOnLoad ? 1 : 2;
178
+ if (shrink >= 8 * factor) {
179
+ jpegShrinkOnLoad = 8;
180
+ } else if (shrink >= 4 * factor) {
181
+ jpegShrinkOnLoad = 4;
182
+ } else if (shrink >= 2 * factor) {
183
+ jpegShrinkOnLoad = 2;
184
+ }
185
+ // Lower shrink-on-load for known libjpeg rounding errors
186
+ if (jpegShrinkOnLoad > 1 && static_cast<int>(shrink) == jpegShrinkOnLoad) {
187
+ jpegShrinkOnLoad /= 2;
188
+ }
189
+ } else if (inputImageType == sharp::ImageType::WEBP ||
190
+ inputImageType == sharp::ImageType::SVG ||
191
+ inputImageType == sharp::ImageType::PDF) {
192
+ scale = 1.0 / shrink;
246
193
  }
247
194
  }
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
195
+
196
+ // Reload input using shrink-on-load, it'll be an integer shrink
197
+ // factor for jpegload*, a double scale factor for webpload*,
198
+ // pdfload* and svgload*
199
+ if (jpegShrinkOnLoad > 1) {
256
200
  vips::VOption *option = VImage::option()
257
201
  ->set("access", baton->input->access)
258
- ->set("shrink", shrink_on_load)
202
+ ->set("shrink", jpegShrinkOnLoad)
259
203
  ->set("fail", baton->input->failOnError);
260
204
  if (baton->input->buffer != nullptr) {
205
+ // Reload JPEG buffer
261
206
  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
- }
207
+ image = VImage::jpegload_buffer(blob, option);
269
208
  vips_area_unref(reinterpret_cast<VipsArea*>(blob));
270
209
  } else {
271
- if (inputImageType == sharp::ImageType::JPEG) {
272
- // Reload JPEG file
273
- image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option);
210
+ // Reload JPEG file
211
+ image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option);
212
+ }
213
+ } else if (scale != 1.0) {
214
+ vips::VOption *option = VImage::option()
215
+ ->set("access", baton->input->access)
216
+ ->set("scale", scale)
217
+ ->set("fail", baton->input->failOnError);
218
+ if (inputImageType == sharp::ImageType::WEBP) {
219
+ option->set("n", baton->input->pages);
220
+ option->set("page", baton->input->page);
221
+
222
+ if (baton->input->buffer != nullptr) {
223
+ // Reload WebP buffer
224
+ VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
225
+ image = VImage::webpload_buffer(blob, option);
226
+ vips_area_unref(reinterpret_cast<VipsArea*>(blob));
274
227
  } else {
275
228
  // Reload WebP file
276
229
  image = VImage::webpload(const_cast<char*>(baton->input->file.data()), option);
277
230
  }
278
- }
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);
231
+ } else if (inputImageType == sharp::ImageType::SVG) {
232
+ option->set("unlimited", baton->input->unlimited);
233
+ option->set("dpi", baton->input->density);
234
+
235
+ if (baton->input->buffer != nullptr) {
236
+ // Reload SVG buffer
237
+ VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
238
+ image = VImage::svgload_buffer(blob, option);
239
+ vips_area_unref(reinterpret_cast<VipsArea*>(blob));
240
+ } else {
241
+ // Reload SVG file
242
+ image = VImage::svgload(const_cast<char*>(baton->input->file.data()), option);
243
+ }
244
+
245
+ sharp::SetDensity(image, baton->input->density);
246
+ } else if (inputImageType == sharp::ImageType::PDF) {
247
+ option->set("n", baton->input->pages);
248
+ option->set("page", baton->input->page);
249
+ option->set("dpi", baton->input->density);
250
+
251
+ if (baton->input->buffer != nullptr) {
252
+ // Reload PDF buffer
253
+ VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
254
+ image = VImage::pdfload_buffer(blob, option);
255
+ vips_area_unref(reinterpret_cast<VipsArea*>(blob));
256
+ } else {
257
+ // Reload PDF file
258
+ image = VImage::pdfload(const_cast<char*>(baton->input->file.data()), option);
259
+ }
260
+
261
+ sharp::SetDensity(image, baton->input->density);
290
262
  }
291
263
  }
292
- // Remove animation properties from single page images
293
- if (baton->input->pages == 1) {
294
- image = sharp::RemoveAnimationProperties(image);
264
+
265
+ // Any pre-shrinking may already have been done
266
+ inputWidth = image.width();
267
+ inputHeight = image.height();
268
+
269
+ // After pre-shrink, but before the main shrink stage
270
+ // Reuse the initial pageHeight if we didn't pre-shrink
271
+ if (shouldPreShrink) {
272
+ pageHeight = sharp::GetPageHeight(image);
273
+ }
274
+
275
+ // Shrink to pageHeight, so we work for multi-page images
276
+ std::tie(hshrink, vshrink) = sharp::ResolveShrink(
277
+ inputWidth, pageHeight, targetResizeWidth, targetResizeHeight,
278
+ baton->canvas, swap, baton->withoutEnlargement, baton->withoutReduction);
279
+
280
+ int targetHeight = static_cast<int>(std::rint(static_cast<double>(pageHeight) / vshrink));
281
+ int targetPageHeight = targetHeight;
282
+
283
+ // In toilet-roll mode, we must adjust vshrink so that we exactly hit
284
+ // pageHeight or we'll have pixels straddling pixel boundaries
285
+ if (inputHeight > pageHeight) {
286
+ targetHeight *= nPages;
287
+ vshrink = static_cast<double>(inputHeight) / targetHeight;
295
288
  }
296
289
 
297
290
  // Ensure we're using a device-independent colour space
@@ -299,7 +292,8 @@ class PipelineWorker : public Napi::AsyncWorker {
299
292
  if (
300
293
  sharp::HasProfile(image) &&
301
294
  image.interpretation() != VIPS_INTERPRETATION_LABS &&
302
- image.interpretation() != VIPS_INTERPRETATION_GREY16
295
+ image.interpretation() != VIPS_INTERPRETATION_GREY16 &&
296
+ image.interpretation() != VIPS_INTERPRETATION_B_W
303
297
  ) {
304
298
  // Convert to sRGB/P3 using embedded profile
305
299
  try {
@@ -345,7 +339,7 @@ class PipelineWorker : public Napi::AsyncWorker {
345
339
  image = image.colourspace(VIPS_INTERPRETATION_B_W);
346
340
  }
347
341
 
348
- bool const shouldResize = xfactor != 1.0 || yfactor != 1.0;
342
+ bool const shouldResize = hshrink != 1.0 || vshrink != 1.0;
349
343
  bool const shouldBlur = baton->blurSigma != 0.0;
350
344
  bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
351
345
  bool const shouldSharpen = baton->sharpenSigma != 0.0;
@@ -379,39 +373,33 @@ class PipelineWorker : public Napi::AsyncWorker {
379
373
  ) {
380
374
  throw vips::VError("Unknown kernel");
381
375
  }
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)
376
+ image = image.resize(1.0 / hshrink, VImage::option()
377
+ ->set("vscale", 1.0 / vshrink)
397
378
  ->set("kernel", kernel));
398
379
  }
399
380
 
400
381
  // Rotate post-extract 90-angle
401
- if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
402
- image = image.rot(rotation);
403
- image = sharp::RemoveExifOrientation(image);
382
+ if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
383
+ image = image.rot(rotation);
384
+ if (flip) {
385
+ image = image.flip(VIPS_DIRECTION_VERTICAL);
386
+ flip = FALSE;
387
+ }
388
+ if (flop) {
389
+ image = image.flip(VIPS_DIRECTION_HORIZONTAL);
390
+ flop = FALSE;
391
+ }
392
+ image = sharp::RemoveExifOrientation(image);
404
393
  }
405
394
 
406
-
407
395
  // Flip (mirror about Y axis)
408
- if (baton->flip) {
396
+ if (baton->flip || flip) {
409
397
  image = image.flip(VIPS_DIRECTION_VERTICAL);
410
398
  image = sharp::RemoveExifOrientation(image);
411
399
  }
412
400
 
413
401
  // Flop (mirror about X axis)
414
- if (baton->flop) {
402
+ if (baton->flop || flop) {
415
403
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
416
404
  image = sharp::RemoveExifOrientation(image);
417
405
  }
@@ -429,52 +417,68 @@ class PipelineWorker : public Napi::AsyncWorker {
429
417
  image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
430
418
  }
431
419
 
420
+ inputWidth = image.width();
421
+ inputHeight = nPages > 1 ? targetPageHeight : image.height();
422
+
423
+ // Resolve dimensions
424
+ if (baton->width <= 0) {
425
+ baton->width = inputWidth;
426
+ }
427
+ if (baton->height <= 0) {
428
+ baton->height = inputHeight;
429
+ }
430
+
432
431
  // Crop/embed
433
- if (image.width() != baton->width || image.height() != baton->height) {
434
- if (baton->canvas == Canvas::EMBED) {
432
+ if (inputWidth != baton->width || inputHeight != baton->height) {
433
+ if (baton->canvas == sharp::Canvas::EMBED) {
435
434
  std::vector<double> background;
436
435
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
437
436
 
438
437
  // Embed
439
438
 
440
- // Calculate where to position the embeded image if gravity specified, else center.
439
+ // Calculate where to position the embedded image if gravity specified, else center.
441
440
  int left;
442
441
  int top;
443
442
 
444
- left = static_cast<int>(round((baton->width - image.width()) / 2));
445
- top = static_cast<int>(round((baton->height - image.height()) / 2));
443
+ left = static_cast<int>(round((baton->width - inputWidth) / 2));
444
+ top = static_cast<int>(round((baton->height - inputHeight) / 2));
446
445
 
447
- int width = std::max(image.width(), baton->width);
448
- int height = std::max(image.height(), baton->height);
446
+ int width = std::max(inputWidth, baton->width);
447
+ int height = std::max(inputHeight, baton->height);
449
448
  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));
449
+ inputWidth, inputHeight, baton->width, baton->height, baton->position);
450
+
451
+ image = nPages > 1
452
+ ? sharp::EmbedMultiPage(image,
453
+ left, top, width, height, background, nPages, &targetPageHeight)
454
+ : image.embed(left, top, width, height, VImage::option()
455
+ ->set("extend", VIPS_EXTEND_BACKGROUND)
456
+ ->set("background", background));
457
+ } else if (baton->canvas == sharp::Canvas::CROP) {
458
+ if (baton->width > inputWidth) {
459
+ baton->width = inputWidth;
460
+ }
461
+ if (baton->height > inputHeight) {
462
+ baton->height = inputHeight;
463
+ }
455
464
 
456
- } else if (
457
- baton->canvas != Canvas::IGNORE_ASPECT &&
458
- (image.width() > baton->width || image.height() > baton->height)
459
- ) {
460
- // Crop/max/min
465
+ // Crop
461
466
  if (baton->position < 9) {
462
467
  // Gravity-based crop
463
468
  int left;
464
469
  int top;
465
470
  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);
471
+ inputWidth, inputHeight, baton->width, baton->height, baton->position);
472
+ int width = std::min(inputWidth, baton->width);
473
+ int height = std::min(inputHeight, baton->height);
474
+
475
+ image = nPages > 1
476
+ ? sharp::CropMultiPage(image,
477
+ left, top, width, height, nPages, &targetPageHeight)
478
+ : image.extract_area(left, top, width, height);
470
479
  } else {
471
480
  // 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
- }
481
+ MultiPageUnsupported(nPages, "Resize strategy");
478
482
  image = image.tilecache(VImage::option()
479
483
  ->set("access", VIPS_ACCESS_RANDOM)
480
484
  ->set("threaded", TRUE));
@@ -489,6 +493,7 @@ class PipelineWorker : public Napi::AsyncWorker {
489
493
 
490
494
  // Rotate post-extract non-90 angle
491
495
  if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
496
+ MultiPageUnsupported(nPages, "Rotate");
492
497
  std::vector<double> background;
493
498
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
494
499
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
@@ -496,20 +501,32 @@ class PipelineWorker : public Napi::AsyncWorker {
496
501
 
497
502
  // Post extraction
498
503
  if (baton->topOffsetPost != -1) {
499
- image = image.extract_area(
500
- baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
504
+ if (nPages > 1) {
505
+ image = sharp::CropMultiPage(image,
506
+ baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost,
507
+ nPages, &targetPageHeight);
508
+
509
+ // heightPost is used in the info object, so update to reflect the number of pages
510
+ baton->heightPost *= nPages;
511
+ } else {
512
+ image = image.extract_area(
513
+ baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
514
+ }
501
515
  }
502
516
 
503
517
  // Affine transform
504
518
  if (baton->affineMatrix.size() > 0) {
519
+ MultiPageUnsupported(nPages, "Affine");
505
520
  std::vector<double> background;
506
521
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
522
+ vips::VInterpolate interp = vips::VInterpolate::new_from_name(
523
+ const_cast<char*>(baton->affineInterpolator.data()));
507
524
  image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
508
525
  ->set("idx", baton->affineIdx)
509
526
  ->set("idy", baton->affineIdy)
510
527
  ->set("odx", baton->affineOdx)
511
528
  ->set("ody", baton->affineOdy)
512
- ->set("interpolate", baton->affineInterpolator));
529
+ ->set("interpolate", interp));
513
530
  }
514
531
 
515
532
  // Extend edges
@@ -519,10 +536,13 @@ class PipelineWorker : public Napi::AsyncWorker {
519
536
 
520
537
  // Embed
521
538
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
522
- baton->height = image.height() + baton->extendTop + baton->extendBottom;
539
+ baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
523
540
 
524
- image = image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
525
- VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
541
+ image = nPages > 1
542
+ ? sharp::EmbedMultiPage(image,
543
+ baton->extendLeft, baton->extendTop, baton->width, baton->height, background, nPages, &targetPageHeight)
544
+ : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
545
+ VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
526
546
  }
527
547
  // Median - must happen before blurring, due to the utility of blurring after thresholding
528
548
  if (shouldApplyMedian) {
@@ -562,6 +582,8 @@ class PipelineWorker : public Napi::AsyncWorker {
562
582
 
563
583
  // Composite
564
584
  if (shouldComposite) {
585
+ std::vector<VImage> images = { image };
586
+ std::vector<int> modes, xs, ys;
565
587
  for (Composite *composite : baton->composite) {
566
588
  VImage compositeImage;
567
589
  sharp::ImageType compositeImageType = sharp::ImageType::UNKNOWN;
@@ -607,12 +629,12 @@ class PipelineWorker : public Napi::AsyncWorker {
607
629
  // gravity was used for extract_area, set it back to its default value of 0
608
630
  composite->gravity = 0;
609
631
  }
610
- // Ensure image to composite is sRGB with premultiplied alpha
632
+ // Ensure image to composite is sRGB with unpremultiplied alpha
611
633
  compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB);
612
634
  if (!sharp::HasAlpha(compositeImage)) {
613
635
  compositeImage = sharp::EnsureAlpha(compositeImage, 1);
614
636
  }
615
- if (!composite->premultiplied) compositeImage = compositeImage.premultiply();
637
+ if (composite->premultiplied) compositeImage = compositeImage.unpremultiply();
616
638
  // Calculate position
617
639
  int left;
618
640
  int top;
@@ -630,12 +652,12 @@ class PipelineWorker : public Napi::AsyncWorker {
630
652
  std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
631
653
  compositeImage.width(), compositeImage.height(), composite->gravity);
632
654
  }
633
- // Composite
634
- image = image.composite2(compositeImage, composite->mode, VImage::option()
635
- ->set("premultiplied", TRUE)
636
- ->set("x", left)
637
- ->set("y", top));
655
+ images.push_back(compositeImage);
656
+ modes.push_back(composite->mode);
657
+ xs.push_back(left);
658
+ ys.push_back(top);
638
659
  }
660
+ image = image.composite(images, modes, VImage::option()->set("x", xs)->set("y", ys));
639
661
  }
640
662
 
641
663
  // Reverse premultiplication after all transformations:
@@ -762,14 +784,8 @@ class PipelineWorker : public Napi::AsyncWorker {
762
784
  baton->width = image.width();
763
785
  baton->height = image.height();
764
786
 
765
- bool const supportsGifOutput = vips_type_find("VipsOperation", "magicksave") != 0 &&
766
- vips_type_find("VipsOperation", "magicksave_buffer") != 0;
767
-
768
787
  image = sharp::SetAnimationProperties(
769
- image,
770
- baton->pageHeight,
771
- baton->delay,
772
- baton->loop);
788
+ image, nPages, targetPageHeight, baton->delay, baton->loop);
773
789
 
774
790
  // Output
775
791
  sharp::SetTimeout(image, baton->timeoutSeconds);
@@ -817,8 +833,7 @@ class PipelineWorker : public Napi::AsyncWorker {
817
833
  vips_area_unref(area);
818
834
  baton->formatOut = "jp2";
819
835
  } else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
820
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
821
- inputImageType == sharp::ImageType::SVG))) {
836
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
822
837
  // Write PNG to buffer
823
838
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
824
839
  VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option()
@@ -828,7 +843,8 @@ class PipelineWorker : public Napi::AsyncWorker {
828
843
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
829
844
  ->set("palette", baton->pngPalette)
830
845
  ->set("Q", baton->pngQuality)
831
- ->set("bitdepth", baton->pngBitdepth)
846
+ ->set("effort", baton->pngEffort)
847
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
832
848
  ->set("dither", baton->pngDither)));
833
849
  baton->bufferOut = static_cast<char*>(area->data);
834
850
  baton->bufferOutLength = area->length;
@@ -845,7 +861,7 @@ class PipelineWorker : public Napi::AsyncWorker {
845
861
  ->set("lossless", baton->webpLossless)
846
862
  ->set("near_lossless", baton->webpNearLossless)
847
863
  ->set("smart_subsample", baton->webpSmartSubsample)
848
- ->set("reduction_effort", baton->webpReductionEffort)
864
+ ->set("effort", baton->webpEffort)
849
865
  ->set("alpha_q", baton->webpAlphaQuality)));
850
866
  baton->bufferOut = static_cast<char*>(area->data);
851
867
  baton->bufferOutLength = area->length;
@@ -853,14 +869,14 @@ class PipelineWorker : public Napi::AsyncWorker {
853
869
  vips_area_unref(area);
854
870
  baton->formatOut = "webp";
855
871
  } else if (baton->formatOut == "gif" ||
856
- (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
872
+ (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF)) {
857
873
  // Write GIF to buffer
858
874
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
859
- VipsArea *area = reinterpret_cast<VipsArea*>(image.magicksave_buffer(VImage::option()
875
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option()
860
876
  ->set("strip", !baton->withMetadata)
861
- ->set("optimize_gif_frames", TRUE)
862
- ->set("optimize_gif_transparency", TRUE)
863
- ->set("format", "gif")));
877
+ ->set("bitdepth", baton->gifBitdepth)
878
+ ->set("effort", baton->gifEffort)
879
+ ->set("dither", baton->gifDither)));
864
880
  baton->bufferOut = static_cast<char*>(area->data);
865
881
  baton->bufferOutLength = area->length;
866
882
  area->free_fn = nullptr;
@@ -888,7 +904,8 @@ class PipelineWorker : public Napi::AsyncWorker {
888
904
  ->set("tile_height", baton->tiffTileHeight)
889
905
  ->set("tile_width", baton->tiffTileWidth)
890
906
  ->set("xres", baton->tiffXres)
891
- ->set("yres", baton->tiffYres)));
907
+ ->set("yres", baton->tiffYres)
908
+ ->set("resunit", baton->tiffResolutionUnit)));
892
909
  baton->bufferOut = static_cast<char*>(area->data);
893
910
  baton->bufferOutLength = area->length;
894
911
  area->free_fn = nullptr;
@@ -902,7 +919,7 @@ class PipelineWorker : public Napi::AsyncWorker {
902
919
  ->set("strip", !baton->withMetadata)
903
920
  ->set("Q", baton->heifQuality)
904
921
  ->set("compression", baton->heifCompression)
905
- ->set("speed", baton->heifSpeed)
922
+ ->set("effort", baton->heifEffort)
906
923
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
907
924
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
908
925
  ->set("lossless", baton->heifLossless)));
@@ -987,8 +1004,7 @@ class PipelineWorker : public Napi::AsyncWorker {
987
1004
  ->set("tile_width", baton->jp2TileWidth));
988
1005
  baton->formatOut = "jp2";
989
1006
  } else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
990
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
991
- inputImageType == sharp::ImageType::SVG))) {
1007
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
992
1008
  // Write PNG to file
993
1009
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
994
1010
  image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
@@ -998,7 +1014,8 @@ class PipelineWorker : public Napi::AsyncWorker {
998
1014
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
999
1015
  ->set("palette", baton->pngPalette)
1000
1016
  ->set("Q", baton->pngQuality)
1001
- ->set("bitdepth", baton->pngBitdepth)
1017
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
1018
+ ->set("effort", baton->pngEffort)
1002
1019
  ->set("dither", baton->pngDither));
1003
1020
  baton->formatOut = "png";
1004
1021
  } else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) ||
@@ -1011,18 +1028,18 @@ class PipelineWorker : public Napi::AsyncWorker {
1011
1028
  ->set("lossless", baton->webpLossless)
1012
1029
  ->set("near_lossless", baton->webpNearLossless)
1013
1030
  ->set("smart_subsample", baton->webpSmartSubsample)
1014
- ->set("reduction_effort", baton->webpReductionEffort)
1031
+ ->set("effort", baton->webpEffort)
1015
1032
  ->set("alpha_q", baton->webpAlphaQuality));
1016
1033
  baton->formatOut = "webp";
1017
1034
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
1018
- (willMatchInput && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
1035
+ (willMatchInput && inputImageType == sharp::ImageType::GIF)) {
1019
1036
  // Write GIF to file
1020
1037
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
1021
- image.magicksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1038
+ image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1022
1039
  ->set("strip", !baton->withMetadata)
1023
- ->set("optimize_gif_frames", TRUE)
1024
- ->set("optimize_gif_transparency", TRUE)
1025
- ->set("format", "gif"));
1040
+ ->set("bitdepth", baton->gifBitdepth)
1041
+ ->set("effort", baton->gifEffort)
1042
+ ->set("dither", baton->gifDither));
1026
1043
  baton->formatOut = "gif";
1027
1044
  } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
1028
1045
  (willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
@@ -1046,7 +1063,8 @@ class PipelineWorker : public Napi::AsyncWorker {
1046
1063
  ->set("tile_height", baton->tiffTileHeight)
1047
1064
  ->set("tile_width", baton->tiffTileWidth)
1048
1065
  ->set("xres", baton->tiffXres)
1049
- ->set("yres", baton->tiffYres));
1066
+ ->set("yres", baton->tiffYres)
1067
+ ->set("resunit", baton->tiffResolutionUnit));
1050
1068
  baton->formatOut = "tiff";
1051
1069
  } else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
1052
1070
  (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
@@ -1056,7 +1074,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1056
1074
  ->set("strip", !baton->withMetadata)
1057
1075
  ->set("Q", baton->heifQuality)
1058
1076
  ->set("compression", baton->heifCompression)
1059
- ->set("speed", baton->heifSpeed)
1077
+ ->set("effort", baton->heifEffort)
1060
1078
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1061
1079
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1062
1080
  ->set("lossless", baton->heifLossless));
@@ -1081,7 +1099,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1081
1099
  {"lossless", baton->webpLossless ? "TRUE" : "FALSE"},
1082
1100
  {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"},
1083
1101
  {"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"},
1084
- {"reduction_effort", std::to_string(baton->webpReductionEffort)}
1102
+ {"effort", std::to_string(baton->webpEffort)}
1085
1103
  };
1086
1104
  suffix = AssembleSuffixString(".webp", options);
1087
1105
  } else {
@@ -1230,6 +1248,12 @@ class PipelineWorker : public Napi::AsyncWorker {
1230
1248
  Napi::FunctionReference debuglog;
1231
1249
  Napi::FunctionReference queueListener;
1232
1250
 
1251
+ void MultiPageUnsupported(int const pages, std::string op) {
1252
+ if (pages > 1) {
1253
+ throw vips::VError(op + " is not supported for multi-page images");
1254
+ }
1255
+ }
1256
+
1233
1257
  /*
1234
1258
  Calculate the angle of rotation and need-to-flip for the given Exif orientation
1235
1259
  By default, returns zero, i.e. no rotation.
@@ -1320,15 +1344,15 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1320
1344
  // Canvas option
1321
1345
  std::string canvas = sharp::AttrAsStr(options, "canvas");
1322
1346
  if (canvas == "crop") {
1323
- baton->canvas = Canvas::CROP;
1347
+ baton->canvas = sharp::Canvas::CROP;
1324
1348
  } else if (canvas == "embed") {
1325
- baton->canvas = Canvas::EMBED;
1349
+ baton->canvas = sharp::Canvas::EMBED;
1326
1350
  } else if (canvas == "max") {
1327
- baton->canvas = Canvas::MAX;
1351
+ baton->canvas = sharp::Canvas::MAX;
1328
1352
  } else if (canvas == "min") {
1329
- baton->canvas = Canvas::MIN;
1353
+ baton->canvas = sharp::Canvas::MIN;
1330
1354
  } else if (canvas == "ignore_aspect") {
1331
- baton->canvas = Canvas::IGNORE_ASPECT;
1355
+ baton->canvas = sharp::Canvas::IGNORE_ASPECT;
1332
1356
  }
1333
1357
  // Tint chroma
1334
1358
  baton->tintA = sharp::AttrAsDouble(options, "tintA");
@@ -1351,6 +1375,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1351
1375
  }
1352
1376
  // Resize options
1353
1377
  baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement");
1378
+ baton->withoutReduction = sharp::AttrAsBool(options, "withoutReduction");
1354
1379
  baton->position = sharp::AttrAsInt32(options, "position");
1355
1380
  baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground");
1356
1381
  baton->kernel = sharp::AttrAsStr(options, "kernel");
@@ -1408,8 +1433,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1408
1433
  baton->affineIdy = sharp::AttrAsDouble(options, "affineIdy");
1409
1434
  baton->affineOdx = sharp::AttrAsDouble(options, "affineOdx");
1410
1435
  baton->affineOdy = sharp::AttrAsDouble(options, "affineOdy");
1411
- baton->affineInterpolator = vips::VInterpolate::new_from_name(sharp::AttrAsStr(options, "affineInterpolator").data());
1412
-
1436
+ baton->affineInterpolator = sharp::AttrAsStr(options, "affineInterpolator");
1413
1437
  baton->removeAlpha = sharp::AttrAsBool(options, "removeAlpha");
1414
1438
  baton->ensureAlpha = sharp::AttrAsDouble(options, "ensureAlpha");
1415
1439
  if (options.Has("boolean")) {
@@ -1475,6 +1499,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1475
1499
  baton->pngAdaptiveFiltering = sharp::AttrAsBool(options, "pngAdaptiveFiltering");
1476
1500
  baton->pngPalette = sharp::AttrAsBool(options, "pngPalette");
1477
1501
  baton->pngQuality = sharp::AttrAsUint32(options, "pngQuality");
1502
+ baton->pngEffort = sharp::AttrAsUint32(options, "pngEffort");
1478
1503
  baton->pngBitdepth = sharp::AttrAsUint32(options, "pngBitdepth");
1479
1504
  baton->pngDither = sharp::AttrAsDouble(options, "pngDither");
1480
1505
  baton->jp2Quality = sharp::AttrAsUint32(options, "jp2Quality");
@@ -1487,7 +1512,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1487
1512
  baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
1488
1513
  baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
1489
1514
  baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
1490
- baton->webpReductionEffort = sharp::AttrAsUint32(options, "webpReductionEffort");
1515
+ baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1516
+ baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1517
+ baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1518
+ baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1491
1519
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1492
1520
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
1493
1521
  baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
@@ -1506,30 +1534,28 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1506
1534
  baton->tiffPredictor = static_cast<VipsForeignTiffPredictor>(
1507
1535
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR,
1508
1536
  sharp::AttrAsStr(options, "tiffPredictor").data()));
1537
+ baton->tiffResolutionUnit = static_cast<VipsForeignTiffResunit>(
1538
+ vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_RESUNIT,
1539
+ sharp::AttrAsStr(options, "tiffResolutionUnit").data()));
1540
+
1509
1541
  baton->heifQuality = sharp::AttrAsUint32(options, "heifQuality");
1510
1542
  baton->heifLossless = sharp::AttrAsBool(options, "heifLossless");
1511
1543
  baton->heifCompression = static_cast<VipsForeignHeifCompression>(
1512
1544
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
1513
1545
  sharp::AttrAsStr(options, "heifCompression").data()));
1514
- baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed");
1546
+ baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1515
1547
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1516
-
1517
1548
  // Raw output
1518
1549
  baton->rawDepth = static_cast<VipsBandFormat>(
1519
1550
  vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT,
1520
1551
  sharp::AttrAsStr(options, "rawDepth").data()));
1521
-
1522
- // Animated output
1523
- if (sharp::HasAttr(options, "pageHeight")) {
1524
- baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight");
1525
- }
1552
+ // Animated output properties
1526
1553
  if (sharp::HasAttr(options, "loop")) {
1527
1554
  baton->loop = sharp::AttrAsUint32(options, "loop");
1528
1555
  }
1529
1556
  if (sharp::HasAttr(options, "delay")) {
1530
1557
  baton->delay = sharp::AttrAsInt32Vector(options, "delay");
1531
1558
  }
1532
-
1533
1559
  // Tile output
1534
1560
  baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
1535
1561
  baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");