UNPKG

78.6 kBtext/x-cView Raw
1// Copyright 2013 Lovell Fuller and others.
2// SPDX-License-Identifier: Apache-2.0
3
4#include <algorithm>
5#include <cmath>
6#include <map>
7#include <memory>
8#include <numeric>
9#include <string>
10#include <tuple>
11#include <utility>
12#include <vector>
13#include <sys/types.h>
14#include <sys/stat.h>
15
16#include <vips/vips8>
17#include <napi.h>
18
19#include "common.h"
20#include "operations.h"
21#include "pipeline.h"
22
23#if defined(WIN32)
24#define STAT64_STRUCT __stat64
25#define STAT64_FUNCTION _stat64
26#elif defined(__APPLE__)
27#define STAT64_STRUCT stat
28#define STAT64_FUNCTION stat
29#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
30#define STAT64_STRUCT stat
31#define STAT64_FUNCTION stat
32#else
33#define STAT64_STRUCT stat64
34#define STAT64_FUNCTION stat64
35#endif
36
37class PipelineWorker : public Napi::AsyncWorker {
38 public:
39 PipelineWorker(Napi::Function callback, PipelineBaton *baton,
40 Napi::Function debuglog, Napi::Function queueListener) :
41 Napi::AsyncWorker(callback),
42 baton(baton),
43 debuglog(Napi::Persistent(debuglog)),
44 queueListener(Napi::Persistent(queueListener)) {}
45 ~PipelineWorker() {}
46
47 // libuv worker
48 void Execute() {
49 // Decrement queued task counter
50 g_atomic_int_dec_and_test(&sharp::counterQueue);
51 // Increment processing task counter
52 g_atomic_int_inc(&sharp::counterProcess);
53
54 try {
55 // Open input
56 vips::VImage image;
57 sharp::ImageType inputImageType;
58 std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
59 image = sharp::EnsureColourspace(image, baton->colourspaceInput);
60
61 int nPages = baton->input->pages;
62 if (nPages == -1) {
63 // Resolve the number of pages if we need to render until the end of the document
64 nPages = image.get_typeof(VIPS_META_N_PAGES) != 0
65 ? image.get_int(VIPS_META_N_PAGES) - baton->input->page
66 : 1;
67 }
68
69 // Get pre-resize page height
70 int pageHeight = sharp::GetPageHeight(image);
71
72 // Calculate angle of rotation
73 VipsAngle rotation = VIPS_ANGLE_D0;
74 VipsAngle autoRotation = VIPS_ANGLE_D0;
75 bool autoFlip = FALSE;
76 bool autoFlop = FALSE;
77
78 if (baton->useExifOrientation) {
79 // Rotate and flip image according to Exif orientation
80 std::tie(autoRotation, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
81 image = sharp::RemoveExifOrientation(image);
82 } else {
83 rotation = CalculateAngleRotation(baton->angle);
84 }
85
86 // Rotate pre-extract
87 bool const shouldRotateBefore = baton->rotateBeforePreExtract &&
88 (rotation != VIPS_ANGLE_D0 || autoRotation != VIPS_ANGLE_D0 ||
89 autoFlip || baton->flip || autoFlop || baton->flop ||
90 baton->rotationAngle != 0.0);
91
92 if (shouldRotateBefore) {
93 if (autoRotation != VIPS_ANGLE_D0) {
94 image = image.rot(autoRotation);
95 autoRotation = VIPS_ANGLE_D0;
96 }
97 if (autoFlip) {
98 image = image.flip(VIPS_DIRECTION_VERTICAL);
99 autoFlip = FALSE;
100 } else if (baton->flip) {
101 image = image.flip(VIPS_DIRECTION_VERTICAL);
102 baton->flip = FALSE;
103 }
104 if (autoFlop) {
105 image = image.flip(VIPS_DIRECTION_HORIZONTAL);
106 autoFlop = FALSE;
107 } else if (baton->flop) {
108 image = image.flip(VIPS_DIRECTION_HORIZONTAL);
109 baton->flop = FALSE;
110 }
111 if (rotation != VIPS_ANGLE_D0) {
112 image = image.rot(rotation);
113 rotation = VIPS_ANGLE_D0;
114 }
115 if (baton->rotationAngle != 0.0) {
116 MultiPageUnsupported(nPages, "Rotate");
117 std::vector<double> background;
118 std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, FALSE);
119 image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
120 }
121 }
122
123 // Trim
124 if (baton->trimThreshold > 0.0) {
125 MultiPageUnsupported(nPages, "Trim");
126 image = sharp::Trim(image, baton->trimBackground, baton->trimThreshold);
127 baton->trimOffsetLeft = image.xoffset();
128 baton->trimOffsetTop = image.yoffset();
129 }
130
131 // Pre extraction
132 if (baton->topOffsetPre != -1) {
133 image = nPages > 1
134 ? sharp::CropMultiPage(image,
135 baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre, nPages, &pageHeight)
136 : image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre);
137 }
138
139 // Get pre-resize image width and height
140 int inputWidth = image.width();
141 int inputHeight = image.height();
142
143 // Is there just one page? Shrink to inputHeight instead
144 if (nPages == 1) {
145 pageHeight = inputHeight;
146 }
147
148 // Scaling calculations
149 double hshrink;
150 double vshrink;
151 int targetResizeWidth = baton->width;
152 int targetResizeHeight = baton->height;
153
154 // Swap input output width and height when rotating by 90 or 270 degrees
155 bool swap = !baton->rotateBeforePreExtract &&
156 (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270 ||
157 autoRotation == VIPS_ANGLE_D90 || autoRotation == VIPS_ANGLE_D270);
158
159 // Shrink to pageHeight, so we work for multi-page images
160 std::tie(hshrink, vshrink) = sharp::ResolveShrink(
161 inputWidth, pageHeight, targetResizeWidth, targetResizeHeight,
162 baton->canvas, swap, baton->withoutEnlargement, baton->withoutReduction);
163
164 // The jpeg preload shrink.
165 int jpegShrinkOnLoad = 1;
166
167 // WebP, PDF, SVG scale
168 double scale = 1.0;
169
170 // Try to reload input using shrink-on-load for JPEG, WebP, SVG and PDF, when:
171 // - the width or height parameters are specified;
172 // - gamma correction doesn't need to be applied;
173 // - trimming or pre-resize extract isn't required;
174 // - input colourspace is not specified;
175 bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
176 baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0 &&
177 baton->colourspaceInput == VIPS_INTERPRETATION_LAST && !shouldRotateBefore;
178
179 if (shouldPreShrink) {
180 // The common part of the shrink: the bit by which both axes must be shrunk
181 double shrink = std::min(hshrink, vshrink);
182
183 if (inputImageType == sharp::ImageType::JPEG) {
184 // Leave at least a factor of two for the final resize step, when fastShrinkOnLoad: false
185 // for more consistent results and to avoid extra sharpness to the image
186 int factor = baton->fastShrinkOnLoad ? 1 : 2;
187 if (shrink >= 8 * factor) {
188 jpegShrinkOnLoad = 8;
189 } else if (shrink >= 4 * factor) {
190 jpegShrinkOnLoad = 4;
191 } else if (shrink >= 2 * factor) {
192 jpegShrinkOnLoad = 2;
193 }
194 // Lower shrink-on-load for known libjpeg rounding errors
195 if (jpegShrinkOnLoad > 1 && static_cast<int>(shrink) == jpegShrinkOnLoad) {
196 jpegShrinkOnLoad /= 2;
197 }
198 } else if (inputImageType == sharp::ImageType::WEBP && baton->fastShrinkOnLoad && shrink > 1.0) {
199 // Avoid upscaling via webp
200 scale = 1.0 / shrink;
201 } else if (inputImageType == sharp::ImageType::SVG ||
202 inputImageType == sharp::ImageType::PDF) {
203 scale = 1.0 / shrink;
204 }
205 }
206
207 // Reload input using shrink-on-load, it'll be an integer shrink
208 // factor for jpegload*, a double scale factor for webpload*,
209 // pdfload* and svgload*
210 if (jpegShrinkOnLoad > 1) {
211 vips::VOption *option = VImage::option()
212 ->set("access", baton->input->access)
213 ->set("shrink", jpegShrinkOnLoad)
214 ->set("unlimited", baton->input->unlimited)
215 ->set("fail_on", baton->input->failOn);
216 if (baton->input->buffer != nullptr) {
217 // Reload JPEG buffer
218 VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
219 image = VImage::jpegload_buffer(blob, option);
220 vips_area_unref(reinterpret_cast<VipsArea*>(blob));
221 } else {
222 // Reload JPEG file
223 image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option);
224 }
225 } else if (scale != 1.0) {
226 vips::VOption *option = VImage::option()
227 ->set("access", baton->input->access)
228 ->set("scale", scale)
229 ->set("fail_on", baton->input->failOn);
230 if (inputImageType == sharp::ImageType::WEBP) {
231 option->set("n", baton->input->pages);
232 option->set("page", baton->input->page);
233
234 if (baton->input->buffer != nullptr) {
235 // Reload WebP buffer
236 VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
237 image = VImage::webpload_buffer(blob, option);
238 vips_area_unref(reinterpret_cast<VipsArea*>(blob));
239 } else {
240 // Reload WebP file
241 image = VImage::webpload(const_cast<char*>(baton->input->file.data()), option);
242 }
243 } else if (inputImageType == sharp::ImageType::SVG) {
244 option->set("unlimited", baton->input->unlimited);
245 option->set("dpi", baton->input->density);
246
247 if (baton->input->buffer != nullptr) {
248 // Reload SVG buffer
249 VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
250 image = VImage::svgload_buffer(blob, option);
251 vips_area_unref(reinterpret_cast<VipsArea*>(blob));
252 } else {
253 // Reload SVG file
254 image = VImage::svgload(const_cast<char*>(baton->input->file.data()), option);
255 }
256 sharp::SetDensity(image, baton->input->density);
257 if (image.width() > 32767 || image.height() > 32767) {
258 throw vips::VError("Input SVG image will exceed 32767x32767 pixel limit when scaled");
259 }
260 } else if (inputImageType == sharp::ImageType::PDF) {
261 option->set("n", baton->input->pages);
262 option->set("page", baton->input->page);
263 option->set("dpi", baton->input->density);
264
265 if (baton->input->buffer != nullptr) {
266 // Reload PDF buffer
267 VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
268 image = VImage::pdfload_buffer(blob, option);
269 vips_area_unref(reinterpret_cast<VipsArea*>(blob));
270 } else {
271 // Reload PDF file
272 image = VImage::pdfload(const_cast<char*>(baton->input->file.data()), option);
273 }
274
275 sharp::SetDensity(image, baton->input->density);
276 }
277 } else {
278 if (inputImageType == sharp::ImageType::SVG && (image.width() > 32767 || image.height() > 32767)) {
279 throw vips::VError("Input SVG image exceeds 32767x32767 pixel limit");
280 }
281 }
282
283 // Any pre-shrinking may already have been done
284 inputWidth = image.width();
285 inputHeight = image.height();
286
287 // After pre-shrink, but before the main shrink stage
288 // Reuse the initial pageHeight if we didn't pre-shrink
289 if (shouldPreShrink) {
290 pageHeight = sharp::GetPageHeight(image);
291 }
292
293 // Shrink to pageHeight, so we work for multi-page images
294 std::tie(hshrink, vshrink) = sharp::ResolveShrink(
295 inputWidth, pageHeight, targetResizeWidth, targetResizeHeight,
296 baton->canvas, swap, baton->withoutEnlargement, baton->withoutReduction);
297
298 int targetHeight = static_cast<int>(std::rint(static_cast<double>(pageHeight) / vshrink));
299 int targetPageHeight = targetHeight;
300
301 // In toilet-roll mode, we must adjust vshrink so that we exactly hit
302 // pageHeight or we'll have pixels straddling pixel boundaries
303 if (inputHeight > pageHeight) {
304 targetHeight *= nPages;
305 vshrink = static_cast<double>(inputHeight) / targetHeight;
306 }
307
308 // Ensure we're using a device-independent colour space
309 char const *processingProfile = image.interpretation() == VIPS_INTERPRETATION_RGB16 ? "p3" : "srgb";
310 if (
311 sharp::HasProfile(image) &&
312 image.interpretation() != VIPS_INTERPRETATION_LABS &&
313 image.interpretation() != VIPS_INTERPRETATION_GREY16 &&
314 !baton->input->ignoreIcc
315 ) {
316 // Convert to sRGB/P3 using embedded profile
317 try {
318 image = image.icc_transform(processingProfile, VImage::option()
319 ->set("embedded", TRUE)
320 ->set("depth", image.interpretation() == VIPS_INTERPRETATION_RGB16 ? 16 : 8)
321 ->set("intent", VIPS_INTENT_PERCEPTUAL));
322 } catch(...) {
323 sharp::VipsWarningCallback(nullptr, G_LOG_LEVEL_WARNING, "Invalid embedded profile", nullptr);
324 }
325 } else if (
326 image.interpretation() == VIPS_INTERPRETATION_CMYK &&
327 baton->colourspaceInput != VIPS_INTERPRETATION_CMYK
328 ) {
329 image = image.icc_transform(processingProfile, VImage::option()
330 ->set("input_profile", "cmyk")
331 ->set("intent", VIPS_INTENT_PERCEPTUAL));
332 }
333
334 // Flatten image to remove alpha channel
335 if (baton->flatten && sharp::HasAlpha(image)) {
336 image = sharp::Flatten(image, baton->flattenBackground);
337 }
338
339 // Negate the colours in the image
340 if (baton->negate) {
341 image = sharp::Negate(image, baton->negateAlpha);
342 }
343
344 // Gamma encoding (darken)
345 if (baton->gamma >= 1 && baton->gamma <= 3) {
346 image = sharp::Gamma(image, 1.0 / baton->gamma);
347 }
348
349 // Convert to greyscale (linear, therefore after gamma encoding, if any)
350 if (baton->greyscale) {
351 image = image.colourspace(VIPS_INTERPRETATION_B_W);
352 }
353
354 bool const shouldResize = hshrink != 1.0 || vshrink != 1.0;
355 bool const shouldBlur = baton->blurSigma != 0.0;
356 bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
357 bool const shouldSharpen = baton->sharpenSigma != 0.0;
358 bool const shouldComposite = !baton->composite.empty();
359
360 if (shouldComposite && !sharp::HasAlpha(image)) {
361 image = sharp::EnsureAlpha(image, 1);
362 }
363
364 VipsBandFormat premultiplyFormat = image.format();
365 bool const shouldPremultiplyAlpha = sharp::HasAlpha(image) &&
366 (shouldResize || shouldBlur || shouldConv || shouldSharpen);
367
368 if (shouldPremultiplyAlpha) {
369 image = image.premultiply().cast(premultiplyFormat);
370 }
371
372 // Resize
373 if (shouldResize) {
374 image = image.resize(1.0 / hshrink, VImage::option()
375 ->set("vscale", 1.0 / vshrink)
376 ->set("kernel", baton->kernel));
377 }
378
379 // Auto-rotate post-extract
380 if (autoRotation != VIPS_ANGLE_D0) {
381 image = image.rot(autoRotation);
382 }
383 // Flip (mirror about Y axis)
384 if (baton->flip || autoFlip) {
385 image = image.flip(VIPS_DIRECTION_VERTICAL);
386 }
387 // Flop (mirror about X axis)
388 if (baton->flop || autoFlop) {
389 image = image.flip(VIPS_DIRECTION_HORIZONTAL);
390 }
391 // Rotate post-extract 90-angle
392 if (rotation != VIPS_ANGLE_D0) {
393 image = image.rot(rotation);
394 }
395
396 // Join additional color channels to the image
397 if (!baton->joinChannelIn.empty()) {
398 VImage joinImage;
399 sharp::ImageType joinImageType = sharp::ImageType::UNKNOWN;
400
401 for (unsigned int i = 0; i < baton->joinChannelIn.size(); i++) {
402 std::tie(joinImage, joinImageType) = sharp::OpenInput(baton->joinChannelIn[i]);
403 joinImage = sharp::EnsureColourspace(joinImage, baton->colourspaceInput);
404 image = image.bandjoin(joinImage);
405 }
406 image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
407 image = sharp::RemoveGifPalette(image);
408 }
409
410 inputWidth = image.width();
411 inputHeight = nPages > 1 ? targetPageHeight : image.height();
412
413 // Resolve dimensions
414 if (baton->width <= 0) {
415 baton->width = inputWidth;
416 }
417 if (baton->height <= 0) {
418 baton->height = inputHeight;
419 }
420
421 // Crop/embed
422 if (inputWidth != baton->width || inputHeight != baton->height) {
423 if (baton->canvas == sharp::Canvas::EMBED) {
424 std::vector<double> background;
425 std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
426
427 // Embed
428 int left;
429 int top;
430 std::tie(left, top) = sharp::CalculateEmbedPosition(
431 inputWidth, inputHeight, baton->width, baton->height, baton->position);
432 int width = std::max(inputWidth, baton->width);
433 int height = std::max(inputHeight, baton->height);
434
435 image = nPages > 1
436 ? sharp::EmbedMultiPage(image,
437 left, top, width, height, VIPS_EXTEND_BACKGROUND, background, nPages, &targetPageHeight)
438 : image.embed(left, top, width, height, VImage::option()
439 ->set("extend", VIPS_EXTEND_BACKGROUND)
440 ->set("background", background));
441 } else if (baton->canvas == sharp::Canvas::CROP) {
442 if (baton->width > inputWidth) {
443 baton->width = inputWidth;
444 }
445 if (baton->height > inputHeight) {
446 baton->height = inputHeight;
447 }
448
449 // Crop
450 if (baton->position < 9) {
451 // Gravity-based crop
452 int left;
453 int top;
454
455 std::tie(left, top) = sharp::CalculateCrop(
456 inputWidth, inputHeight, baton->width, baton->height, baton->position);
457 int width = std::min(inputWidth, baton->width);
458 int height = std::min(inputHeight, baton->height);
459
460 image = nPages > 1
461 ? sharp::CropMultiPage(image,
462 left, top, width, height, nPages, &targetPageHeight)
463 : image.extract_area(left, top, width, height);
464 } else {
465 int attention_x;
466 int attention_y;
467
468 // Attention-based or Entropy-based crop
469 MultiPageUnsupported(nPages, "Resize strategy");
470 image = image.tilecache(VImage::option()
471 ->set("access", VIPS_ACCESS_RANDOM)
472 ->set("threaded", TRUE));
473
474 image = image.smartcrop(baton->width, baton->height, VImage::option()
475 ->set("interesting", baton->position == 16 ? VIPS_INTERESTING_ENTROPY : VIPS_INTERESTING_ATTENTION)
476 ->set("premultiplied", shouldPremultiplyAlpha)
477 ->set("attention_x", &attention_x)
478 ->set("attention_y", &attention_y));
479 baton->hasCropOffset = true;
480 baton->cropOffsetLeft = static_cast<int>(image.xoffset());
481 baton->cropOffsetTop = static_cast<int>(image.yoffset());
482 baton->hasAttentionCenter = true;
483 baton->attentionX = static_cast<int>(attention_x * jpegShrinkOnLoad / scale);
484 baton->attentionY = static_cast<int>(attention_y * jpegShrinkOnLoad / scale);
485 }
486 }
487 }
488
489 // Rotate post-extract non-90 angle
490 if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
491 MultiPageUnsupported(nPages, "Rotate");
492 std::vector<double> background;
493 std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, shouldPremultiplyAlpha);
494 image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background));
495 }
496
497 // Post extraction
498 if (baton->topOffsetPost != -1) {
499 if (nPages > 1) {
500 image = sharp::CropMultiPage(image,
501 baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost,
502 nPages, &targetPageHeight);
503
504 // heightPost is used in the info object, so update to reflect the number of pages
505 baton->heightPost *= nPages;
506 } else {
507 image = image.extract_area(
508 baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
509 }
510 }
511
512 // Affine transform
513 if (!baton->affineMatrix.empty()) {
514 MultiPageUnsupported(nPages, "Affine");
515 std::vector<double> background;
516 std::tie(image, background) = sharp::ApplyAlpha(image, baton->affineBackground, shouldPremultiplyAlpha);
517 vips::VInterpolate interp = vips::VInterpolate::new_from_name(
518 const_cast<char*>(baton->affineInterpolator.data()));
519 image = image.affine(baton->affineMatrix, VImage::option()->set("background", background)
520 ->set("idx", baton->affineIdx)
521 ->set("idy", baton->affineIdy)
522 ->set("odx", baton->affineOdx)
523 ->set("ody", baton->affineOdy)
524 ->set("interpolate", interp));
525 }
526
527 // Extend edges
528 if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
529 // Embed
530 baton->width = image.width() + baton->extendLeft + baton->extendRight;
531 baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
532
533 if (baton->extendWith == VIPS_EXTEND_BACKGROUND) {
534 std::vector<double> background;
535 std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
536
537 image = nPages > 1
538 ? sharp::EmbedMultiPage(image,
539 baton->extendLeft, baton->extendTop, baton->width, baton->height,
540 baton->extendWith, background, nPages, &targetPageHeight)
541 : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
542 VImage::option()->set("extend", baton->extendWith)->set("background", background));
543 } else {
544 std::vector<double> ignoredBackground(1);
545 image = nPages > 1
546 ? sharp::EmbedMultiPage(image,
547 baton->extendLeft, baton->extendTop, baton->width, baton->height,
548 baton->extendWith, ignoredBackground, nPages, &targetPageHeight)
549 : image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
550 VImage::option()->set("extend", baton->extendWith));
551 }
552 }
553 // Median - must happen before blurring, due to the utility of blurring after thresholding
554 if (baton->medianSize > 0) {
555 image = image.median(baton->medianSize);
556 }
557
558 // Threshold - must happen before blurring, due to the utility of blurring after thresholding
559 // Threshold - must happen before unflatten to enable non-white unflattening
560 if (baton->threshold != 0) {
561 image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale);
562 }
563
564 // Blur
565 if (shouldBlur) {
566 image = sharp::Blur(image, baton->blurSigma);
567 }
568
569 // Unflatten the image
570 if (baton->unflatten) {
571 image = sharp::Unflatten(image);
572 }
573
574 // Convolve
575 if (shouldConv) {
576 image = sharp::Convolve(image,
577 baton->convKernelWidth, baton->convKernelHeight,
578 baton->convKernelScale, baton->convKernelOffset,
579 baton->convKernel);
580 }
581
582 // Recomb
583 if (baton->recombMatrix != NULL) {
584 image = sharp::Recomb(image, baton->recombMatrix);
585 }
586
587 // Modulate
588 if (baton->brightness != 1.0 || baton->saturation != 1.0 || baton->hue != 0.0 || baton->lightness != 0.0) {
589 image = sharp::Modulate(image, baton->brightness, baton->saturation, baton->hue, baton->lightness);
590 }
591
592 // Sharpen
593 if (shouldSharpen) {
594 image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenM1, baton->sharpenM2,
595 baton->sharpenX1, baton->sharpenY2, baton->sharpenY3);
596 }
597
598 // Reverse premultiplication after all transformations
599 if (shouldPremultiplyAlpha) {
600 image = image.unpremultiply().cast(premultiplyFormat);
601 }
602 baton->premultiplied = shouldPremultiplyAlpha;
603
604 // Composite
605 if (shouldComposite) {
606 std::vector<VImage> images = { image };
607 std::vector<int> modes, xs, ys;
608 for (Composite *composite : baton->composite) {
609 VImage compositeImage;
610 sharp::ImageType compositeImageType = sharp::ImageType::UNKNOWN;
611 std::tie(compositeImage, compositeImageType) = sharp::OpenInput(composite->input);
612 compositeImage = sharp::EnsureColourspace(compositeImage, baton->colourspaceInput);
613 // Verify within current dimensions
614 if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
615 throw vips::VError("Image to composite must have same dimensions or smaller");
616 }
617 // Check if overlay is tiled
618 if (composite->tile) {
619 int across = 0;
620 int down = 0;
621 // Use gravity in overlay
622 if (compositeImage.width() <= image.width()) {
623 across = static_cast<int>(ceil(static_cast<double>(image.width()) / compositeImage.width()));
624 // Ensure odd number of tiles across when gravity is centre, north or south
625 if (composite->gravity == 0 || composite->gravity == 1 || composite->gravity == 3) {
626 across |= 1;
627 }
628 }
629 if (compositeImage.height() <= image.height()) {
630 down = static_cast<int>(ceil(static_cast<double>(image.height()) / compositeImage.height()));
631 // Ensure odd number of tiles down when gravity is centre, east or west
632 if (composite->gravity == 0 || composite->gravity == 2 || composite->gravity == 4) {
633 down |= 1;
634 }
635 }
636 if (across != 0 || down != 0) {
637 int left;
638 int top;
639 compositeImage = compositeImage.replicate(across, down);
640 if (composite->hasOffset) {
641 std::tie(left, top) = sharp::CalculateCrop(
642 compositeImage.width(), compositeImage.height(), image.width(), image.height(),
643 composite->left, composite->top);
644 } else {
645 std::tie(left, top) = sharp::CalculateCrop(
646 compositeImage.width(), compositeImage.height(), image.width(), image.height(), composite->gravity);
647 }
648 compositeImage = compositeImage.extract_area(left, top, image.width(), image.height());
649 }
650 // gravity was used for extract_area, set it back to its default value of 0
651 composite->gravity = 0;
652 }
653 // Ensure image to composite is sRGB with unpremultiplied alpha
654 compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB);
655 if (!sharp::HasAlpha(compositeImage)) {
656 compositeImage = sharp::EnsureAlpha(compositeImage, 1);
657 }
658 if (composite->premultiplied) compositeImage = compositeImage.unpremultiply();
659 // Calculate position
660 int left;
661 int top;
662 if (composite->hasOffset) {
663 // Composite image at given offsets
664 if (composite->tile) {
665 std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
666 compositeImage.width(), compositeImage.height(), composite->left, composite->top);
667 } else {
668 left = composite->left;
669 top = composite->top;
670 }
671 } else {
672 // Composite image with given gravity
673 std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
674 compositeImage.width(), compositeImage.height(), composite->gravity);
675 }
676 images.push_back(compositeImage);
677 modes.push_back(composite->mode);
678 xs.push_back(left);
679 ys.push_back(top);
680 }
681 image = VImage::composite(images, modes, VImage::option()->set("x", xs)->set("y", ys));
682 image = sharp::RemoveGifPalette(image);
683 }
684
685 // Gamma decoding (brighten)
686 if (baton->gammaOut >= 1 && baton->gammaOut <= 3) {
687 image = sharp::Gamma(image, baton->gammaOut);
688 }
689
690 // Linear adjustment (a * in + b)
691 if (!baton->linearA.empty()) {
692 image = sharp::Linear(image, baton->linearA, baton->linearB);
693 }
694
695 // Apply normalisation - stretch luminance to cover full dynamic range
696 if (baton->normalise) {
697 image = sharp::Normalise(image, baton->normaliseLower, baton->normaliseUpper);
698 }
699
700 // Apply contrast limiting adaptive histogram equalization (CLAHE)
701 if (baton->claheWidth != 0 && baton->claheHeight != 0) {
702 image = sharp::Clahe(image, baton->claheWidth, baton->claheHeight, baton->claheMaxSlope);
703 }
704
705 // Apply bitwise boolean operation between images
706 if (baton->boolean != nullptr) {
707 VImage booleanImage;
708 sharp::ImageType booleanImageType = sharp::ImageType::UNKNOWN;
709 std::tie(booleanImage, booleanImageType) = sharp::OpenInput(baton->boolean);
710 booleanImage = sharp::EnsureColourspace(booleanImage, baton->colourspaceInput);
711 image = sharp::Boolean(image, booleanImage, baton->booleanOp);
712 image = sharp::RemoveGifPalette(image);
713 }
714
715 // Apply per-channel Bandbool bitwise operations after all other operations
716 if (baton->bandBoolOp >= VIPS_OPERATION_BOOLEAN_AND && baton->bandBoolOp < VIPS_OPERATION_BOOLEAN_LAST) {
717 image = sharp::Bandbool(image, baton->bandBoolOp);
718 }
719
720 // Tint the image
721 if (baton->tintA < 128.0 || baton->tintB < 128.0) {
722 image = sharp::Tint(image, baton->tintA, baton->tintB);
723 }
724
725 // Remove alpha channel, if any
726 if (baton->removeAlpha) {
727 image = sharp::RemoveAlpha(image);
728 }
729
730 // Ensure alpha channel, if missing
731 if (baton->ensureAlpha != -1) {
732 image = sharp::EnsureAlpha(image, baton->ensureAlpha);
733 }
734
735 // Convert image to sRGB, if not already
736 if (sharp::Is16Bit(image.interpretation())) {
737 image = image.cast(VIPS_FORMAT_USHORT);
738 }
739 if (image.interpretation() != baton->colourspace) {
740 // Convert colourspace, pass the current known interpretation so libvips doesn't have to guess
741 image = image.colourspace(baton->colourspace, VImage::option()->set("source_space", image.interpretation()));
742 // Transform colours from embedded profile to output profile
743 if (baton->withMetadata && sharp::HasProfile(image) && baton->withMetadataIcc.empty()) {
744 image = image.icc_transform("srgb", VImage::option()
745 ->set("embedded", TRUE)
746 ->set("intent", VIPS_INTENT_PERCEPTUAL));
747 }
748 }
749
750 // Extract channel
751 if (baton->extractChannel > -1) {
752 if (baton->extractChannel >= image.bands()) {
753 if (baton->extractChannel == 3 && sharp::HasAlpha(image)) {
754 baton->extractChannel = image.bands() - 1;
755 } else {
756 (baton->err)
757 .append("Cannot extract channel ").append(std::to_string(baton->extractChannel))
758 .append(" from image with channels 0-").append(std::to_string(image.bands() - 1));
759 return Error();
760 }
761 }
762 VipsInterpretation colourspace = sharp::Is16Bit(image.interpretation())
763 ? VIPS_INTERPRETATION_GREY16
764 : VIPS_INTERPRETATION_B_W;
765 image = image
766 .extract_band(baton->extractChannel)
767 .copy(VImage::option()->set("interpretation", colourspace));
768 }
769
770 // Apply output ICC profile
771 if (!baton->withMetadataIcc.empty()) {
772 image = image.icc_transform(
773 const_cast<char*>(baton->withMetadataIcc.data()),
774 VImage::option()
775 ->set("input_profile", processingProfile)
776 ->set("embedded", TRUE)
777 ->set("intent", VIPS_INTENT_PERCEPTUAL));
778 }
779 // Override EXIF Orientation tag
780 if (baton->withMetadata && baton->withMetadataOrientation != -1) {
781 image = sharp::SetExifOrientation(image, baton->withMetadataOrientation);
782 }
783 // Override pixel density
784 if (baton->withMetadataDensity > 0) {
785 image = sharp::SetDensity(image, baton->withMetadataDensity);
786 }
787 // Metadata key/value pairs, e.g. EXIF
788 if (!baton->withMetadataStrs.empty()) {
789 image = image.copy();
790 for (const auto& s : baton->withMetadataStrs) {
791 image.set(s.first.data(), s.second.data());
792 }
793 }
794
795 // Number of channels used in output image
796 baton->channels = image.bands();
797 baton->width = image.width();
798 baton->height = image.height();
799
800 image = sharp::SetAnimationProperties(
801 image, nPages, targetPageHeight, baton->delay, baton->loop);
802
803 // Output
804 sharp::SetTimeout(image, baton->timeoutSeconds);
805 if (baton->fileOut.empty()) {
806 // Buffer output
807 if (baton->formatOut == "jpeg" || (baton->formatOut == "input" && inputImageType == sharp::ImageType::JPEG)) {
808 // Write JPEG to buffer
809 sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
810 VipsArea *area = reinterpret_cast<VipsArea*>(image.jpegsave_buffer(VImage::option()
811 ->set("strip", !baton->withMetadata)
812 ->set("Q", baton->jpegQuality)
813 ->set("interlace", baton->jpegProgressive)
814 ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
815 ? VIPS_FOREIGN_SUBSAMPLE_OFF
816 : VIPS_FOREIGN_SUBSAMPLE_ON)
817 ->set("trellis_quant", baton->jpegTrellisQuantisation)
818 ->set("quant_table", baton->jpegQuantisationTable)
819 ->set("overshoot_deringing", baton->jpegOvershootDeringing)
820 ->set("optimize_scans", baton->jpegOptimiseScans)
821 ->set("optimize_coding", baton->jpegOptimiseCoding)));
822 baton->bufferOut = static_cast<char*>(area->data);
823 baton->bufferOutLength = area->length;
824 area->free_fn = nullptr;
825 vips_area_unref(area);
826 baton->formatOut = "jpeg";
827 if (baton->colourspace == VIPS_INTERPRETATION_CMYK) {
828 baton->channels = std::min(baton->channels, 4);
829 } else {
830 baton->channels = std::min(baton->channels, 3);
831 }
832 } else if (baton->formatOut == "jp2" || (baton->formatOut == "input"
833 && inputImageType == sharp::ImageType::JP2)) {
834 // Write JP2 to Buffer
835 sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
836 VipsArea *area = reinterpret_cast<VipsArea*>(image.jp2ksave_buffer(VImage::option()
837 ->set("Q", baton->jp2Quality)
838 ->set("lossless", baton->jp2Lossless)
839 ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
840 ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
841 ->set("tile_height", baton->jp2TileHeight)
842 ->set("tile_width", baton->jp2TileWidth)));
843 baton->bufferOut = static_cast<char*>(area->data);
844 baton->bufferOutLength = area->length;
845 area->free_fn = nullptr;
846 vips_area_unref(area);
847 baton->formatOut = "jp2";
848 } else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
849 (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
850 // Write PNG to buffer
851 sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
852 VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option()
853 ->set("strip", !baton->withMetadata)
854 ->set("interlace", baton->pngProgressive)
855 ->set("compression", baton->pngCompressionLevel)
856 ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
857 ->set("palette", baton->pngPalette)
858 ->set("Q", baton->pngQuality)
859 ->set("effort", baton->pngEffort)
860 ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
861 ->set("dither", baton->pngDither)));
862 baton->bufferOut = static_cast<char*>(area->data);
863 baton->bufferOutLength = area->length;
864 area->free_fn = nullptr;
865 vips_area_unref(area);
866 baton->formatOut = "png";
867 } else if (baton->formatOut == "webp" ||
868 (baton->formatOut == "input" && inputImageType == sharp::ImageType::WEBP)) {
869 // Write WEBP to buffer
870 sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP);
871 VipsArea *area = reinterpret_cast<VipsArea*>(image.webpsave_buffer(VImage::option()
872 ->set("strip", !baton->withMetadata)
873 ->set("Q", baton->webpQuality)
874 ->set("lossless", baton->webpLossless)
875 ->set("near_lossless", baton->webpNearLossless)
876 ->set("smart_subsample", baton->webpSmartSubsample)
877 ->set("effort", baton->webpEffort)
878 ->set("min_size", baton->webpMinSize)
879 ->set("mixed", baton->webpMixed)
880 ->set("alpha_q", baton->webpAlphaQuality)));
881 baton->bufferOut = static_cast<char*>(area->data);
882 baton->bufferOutLength = area->length;
883 area->free_fn = nullptr;
884 vips_area_unref(area);
885 baton->formatOut = "webp";
886 } else if (baton->formatOut == "gif" ||
887 (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF)) {
888 // Write GIF to buffer
889 sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
890 VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option()
891 ->set("strip", !baton->withMetadata)
892 ->set("bitdepth", baton->gifBitdepth)
893 ->set("effort", baton->gifEffort)
894 ->set("reuse", baton->gifReuse)
895 ->set("interlace", baton->gifProgressive)
896 ->set("interframe_maxerror", baton->gifInterFrameMaxError)
897 ->set("interpalette_maxerror", baton->gifInterPaletteMaxError)
898 ->set("dither", baton->gifDither)));
899 baton->bufferOut = static_cast<char*>(area->data);
900 baton->bufferOutLength = area->length;
901 area->free_fn = nullptr;
902 vips_area_unref(area);
903 baton->formatOut = "gif";
904 } else if (baton->formatOut == "tiff" ||
905 (baton->formatOut == "input" && inputImageType == sharp::ImageType::TIFF)) {
906 // Write TIFF to buffer
907 if (baton->tiffCompression == VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) {
908 sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
909 baton->channels = std::min(baton->channels, 3);
910 }
911 // Cast pixel values to float, if required
912 if (baton->tiffPredictor == VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT) {
913 image = image.cast(VIPS_FORMAT_FLOAT);
914 }
915 VipsArea *area = reinterpret_cast<VipsArea*>(image.tiffsave_buffer(VImage::option()
916 ->set("strip", !baton->withMetadata)
917 ->set("Q", baton->tiffQuality)
918 ->set("bitdepth", baton->tiffBitdepth)
919 ->set("compression", baton->tiffCompression)
920 ->set("predictor", baton->tiffPredictor)
921 ->set("pyramid", baton->tiffPyramid)
922 ->set("tile", baton->tiffTile)
923 ->set("tile_height", baton->tiffTileHeight)
924 ->set("tile_width", baton->tiffTileWidth)
925 ->set("xres", baton->tiffXres)
926 ->set("yres", baton->tiffYres)
927 ->set("resunit", baton->tiffResolutionUnit)));
928 baton->bufferOut = static_cast<char*>(area->data);
929 baton->bufferOutLength = area->length;
930 area->free_fn = nullptr;
931 vips_area_unref(area);
932 baton->formatOut = "tiff";
933 } else if (baton->formatOut == "heif" ||
934 (baton->formatOut == "input" && inputImageType == sharp::ImageType::HEIF)) {
935 // Write HEIF to buffer
936 image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
937 VipsArea *area = reinterpret_cast<VipsArea*>(image.heifsave_buffer(VImage::option()
938 ->set("strip", !baton->withMetadata)
939 ->set("Q", baton->heifQuality)
940 ->set("compression", baton->heifCompression)
941 ->set("effort", baton->heifEffort)
942 ->set("bitdepth", 8)
943 ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
944 ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
945 ->set("lossless", baton->heifLossless)));
946 baton->bufferOut = static_cast<char*>(area->data);
947 baton->bufferOutLength = area->length;
948 area->free_fn = nullptr;
949 vips_area_unref(area);
950 baton->formatOut = "heif";
951 } else if (baton->formatOut == "dz") {
952 // Write DZ to buffer
953 baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
954 if (!sharp::HasAlpha(image)) {
955 baton->tileBackground.pop_back();
956 }
957 vips::VOption *options = BuildOptionsDZ(baton);
958 VipsArea *area = reinterpret_cast<VipsArea*>(image.dzsave_buffer(options));
959 baton->bufferOut = static_cast<char*>(area->data);
960 baton->bufferOutLength = area->length;
961 area->free_fn = nullptr;
962 vips_area_unref(area);
963 baton->formatOut = "dz";
964 } else if (baton->formatOut == "jxl" ||
965 (baton->formatOut == "input" && inputImageType == sharp::ImageType::JXL)) {
966 // Write JXL to buffer
967 image = sharp::RemoveAnimationProperties(image);
968 VipsArea *area = reinterpret_cast<VipsArea*>(image.jxlsave_buffer(VImage::option()
969 ->set("strip", !baton->withMetadata)
970 ->set("distance", baton->jxlDistance)
971 ->set("tier", baton->jxlDecodingTier)
972 ->set("effort", baton->jxlEffort)
973 ->set("lossless", baton->jxlLossless)));
974 baton->bufferOut = static_cast<char*>(area->data);
975 baton->bufferOutLength = area->length;
976 area->free_fn = nullptr;
977 vips_area_unref(area);
978 baton->formatOut = "jxl";
979 } else if (baton->formatOut == "raw" ||
980 (baton->formatOut == "input" && inputImageType == sharp::ImageType::RAW)) {
981 // Write raw, uncompressed image data to buffer
982 if (baton->greyscale || image.interpretation() == VIPS_INTERPRETATION_B_W) {
983 // Extract first band for greyscale image
984 image = image[0];
985 baton->channels = 1;
986 }
987 if (image.format() != baton->rawDepth) {
988 // Cast pixels to requested format
989 image = image.cast(baton->rawDepth);
990 }
991 // Get raw image data
992 baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
993 if (baton->bufferOut == nullptr) {
994 (baton->err).append("Could not allocate enough memory for raw output");
995 return Error();
996 }
997 baton->formatOut = "raw";
998 } else {
999 // Unsupported output format
1000 (baton->err).append("Unsupported output format ");
1001 if (baton->formatOut == "input") {
1002 (baton->err).append(ImageTypeId(inputImageType));
1003 } else {
1004 (baton->err).append(baton->formatOut);
1005 }
1006 return Error();
1007 }
1008 } else {
1009 // File output
1010 bool const isJpeg = sharp::IsJpeg(baton->fileOut);
1011 bool const isPng = sharp::IsPng(baton->fileOut);
1012 bool const isWebp = sharp::IsWebp(baton->fileOut);
1013 bool const isGif = sharp::IsGif(baton->fileOut);
1014 bool const isTiff = sharp::IsTiff(baton->fileOut);
1015 bool const isJp2 = sharp::IsJp2(baton->fileOut);
1016 bool const isHeif = sharp::IsHeif(baton->fileOut);
1017 bool const isJxl = sharp::IsJxl(baton->fileOut);
1018 bool const isDz = sharp::IsDz(baton->fileOut);
1019 bool const isDzZip = sharp::IsDzZip(baton->fileOut);
1020 bool const isV = sharp::IsV(baton->fileOut);
1021 bool const mightMatchInput = baton->formatOut == "input";
1022 bool const willMatchInput = mightMatchInput &&
1023 !(isJpeg || isPng || isWebp || isGif || isTiff || isJp2 || isHeif || isDz || isDzZip || isV);
1024
1025 if (baton->formatOut == "jpeg" || (mightMatchInput && isJpeg) ||
1026 (willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
1027 // Write JPEG to file
1028 sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
1029 image.jpegsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1030 ->set("strip", !baton->withMetadata)
1031 ->set("Q", baton->jpegQuality)
1032 ->set("interlace", baton->jpegProgressive)
1033 ->set("subsample_mode", baton->jpegChromaSubsampling == "4:4:4"
1034 ? VIPS_FOREIGN_SUBSAMPLE_OFF
1035 : VIPS_FOREIGN_SUBSAMPLE_ON)
1036 ->set("trellis_quant", baton->jpegTrellisQuantisation)
1037 ->set("quant_table", baton->jpegQuantisationTable)
1038 ->set("overshoot_deringing", baton->jpegOvershootDeringing)
1039 ->set("optimize_scans", baton->jpegOptimiseScans)
1040 ->set("optimize_coding", baton->jpegOptimiseCoding));
1041 baton->formatOut = "jpeg";
1042 baton->channels = std::min(baton->channels, 3);
1043 } else if (baton->formatOut == "jp2" || (mightMatchInput && isJp2) ||
1044 (willMatchInput && (inputImageType == sharp::ImageType::JP2))) {
1045 // Write JP2 to file
1046 sharp::AssertImageTypeDimensions(image, sharp::ImageType::JP2);
1047 image.jp2ksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1048 ->set("Q", baton->jp2Quality)
1049 ->set("lossless", baton->jp2Lossless)
1050 ->set("subsample_mode", baton->jp2ChromaSubsampling == "4:4:4"
1051 ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1052 ->set("tile_height", baton->jp2TileHeight)
1053 ->set("tile_width", baton->jp2TileWidth));
1054 baton->formatOut = "jp2";
1055 } else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
1056 (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
1057 // Write PNG to file
1058 sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
1059 image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1060 ->set("strip", !baton->withMetadata)
1061 ->set("interlace", baton->pngProgressive)
1062 ->set("compression", baton->pngCompressionLevel)
1063 ->set("filter", baton->pngAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_ALL : VIPS_FOREIGN_PNG_FILTER_NONE)
1064 ->set("palette", baton->pngPalette)
1065 ->set("Q", baton->pngQuality)
1066 ->set("bitdepth", sharp::Is16Bit(image.interpretation()) ? 16 : baton->pngBitdepth)
1067 ->set("effort", baton->pngEffort)
1068 ->set("dither", baton->pngDither));
1069 baton->formatOut = "png";
1070 } else if (baton->formatOut == "webp" || (mightMatchInput && isWebp) ||
1071 (willMatchInput && inputImageType == sharp::ImageType::WEBP)) {
1072 // Write WEBP to file
1073 sharp::AssertImageTypeDimensions(image, sharp::ImageType::WEBP);
1074 image.webpsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1075 ->set("strip", !baton->withMetadata)
1076 ->set("Q", baton->webpQuality)
1077 ->set("lossless", baton->webpLossless)
1078 ->set("near_lossless", baton->webpNearLossless)
1079 ->set("smart_subsample", baton->webpSmartSubsample)
1080 ->set("effort", baton->webpEffort)
1081 ->set("min_size", baton->webpMinSize)
1082 ->set("mixed", baton->webpMixed)
1083 ->set("alpha_q", baton->webpAlphaQuality));
1084 baton->formatOut = "webp";
1085 } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
1086 (willMatchInput && inputImageType == sharp::ImageType::GIF)) {
1087 // Write GIF to file
1088 sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
1089 image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1090 ->set("strip", !baton->withMetadata)
1091 ->set("bitdepth", baton->gifBitdepth)
1092 ->set("effort", baton->gifEffort)
1093 ->set("reuse", baton->gifReuse)
1094 ->set("interlace", baton->gifProgressive)
1095 ->set("dither", baton->gifDither));
1096 baton->formatOut = "gif";
1097 } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
1098 (willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
1099 // Write TIFF to file
1100 if (baton->tiffCompression == VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) {
1101 sharp::AssertImageTypeDimensions(image, sharp::ImageType::JPEG);
1102 baton->channels = std::min(baton->channels, 3);
1103 }
1104 // Cast pixel values to float, if required
1105 if (baton->tiffPredictor == VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT) {
1106 image = image.cast(VIPS_FORMAT_FLOAT);
1107 }
1108 image.tiffsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1109 ->set("strip", !baton->withMetadata)
1110 ->set("Q", baton->tiffQuality)
1111 ->set("bitdepth", baton->tiffBitdepth)
1112 ->set("compression", baton->tiffCompression)
1113 ->set("predictor", baton->tiffPredictor)
1114 ->set("pyramid", baton->tiffPyramid)
1115 ->set("tile", baton->tiffTile)
1116 ->set("tile_height", baton->tiffTileHeight)
1117 ->set("tile_width", baton->tiffTileWidth)
1118 ->set("xres", baton->tiffXres)
1119 ->set("yres", baton->tiffYres)
1120 ->set("resunit", baton->tiffResolutionUnit));
1121 baton->formatOut = "tiff";
1122 } else if (baton->formatOut == "heif" || (mightMatchInput && isHeif) ||
1123 (willMatchInput && inputImageType == sharp::ImageType::HEIF)) {
1124 // Write HEIF to file
1125 image = sharp::RemoveAnimationProperties(image).cast(VIPS_FORMAT_UCHAR);
1126 image.heifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1127 ->set("strip", !baton->withMetadata)
1128 ->set("Q", baton->heifQuality)
1129 ->set("compression", baton->heifCompression)
1130 ->set("effort", baton->heifEffort)
1131 ->set("bitdepth", 8)
1132 ->set("subsample_mode", baton->heifChromaSubsampling == "4:4:4"
1133 ? VIPS_FOREIGN_SUBSAMPLE_OFF : VIPS_FOREIGN_SUBSAMPLE_ON)
1134 ->set("lossless", baton->heifLossless));
1135 baton->formatOut = "heif";
1136 } else if (baton->formatOut == "jxl" || (mightMatchInput && isJxl) ||
1137 (willMatchInput && inputImageType == sharp::ImageType::JXL)) {
1138 // Write JXL to file
1139 image = sharp::RemoveAnimationProperties(image);
1140 image.jxlsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1141 ->set("strip", !baton->withMetadata)
1142 ->set("distance", baton->jxlDistance)
1143 ->set("tier", baton->jxlDecodingTier)
1144 ->set("effort", baton->jxlEffort)
1145 ->set("lossless", baton->jxlLossless));
1146 baton->formatOut = "jxl";
1147 } else if (baton->formatOut == "dz" || isDz || isDzZip) {
1148 // Write DZ to file
1149 if (isDzZip) {
1150 baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
1151 }
1152 if (!sharp::HasAlpha(image)) {
1153 baton->tileBackground.pop_back();
1154 }
1155 vips::VOption *options = BuildOptionsDZ(baton);
1156 image.dzsave(const_cast<char*>(baton->fileOut.data()), options);
1157 baton->formatOut = "dz";
1158 } else if (baton->formatOut == "v" || (mightMatchInput && isV) ||
1159 (willMatchInput && inputImageType == sharp::ImageType::VIPS)) {
1160 // Write V to file
1161 image.vipssave(const_cast<char*>(baton->fileOut.data()), VImage::option()
1162 ->set("strip", !baton->withMetadata));
1163 baton->formatOut = "v";
1164 } else {
1165 // Unsupported output format
1166 (baton->err).append("Unsupported output format " + baton->fileOut);
1167 return Error();
1168 }
1169 }
1170 } catch (vips::VError const &err) {
1171 char const *what = err.what();
1172 if (what && what[0]) {
1173 (baton->err).append(what);
1174 } else {
1175 (baton->err).append("Unknown error");
1176 }
1177 }
1178 // Clean up libvips' per-request data and threads
1179 vips_error_clear();
1180 vips_thread_shutdown();
1181 }
1182
1183 void OnOK() {
1184 Napi::Env env = Env();
1185 Napi::HandleScope scope(env);
1186
1187 // Handle warnings
1188 std::string warning = sharp::VipsWarningPop();
1189 while (!warning.empty()) {
1190 debuglog.MakeCallback(Receiver().Value(), { Napi::String::New(env, warning) });
1191 warning = sharp::VipsWarningPop();
1192 }
1193
1194 if (baton->err.empty()) {
1195 int width = baton->width;
1196 int height = baton->height;
1197 if (baton->topOffsetPre != -1 && (baton->width == -1 || baton->height == -1)) {
1198 width = baton->widthPre;
1199 height = baton->heightPre;
1200 }
1201 if (baton->topOffsetPost != -1) {
1202 width = baton->widthPost;
1203 height = baton->heightPost;
1204 }
1205 // Info Object
1206 Napi::Object info = Napi::Object::New(env);
1207 info.Set("format", baton->formatOut);
1208 info.Set("width", static_cast<uint32_t>(width));
1209 info.Set("height", static_cast<uint32_t>(height));
1210 info.Set("channels", static_cast<uint32_t>(baton->channels));
1211 if (baton->formatOut == "raw") {
1212 info.Set("depth", vips_enum_nick(VIPS_TYPE_BAND_FORMAT, baton->rawDepth));
1213 }
1214 info.Set("premultiplied", baton->premultiplied);
1215 if (baton->hasCropOffset) {
1216 info.Set("cropOffsetLeft", static_cast<int32_t>(baton->cropOffsetLeft));
1217 info.Set("cropOffsetTop", static_cast<int32_t>(baton->cropOffsetTop));
1218 }
1219 if (baton->hasAttentionCenter) {
1220 info.Set("attentionX", static_cast<int32_t>(baton->attentionX));
1221 info.Set("attentionY", static_cast<int32_t>(baton->attentionY));
1222 }
1223 if (baton->trimThreshold > 0.0) {
1224 info.Set("trimOffsetLeft", static_cast<int32_t>(baton->trimOffsetLeft));
1225 info.Set("trimOffsetTop", static_cast<int32_t>(baton->trimOffsetTop));
1226 }
1227
1228 if (baton->input->textAutofitDpi) {
1229 info.Set("textAutofitDpi", static_cast<uint32_t>(baton->input->textAutofitDpi));
1230 }
1231
1232 if (baton->bufferOutLength > 0) {
1233 // Add buffer size to info
1234 info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));
1235 // Pass ownership of output data to Buffer instance
1236 Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
1237 baton->bufferOutLength, sharp::FreeCallback);
1238 Callback().MakeCallback(Receiver().Value(), { env.Null(), data, info });
1239 } else {
1240 // Add file size to info
1241 struct STAT64_STRUCT st;
1242 if (STAT64_FUNCTION(baton->fileOut.data(), &st) == 0) {
1243 info.Set("size", static_cast<uint32_t>(st.st_size));
1244 }
1245 Callback().MakeCallback(Receiver().Value(), { env.Null(), info });
1246 }
1247 } else {
1248 Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() });
1249 }
1250
1251 // Delete baton
1252 delete baton->input;
1253 delete baton->boolean;
1254 for (Composite *composite : baton->composite) {
1255 delete composite->input;
1256 delete composite;
1257 }
1258 for (sharp::InputDescriptor *input : baton->joinChannelIn) {
1259 delete input;
1260 }
1261 delete baton;
1262
1263 // Decrement processing task counter
1264 g_atomic_int_dec_and_test(&sharp::counterProcess);
1265 Napi::Number queueLength = Napi::Number::New(env, static_cast<double>(sharp::counterQueue));
1266 queueListener.MakeCallback(Receiver().Value(), { queueLength });
1267 }
1268
1269 private:
1270 PipelineBaton *baton;
1271 Napi::FunctionReference debuglog;
1272 Napi::FunctionReference queueListener;
1273
1274 void MultiPageUnsupported(int const pages, std::string op) {
1275 if (pages > 1) {
1276 throw vips::VError(op + " is not supported for multi-page images");
1277 }
1278 }
1279
1280 /*
1281 Calculate the angle of rotation and need-to-flip for the given Exif orientation
1282 By default, returns zero, i.e. no rotation.
1283 */
1284 std::tuple<VipsAngle, bool, bool>
1285 CalculateExifRotationAndFlip(int const exifOrientation) {
1286 VipsAngle rotate = VIPS_ANGLE_D0;
1287 bool flip = FALSE;
1288 bool flop = FALSE;
1289 switch (exifOrientation) {
1290 case 6: rotate = VIPS_ANGLE_D90; break;
1291 case 3: rotate = VIPS_ANGLE_D180; break;
1292 case 8: rotate = VIPS_ANGLE_D270; break;
1293 case 2: flop = TRUE; break; // flop 1
1294 case 7: flip = TRUE; rotate = VIPS_ANGLE_D90; break; // flip 6
1295 case 4: flop = TRUE; rotate = VIPS_ANGLE_D180; break; // flop 3
1296 case 5: flip = TRUE; rotate = VIPS_ANGLE_D270; break; // flip 8
1297 }
1298 return std::make_tuple(rotate, flip, flop);
1299 }
1300
1301 /*
1302 Calculate the rotation for the given angle.
1303 Supports any positive or negative angle that is a multiple of 90.
1304 */
1305 VipsAngle
1306 CalculateAngleRotation(int angle) {
1307 angle = angle % 360;
1308 if (angle < 0)
1309 angle = 360 + angle;
1310 switch (angle) {
1311 case 90: return VIPS_ANGLE_D90;
1312 case 180: return VIPS_ANGLE_D180;
1313 case 270: return VIPS_ANGLE_D270;
1314 }
1315 return VIPS_ANGLE_D0;
1316 }
1317
1318 /*
1319 Assemble the suffix argument to dzsave, which is the format (by extname)
1320 alongside comma-separated arguments to the corresponding `formatsave` vips
1321 action.
1322 */
1323 std::string
1324 AssembleSuffixString(std::string extname, std::vector<std::pair<std::string, std::string>> options) {
1325 std::string argument;
1326 for (auto const &option : options) {
1327 if (!argument.empty()) {
1328 argument += ",";
1329 }
1330 argument += option.first + "=" + option.second;
1331 }
1332 return extname + "[" + argument + "]";
1333 }
1334
1335 /*
1336 Build VOption for dzsave
1337 */
1338 vips::VOption*
1339 BuildOptionsDZ(PipelineBaton *baton) {
1340 // Forward format options through suffix
1341 std::string suffix;
1342 if (baton->tileFormat == "png") {
1343 std::vector<std::pair<std::string, std::string>> options {
1344 {"interlace", baton->pngProgressive ? "TRUE" : "FALSE"},
1345 {"compression", std::to_string(baton->pngCompressionLevel)},
1346 {"filter", baton->pngAdaptiveFiltering ? "all" : "none"}
1347 };
1348 suffix = AssembleSuffixString(".png", options);
1349 } else if (baton->tileFormat == "webp") {
1350 std::vector<std::pair<std::string, std::string>> options {
1351 {"Q", std::to_string(baton->webpQuality)},
1352 {"alpha_q", std::to_string(baton->webpAlphaQuality)},
1353 {"lossless", baton->webpLossless ? "TRUE" : "FALSE"},
1354 {"near_lossless", baton->webpNearLossless ? "TRUE" : "FALSE"},
1355 {"smart_subsample", baton->webpSmartSubsample ? "TRUE" : "FALSE"},
1356 {"min_size", baton->webpMinSize ? "TRUE" : "FALSE"},
1357 {"mixed", baton->webpMixed ? "TRUE" : "FALSE"},
1358 {"effort", std::to_string(baton->webpEffort)}
1359 };
1360 suffix = AssembleSuffixString(".webp", options);
1361 } else {
1362 std::vector<std::pair<std::string, std::string>> options {
1363 {"Q", std::to_string(baton->jpegQuality)},
1364 {"interlace", baton->jpegProgressive ? "TRUE" : "FALSE"},
1365 {"subsample_mode", baton->jpegChromaSubsampling == "4:4:4" ? "off" : "on"},
1366 {"trellis_quant", baton->jpegTrellisQuantisation ? "TRUE" : "FALSE"},
1367 {"quant_table", std::to_string(baton->jpegQuantisationTable)},
1368 {"overshoot_deringing", baton->jpegOvershootDeringing ? "TRUE": "FALSE"},
1369 {"optimize_scans", baton->jpegOptimiseScans ? "TRUE": "FALSE"},
1370 {"optimize_coding", baton->jpegOptimiseCoding ? "TRUE": "FALSE"}
1371 };
1372 std::string extname = baton->tileLayout == VIPS_FOREIGN_DZ_LAYOUT_DZ ? ".jpeg" : ".jpg";
1373 suffix = AssembleSuffixString(extname, options);
1374 }
1375 vips::VOption *options = VImage::option()
1376 ->set("strip", !baton->withMetadata)
1377 ->set("tile_size", baton->tileSize)
1378 ->set("overlap", baton->tileOverlap)
1379 ->set("container", baton->tileContainer)
1380 ->set("layout", baton->tileLayout)
1381 ->set("suffix", const_cast<char*>(suffix.data()))
1382 ->set("angle", CalculateAngleRotation(baton->tileAngle))
1383 ->set("background", baton->tileBackground)
1384 ->set("centre", baton->tileCentre)
1385 ->set("id", const_cast<char*>(baton->tileId.data()))
1386 ->set("skip_blanks", baton->tileSkipBlanks);
1387 if (baton->tileDepth < VIPS_FOREIGN_DZ_DEPTH_LAST) {
1388 options->set("depth", baton->tileDepth);
1389 }
1390 if (!baton->tileBasename.empty()) {
1391 options->set("basename", const_cast<char*>(baton->tileBasename.data()));
1392 }
1393 return options;
1394 }
1395
1396 /*
1397 Clear all thread-local data.
1398 */
1399 void Error() {
1400 // Clean up libvips' per-request data and threads
1401 vips_error_clear();
1402 vips_thread_shutdown();
1403 }
1404};
1405
1406/*
1407 pipeline(options, output, callback)
1408*/
1409Napi::Value pipeline(const Napi::CallbackInfo& info) {
1410 // V8 objects are converted to non-V8 types held in the baton struct
1411 PipelineBaton *baton = new PipelineBaton;
1412 Napi::Object options = info[size_t(0)].As<Napi::Object>();
1413
1414 // Input
1415 baton->input = sharp::CreateInputDescriptor(options.Get("input").As<Napi::Object>());
1416 // Extract image options
1417 baton->topOffsetPre = sharp::AttrAsInt32(options, "topOffsetPre");
1418 baton->leftOffsetPre = sharp::AttrAsInt32(options, "leftOffsetPre");
1419 baton->widthPre = sharp::AttrAsInt32(options, "widthPre");
1420 baton->heightPre = sharp::AttrAsInt32(options, "heightPre");
1421 baton->topOffsetPost = sharp::AttrAsInt32(options, "topOffsetPost");
1422 baton->leftOffsetPost = sharp::AttrAsInt32(options, "leftOffsetPost");
1423 baton->widthPost = sharp::AttrAsInt32(options, "widthPost");
1424 baton->heightPost = sharp::AttrAsInt32(options, "heightPost");
1425 // Output image dimensions
1426 baton->width = sharp::AttrAsInt32(options, "width");
1427 baton->height = sharp::AttrAsInt32(options, "height");
1428 // Canvas option
1429 std::string canvas = sharp::AttrAsStr(options, "canvas");
1430 if (canvas == "crop") {
1431 baton->canvas = sharp::Canvas::CROP;
1432 } else if (canvas == "embed") {
1433 baton->canvas = sharp::Canvas::EMBED;
1434 } else if (canvas == "max") {
1435 baton->canvas = sharp::Canvas::MAX;
1436 } else if (canvas == "min") {
1437 baton->canvas = sharp::Canvas::MIN;
1438 } else if (canvas == "ignore_aspect") {
1439 baton->canvas = sharp::Canvas::IGNORE_ASPECT;
1440 }
1441 // Composite
1442 Napi::Array compositeArray = options.Get("composite").As<Napi::Array>();
1443 for (unsigned int i = 0; i < compositeArray.Length(); i++) {
1444 Napi::Object compositeObject = compositeArray.Get(i).As<Napi::Object>();
1445 Composite *composite = new Composite;
1446 composite->input = sharp::CreateInputDescriptor(compositeObject.Get("input").As<Napi::Object>());
1447 composite->mode = sharp::AttrAsEnum<VipsBlendMode>(compositeObject, "blend", VIPS_TYPE_BLEND_MODE);
1448 composite->gravity = sharp::AttrAsUint32(compositeObject, "gravity");
1449 composite->left = sharp::AttrAsInt32(compositeObject, "left");
1450 composite->top = sharp::AttrAsInt32(compositeObject, "top");
1451 composite->hasOffset = sharp::AttrAsBool(compositeObject, "hasOffset");
1452 composite->tile = sharp::AttrAsBool(compositeObject, "tile");
1453 composite->premultiplied = sharp::AttrAsBool(compositeObject, "premultiplied");
1454 baton->composite.push_back(composite);
1455 }
1456 // Resize options
1457 baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement");
1458 baton->withoutReduction = sharp::AttrAsBool(options, "withoutReduction");
1459 baton->position = sharp::AttrAsInt32(options, "position");
1460 baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground");
1461 baton->kernel = sharp::AttrAsEnum<VipsKernel>(options, "kernel", VIPS_TYPE_KERNEL);
1462 baton->fastShrinkOnLoad = sharp::AttrAsBool(options, "fastShrinkOnLoad");
1463 // Join Channel Options
1464 if (options.Has("joinChannelIn")) {
1465 Napi::Array joinChannelArray = options.Get("joinChannelIn").As<Napi::Array>();
1466 for (unsigned int i = 0; i < joinChannelArray.Length(); i++) {
1467 baton->joinChannelIn.push_back(
1468 sharp::CreateInputDescriptor(joinChannelArray.Get(i).As<Napi::Object>()));
1469 }
1470 }
1471 // Operators
1472 baton->flatten = sharp::AttrAsBool(options, "flatten");
1473 baton->flattenBackground = sharp::AttrAsVectorOfDouble(options, "flattenBackground");
1474 baton->unflatten = sharp::AttrAsBool(options, "unflatten");
1475 baton->negate = sharp::AttrAsBool(options, "negate");
1476 baton->negateAlpha = sharp::AttrAsBool(options, "negateAlpha");
1477 baton->blurSigma = sharp::AttrAsDouble(options, "blurSigma");
1478 baton->brightness = sharp::AttrAsDouble(options, "brightness");
1479 baton->saturation = sharp::AttrAsDouble(options, "saturation");
1480 baton->hue = sharp::AttrAsInt32(options, "hue");
1481 baton->lightness = sharp::AttrAsDouble(options, "lightness");
1482 baton->medianSize = sharp::AttrAsUint32(options, "medianSize");
1483 baton->sharpenSigma = sharp::AttrAsDouble(options, "sharpenSigma");
1484 baton->sharpenM1 = sharp::AttrAsDouble(options, "sharpenM1");
1485 baton->sharpenM2 = sharp::AttrAsDouble(options, "sharpenM2");
1486 baton->sharpenX1 = sharp::AttrAsDouble(options, "sharpenX1");
1487 baton->sharpenY2 = sharp::AttrAsDouble(options, "sharpenY2");
1488 baton->sharpenY3 = sharp::AttrAsDouble(options, "sharpenY3");
1489 baton->threshold = sharp::AttrAsInt32(options, "threshold");
1490 baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale");
1491 baton->trimBackground = sharp::AttrAsVectorOfDouble(options, "trimBackground");
1492 baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
1493 baton->gamma = sharp::AttrAsDouble(options, "gamma");
1494 baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
1495 baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
1496 baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB");
1497 baton->greyscale = sharp::AttrAsBool(options, "greyscale");
1498 baton->normalise = sharp::AttrAsBool(options, "normalise");
1499 baton->normaliseLower = sharp::AttrAsUint32(options, "normaliseLower");
1500 baton->normaliseUpper = sharp::AttrAsUint32(options, "normaliseUpper");
1501 baton->tintA = sharp::AttrAsDouble(options, "tintA");
1502 baton->tintB = sharp::AttrAsDouble(options, "tintB");
1503 baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
1504 baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
1505 baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
1506 baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation");
1507 baton->angle = sharp::AttrAsInt32(options, "angle");
1508 baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
1509 baton->rotationBackground = sharp::AttrAsVectorOfDouble(options, "rotationBackground");
1510 baton->rotateBeforePreExtract = sharp::AttrAsBool(options, "rotateBeforePreExtract");
1511 baton->flip = sharp::AttrAsBool(options, "flip");
1512 baton->flop = sharp::AttrAsBool(options, "flop");
1513 baton->extendTop = sharp::AttrAsInt32(options, "extendTop");
1514 baton->extendBottom = sharp::AttrAsInt32(options, "extendBottom");
1515 baton->extendLeft = sharp::AttrAsInt32(options, "extendLeft");
1516 baton->extendRight = sharp::AttrAsInt32(options, "extendRight");
1517 baton->extendBackground = sharp::AttrAsVectorOfDouble(options, "extendBackground");
1518 baton->extendWith = sharp::AttrAsEnum<VipsExtend>(options, "extendWith", VIPS_TYPE_EXTEND);
1519 baton->extractChannel = sharp::AttrAsInt32(options, "extractChannel");
1520 baton->affineMatrix = sharp::AttrAsVectorOfDouble(options, "affineMatrix");
1521 baton->affineBackground = sharp::AttrAsVectorOfDouble(options, "affineBackground");
1522 baton->affineIdx = sharp::AttrAsDouble(options, "affineIdx");
1523 baton->affineIdy = sharp::AttrAsDouble(options, "affineIdy");
1524 baton->affineOdx = sharp::AttrAsDouble(options, "affineOdx");
1525 baton->affineOdy = sharp::AttrAsDouble(options, "affineOdy");
1526 baton->affineInterpolator = sharp::AttrAsStr(options, "affineInterpolator");
1527 baton->removeAlpha = sharp::AttrAsBool(options, "removeAlpha");
1528 baton->ensureAlpha = sharp::AttrAsDouble(options, "ensureAlpha");
1529 if (options.Has("boolean")) {
1530 baton->boolean = sharp::CreateInputDescriptor(options.Get("boolean").As<Napi::Object>());
1531 baton->booleanOp = sharp::AttrAsEnum<VipsOperationBoolean>(options, "booleanOp", VIPS_TYPE_OPERATION_BOOLEAN);
1532 }
1533 if (options.Has("bandBoolOp")) {
1534 baton->bandBoolOp = sharp::AttrAsEnum<VipsOperationBoolean>(options, "bandBoolOp", VIPS_TYPE_OPERATION_BOOLEAN);
1535 }
1536 if (options.Has("convKernel")) {
1537 Napi::Object kernel = options.Get("convKernel").As<Napi::Object>();
1538 baton->convKernelWidth = sharp::AttrAsUint32(kernel, "width");
1539 baton->convKernelHeight = sharp::AttrAsUint32(kernel, "height");
1540 baton->convKernelScale = sharp::AttrAsDouble(kernel, "scale");
1541 baton->convKernelOffset = sharp::AttrAsDouble(kernel, "offset");
1542 size_t const kernelSize = static_cast<size_t>(baton->convKernelWidth * baton->convKernelHeight);
1543 baton->convKernel = std::unique_ptr<double[]>(new double[kernelSize]);
1544 Napi::Array kdata = kernel.Get("kernel").As<Napi::Array>();
1545 for (unsigned int i = 0; i < kernelSize; i++) {
1546 baton->convKernel[i] = sharp::AttrAsDouble(kdata, i);
1547 }
1548 }
1549 if (options.Has("recombMatrix")) {
1550 baton->recombMatrix = std::unique_ptr<double[]>(new double[9]);
1551 Napi::Array recombMatrix = options.Get("recombMatrix").As<Napi::Array>();
1552 for (unsigned int i = 0; i < 9; i++) {
1553 baton->recombMatrix[i] = sharp::AttrAsDouble(recombMatrix, i);
1554 }
1555 }
1556 baton->colourspaceInput = sharp::AttrAsEnum<VipsInterpretation>(
1557 options, "colourspaceInput", VIPS_TYPE_INTERPRETATION);
1558 if (baton->colourspaceInput == VIPS_INTERPRETATION_ERROR) {
1559 baton->colourspaceInput = VIPS_INTERPRETATION_LAST;
1560 }
1561 baton->colourspace = sharp::AttrAsEnum<VipsInterpretation>(options, "colourspace", VIPS_TYPE_INTERPRETATION);
1562 if (baton->colourspace == VIPS_INTERPRETATION_ERROR) {
1563 baton->colourspace = VIPS_INTERPRETATION_sRGB;
1564 }
1565 // Output
1566 baton->formatOut = sharp::AttrAsStr(options, "formatOut");
1567 baton->fileOut = sharp::AttrAsStr(options, "fileOut");
1568 baton->withMetadata = sharp::AttrAsBool(options, "withMetadata");
1569 baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
1570 baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");
1571 baton->withMetadataIcc = sharp::AttrAsStr(options, "withMetadataIcc");
1572 Napi::Object mdStrs = options.Get("withMetadataStrs").As<Napi::Object>();
1573 Napi::Array mdStrKeys = mdStrs.GetPropertyNames();
1574 for (unsigned int i = 0; i < mdStrKeys.Length(); i++) {
1575 std::string k = sharp::AttrAsStr(mdStrKeys, i);
1576 if (mdStrs.HasOwnProperty(k)) {
1577 baton->withMetadataStrs.insert(std::make_pair(k, sharp::AttrAsStr(mdStrs, k)));
1578 }
1579 }
1580 baton->timeoutSeconds = sharp::AttrAsUint32(options, "timeoutSeconds");
1581 // Format-specific
1582 baton->jpegQuality = sharp::AttrAsUint32(options, "jpegQuality");
1583 baton->jpegProgressive = sharp::AttrAsBool(options, "jpegProgressive");
1584 baton->jpegChromaSubsampling = sharp::AttrAsStr(options, "jpegChromaSubsampling");
1585 baton->jpegTrellisQuantisation = sharp::AttrAsBool(options, "jpegTrellisQuantisation");
1586 baton->jpegQuantisationTable = sharp::AttrAsUint32(options, "jpegQuantisationTable");
1587 baton->jpegOvershootDeringing = sharp::AttrAsBool(options, "jpegOvershootDeringing");
1588 baton->jpegOptimiseScans = sharp::AttrAsBool(options, "jpegOptimiseScans");
1589 baton->jpegOptimiseCoding = sharp::AttrAsBool(options, "jpegOptimiseCoding");
1590 baton->pngProgressive = sharp::AttrAsBool(options, "pngProgressive");
1591 baton->pngCompressionLevel = sharp::AttrAsUint32(options, "pngCompressionLevel");
1592 baton->pngAdaptiveFiltering = sharp::AttrAsBool(options, "pngAdaptiveFiltering");
1593 baton->pngPalette = sharp::AttrAsBool(options, "pngPalette");
1594 baton->pngQuality = sharp::AttrAsUint32(options, "pngQuality");
1595 baton->pngEffort = sharp::AttrAsUint32(options, "pngEffort");
1596 baton->pngBitdepth = sharp::AttrAsUint32(options, "pngBitdepth");
1597 baton->pngDither = sharp::AttrAsDouble(options, "pngDither");
1598 baton->jp2Quality = sharp::AttrAsUint32(options, "jp2Quality");
1599 baton->jp2Lossless = sharp::AttrAsBool(options, "jp2Lossless");
1600 baton->jp2TileHeight = sharp::AttrAsUint32(options, "jp2TileHeight");
1601 baton->jp2TileWidth = sharp::AttrAsUint32(options, "jp2TileWidth");
1602 baton->jp2ChromaSubsampling = sharp::AttrAsStr(options, "jp2ChromaSubsampling");
1603 baton->webpQuality = sharp::AttrAsUint32(options, "webpQuality");
1604 baton->webpAlphaQuality = sharp::AttrAsUint32(options, "webpAlphaQuality");
1605 baton->webpLossless = sharp::AttrAsBool(options, "webpLossless");
1606 baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
1607 baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
1608 baton->webpEffort = sharp::AttrAsUint32(options, "webpEffort");
1609 baton->webpMinSize = sharp::AttrAsBool(options, "webpMinSize");
1610 baton->webpMixed = sharp::AttrAsBool(options, "webpMixed");
1611 baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
1612 baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
1613 baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
1614 baton->gifInterFrameMaxError = sharp::AttrAsDouble(options, "gifInterFrameMaxError");
1615 baton->gifInterPaletteMaxError = sharp::AttrAsDouble(options, "gifInterPaletteMaxError");
1616 baton->gifReuse = sharp::AttrAsBool(options, "gifReuse");
1617 baton->gifProgressive = sharp::AttrAsBool(options, "gifProgressive");
1618 baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
1619 baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
1620 baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
1621 baton->tiffTile = sharp::AttrAsBool(options, "tiffTile");
1622 baton->tiffTileWidth = sharp::AttrAsUint32(options, "tiffTileWidth");
1623 baton->tiffTileHeight = sharp::AttrAsUint32(options, "tiffTileHeight");
1624 baton->tiffXres = sharp::AttrAsDouble(options, "tiffXres");
1625 baton->tiffYres = sharp::AttrAsDouble(options, "tiffYres");
1626 if (baton->tiffXres == 1.0 && baton->tiffYres == 1.0 && baton->withMetadataDensity > 0) {
1627 baton->tiffXres = baton->tiffYres = baton->withMetadataDensity / 25.4;
1628 }
1629 baton->tiffCompression = sharp::AttrAsEnum<VipsForeignTiffCompression>(
1630 options, "tiffCompression", VIPS_TYPE_FOREIGN_TIFF_COMPRESSION);
1631 baton->tiffPredictor = sharp::AttrAsEnum<VipsForeignTiffPredictor>(
1632 options, "tiffPredictor", VIPS_TYPE_FOREIGN_TIFF_PREDICTOR);
1633 baton->tiffResolutionUnit = sharp::AttrAsEnum<VipsForeignTiffResunit>(
1634 options, "tiffResolutionUnit", VIPS_TYPE_FOREIGN_TIFF_RESUNIT);
1635 baton->heifQuality = sharp::AttrAsUint32(options, "heifQuality");
1636 baton->heifLossless = sharp::AttrAsBool(options, "heifLossless");
1637 baton->heifCompression = sharp::AttrAsEnum<VipsForeignHeifCompression>(
1638 options, "heifCompression", VIPS_TYPE_FOREIGN_HEIF_COMPRESSION);
1639 baton->heifEffort = sharp::AttrAsUint32(options, "heifEffort");
1640 baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
1641 baton->jxlDistance = sharp::AttrAsDouble(options, "jxlDistance");
1642 baton->jxlDecodingTier = sharp::AttrAsUint32(options, "jxlDecodingTier");
1643 baton->jxlEffort = sharp::AttrAsUint32(options, "jxlEffort");
1644 baton->jxlLossless = sharp::AttrAsBool(options, "jxlLossless");
1645 baton->rawDepth = sharp::AttrAsEnum<VipsBandFormat>(options, "rawDepth", VIPS_TYPE_BAND_FORMAT);
1646 // Animated output properties
1647 if (sharp::HasAttr(options, "loop")) {
1648 baton->loop = sharp::AttrAsUint32(options, "loop");
1649 }
1650 if (sharp::HasAttr(options, "delay")) {
1651 baton->delay = sharp::AttrAsInt32Vector(options, "delay");
1652 }
1653 baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
1654 baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");
1655 baton->tileAngle = sharp::AttrAsInt32(options, "tileAngle");
1656 baton->tileBackground = sharp::AttrAsVectorOfDouble(options, "tileBackground");
1657 baton->tileSkipBlanks = sharp::AttrAsInt32(options, "tileSkipBlanks");
1658 baton->tileContainer = sharp::AttrAsEnum<VipsForeignDzContainer>(
1659 options, "tileContainer", VIPS_TYPE_FOREIGN_DZ_CONTAINER);
1660 baton->tileLayout = sharp::AttrAsEnum<VipsForeignDzLayout>(options, "tileLayout", VIPS_TYPE_FOREIGN_DZ_LAYOUT);
1661 baton->tileFormat = sharp::AttrAsStr(options, "tileFormat");
1662 baton->tileDepth = sharp::AttrAsEnum<VipsForeignDzDepth>(options, "tileDepth", VIPS_TYPE_FOREIGN_DZ_DEPTH);
1663 baton->tileCentre = sharp::AttrAsBool(options, "tileCentre");
1664 baton->tileId = sharp::AttrAsStr(options, "tileId");
1665 baton->tileBasename = sharp::AttrAsStr(options, "tileBasename");
1666
1667 // Force random access for certain operations
1668 if (baton->input->access == VIPS_ACCESS_SEQUENTIAL) {
1669 if (
1670 baton->trimThreshold > 0.0 ||
1671 baton->normalise ||
1672 baton->position == 16 || baton->position == 17 ||
1673 baton->angle != 0 ||
1674 baton->rotationAngle != 0.0 ||
1675 baton->tileAngle != 0 ||
1676 baton->useExifOrientation ||
1677 baton->flip ||
1678 baton->claheWidth != 0 ||
1679 !baton->affineMatrix.empty()
1680 ) {
1681 baton->input->access = VIPS_ACCESS_RANDOM;
1682 }
1683 }
1684
1685 // Function to notify of libvips warnings
1686 Napi::Function debuglog = options.Get("debuglog").As<Napi::Function>();
1687
1688 // Function to notify of queue length changes
1689 Napi::Function queueListener = options.Get("queueListener").As<Napi::Function>();
1690
1691 // Join queue for worker thread
1692 Napi::Function callback = info[size_t(1)].As<Napi::Function>();
1693 PipelineWorker *worker = new PipelineWorker(callback, baton, debuglog, queueListener);
1694 worker->Receiver().Set("options", options);
1695 worker->Queue();
1696
1697 // Increment queued task counter
1698 g_atomic_int_inc(&sharp::counterQueue);
1699 Napi::Number queueLength = Napi::Number::New(info.Env(), static_cast<double>(sharp::counterQueue));
1700 queueListener.MakeCallback(info.This(), { queueLength });
1701
1702 return info.Env().Undefined();
1703}