sharp 0.27.1 → 0.30.5

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
@@ -67,16 +67,26 @@ class PipelineWorker : public Napi::AsyncWorker {
67
67
  vips::VImage image;
68
68
  sharp::ImageType inputImageType;
69
69
  std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
70
+ image = sharp::EnsureColourspace(image, baton->colourspaceInput);
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);
70
82
 
71
83
  // Calculate angle of rotation
72
84
  VipsAngle rotation;
85
+ bool flip = FALSE;
86
+ bool flop = FALSE;
73
87
  if (baton->useExifOrientation) {
74
88
  // Rotate and flip image according to Exif orientation
75
- bool flip;
76
- bool flop;
77
89
  std::tie(rotation, flip, flop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
78
- baton->flip = baton->flip || flip;
79
- baton->flop = baton->flop || flop;
80
90
  } else {
81
91
  rotation = CalculateAngleRotation(baton->angle);
82
92
  }
@@ -85,17 +95,29 @@ class PipelineWorker : public Napi::AsyncWorker {
85
95
  if (baton->rotateBeforePreExtract) {
86
96
  if (rotation != VIPS_ANGLE_D0) {
87
97
  image = image.rot(rotation);
98
+ }
99
+ if (flip) {
100
+ image = image.flip(VIPS_DIRECTION_VERTICAL);
101
+ }
102
+ if (flop) {
103
+ image = image.flip(VIPS_DIRECTION_HORIZONTAL);
104
+ }
105
+ if (rotation != VIPS_ANGLE_D0 || flip || flop) {
88
106
  image = sharp::RemoveExifOrientation(image);
89
107
  }
108
+ flop = FALSE;
109
+ flip = FALSE;
90
110
  if (baton->rotationAngle != 0.0) {
111
+ MultiPageUnsupported(nPages, "Rotate");
91
112
  std::vector<double> background;
92
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
113
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, FALSE);
93
114
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
94
115
  }
95
116
  }
96
117
 
97
118
  // Trim
98
119
  if (baton->trimThreshold > 0.0) {
120
+ MultiPageUnsupported(nPages, "Trim");
99
121
  image = sharp::Trim(image, baton->trimThreshold);
100
122
  baton->trimOffsetLeft = image.xoffset();
101
123
  baton->trimOffsetTop = image.yoffset();
@@ -103,199 +125,187 @@ class PipelineWorker : public Napi::AsyncWorker {
103
125
 
104
126
  // Pre extraction
105
127
  if (baton->topOffsetPre != -1) {
106
- image = image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre);
128
+ image = nPages > 1
129
+ ? sharp::CropMultiPage(image,
130
+ baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre, nPages, &pageHeight)
131
+ : image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre);
107
132
  }
108
133
 
109
134
  // Get pre-resize image width and height
110
135
  int inputWidth = image.width();
111
136
  int inputHeight = image.height();
112
- if (!baton->rotateBeforePreExtract &&
113
- (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) {
114
- // Swap input output width and height when rotating by 90 or 270 degrees
115
- std::swap(inputWidth, inputHeight);
116
- }
117
137
 
118
- // If withoutEnlargement is specified,
119
- // Override target width and height if exceeds respective value from input file
120
- if (baton->withoutEnlargement) {
121
- if (baton->width > inputWidth) {
122
- baton->width = inputWidth;
123
- }
124
- if (baton->height > inputHeight) {
125
- baton->height = inputHeight;
126
- }
138
+ // Is there just one page? Shrink to inputHeight instead
139
+ if (nPages == 1) {
140
+ pageHeight = inputHeight;
127
141
  }
128
142
 
129
143
  // Scaling calculations
130
- double xfactor = 1.0;
131
- double yfactor = 1.0;
144
+ double hshrink;
145
+ double vshrink;
132
146
  int targetResizeWidth = baton->width;
133
147
  int targetResizeHeight = baton->height;
134
- if (baton->width > 0 && baton->height > 0) {
135
- // Fixed width and height
136
- xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
137
- yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
138
- switch (baton->canvas) {
139
- case Canvas::CROP:
140
- if (xfactor < yfactor) {
141
- targetResizeHeight = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
142
- yfactor = xfactor;
143
- } else {
144
- targetResizeWidth = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
145
- xfactor = yfactor;
146
- }
147
- break;
148
- case Canvas::EMBED:
149
- if (xfactor > yfactor) {
150
- targetResizeHeight = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
151
- yfactor = xfactor;
152
- } else {
153
- targetResizeWidth = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
154
- xfactor = yfactor;
155
- }
156
- break;
157
- case Canvas::MAX:
158
- if (xfactor > yfactor) {
159
- targetResizeHeight = baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
160
- yfactor = xfactor;
161
- } else {
162
- targetResizeWidth = baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
163
- xfactor = yfactor;
164
- }
165
- break;
166
- case Canvas::MIN:
167
- if (xfactor < yfactor) {
168
- targetResizeHeight = baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
169
- yfactor = xfactor;
170
- } else {
171
- targetResizeWidth = baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
172
- xfactor = yfactor;
173
- }
174
- break;
175
- case Canvas::IGNORE_ASPECT:
176
- if (!baton->rotateBeforePreExtract &&
177
- (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) {
178
- std::swap(xfactor, yfactor);
179
- }
180
- break;
181
- }
182
- } else if (baton->width > 0) {
183
- // Fixed width
184
- xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
185
- if (baton->canvas == Canvas::IGNORE_ASPECT) {
186
- targetResizeHeight = baton->height = inputHeight;
187
- } else {
188
- // Auto height
189
- yfactor = xfactor;
190
- targetResizeHeight = baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / yfactor));
191
- }
192
- } else if (baton->height > 0) {
193
- // Fixed height
194
- yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
195
- if (baton->canvas == Canvas::IGNORE_ASPECT) {
196
- targetResizeWidth = baton->width = inputWidth;
197
- } else {
198
- // Auto width
199
- xfactor = yfactor;
200
- targetResizeWidth = baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / xfactor));
201
- }
202
- } else {
203
- // Identity transform
204
- baton->width = inputWidth;
205
- baton->height = inputHeight;
206
- }
207
-
208
- // Calculate integral box shrink
209
- int xshrink = std::max(1, static_cast<int>(floor(xfactor)));
210
- int yshrink = std::max(1, static_cast<int>(floor(yfactor)));
211
-
212
- // Calculate residual float affine transformation
213
- double xresidual = static_cast<double>(xshrink) / xfactor;
214
- double yresidual = static_cast<double>(yshrink) / yfactor;
215
148
 
