UNPKG

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