sharp 0.29.1 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,190 +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
  }
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);
278
262
  }
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
- }
263
+ }
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;
291
288
  }
292
289
 
293
290
  // Ensure we're using a device-independent colour space
@@ -341,7 +338,7 @@ class PipelineWorker : public Napi::AsyncWorker {
341
338
  image = image.colourspace(VIPS_INTERPRETATION_B_W);
342
339
  }
343
340
 
344
- bool const shouldResize = xfactor != 1.0 || yfactor != 1.0;
341
+ bool const shouldResize = hshrink != 1.0 || vshrink != 1.0;
345
342
  bool const shouldBlur = baton->blurSigma != 0.0;
346
343
  bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
347
344
  bool const shouldSharpen = baton->sharpenSigma != 0.0;
@@ -375,35 +372,33 @@ class PipelineWorker : public Napi::AsyncWorker {
375
372
  ) {
376
373
  throw vips::VError("Unknown kernel");
377
374
  }
378
- // Ensure shortest edge is at least 1 pixel
379
- if (image.width() / xfactor < 0.5) {
380
- xfactor = 2 * image.width();
381
- baton->width = 1;
382
- }
383
- if (image.height() / yfactor < 0.5) {
384
- yfactor = 2 * image.height();
385
- baton->height = 1;
386
- }
387
- image = image.resize(1.0 / xfactor, VImage::option()
388
- ->set("vscale", 1.0 / yfactor)
375
+ image = image.resize(1.0 / hshrink, VImage::option()
376
+ ->set("vscale", 1.0 / vshrink)
389
377
  ->set("kernel", kernel));
390
378
  }
391
379
 
392
380
  // Rotate post-extract 90-angle
393
- if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
394
- image = image.rot(rotation);
395
- image = sharp::RemoveExifOrientation(image);
381
+ if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
382
+ image = image.rot(rotation);
383
+ if (flip) {
384
+ image = image.flip(VIPS_DIRECTION_VERTICAL);
385
+ flip = FALSE;
386
+ }
387
+ if (flop) {
388
+ image = image.flip(VIPS_DIRECTION_HORIZONTAL);
389
+ flop = FALSE;
390
+ }
391
+ image = sharp::RemoveExifOrientation(image);
396
392
  }
397
393
 
398
-
399
394
  // Flip (mirror about Y axis)