216
- // If integral x and y shrink are equal, try to use shrink-on-load for JPEG and WebP,
217
- // but not when applying gamma correction, pre-resize extract or trim
218
- int shrink_on_load = 1;
219
-
220
- int shrink_on_load_factor = 1;
221
- // Leave at least a factor of two for the final resize step, when fastShrinkOnLoad: false
222
- // for more consistent results and avoid occasional small image shifting
223
- if (!baton->fastShrinkOnLoad) {
224
- shrink_on_load_factor = 2;
225
- }
226
- if (
227
- xshrink == yshrink && xshrink >= 2 * shrink_on_load_factor &&
228
- (inputImageType == sharp::ImageType::JPEG || inputImageType == sharp::ImageType::WEBP) &&
229
- baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0
230
- ) {
231
- if (xshrink >= 8 * shrink_on_load_factor) {
232
- xfactor = xfactor / 8;
233
- yfactor = yfactor / 8;
234
- shrink_on_load = 8;
235
- } else if (xshrink >= 4 * shrink_on_load_factor) {
236
- xfactor = xfactor / 4;
237
- yfactor = yfactor / 4;
238
- shrink_on_load = 4;
239
- } else if (xshrink >= 2 * shrink_on_load_factor) {
240
- xfactor = xfactor / 2;
241
- yfactor = yfactor / 2;
242
- shrink_on_load = 2;
149
+ // Swap input output width and height when rotating by 90 or 270 degrees
150
+ bool swap = !baton->rotateBeforePreExtract && (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270);
151
+
152
+ // Shrink to pageHeight, so we work for multi-page images
153
+ std::tie(hshrink, vshrink) = sharp::ResolveShrink(
154
+ inputWidth, pageHeight, targetResizeWidth, targetResizeHeight,
155
+ baton->canvas, swap, baton->withoutEnlargement, baton->withoutReduction);
156
+
157
+ // The jpeg preload shrink.
158
+ int jpegShrinkOnLoad = 1;
159
+
160
+ // WebP, PDF, SVG scale
161
+ double scale = 1.0;
162
+
163
+ // Try to reload input using shrink-on-load for JPEG, WebP, SVG and PDF, when:
164
+ // - the width or height parameters are specified;
165
+ // - gamma correction doesn't need to be applied;
166
+ // - trimming or pre-resize extract isn't required;
167
+ // - input colourspace is not specified;
168
+ bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
169
+ baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0 &&
170
+ baton->colourspaceInput == VIPS_INTERPRETATION_LAST;
171
+
172
+ if (shouldPreShrink) {
173
+ // The common part of the shrink: the bit by which both axes must be shrunk
174
+ double shrink = std::min(hshrink, vshrink);
175
+
176
+ if (inputImageType == sharp::ImageType::JPEG) {
177
+ // Leave at least a factor of two for the final resize step, when fastShrinkOnLoad: false
178
+ // for more consistent results and to avoid extra sharpness to the image
179
+ int factor = baton->fastShrinkOnLoad ? 1 : 2;
180
+ if (shrink >= 8 * factor) {
181
+ jpegShrinkOnLoad = 8;
182
+ } else if (shrink >= 4 * factor) {
183
+ jpegShrinkOnLoad = 4;
184
+ } else if (shrink >= 2 * factor) {
185
+ jpegShrinkOnLoad = 2;
186
+ }
187
+ // Lower shrink-on-load for known libjpeg rounding errors
188
+ if (jpegShrinkOnLoad > 1 && static_cast<int>(shrink) == jpegShrinkOnLoad) {
189
+ jpegShrinkOnLoad /= 2;
190
+ }
191
+ } else if (inputImageType == sharp::ImageType::WEBP ||
192
+ inputImageType == sharp::ImageType::SVG ||
193
+ inputImageType == sharp::ImageType::PDF) {
194
+ scale = 1.0 / shrink;
243
195
  }
244
196
  }
245
- // Help ensure a final kernel-based reduction to prevent shrink aliasing
246
- if (shrink_on_load > 1 && (xresidual == 1.0 || yresidual == 1.0)) {
247
- shrink_on_load = shrink_on_load / 2;
248
- xfactor = xfactor * 2;
249
- yfactor = yfactor * 2;
250
- }
251
- if (shrink_on_load > 1) {
252
- // Reload input using shrink-on-load
197
+
198
+ // Reload input using shrink-on-load, it'll be an integer shrink
199
+ // factor for jpegload*, a double scale factor for webpload*,
200
+ // pdfload* and svgload*
201
+ if (jpegShrinkOnLoad > 1) {
253
202
  vips::VOption *option = VImage::option()
254
203
  ->set("access", baton->input->access)
255
- ->set("shrink", shrink_on_load)
256
- ->set("fail", baton->input->failOnError);
204
+ ->set("shrink", jpegShrinkOnLoad)
205
+ ->set("fail_on", baton->input->failOn);
257
206
  if (baton->input->buffer != nullptr) {
207
+ // Reload JPEG buffer
258
208
  VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
259
- if (inputImageType == sharp::ImageType::JPEG) {
260
- // Reload JPEG buffer
261
- image = VImage::jpegload_buffer(blob, option);
262
- } else {
263
- // Reload WebP buffer
264
- image = VImage::webpload_buffer(blob, option);
265
- }
209
+ image = VImage::jpegload_buffer(blob, option);
266
210
  vips_area_unref(reinterpret_cast<VipsArea*>(blob));
267
211
  } else {
268
- if (inputImageType == sharp::ImageType::JPEG) {
269
- // Reload JPEG file
270
- image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option);
212
+ // Reload JPEG file
213
+ image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option);
214
+ }
215
+ } else if (scale != 1.0) {
216
+ vips::VOption *option = VImage::option()
217
+ ->set("access", baton->input->access)
218
+ ->set("scale", scale)
219
+ ->set("fail_on", baton->input->failOn);
220
+ if (inputImageType == sharp::ImageType::WEBP) {
221
+ option->set("n", baton->input->pages);
222
+ option->set("page", baton->input->page);
223
+
224
+ if (baton->input->buffer != nullptr) {
225
+ // Reload WebP buffer
226
+ VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
227
+ image = VImage::webpload_buffer(blob, option);
228
+ vips_area_unref(reinterpret_cast<VipsArea*>(blob));
271
229
  } else {
272
230
  // Reload WebP file
273
231
  image = VImage::webpload(const_cast<char*>(baton->input->file.data()), option);
274
232
  }
233
+ } else if (inputImageType == sharp::ImageType::SVG) {
234
+ option->set("unlimited", baton->input->unlimited);
235
+ option->set("dpi", baton->input->density);
236
+
237
+ if (baton->input->buffer != nullptr) {
238
+ // Reload SVG buffer
239
+ VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
240
+ image = VImage::svgload_buffer(blob, option);
241
+ vips_area_unref(reinterpret_cast<VipsArea*>(blob));
242
+ } else {
243
+ // Reload SVG file
244
+ image = VImage::svgload(const_cast<char*>(baton->input->file.data()), option);
245
+ }
246
+ sharp::SetDensity(image, baton->input->density);
247
+ if (image.width() > 32767 || image.height() > 32767) {
248
+ throw vips::VError("Input SVG image will exceed 32767x32767 pixel limit when scaled");
249
+ }
250
+ } else if (inputImageType == sharp::ImageType::PDF) {
251
+ option->set("n", baton->input->pages);
252
+ option->set("page", baton->input->page);
253
+ option->set("dpi", baton->input->density);
254
+
255
+ if (baton->input->buffer != nullptr) {
256
+ // Reload PDF buffer
257
+ VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
258
+ image = VImage::pdfload_buffer(blob, option);
259
+ vips_area_unref(reinterpret_cast<VipsArea*>(blob));
260
+ } else {
261
+ // Reload PDF file
262
+ image = VImage::pdfload(const_cast<char*>(baton->input->file.data()), option);
263
+ }
264
+
265
+ sharp::SetDensity(image, baton->input->density);
275
266
  }
276
- // Recalculate integral shrink and double residual
277
- int const shrunkOnLoadWidth = image.width();
278
- int const shrunkOnLoadHeight = image.height();
279
- if (!baton->rotateBeforePreExtract &&
280
- (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) {
281
- // Swap when rotating by 90 or 270 degrees
282
- xfactor = static_cast<double>(shrunkOnLoadWidth) / static_cast<double>(targetResizeHeight);
283
- yfactor = static_cast<double>(shrunkOnLoadHeight) / static_cast<double>(targetResizeWidth);
284
- } else {
285
- xfactor = static_cast<double>(shrunkOnLoadWidth) / static_cast<double>(targetResizeWidth);
286
- yfactor = static_cast<double>(shrunkOnLoadHeight) / static_cast<double>(targetResizeHeight);
267
+ } else {
268
+ if (inputImageType == sharp::ImageType::SVG && (image.width() > 32767 || image.height() > 32767)) {
269
+ throw vips::VError("Input SVG image exceeds 32767x32767 pixel limit");
287
270
  }
288
271
  }
289
272
 
273
+ // Any pre-shrinking may already have been done
274
+ inputWidth = image.width();
275
+ inputHeight = image.height();
276
+
277
+ // After pre-shrink, but before the main shrink stage
278
+ // Reuse the initial pageHeight if we didn't pre-shrink
279
+ if (shouldPreShrink) {
280
+ pageHeight = sharp::GetPageHeight(image);
281
+ }
282
+
283
+ // Shrink to pageHeight, so we work for multi-page images
284
+ std::tie(hshrink, vshrink) = sharp::ResolveShrink(
285
+ inputWidth, pageHeight, targetResizeWidth, targetResizeHeight,
286
+ baton->canvas, swap, baton->withoutEnlargement, baton->withoutReduction);
287
+
288
+ int targetHeight = static_cast<int>(std::rint(static_cast<double>(pageHeight) / vshrink));
289
+ int targetPageHeight = targetHeight;
290
+
291
+ // In toilet-roll mode, we must adjust vshrink so that we exactly hit
292
+ // pageHeight or we'll have pixels straddling pixel boundaries
293
+ if (inputHeight > pageHeight) {
294
+ targetHeight *= nPages;
295
+ vshrink = static_cast<double>(inputHeight) / targetHeight;
296
+ }
297
+
290
298
  // Ensure we're using a device-independent colour space
299
+ char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb";
291
300
  if (
292
301
  sharp::HasProfile(image) &&
293
302
  image.interpretation() != VIPS_INTERPRETATION_LABS &&
294
- image.interpretation() != VIPS_INTERPRETATION_GREY16
303
+ image.interpretation() != VIPS_INTERPRETATION_GREY16 &&
304
+ image.interpretation() != VIPS_INTERPRETATION_B_W
295
305
  ) {
296
- // Convert to sRGB using embedded profile
306
+ // Convert to sRGB/P3 using embedded profile
297
307
  try {
298
- image = image.icc_transform("srgb", VImage::option()
308
+ image = image.icc_transform(processingProfile, VImage::option()
299
309
  ->set("embedded", TRUE)
300
310
  ->set("depth", image.interpretation() == VIPS_INTERPRETATION_RGB16 ? 16 : 8)
301
311
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
@@ -303,7 +313,7 @@ class PipelineWorker : public Napi::AsyncWorker {
303
313
  // Ignore failure of embedded profile
304
314
  }
305
315
  } else if (image.interpretation() == VIPS_INTERPRETATION_CMYK) {
306
- image = image.icc_transform("srgb", VImage::option()
316
+ image = image.icc_transform(processingProfile, VImage::option()
307
317
  ->set("input_profile", "cmyk")
308
318
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
309
319
  }
@@ -324,7 +334,7 @@ class PipelineWorker : public Napi::AsyncWorker {
324
334
 
325
335
  // Negate the colours in the image
326
336
  if (baton->negate) {
327
- image = image.invert();
337
+ image = sharp::Negate(image, baton->negateAlpha);
328
338
  }
329
339
 
330
340
  // Gamma encoding (darken)
@@ -337,20 +347,22 @@ class PipelineWorker : public Napi::AsyncWorker {
337
347
  image = image.colourspace(VIPS_INTERPRETATION_B_W);
338
348
  }
339
349
 
340
- bool const shouldResize = xfactor != 1.0 || yfactor != 1.0;
350
+ bool const shouldResize = hshrink != 1.0 || vshrink != 1.0;
341
351
  bool const shouldBlur = baton->blurSigma != 0.0;
342
352
  bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
343
353
  bool const shouldSharpen = baton->sharpenSigma != 0.0;
344
354
  bool const shouldApplyMedian = baton->medianSize > 0;
345
355
  bool const shouldComposite = !baton->composite.empty();
346
- bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0;
356
+ bool const shouldModulate = baton->brightness != 1.0 || baton->saturation != 1.0 ||
357
+ baton->hue != 0.0 || baton->lightness != 0.0;
358
+ bool const shouldApplyClahe = baton->claheWidth != 0 && baton->claheHeight != 0;
347
359
 
348
360
  if (shouldComposite && !sharp::HasAlpha(image)) {
349
- image = sharp::EnsureAlpha(image);
361
+ image = sharp::EnsureAlpha(image, 1);
350
362
  }
351
363
 
352
364
  bool const shouldPremultiplyAlpha = sharp::HasAlpha(image) &&
353
- (shouldResize || shouldBlur || shouldConv || shouldSharpen || shouldComposite);
365
+ (shouldResize || shouldBlur || shouldConv || shouldSharpen);
354
366
 
355
367
  // Premultiply image alpha channel before all transformations to avoid
356
368
  // dark fringing around bright pixels
@@ -369,35 +381,33 @@ class PipelineWorker : public Napi::AsyncWorker {
369
381
  ) {
370
382
  throw vips::VError("Unknown kernel");
371
383
  }
372
- // Ensure shortest edge is at least 1 pixel
373
- if (image.width() / xfactor < 0.5) {
374
- xfactor = 2 * image.width();
375
- baton->width = 1;
376
- }
377
- if (image.height() / yfactor < 0.5) {
378
- yfactor = 2 * image.height();
379
- baton->height = 1;
380
- }
381
- image = image.resize(1.0 / xfactor, VImage::option()
382
- ->set("vscale", 1.0 / yfactor)
384
+ image = image.resize(1.0 / hshrink, VImage::option()
385
+ ->set("vscale", 1.0 / vshrink)
383
386
  ->set("kernel", kernel));
384
387
  }
385
388
 
386
389
  // Rotate post-extract 90-angle
387
- if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
388
- image = image.rot(rotation);
389
- image = sharp::RemoveExifOrientation(image);
390
+ if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
391
+ image = image.rot(rotation);
392
+ if (flip) {
393
+ image = image.flip(VIPS_DIRECTION_VERTICAL);
394
+ flip = FALSE;
395
+ }
396
+ if (flop) {
397
+ image = image.flip(VIPS_DIRECTION_HORIZONTAL);
398
+ flop = FALSE;
399
+ }
400
+ image = sharp::RemoveExifOrientation(image);
390
401
  }
391
402
 
392
-
393
403
  // Flip (mirror about Y axis)
394
- if (baton->flip) {
404
+ if (baton->flip || flip) {
395
405
  image = image.flip(VIPS_DIRECTION_VERTICAL);
396
406
  image = sharp::RemoveExifOrientation(image);
397
407
  }
398
408
 
399
409
  // Flop (mirror about X axis)
400
- if (baton->flop) {
410
+ if (baton->flop || flop) {
401
411
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
402
412
  image = sharp::RemoveExifOrientation(image);
403
413
  }
@@ -409,57 +419,74 @@ class PipelineWorker : public Napi::AsyncWorker {
409
419
 
410
420
  for (unsigned int i = 0; i < baton->joinChannelIn.size(); i++) {
411
421
  std::tie(joinImage, joinImageType) = sharp::OpenInput(baton->joinChannelIn[i]);
422
+ joinImage = sharp::EnsureColourspace(joinImage, baton->colourspaceInput);
412
423
  image = image.bandjoin(joinImage);
413
424
  }
414
425
  image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
415
426
  }
416
427
 
428
+ inputWidth = image.width();
429
+ inputHeight = nPages > 1 ? targetPageHeight : image.height();
430
+
431
+ // Resolve dimensions
432
+ if (baton->width <= 0) {
433
+ baton->width = inputWidth;
434
+ }
435
+ if (baton->height <= 0) {
436
+ baton->height = inputHeight;
437
+ }
438
+
417
439
  // Crop/embed
418
- if (image.width() != baton->width || image.height() != baton->height) {
419
- if (baton->canvas == Canvas::EMBED) {
440
+ if (inputWidth != baton->width || inputHeight != baton->height) {
441
+ if (baton->canvas == sharp::Canvas::EMBED) {
420
442
  std::vector<double> background;
421
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground);
443
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
422
444
 
423
445
  // Embed
424
446
 
425
- // Calculate where to position the embeded image if gravity specified, else center.
447
+ // Calculate where to position the embedded image if gravity specified, else center.
426
448
  int left;
427
449
  int top;
428
450
 
429
- left = static_cast<int>(round((baton->width - image.width()) / 2));
430
- top = static_cast<int>(round((baton->height - image.height()) / 2));
451
+ left = static_cast<int>(round((baton->width - inputWidth) / 2));
452
+ top = static_cast<int>(round((baton->height - inputHeight) / 2));
431
453
 
432
- int width = std::max(image.width(), baton->width);
433
- int height = std::max(image.height(), baton->height);
454
+ int width = std::max(inputWidth, baton->width);
455
+ int height = std::max(inputHeight, baton->height);
434
456
  std::tie(left, top) = sharp::CalculateEmbedPosition(
435
- image.width(), image.height(), baton->width, baton->height, baton->position);
436
-
437
- image = image.embed(left, top, width, height, VImage::option()
438
- ->set("extend", VIPS_EXTEND_BACKGROUND)
439
- ->set("background", background));
457
+ inputWidth, inputHeight, baton->width, baton->height, baton->position);
458
+
459
+ image = nPages > 1
460
+ ? sharp::EmbedMultiPage(image,
461
+ left, top, width, height, background, nPages, &targetPageHeight)
462
+ : image.embed(left, top, width, height, VImage::option()
463
+ ->set("extend", VIPS_EXTEND_BACKGROUND)
464
+ ->set("background", background));
465
+ } else if (baton->canvas == sharp::Canvas::CROP) {
466
+ if (baton->width > inputWidth) {
467
+ baton->width = inputWidth;
468
+ }
469
+ if (baton->height > inputHeight) {
470
+ baton->height = inputHeight;
471
+ }
440
472
 
441
- } else if (
442
- baton->canvas != Canvas::IGNORE_ASPECT &&
443
- (image.width() > baton->width || image.height() > baton->height)
444
- ) {
445
- // Crop/max/min
473
+ // Crop
446
474
  if (baton->position < 9) {
447
475
  // Gravity-based crop
448
476
  int left;
449
477
  int top;
450
478
  std::tie(left, top) = sharp::CalculateCrop(
451
- image.width(), image.height(), baton->width, baton->height, baton->position);
452
- int width = std::min(image.width(), baton->width);
453
- int height = std::min(image.height(), baton->height);
454
- image = image.extract_area(left, top, width, height);
479
+ inputWidth, inputHeight, baton->width, baton->height, baton->position);
480
+ int width = std::min(inputWidth, baton->width);
481
+ int height = std::min(inputHeight, baton->height);
482
+
483
+ image = nPages > 1
484
+ ? sharp::CropMultiPage(image,
485
+ left, top, width, height, nPages, &targetPageHeight)
486
+ : image.extract_area(left, top, width, height);
455
487
  } else {
456
488
  // Attention-based or Entropy-based crop
457
- if (baton->width > image.width()) {
458
- baton->width = image.width();
459
- }
460
- if (baton->height > image.height()) {
461
- baton->height = image.height();
462
- }
489
+ MultiPageUnsupported(nPages, "Resize strategy");
463
490
  image = image.tilecache(VImage::option()
464
491
  ->set("access", VIPS_ACCESS_RANDOM)
465
492
  ->set("threaded", TRUE));
@@ -474,40 +501,56 @@ class PipelineWorker : public Napi::AsyncWorker {
474
501
 
475
502
  // Rotate post-extract non-90 angle
476
503
  if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
504
+ MultiPageUnsupported(nPages, "Rotate");
477
505
  std::vector<double> background;
478
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground);
506
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
479
507
  image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
480
508
  }
481
509
 
482
510
  // Post extraction
483
511
  if (baton->topOffsetPost != -1) {
484
- image = image.extract_area(
485
- baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
512
+ if (nPages > 1) {
513
+ image = sharp::CropMultiPage(image,
514
+ baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost,
515
+ nPages, &targetPageHeight);
516
+
517
+ // heightPost is used in the info object, so update to reflect the number of pages
518
+ baton->heightPost *= nPages;
519
+ } else {
520
+ image = image.extract_area(
521
+ baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
522
+ }
486
523
  }
487
524
 
488
525
  // Affine transform
489
526
  if (baton->affineMatrix.size() > 0) {
527
+ MultiPageUnsupported(nPages, "Affine");
490
528
  std::vector<double> background;
491
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground);
529
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
530
+ vips::VInterpolate interp = vips::VInterpolate::new_from_name(
531
+ const_cast<char*>(baton->affineInterpolator.data()));
492
532
  image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
493
533
  ->set("idx", baton->affineIdx)
494
534
  ->set("idy", baton->affineIdy)
495
535
  ->set("odx", baton->affineOdx)
496
536
  ->set("ody", baton->affineOdy)
497
- ->set("interpolate", baton->affineInterpolator));
537
+ ->set("interpolate", interp));
498
538
  }
499
539
 
500
540
  // Extend edges
501
541
  if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
502
542
  std::vector<double> background;
503
- std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground);
543
+ std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
504
544
 
505
545
  // Embed
506
546
  baton->width = image.width() + baton->extendLeft + baton->extendRight;
507
- baton->height = image.height() + baton->extendTop + baton->extendBottom;
547
+ baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
508
548
 
509
- image = image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
510
- VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
549
+ image = nPages > 1
550
+ ? sharp::EmbedMultiPage(image,
551
+ baton->extendLeft, baton->extendTop, baton->width, baton->height, background, nPages, &targetPageHeight)
552
+ : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
553
+ VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
511
554
  }
512
555
  // Median - must happen before blurring, due to the utility of blurring after thresholding
513
556
  if (shouldApplyMedian) {
@@ -537,20 +580,24 @@ class PipelineWorker : public Napi::AsyncWorker {
537
580
  }
538
581
 
539
582
  if (shouldModulate) {
540
- image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue);
583
+ image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
541
584
  }
542
585
 
543
586
  // Sharpen
544
587
  if (shouldSharpen) {
545
- image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
588
+ image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenM1, baton->sharpenM2,
589
+ baton->sharpenX1, baton->sharpenY2, baton->sharpenY3);
546
590
  }
547
591
 
548
592
  // Composite
549
593
  if (shouldComposite) {
594
+ std::vector<VImage> images = { image };
595
+ std::vector<int> modes, xs, ys;
550
596
  for (Composite *composite : baton->composite) {
551
597
  VImage compositeImage;
552
598
  sharp::ImageType compositeImageType = sharp::ImageType::UNKNOWN;
553
- std::tie(compositeImage, compositeImageType) = OpenInput(composite->input);
599
+ std::tie(compositeImage, compositeImageType) = sharp::OpenInput(composite->input);
600
+ compositeImage = sharp::EnsureColourspace(compositeImage, baton->colourspaceInput);
554
601
  // Verify within current dimensions
555
602
  if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
556
603
  throw vips::VError("Image to composite must have same dimensions or smaller");
@@ -562,9 +609,17 @@ class PipelineWorker : public Napi::AsyncWorker {
562
609
  // Use gravity in overlay
563
610
  if (compositeImage.width() <= baton->width) {
564
611
  across = static_cast<int>(ceil(static_cast<double>(image.width()) / compositeImage.width()));
612
+ // Ensure odd number of tiles across when gravity is centre, north or south
613
+ if (composite->gravity == 0 || composite->gravity == 1 || composite->gravity == 3) {
614
+ across |= 1;
615
+ }
565
616
  }
566
617
  if (compositeImage.height() <= baton->height) {
567
618
  down = static_cast<int>(ceil(static_cast<double>(image.height()) / compositeImage.height()));
619
+ // Ensure odd number of tiles down when gravity is centre, east or west
620
+ if (composite->gravity == 0 || composite->gravity == 2 || composite->gravity == 4) {
621
+ down |= 1;
622
+ }
568
623
  }
569
624
  if (across != 0 || down != 0) {
570
625
  int left;
@@ -583,30 +638,35 @@ class PipelineWorker : public Napi::AsyncWorker {
583
638
  // gravity was used for extract_area, set it back to its default value of 0
584
639
  composite->gravity = 0;
585
640
  }
586
- // Ensure image to composite is sRGB with premultiplied alpha
641
+ // Ensure image to composite is sRGB with unpremultiplied alpha
587
642
  compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB);
588
643
  if (!sharp::HasAlpha(compositeImage)) {
589
- compositeImage = sharp::EnsureAlpha(compositeImage);
644
+ compositeImage = sharp::EnsureAlpha(compositeImage, 1);
590
645
  }
591
- if (!composite->premultiplied) compositeImage = compositeImage.premultiply();
646
+ if (composite->premultiplied) compositeImage = compositeImage.unpremultiply();
592
647
  // Calculate position
593
648
  int left;
594
649
  int top;
595
650
  if (composite->hasOffset) {
596
651
  // Composite image at given offsets
597
- std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
598
- compositeImage.width(), compositeImage.height(), composite->left, composite->top);
652
+ if (composite->tile) {
653
+ std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
654
+ compositeImage.width(), compositeImage.height(), composite->left, composite->top);
655
+ } else {
656
+ left = composite->left;
657
+ top = composite->top;
658
+ }
599
659
  } else {
600
660
  // Composite image with given gravity
601
661
  std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
602
662
  compositeImage.width(), compositeImage.height(), composite->gravity);
603
663
  }
604
- // Composite
605
- image = image.composite2(compositeImage, composite->mode, VImage::option()
606
- ->set("premultiplied", TRUE)
607
- ->set("x", left)
608
- ->set("y", top));
664
+ images.push_back(compositeImage);
665
+ modes.push_back(composite->mode);
666
+ xs.push_back(left);
667
+ ys.push_back(top);
609
668
  }
669
+ image = image.composite(images, modes, VImage::option()->set("x", xs)->set("y", ys));
610
670
  }
611
671
 
612
672
  // Reverse premultiplication after all transformations:
@@ -636,11 +696,17 @@ class PipelineWorker : public Napi::AsyncWorker {
636
696
  image = sharp::Normalise(image);
637
697
  }
638
698
 
699
+ // Apply contrast limiting adaptive histogram equalization (CLAHE)
700
+ if (shouldApplyClahe) {
701
+ image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope);
702
+ }
703
+
639
704
  // Apply bitwise boolean operation between images
640
705
  if (baton->boolean != nullptr) {
641
706
  VImage booleanImage;
642
707
  sharp::ImageType booleanImageType = sharp::ImageType::UNKNOWN;
643
708
  std::tie(booleanImage, booleanImageType) = sharp::OpenInput(baton->boolean);
709
+ booleanImage = sharp::EnsureColourspace(booleanImage, baton->colourspaceInput);
644
710
  image = sharp::Boolean(image, booleanImage, baton->booleanOp);
645
711
  }
646
712
 
@@ -678,8 +744,8 @@ class PipelineWorker : public Napi::AsyncWorker {
678
744
  }
679
745
 
680
746
  // Ensure alpha channel, if missing
681
- if (baton->ensureAlpha) {
682
- image = sharp::EnsureAlpha(image);
747
+ if (baton->ensureAlpha != -1) {
748
+ image = sharp::EnsureAlpha(image, baton->ensureAlpha);
683
749
  }
684
750
 
685
751
  // Convert image to sRGB, if not already
@@ -690,9 +756,10 @@ class PipelineWorker : public Napi::AsyncWorker {
690
756
  // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess
691
757
  image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
692
758
  // Transform colours from embedded profile to output profile
693
- if (baton->withMetadata && sharp::HasProfile(image)) {
694
- image = image.icc_transform(vips_enum_nick(VIPS_TYPE_INTERPRETATION, baton->colourspace),
695
- VImage::option()->set("embedded", TRUE));
759
+ if (baton->withMetadata && sharp::HasProfile(image) && baton->withMetadataIcc.empty()) {
760
+ image = image.icc_transform("srgb", VImage::option()
761
+ ->set("embedded", TRUE)
762
+ ->set("intent", VIPS_INTENT_PERCEPTUAL));
696
763
  }
697
764
  }
698
765
 
@@ -701,30 +768,36 @@ class PipelineWorker : public Napi::AsyncWorker {
701
768
  image = image.icc_transform(
702
769
  const_cast<char*>(baton->withMetadataIcc.data()),
703
770
  VImage::option()
704
- ->set("input_profile", "srgb")
771
+ ->set("input_profile", processingProfile)
772
+ ->set("embedded", TRUE)
705
773
  ->set("intent", VIPS_INTENT_PERCEPTUAL));
706
774
  }
707
-
708
775
  // Override EXIF Orientation tag
709
776
  if (baton->withMetadata && baton->withMetadataOrientation != -1) {
710
777
  image = sharp::SetExifOrientation(image, baton->withMetadataOrientation);
711
778
  }
779
+ // Override pixel density
780
+ if (baton->withMetadataDensity > 0) {
781
+ image = sharp::SetDensity(image, baton->withMetadataDensity);
782
+ }
783
+ // Metadata key/value pairs, e.g. EXIF
784
+ if (!baton->withMetadataStrs.empty()) {
785
+ image = image.copy();
786
+ for (const auto& s : baton->withMetadataStrs) {
787
+ image.set(s.first.data(), s.second.data());
788
+ }
789
+ }
712
790
 
713
791
  // Number of channels used in output image
714
792
  baton->channels = image.bands();
715
793
  baton->width = image.width();
716
794
  baton->height = image.height();
717
795
 
718
- bool const supportsGifOutput = vips_type_find("VipsOperation", "magicksave") != 0 &&
719
- vips_type_find("VipsOperation", "magicksave_buffer") != 0;
720
-
721
796
  image = sharp::SetAnimationProperties(
722
- image,
723
- baton->pageHeight,
724
- baton->delay,
725
- baton->loop);
797
+ image, nPages, targetPageHeight, baton->delay, baton->loop);
726
798
 
727
799
  // Output
800
+ sharp::SetTimeout(image, baton->timeoutSeconds);
728
801
  if (baton->fileOut.empty()) {
729
802
  // Buffer output
730
803
  if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
@@ -735,8 +808,8 @@ class PipelineWorker : public Napi::AsyncWorker {
735
808
  ->set("Q", baton->jpegQuality)
736
809
  ->set("interlace", baton->jpegProgressive)
737
810
  ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
738
- ? VIPS_FOREIGN_JPEG_SUBSAMPLE_OFF
739
- : VIPS_FOREIGN_JPEG_SUBSAMPLE_ON)
811
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
812
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
740
813
  ->set("trellis_quant", baton->jpegTrellisQuantisation)
741
814
  ->set("quant_table", baton->jpegQuantisationTable)
742
815
  ->set("overshoot_deringing", baton->jpegOvershootDeringing)
@@ -752,9 +825,24 @@ class PipelineWorker : public Napi::AsyncWorker {
752
825
  } else {
753
826
  baton->channels = std::min(baton->channels, 3);
754
827
  }
828
+ } else if (baton->formatOut == "jp2" || (baton->formatOut == "input"
829
+ && inputImageType == sharp::ImageType::JP2)) {
830
+ // Write JP2 to Buffer
831
+ sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
832
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.jp2ksave_buffer(VImage::option()
833
+ ->set("Q", baton->jp2Quality)
834
+ ->set("lossless", baton->jp2Lossless)
835
+ ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
836
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
837
+ ->set("tile_height", baton->jp2TileHeight)
838
+ ->set("tile_width", baton->jp2TileWidth)));
839
+ baton->bufferOut = static_cast<char*>(area->data);
840
+ baton->bufferOutLength = area->length;
841
+ area->free_fn = nullptr;
842
+ vips_area_unref(area);
843
+ baton->formatOut = "jp2";
755
844
  } else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
756
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
757
- inputImageType == sharp::ImageType::SVG))) {
845
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
758
846
  // Write PNG to buffer
759
847
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
760
848
  VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option()
@@ -764,7 +852,8 @@ class PipelineWorker : public Napi::AsyncWorker {
764
852
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
765
853
  ->set("palette", baton->pngPalette)
766
854
  ->set("Q", baton->pngQuality)
767
- ->set("colours", baton->pngColours)
855
+ ->set("effort", baton->pngEffort)
856
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
768
857
  ->set("dither", baton->pngDither)));
769
858
  baton->bufferOut = static_cast<char*>(area->data);
770
859
  baton->bufferOutLength = area->length;
@@ -781,7 +870,7 @@ class PipelineWorker : public Napi::AsyncWorker {
781
870
  ->set("lossless", baton->webpLossless)
782
871
  ->set("near_lossless", baton->webpNearLossless)
783
872
  ->set("smart_subsample", baton->webpSmartSubsample)
784
- ->set("reduction_effort", baton->webpReductionEffort)
873
+ ->set("effort", baton->webpEffort)
785
874
  ->set("alpha_q", baton->webpAlphaQuality)));
786
875
  baton->bufferOut = static_cast<char*>(area->data);
787
876
  baton->bufferOutLength = area->length;
@@ -789,14 +878,14 @@ class PipelineWorker : public Napi::AsyncWorker {
789
878
  vips_area_unref(area);
790
879
  baton->formatOut = "webp";
791
880
  } else if (baton->formatOut == "gif" ||
792
- (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
881
+ (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF)) {
793
882
  // Write GIF to buffer
794
883
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
795
- VipsArea *area = reinterpret_cast<VipsArea*>(image.magicksave_buffer(VImage::option()
884
+ VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option()
796
885
  ->set("strip", !baton->withMetadata)
797
- ->set("optimize_gif_frames", TRUE)
798
- ->set("optimize_gif_transparency", TRUE)
799
- ->set("format", "gif")));
886
+ ->set("bitdepth", baton->gifBitdepth)
887
+ ->set("effort", baton->gifEffort)
888
+ ->set("dither", baton->gifDither)));
800
889
  baton->bufferOut = static_cast<char*>(area->data);
801
890
  baton->bufferOutLength = area->length;
802
891
  area->free_fn = nullptr;
@@ -824,7 +913,8 @@ class PipelineWorker : public Napi::AsyncWorker {
824
913
  ->set("tile_height", baton->tiffTileHeight)
825
914
  ->set("tile_width", baton->tiffTileWidth)
826
915
  ->set("xres", baton->tiffXres)
827
- ->set("yres", baton->tiffYres)));
916
+ ->set("yres", baton->tiffYres)
917
+ ->set("resunit", baton->tiffResolutionUnit)));
828
918
  baton->bufferOut = static_cast<char*>(area->data);
829
919
  baton->bufferOutLength = area->length;
830
920
  area->free_fn = nullptr;
@@ -833,15 +923,14 @@ class PipelineWorker : public Napi::AsyncWorker {
833
923
  } else if (baton->formatOut == "heif" ||
834
924
  (baton->formatOut == "input" && inputImageType == sharp::ImageType::HEIF)) {
835
925
  // Write HEIF to buffer
926
+ image = sharp::RemoveAnimationProperties(image);
836
927
  VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option()
837
928
  ->set("strip", !baton->withMetadata)
838
- ->set("compression", baton->heifCompression)
839
929
  ->set("Q", baton->heifQuality)
840
- ->set("speed", baton->heifSpeed)
841
- #if defined(VIPS_TYPE_FOREIGN_SUBSAMPLE)
930
+ ->set("compression", baton->heifCompression)
931
+ ->set("effort", baton->heifEffort)
842
932
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
843
933
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
844
- #endif
845
934
  ->set("lossless", baton->heifLossless)));
846
935
  baton->bufferOut = static_cast<char*>(area->data);
847
936
  baton->bufferOutLength = area->length;
@@ -856,9 +945,9 @@ class PipelineWorker : public Napi::AsyncWorker {
856
945
  image = image[0];
857
946
  baton->channels = 1;
858
947
  }