400
- if (baton->flip) {
395
+ if (baton->flip || flip) {
401
396
  image = image.flip(VIPS_DIRECTION_VERTICAL);
402
397
  image = sharp::RemoveExifOrientation(image);
403
398
  }
404
399
 
405
400
  // Flop (mirror about X axis)
406
- if (baton->flop) {
401
+ if (baton->flop || flop) {
407
402
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
408
403
  image = sharp::RemoveExifOrientation(image);
409
404
  }
@@ -421,52 +416,68 @@ class PipelineWorker : public Napi::AsyncWorker {
421
416
  image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
422
417
  }
423
418
 
419
+ inputWidth = image.width();
420
+ inputHeight = nPages > 1 ? targetPageHeight : image.height();
421
+
422
+ // Resolve dimensions
423
+ if (baton->width <= 0) {
424
+ baton->width = inputWidth;
425
+ }
426
+ if (baton->height <= 0) {
427
+ baton->height = inputHeight;
428
+ }
429
+
424
430
  // Crop/embed
425
- if (image.width() != baton->width || image.height() != baton->height) {
426
- if (baton->canvas == Canvas::EMBED) {
431
+ if (inputWidth != baton->width || inputHeight != baton->height) {
432
+ if (baton->canvas == sharp::Canvas::EMBED) {
427
433
  std::vector<double> background;
428
434
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
429
435
 
430
436
  // Embed
431
437
 
432
- // Calculate where to position the embeded image if gravity specified, else center.
438
+ // Calculate where to position the embedded image if gravity specified, else center.
433
439
  int left;
434
440
  int top;
435
441
 
436
- left = static_cast<int>(round((baton->width - image.width()) / 2));
437
- top = static_cast<int>(round((baton->height - image.height()) / 2));
442
+ left = static_cast<int>(round((baton->width - inputWidth) / 2));
443
+ top = static_cast<int>(round((baton->height - inputHeight) / 2));
438
444
 
439
- int width = std::max(image.width(), baton->width);
440
- int height = std::max(image.height(), baton->height);
445
+ int width = std::max(inputWidth, baton->width);
446
+ int height = std::max(inputHeight, baton->height);
441
447
  std::tie(left, top) = sharp::CalculateEmbedPosition(
442
- image.width(), image.height(), baton->width, baton->height, baton->position);
443
-
444
- image = image.embed(left, top, width, height, VImage::option()
445
- ->set("extend", VIPS_EXTEND_BACKGROUND)
446
- ->set("background", background));
448
+ inputWidth, inputHeight, baton->width, baton->height, baton->position);
449
+
450
+ image = nPages > 1
451
+ ? sharp::EmbedMultiPage(image,
452
+ left, top, width, height, background, nPages, &targetPageHeight)
453
+ : image.embed(left, top, width, height, VImage::option()
454
+ ->set("extend", VIPS_EXTEND_BACKGROUND)
455
+ ->set("background", background));
456
+ } else if (baton->canvas == sharp::Canvas::CROP) {
457
+ if (baton->width > inputWidth) {
458
+ baton->width = inputWidth;
459
+ }
460
+ if (baton->height > inputHeight) {
461
+ baton->height = inputHeight;
462
+ }
447
463
 
448
- } else if (
449
- baton->canvas != Canvas::IGNORE_ASPECT &&
450
- (image.width() > baton->width || image.height() > baton->height)
451
- ) {
452
- // Crop/max/min
464
+ // Crop
453
465
  if (baton->position < 9) {
454
466
  // Gravity-based crop
455
467
  int left;
456
468
  int top;
457
469
  std::tie(left, top) = sharp::CalculateCrop(
458
- image.width(), image.height(), baton->width, baton->height, baton->position);
459
- int width = std::min(image.width(), baton->width);
460
- int height = std::min(image.height(), baton->height);
461
- image = image.extract_area(left, top, width, height);
470
+ inputWidth, inputHeight, baton->width, baton->height, baton->position);
471
+ int width = std::min(inputWidth, baton->width);
472
+ int height = std::min(inputHeight, baton->height);
473
+
474
+ image = nPages > 1
475
+ ? sharp::CropMultiPage(image,
476
+ left, top, width, height, nPages, &targetPageHeight)
477
+ : image.extract_area(left, top, width, height);
462
478
  } else {
463
479
  // Attention-based or Entropy-based crop
464
- if (baton->width > image.width()) {
465
- baton->width = image.width();
466
- }
467
- if (baton->height > image.height()) {
468
- baton->height = image.height();
469
- }
480
+ MultiPageUnsupported(nPages, "Resize strategy");
470
481
  image = image.tilecache(VImage::option()
471
482
  ->set("access", VIPS_ACCESS_RANDOM)
472
483
  ->set("threaded", TRUE));
@@ -481,6 +492,7 @@ class PipelineWorker : public Napi::AsyncWorker {
481
492
 
482
493
  // Rotate post-extract non-90 angle
483
494
  if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
495
+ MultiPageUnsupported(nPages, "Rotate");
484
496
  std::vector<double> background;
485
497
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
486
498
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
@@ -488,20 +500,32 @@ class PipelineWorker : public Napi::AsyncWorker {
488
500
 
489
501
  // Post extraction
490
502
  if (baton->topOffsetPost != -1) {
491
- image = image.extract_area(
492
- baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
503
+ if (nPages > 1) {
504
+ image = sharp::CropMultiPage(image,
505
+ baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost,
506
+ nPages, &targetPageHeight);
507
+
508
+ // heightPost is used in the info object, so update to reflect the number of pages
509
+ baton->heightPost *= nPages;
510
+ } else {
511
+ image = image.extract_area(
512
+ baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
513
+ }
493
514
  }
494
515
 
495
516
  // Affine transform
496
517
  if (baton->affineMatrix.size() > 0) {
518
+ MultiPageUnsupported(nPages, "Affine");
497
519
  std::vector<double> background;
498
520
  std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
521
+ vips::VInterpolate interp = vips::VInterpolate::new_from_name(
522
+ const_cast<char*>(baton->affineInterpolator.data()));
499
523
  image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
500
524
  ->set("idx", baton->affineIdx)
501
525
  ->set("idy", baton->affineIdy)
502
526
  ->set("odx", baton->affineOdx)
503
527
  ->set("ody", baton->affineOdy)
504
- ->set("interpolate", baton->affineInterpolator));
528
+ ->set("interpolate", interp));
505
529
  }
506
530
 
507
531
  // Extend edges
@@ -511,10 +535,13 @@ class PipelineWorker : public Napi::AsyncWorker {
511
535
 
512
536
  // Embed
513
537
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
514
- baton->height = image.height() + baton->extendTop + baton->extendBottom;
538
+ baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
515
539
 
516
- image = image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
517
- VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
540
+ image = nPages > 1
541
+ ? sharp::EmbedMultiPage(image,
542
+ baton->extendLeft, baton->extendTop, baton->width, baton->height, background, nPages, &targetPageHeight)
543
+ : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
544
+ VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
518
545
  }
519
546
  // Median - must happen before blurring, due to the utility of blurring after thresholding
520
547
  if (shouldApplyMedian) {
@@ -754,16 +781,11 @@ class PipelineWorker : public Napi::AsyncWorker {
754
781
  baton->width = image.width();
755
782
  baton->height = image.height();
756
783
 
757
- bool const supportsGifOutput = vips_type_find("VipsOperation", "magicksave") != 0 &&
758
- vips_type_find("VipsOperation", "magicksave_buffer") != 0;
759
-
760
784
  image = sharp::SetAnimationProperties(
761
- image,
762
- baton->pageHeight,
763
- baton->delay,
764
- baton->loop);
785
+ image, nPages, targetPageHeight, baton->delay, baton->loop);
765
786
 
766
787
  // Output
788
+ sharp::SetTimeout(image, baton->timeoutSeconds);
767
789
  if (baton->fileOut.empty()) {
768
790
  // Buffer output
769
791
  if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
@@ -808,8 +830,7 @@ class PipelineWorker : public Napi::AsyncWorker {
808
830
  vips_area_unref(area);
809
831
  baton->formatOut = "jp2";
810
832
  } else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
811
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
812
- inputImageType == sharp::ImageType::SVG))) {
833
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
813
834
  // Write PNG to buffer
814
835
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
815
836
  VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option()
@@ -819,7 +840,8 @@ class PipelineWorker : public Napi::AsyncWorker {
819
840
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
820
841
  ->set("palette", baton->pngPalette)
821
842
  ->set("Q", baton->pngQuality)
822
- ->set("bitdepth", baton->pngBitdepth)
843
+ ->set("effort", baton->pngEffort)
844
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
823
845
  ->set("dither", baton->pngDither)));
824
846
  baton->bufferOut = static_cast<char*>(area->data);
825
847
  baton->bufferOutLength = area->length;
@@ -836,7 +858,7 @@ class PipelineWorker : public Napi::AsyncWorker {
836
858
  ->set("lossless", baton->webpLossless)
837
859
  ->set("near_lossless", baton->webpNearLossless)
838
860
  ->set("smart_subsample", baton->webpSmartSubsample)
839
- ->set("reduction_effort", baton->webpReductionEffort)
861
+ ->set("effort", baton->webpEffort)
840
862
  ->set("alpha_q", baton->webpAlphaQuality)));
841
863
  baton->bufferOut = static_cast<char*>(area->data);
842
864
  baton->bufferOutLength = area->length;
@@ -844,14 +866,14 @@ class PipelineWorker : public Napi::AsyncWorker {
844
866
  vips_area_unref(area);
845
867
  baton->formatOut = "webp";
846
868
  } else if (baton->formatOut == "gif" ||
847
- (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
869
+ (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF)) {
848
870
  // Write GIF to buffer
849
871
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
850
- VipsArea *area = reinterpret_cast<VipsArea*>(image.magicksave_buffer(VImage::option()
872
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option()
851
873
  ->set("strip", !baton->withMetadata)
852
- ->set("optimize_gif_frames", TRUE)
853
- ->set("optimize_gif_transparency", TRUE)
854
- ->set("format", "gif")));
874
+ ->set("bitdepth", baton->gifBitdepth)
875
+ ->set("effort", baton->gifEffort)
876
+ ->set("dither", baton->gifDither)));
855
877
  baton->bufferOut = static_cast<char*>(area->data);
856
878
  baton->bufferOutLength = area->length;
857
879
  area->free_fn = nullptr;
@@ -879,7 +901,8 @@ class PipelineWorker : public Napi::AsyncWorker {
879
901
  ->set("tile_height", baton->tiffTileHeight)
880
902
  ->set("tile_width", baton->tiffTileWidth)
881
903
  ->set("xres", baton->tiffXres)
882
- ->set("yres", baton->tiffYres)));
904
+ ->set("yres", baton->tiffYres)
905
+ ->set("resunit", baton->tiffResolutionUnit)));
883
906
  baton->bufferOut = static_cast<char*>(area->data);
884
907
  baton->bufferOutLength = area->length;
885
908
  area->free_fn = nullptr;
@@ -893,7 +916,7 @@ class PipelineWorker : public Napi::AsyncWorker {
893
916
  ->set("strip", !baton->withMetadata)
894
917
  ->set("Q", baton->heifQuality)
895
918
  ->set("compression", baton->heifCompression)
896
- ->set("speed", baton->heifSpeed)
919
+ ->set("effort", baton->heifEffort)
897
920
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
898
921
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
899
922
  ->set("lossless", baton->heifLossless)));
@@ -978,8 +1001,7 @@ class PipelineWorker : public Napi::AsyncWorker {
978
1001
  ->set("tile_width", baton->jp2TileWidth));
979
1002
  baton->formatOut = "jp2";
980
1003
  } else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