859
- if (image.format() != VIPS_FORMAT_UCHAR) {
860
- // Cast pixels to uint8 (unsigned char)
861
- image = image.cast(VIPS_FORMAT_UCHAR);
948
+ if (image.format() != baton->rawDepth) {
949
+ // Cast pixels to requested format
950
+ image = image.cast(baton->rawDepth);
862
951
  }
863
952
  // Get raw image data
864
953
  baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
@@ -884,13 +973,14 @@ class PipelineWorker : public Napi::AsyncWorker {
884
973
  bool const isWebp = sharp::IsWebp(baton->fileOut);
885
974
  bool const isGif = sharp::IsGif(baton->fileOut);
886
975
  bool const isTiff = sharp::IsTiff(baton->fileOut);
976
+ bool const isJp2 = sharp::IsJp2(baton->fileOut);
887
977
  bool const isHeif = sharp::IsHeif(baton->fileOut);
888
978
  bool const isDz = sharp::IsDz(baton->fileOut);
889
979
  bool const isDzZip = sharp::IsDzZip(baton->fileOut);
890
980
  bool const isV = sharp::IsV(baton->fileOut);
891
981
  bool const mightMatchInput = baton->formatOut == "input";
892
982
  bool const willMatchInput = mightMatchInput &&
893
- !(isJpeg || isPng || isWebp || isGif || isTiff || isHeif || isDz || isDzZip || isV);
983
+ !(isJpeg || isPng || isWebp || isGif || isTiff || isJp2 || isHeif || isDz || isDzZip || isV);
894
984
 
895
985
  if (baton->formatOut == "jpeg" || (mightMatchInput && isJpeg) ||
896
986
  (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
@@ -901,8 +991,8 @@ class PipelineWorker : public Napi::AsyncWorker {
901
991
  ->set("Q", baton->jpegQuality)
902
992
  ->set("interlace", baton->jpegProgressive)
903
993
  ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
904
- ? VIPS_FOREIGN_JPEG_SUBSAMPLE_OFF
905
- : VIPS_FOREIGN_JPEG_SUBSAMPLE_ON)
994
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF
995
+ : VIPS_FOREIGN_SUBSAMPLE_ON)
906
996
  ->set("trellis_quant", baton->jpegTrellisQuantisation)
907
997
  ->set("quant_table", baton->jpegQuantisationTable)
908
998
  ->set("overshoot_deringing", baton->jpegOvershootDeringing)
@@ -910,9 +1000,20 @@ class PipelineWorker : public Napi::AsyncWorker {
910
1000
  ->set("optimize_coding", baton->jpegOptimiseCoding));
911
1001
  baton->formatOut = "jpeg";
912
1002
  baton->channels = std::min(baton->channels, 3);
1003
+ } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
1004
+ (willMatchInput && (inputImageType == sharp::ImageType::JP2))) {
1005
+ // Write JP2 to file
1006
+ sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
1007
+ image.jp2ksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1008
+ ->set("Q", baton->jp2Quality)
1009
+ ->set("lossless", baton->jp2Lossless)
1010
+ ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
1011
+ ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1012
+ ->set("tile_height", baton->jp2TileHeight)
1013
+ ->set("tile_width", baton->jp2TileWidth));
1014
+ baton->formatOut = "jp2";
913
1015
  } else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
914
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
915
- inputImageType == sharp::ImageType::SVG))) {
1016
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
916
1017
  // Write PNG to file
917
1018
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
918
1019
  image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
@@ -922,7 +1023,8 @@ class PipelineWorker : public Napi::AsyncWorker {
922
1023
  ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
923
1024
  ->set("palette", baton->pngPalette)
924
1025
  ->set("Q", baton->pngQuality)
925
- ->set("colours", baton->pngColours)
1026
+ ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
1027
+ ->set("effort", baton->pngEffort)
926
1028
  ->set("dither", baton->pngDither));
927
1029
  baton->formatOut = "png";
928
1030
  } else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) ||
@@ -935,18 +1037,18 @@ class PipelineWorker : public Napi::AsyncWorker {
935
1037
  ->set("lossless", baton->webpLossless)
936
1038
  ->set("near_lossless", baton->webpNearLossless)
937
1039
  ->set("smart_subsample", baton->webpSmartSubsample)
938
- ->set("reduction_effort", baton->webpReductionEffort)
1040
+ ->set("effort", baton->webpEffort)
939
1041
  ->set("alpha_q", baton->webpAlphaQuality));
940
1042
  baton->formatOut = "webp";
941
1043
  } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
942
- (willMatchInput && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
1044
+ (willMatchInput && inputImageType == sharp::ImageType::GIF)) {
943
1045
  // Write GIF to file
944
1046
  sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
945
- image.magicksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1047
+ image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
946
1048
  ->set("strip", !baton->withMetadata)
947
- ->set("optimize_gif_frames", TRUE)
948
- ->set("optimize_gif_transparency", TRUE)
949
- ->set("format", "gif"));
1049
+ ->set("bitdepth", baton->gifBitdepth)
1050
+ ->set("effort", baton->gifEffort)
1051
+ ->set("dither", baton->gifDither));
950
1052
  baton->formatOut = "gif";