981
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
982
- inputImageType == sharp::ImageType::SVG))) {
1004
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
983
1005
  // Write PNG to file
984
1006
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
985
1007
  image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
@@ -989,7 +1011,8 @@ class PipelineWorker : public Napi::AsyncWorker {
989
1011
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
990
1012
  ->set("palette", baton->pngPalette)
991
1013
  ->set("Q", baton->pngQuality)
992
- ->set("bitdepth", baton->pngBitdepth)
1014
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
1015
+ ->set("effort", baton->pngEffort)
993
1016
  ->set("dither", baton->pngDither));
994
1017
  baton->formatOut = "png";
995
1018
  } else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) ||
@@ -1002,18 +1025,18 @@ class PipelineWorker : public Napi::AsyncWorker {
1002
1025
  ->set("lossless", baton->webpLossless)
1003
1026
  ->set("near_lossless", baton->webpNearLossless)
1004
1027
  ->set("smart_subsample", baton->webpSmartSubsample)
1005
- ->set("reduction_effort", baton->webpReductionEffort)
1028
+ ->set("effort", baton->webpEffort)
1006
1029
  ->set("alpha_q", baton->webpAlphaQuality));
1007
1030
  baton->formatOut = "webp";
1008
1031
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
1009
- (willMatchInput && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
1032
+ (willMatchInput && inputImageType == sharp::ImageType::GIF)) {
1010
1033
  // Write GIF to file
1011
1034
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
1012
- image.magicksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1035
+ image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1013
1036
  ->set("strip", !baton->withMetadata)
1014
- ->set("optimize_gif_frames", TRUE)
1015
- ->set("optimize_gif_transparency", TRUE)
1016
- ->set("format", "gif"));
1037
+ ->set("bitdepth", baton->gifBitdepth)
1038
+ ->set("effort", baton->gifEffort)
1039
+ ->set("dither", baton->gifDither));
1017
1040
  baton->formatOut = "gif";