951
1053
  } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
952
1054
  (willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
@@ -970,20 +1072,20 @@ class PipelineWorker : public Napi::AsyncWorker {
970
1072
  ->set("tile_height", baton->tiffTileHeight)
971
1073
  ->set("tile_width", baton->tiffTileWidth)
972
1074
  ->set("xres", baton->tiffXres)
973
- ->set("yres", baton->tiffYres));
1075
+ ->set("yres", baton->tiffYres)
1076
+ ->set("resunit", baton->tiffResolutionUnit));
974
1077
  baton->formatOut = "tiff";
975
1078
  } else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
976
1079
  (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
977
1080
  // Write HEIF to file
1081
+ image = sharp::RemoveAnimationProperties(image);
978
1082
  image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
979
1083
  ->set("strip", !baton->withMetadata)
980
1084
  ->set("Q", baton->heifQuality)
981
1085
  ->set("compression", baton->heifCompression)
982
- ->set("speed", baton->heifSpeed)
983
- #if defined(VIPS_TYPE_FOREIGN_SUBSAMPLE)
1086
+ ->set("effort", baton->heifEffort)
984
1087
  ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
985
1088
  ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
986
- #endif
987
1089
  ->set("lossless", baton->heifLossless));
988
1090
  baton->formatOut = "heif";
989
1091
  } else if (baton->formatOut == "dz" || isDz || isDzZip) {
@@ -1006,7 +1108,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1006
1108
  {"lossless", baton->webpLossless ? "TRUE" : "FALSE"},
1007
1109
  {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"},
1008
1110
  {"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"},
1009
- {"reduction_effort", std::to_string(baton->webpReductionEffort)}
1111
+ {"effort", std::to_string(baton->webpEffort)}
1010
1112
  };
1011
1113
  suffix = AssembleSuffixString(".webp", options);
1012
1114
  } else {
@@ -1038,6 +1140,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1038
1140
  ->set("angle", CalculateAngleRotation(baton->tileAngle))
1039
1141
  ->set("background", baton->tileBackground)
1040
1142
  ->set("centre", baton->tileCentre)
1143
+ ->set("id", const_cast<char*>(baton->tileId.data()))
1041
1144
  ->set("skip_blanks", baton->tileSkipBlanks);
1042
1145
  // libvips chooses a default depth based on layout. Instead of replicating that logic here by
1043
1146
  // not passing anything - libvips will handle choice
@@ -1099,6 +1202,9 @@ class PipelineWorker : public Napi::AsyncWorker {
1099
1202
  info.Set("width", static_cast<uint32_t>(width));
1100
1203
  info.Set("height", static_cast<uint32_t>(height));
1101
1204
  info.Set("channels", static_cast<uint32_t>(baton->channels));
1205
+ if (baton->formatOut == "raw") {
1206
+ info.Set("depth", vips_enum_nick(VIPS_TYPE_BAND_FORMAT, baton->rawDepth));
1207
+ }
1102
1208
  info.Set("premultiplied", baton->premultiplied);
1103
1209
  if (baton->hasCropOffset) {
1104
1210
  info.Set("cropOffsetLeft", static_cast<int32_t>(baton->cropOffsetLeft));
@@ -1151,6 +1257,12 @@ class PipelineWorker : public Napi::AsyncWorker {
1151
1257
  Napi::FunctionReference debuglog;
1152
1258
  Napi::FunctionReference queueListener;
1153
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
+
1154
1266
  /*
1155
1267
  Calculate the angle of rotation and need-to-flip for the given Exif orientation
1156
1268
  By default, returns zero, i.e. no rotation.
@@ -1241,15 +1353,15 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1241
1353
  // Canvas option
1242
1354
  std::string canvas = sharp::AttrAsStr(options, "canvas");
1243
1355
  if (canvas == "crop") {
1244
- baton->canvas = Canvas::CROP;
1356
+ baton->canvas = sharp::Canvas::CROP;
1245
1357
  } else if (canvas == "embed") {
1246
- baton->canvas = Canvas::EMBED;
1358
+ baton->canvas = sharp::Canvas::EMBED;
1247
1359
  } else if (canvas == "max") {
1248
- baton->canvas = Canvas::MAX;
1360
+ baton->canvas = sharp::Canvas::MAX;
1249
1361
  } else if (canvas == "min") {
1250
- baton->canvas = Canvas::MIN;
1362
+ baton->canvas = sharp::Canvas::MIN;
1251
1363
  } else if (canvas == "ignore_aspect") {
1252
- baton->canvas = Canvas::IGNORE_ASPECT;
1364
+ baton->canvas = sharp::Canvas::IGNORE_ASPECT;
1253
1365
  }
1254
1366
  // Tint chroma
1255
1367
  baton->tintA = sharp::AttrAsDouble(options, "tintA");
@@ -1272,6 +1384,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1272
1384
  }
1273
1385
  // Resize options
1274
1386
  baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement");
1387
+ baton->withoutReduction = sharp::AttrAsBool(options, "withoutReduction");
1275
1388
  baton->position = sharp::AttrAsInt32(options, "position");
1276
1389
  baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground");
1277
1390
  baton->kernel = sharp::AttrAsStr(options, "kernel");
@@ -1288,14 +1401,19 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1288
1401
  baton->flatten = sharp::AttrAsBool(options, "flatten");
1289
1402
  baton->flattenBackground = sharp::AttrAsVectorOfDouble(options, "flattenBackground");
1290
1403
  baton->negate = sharp::AttrAsBool(options, "negate");
1404
+ baton->negateAlpha = sharp::AttrAsBool(options, "negateAlpha");
1291
1405
  baton->blurSigma = sharp::AttrAsDouble(options, "blurSigma");
1292
1406
  baton->brightness = sharp::AttrAsDouble(options, "brightness");
1293
1407
  baton->saturation = sharp::AttrAsDouble(options, "saturation");
1294
1408
  baton->hue = sharp::AttrAsInt32(options, "hue");
1409
+ baton->lightness = sharp::AttrAsDouble(options, "lightness");
1295
1410
  baton->medianSize = sharp::AttrAsUint32(options, "medianSize");
1296
1411
  baton->sharpenSigma = sharp::AttrAsDouble(options, "sharpenSigma");
1297
- baton->sharpenFlat = sharp::AttrAsDouble(options, "sharpenFlat");
1298
- baton->sharpenJagged = sharp::AttrAsDouble(options, "sharpenJagged");
1412
+ baton->sharpenM1 = sharp::AttrAsDouble(options, "sharpenM1");
1413
+ baton->sharpenM2 = sharp::AttrAsDouble(options, "sharpenM2");
1414
+ baton->sharpenX1 = sharp::AttrAsDouble(options, "sharpenX1");
1415
+ baton->sharpenY2 = sharp::AttrAsDouble(options, "sharpenY2");
1416
+ baton->sharpenY3 = sharp::AttrAsDouble(options, "sharpenY3");
1299
1417
  baton->threshold = sharp::AttrAsInt32(options, "threshold");
1300
1418
  baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale");
1301
1419
  baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
@@ -1305,6 +1423,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1305
1423
  baton->linearB = sharp::AttrAsDouble(options, "linearB");
1306
1424
  baton->greyscale = sharp::AttrAsBool(options, "greyscale");
1307
1425
  baton->normalise = sharp::AttrAsBool(options, "normalise");
1426
+ baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
1427
+ baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
1428
+ baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
1308
1429
  baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation");
1309
1430
  baton->angle = sharp::AttrAsInt32(options, "angle");
1310
1431
  baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
@@ -1324,10 +1445,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1324
1445
  baton->affineIdy = sharp::AttrAsDouble(options, "affineIdy");
1325
1446
  baton->affineOdx = sharp::AttrAsDouble(options, "affineOdx");
1326
1447
  baton->affineOdy = sharp::AttrAsDouble(options, "affineOdy");
1327
- baton->affineInterpolator = vips::VInterpolate::new_from_name(sharp::AttrAsStr(options, "affineInterpolator").data());
1328
-
1448
+ baton->affineInterpolator = sharp::AttrAsStr(options, "affineInterpolator");
1329
1449
  baton->removeAlpha = sharp::AttrAsBool(options, "removeAlpha");
1330
- baton->ensureAlpha = sharp::AttrAsBool(options, "ensureAlpha");
1450
+ baton->ensureAlpha = sharp::AttrAsDouble(options, "ensureAlpha");
1331
1451
  if (options.Has("boolean")) {
1332
1452
  baton->boolean = sharp::CreateInputDescriptor(options.Get("boolean").As<Napi::Object>());
1333
1453
  baton->booleanOp = sharp::GetBooleanOperation(sharp::AttrAsStr(options, "booleanOp"));
@@ -1355,6 +1475,10 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1355
1475
  baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i);
1356
1476
  }
1357
1477
  }
1478
+ baton->colourspaceInput = sharp::GetInterpretation(sharp::AttrAsStr(options, "colourspaceInput"));
1479
+ if (baton->colourspaceInput == VIPS_INTERPRETATION_ERROR) {
1480
+ baton->colourspaceInput = VIPS_INTERPRETATION_LAST;
1481
+ }
1358
1482
  baton->colourspace = sharp::GetInterpretation(sharp::AttrAsStr(options, "colourspace"));
1359
1483
  if (baton->colourspace == VIPS_INTERPRETATION_ERROR) {
1360
1484
  baton->colourspace = VIPS_INTERPRETATION_sRGB;
@@ -1364,7 +1488,15 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1364
1488
  baton->fileOut = sharp::AttrAsStr(options, "fileOut");
1365
1489
  baton->withMetadata = sharp::AttrAsBool(options, "withMetadata");
1366
1490
  baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
1491
+ baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");
1367
1492
  baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc");
1493
+ Napi::Object mdStrs = options.Get("withMetadataStrs").As<Napi::Object>();
1494
+ Napi::Array mdStrKeys = mdStrs.GetPropertyNames();
1495
+ for (unsigned int i = 0; i < mdStrKeys.Length(); i++) {
1496
+ std::string k = sharp::AttrAsStr(mdStrKeys, i);
1497
+ baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k)));
1498
+ }
1499
+ baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1368
1500
  // Format-specific
1369
1501
  baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
1370
1502
  baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");
@@ -1379,14 +1511,23 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1379
1511
  baton->pngAdaptiveFiltering = sharp::AttrAsBool(options, "pngAdaptiveFiltering");
1380
1512
  baton->pngPalette = sharp::AttrAsBool(options, "pngPalette");
1381
1513
  baton->pngQuality = sharp::AttrAsUint32(options, "pngQuality");
1382
- baton->pngColours = sharp::AttrAsUint32(options, "pngColours");
1514
+ baton->pngEffort = sharp::AttrAsUint32(options, "pngEffort");
1515
+ baton->pngBitdepth = sharp::AttrAsUint32(options, "pngBitdepth");
1383
1516
  baton->pngDither = sharp::AttrAsDouble(options, "pngDither");
1517
+ baton->jp2Quality = sharp::AttrAsUint32(options, "jp2Quality");
1518
+ baton->jp2Lossless = sharp::AttrAsBool(options, "jp2Lossless");
1519
+ baton->jp2TileHeight = sharp::AttrAsUint32(options, "jp2TileHeight");
1520
+ baton->jp2TileWidth = sharp::AttrAsUint32(options, "jp2TileWidth");
1521
+ baton->jp2ChromaSubsampling = sharp::AttrAsStr(options, "jp2ChromaSubsampling");
1384
1522
  baton->webpQuality = sharp::AttrAsUint32(options, "webpQuality");
1385
1523
  baton->webpAlphaQuality = sharp::AttrAsUint32(options, "webpAlphaQuality");
1386
1524
  baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
1387
1525
  baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
1388
1526
  baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
1389
- baton->webpReductionEffort = sharp::AttrAsUint32(options, "webpReductionEffort");
1527
+ baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1528
+ baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1529
+ baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1530
+ baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1390
1531
  baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1391
1532
  baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
1392
1533
  baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
@@ -1395,6 +1536,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1395
1536
  baton->tiffTileHeight = sharp::AttrAsUint32(options, "tiffTileHeight");
1396
1537
  baton->tiffXres = sharp::AttrAsDouble(options, "tiffXres");
1397
1538
  baton->tiffYres = sharp::AttrAsDouble(options, "tiffYres");
1539
+ if (baton->tiffXres == 1.0 && baton->tiffYres == 1.0 && baton->withMetadataDensity > 0) {
1540
+ baton->tiffXres = baton->tiffYres = baton->withMetadataDensity / 25.4;
1541
+ }
1398
1542
  // tiff compression options
1399
1543
  baton->tiffCompression = static_cast<VipsForeignTiffCompression>(
1400
1544
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_COMPRESSION,
@@ -1402,25 +1546,28 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1402
1546
  baton->tiffPredictor = static_cast<VipsForeignTiffPredictor>(
1403
1547
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR,
1404
1548
  sharp::AttrAsStr(options, "tiffPredictor").data()));
1549
+ baton->tiffResolutionUnit = static_cast<VipsForeignTiffResunit>(
1550
+ vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_RESUNIT,
1551
+ sharp::AttrAsStr(options, "tiffResolutionUnit").data()));
1552
+
1405
1553
  baton->heifQuality = sharp::AttrAsUint32(options, "heifQuality");
1406
1554
  baton->heifLossless = sharp::AttrAsBool(options, "heifLossless");
1407
1555
  baton->heifCompression = static_cast<VipsForeignHeifCompression>(
1408
1556
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
1409
1557
  sharp::AttrAsStr(options, "heifCompression").data()));
1410
- baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed");
1558
+ baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1411
1559
  baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1412
-
1413
- // Animated output
1414
- if (sharp::HasAttr(options, "pageHeight")) {
1415
- baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight");
1416
- }
1560
+ // Raw output
1561
+ baton->rawDepth = static_cast<VipsBandFormat>(
1562
+ vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT,
1563
+ sharp::AttrAsStr(options, "rawDepth").data()));
1564
+ // Animated output properties
1417
1565
  if (sharp::HasAttr(options, "loop")) {
1418
1566
  baton->loop = sharp::AttrAsUint32(options, "loop");
1419
1567
  }
1420
1568
  if (sharp::HasAttr(options, "delay")) {
1421
1569
  baton->delay = sharp::AttrAsInt32Vector(options, "delay");
1422
1570
  }
1423
-
1424
1571
  // Tile output
1425
1572
  baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
1426
1573
  baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");
@@ -1438,6 +1585,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1438
1585
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_DZ_DEPTH,
1439
1586
  sharp::AttrAsStr(options, "tileDepth").data()));
1440
1587
  baton->tileCentre = sharp::AttrAsBool(options, "tileCentre");
1588
+ baton->tileId = sharp::AttrAsStr(options, "tileId");
1441
1589
 
1442
1590
  // Force random access for certain operations
1443
1591
  if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) {