1018
1041
  } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
1019
1042
  (willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
@@ -1037,7 +1060,8 @@ class PipelineWorker : public Napi::AsyncWorker {
1037
1060
  ->set("tile_height", baton->tiffTileHeight)
1038
1061
  ->set("tile_width", baton->tiffTileWidth)
1039
1062
  ->set("xres", baton->tiffXres)
1040
- ->set("yres", baton->tiffYres));
1063
+ ->set("yres", baton->tiffYres)
1064
+ ->set("resunit", baton->tiffResolutionUnit));
1041
1065
  baton->formatOut = "tiff";
1042
1066
  } else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
1043
1067
  (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
@@ -1047,7 +1071,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1047
1071
  ->set("strip", !baton->withMetadata)
1048
1072
  ->set("Q", baton->heifQuality)
1049
1073
  ->set("compression", baton->heifCompression)
1050
- ->set("speed", baton->heifSpeed)
1074
+ ->set("effort", baton->heifEffort)
1051
1075
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1052
1076
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1053
1077
  ->set("lossless", baton->heifLossless));
@@ -1072,7 +1096,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1072
1096
  {"lossless", baton->webpLossless ? "TRUE" : "FALSE"},
1073
1097
  {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"},
1074
1098
  {"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"},
1075
- {"reduction_effort", std::to_string(baton->webpReductionEffort)}
1099
+ {"effort", std::to_string(baton->webpEffort)}
1076
1100
  };
1077
1101
  suffix = AssembleSuffixString(".webp", options);
1078
1102
  } else {
@@ -1221,6 +1245,12 @@ class PipelineWorker : public Napi::AsyncWorker {
1221
1245
  Napi::FunctionReference debuglog;
1222
1246
  Napi::FunctionReference queueListener;
1223
1247
 
1248
+ void MultiPageUnsupported(int const pages, std::string op) {
1249
+ if (pages > 1) {
1250
+ throw vips::VError(op + " is not supported for multi-page images");
1251
+ }
1252
+ }
1253
+
1224
1254
  /*
1225
1255
  Calculate the angle of rotation and need-to-flip for the given Exif orientation
1226
1256
  By default, returns zero, i.e. no rotation.
@@ -1311,15 +1341,15 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1311
1341
  // Canvas option
1312
1342
  std::string canvas = sharp::AttrAsStr(options, "canvas");
1313
1343
  if (canvas == "crop") {
1314
- baton->canvas = Canvas::CROP;
1344
+ baton->canvas = sharp::Canvas::CROP;
1315
1345
  } else if (canvas == "embed") {
1316
- baton->canvas = Canvas::EMBED;
1346
+ baton->canvas = sharp::Canvas::EMBED;
1317
1347
  } else if (canvas == "max") {
1318
- baton->canvas = Canvas::MAX;
1348
+ baton->canvas = sharp::Canvas::MAX;
1319
1349
  } else if (canvas == "min") {
1320
- baton->canvas = Canvas::MIN;
1350
+ baton->canvas = sharp::Canvas::MIN;
1321
1351
  } else if (canvas == "ignore_aspect") {
1322
- baton->canvas = Canvas::IGNORE_ASPECT;
1352
+ baton->canvas = sharp::Canvas::IGNORE_ASPECT;
1323
1353
  }
1324
1354
  // Tint chroma
1325
1355
  baton->tintA = sharp::AttrAsDouble(options, "tintA");
@@ -1342,6 +1372,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1342
1372
  }
1343
1373
  // Resize options
1344
1374
  baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement");
1375
+ baton->withoutReduction = sharp::AttrAsBool(options, "withoutReduction");
1345
1376
  baton->position = sharp::AttrAsInt32(options, "position");
1346
1377
  baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground");
1347
1378
  baton->kernel = sharp::AttrAsStr(options, "kernel");
@@ -1399,8 +1430,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1399
1430
  baton->affineIdy = sharp::AttrAsDouble(options, "affineIdy");
1400
1431
  baton->affineOdx = sharp::AttrAsDouble(options, "affineOdx");
1401
1432
  baton->affineOdy = sharp::AttrAsDouble(options, "affineOdy");
1402
- baton->affineInterpolator = vips::VInterpolate::new_from_name(sharp::AttrAsStr(options, "affineInterpolator").data());
1403
-
1433
+ baton->affineInterpolator = sharp::AttrAsStr(options, "affineInterpolator");
1404
1434
  baton->removeAlpha = sharp::AttrAsBool(options, "removeAlpha");
1405
1435
  baton->ensureAlpha = sharp::AttrAsDouble(options, "ensureAlpha");
1406
1436
  if (options.Has("boolean")) {
@@ -1451,6 +1481,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1451
1481
  std::string k = sharp::AttrAsStr(mdStrKeys, i);
1452
1482
  baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k)));
1453
1483
  }
1484
+ baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1454
1485
  // Format-specific
1455
1486
  baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
1456
1487
  baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");
@@ -1465,6 +1496,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1465
1496
  baton->pngAdaptiveFiltering = sharp::AttrAsBool(options, "pngAdaptiveFiltering");
1466
1497
  baton->pngPalette = sharp::AttrAsBool(options, "pngPalette");
1467
1498
  baton->pngQuality = sharp::AttrAsUint32(options, "pngQuality");
1499
+ baton->pngEffort = sharp::AttrAsUint32(options, "pngEffort");
1468
1500
  baton->pngBitdepth = sharp::AttrAsUint32(options, "pngBitdepth");
1469
1501
  baton->pngDither = sharp::AttrAsDouble(options, "pngDither");
1470
1502
  baton->jp2Quality = sharp::AttrAsUint32(options, "jp2Quality");
@@ -1477,7 +1509,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1477
1509
  baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
1478
1510
  baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
1479
1511
  baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
1480
- baton->webpReductionEffort = sharp::AttrAsUint32(options, "webpReductionEffort");
1512
+ baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1513
+ baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1514
+ baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1515
+ baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1481
1516
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1482
1517
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
1483
1518
  baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
@@ -1486,6 +1521,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1486
1521
  baton->tiffTileHeight = sharp::AttrAsUint32(options, "tiffTileHeight");
1487
1522
  baton->tiffXres = sharp::AttrAsDouble(options, "tiffXres");
1488
1523
  baton->tiffYres = sharp::AttrAsDouble(options, "tiffYres");
1524
+ if (baton->tiffXres == 1.0 && baton->tiffYres == 1.0 && baton->withMetadataDensity > 0) {
1525
+ baton->tiffXres = baton->tiffYres = baton->withMetadataDensity / 25.4;
1526
+ }
1489
1527
  // tiff compression options
1490
1528
  baton->tiffCompression = static_cast<VipsForeignTiffCompression>(
1491
1529
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_COMPRESSION,
@@ -1493,30 +1531,28 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1493
1531
  baton->tiffPredictor = static_cast<VipsForeignTiffPredictor>(
1494
1532
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR,
1495
1533
  sharp::AttrAsStr(options, "tiffPredictor").data()));
1534
+ baton->tiffResolutionUnit = static_cast<VipsForeignTiffResunit>(
1535
+ vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_RESUNIT,
1536
+ sharp::AttrAsStr(options, "tiffResolutionUnit").data()));
1537
+
1496
1538
  baton->heifQuality = sharp::AttrAsUint32(options, "heifQuality");
1497
1539
  baton->heifLossless = sharp::AttrAsBool(options, "heifLossless");
1498
1540
  baton->heifCompression = static_cast<VipsForeignHeifCompression>(
1499
1541
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
1500
1542
  sharp::AttrAsStr(options, "heifCompression").data()));
1501
- baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed");
1543
+ baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1502
1544
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1503
-
1504
1545
  // Raw output
1505
1546
  baton->rawDepth = static_cast<VipsBandFormat>(
1506
1547
  vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT,
1507
1548
  sharp::AttrAsStr(options, "rawDepth").data()));
1508
-
1509
- // Animated output
1510
- if (sharp::HasAttr(options, "pageHeight")) {
1511
- baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight");
1512
- }
1549
+ // Animated output properties
1513
1550
  if (sharp::HasAttr(options, "loop")) {
1514
1551
  baton->loop = sharp::AttrAsUint32(options, "loop");
1515
1552
  }
1516
1553
  if (sharp::HasAttr(options, "delay")) {
1517
1554
  baton->delay = sharp::AttrAsInt32Vector(options, "delay");
1518
1555
  }
1519
-
1520
1556
  // Tile output
1521
1557
  baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
1522
1558
  baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");