From e6bfa52b0b76d005085d6a8585324d65983711f3 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Tue, 26 Jul 2016 22:26:55 +0100 Subject: [PATCH] Add raw pixel data support to boolean and withOverlay ops The previously-scattered image opening logic has been refactored to a single ImageDescriptor struct/Object available to both JS and C++ code This removed about 150 LOC but more importantly reduces the complexity of adding/exposing new operations that require an input image. --- docs/api.md | 11 +- docs/changelog.md | 5 + index.js | 116 ++++---- src/common.cc | 124 +++++++-- src/common.h | 63 +++++ src/metadata.cc | 206 +++++--------- src/metadata.h | 35 +++ src/pipeline.cc | 651 ++++++++++++++----------------------------- src/pipeline.h | 33 +-- test/unit/boolean.js | 17 ++ test/unit/overlay.js | 24 ++ 11 files changed, 589 insertions(+), 696 deletions(-) diff --git a/docs/api.md b/docs/api.md index 0d872b52..b446bc1b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -457,7 +457,7 @@ Overlay (composite) a image containing an alpha channel over the processed (resi `image` is one of the following, and must be the same size or smaller than the processed image: -* Buffer containing PNG, WebP, GIF or SVG image data, or +* Buffer containing PNG, WebP, GIF, SVG, raw pixel image data, or * String containing the path to an image file, with most major transparency formats supported. `options`, if present, is an Object with the following optional attributes: @@ -467,6 +467,7 @@ Overlay (composite) a image containing an alpha channel over the processed (resi * `left` is an integral Number representing the pixel offset from the left edge. * `tile` is a Boolean, defaulting to `false`. When set to `true` repeats the overlay image across the entire image with the given `gravity`. * `cutout` is a Boolean, defaulting to `false`. When set to `true` applies only the alpha channel of the overlay image to the image to be overlaid, giving the appearance of one image being cut out of another. +* `raw` an Object containing `width`, `height` and `channels` when providing uncompressed data. If both `top` and `left` are provided, they take precedence over `gravity`. @@ -523,11 +524,11 @@ sharp('input.png') In the above example if `input.png` is a 3 channel RGB image, `output.png` will be a 1 channel grayscale image where each pixel `P = R & G & B`. For example, if `I(1,1) = [247, 170, 14] = [0b11110111, 0b10101010, 0b00001111]` then `O(1,1) = 0b11110111 & 0b10101010 & 0b00001111 = 0b00000010 = 2`. -#### boolean(image, operation) +#### boolean(image, operation, [options]) Perform a bitwise boolean operation with `image`, where `image` is one of the following: -* Buffer containing PNG, WebP, GIF or SVG image data, or +* Buffer containing JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data, or * String containing the path to an image file This operation creates an output image where each pixel is the result of the selected bitwise boolean `operation` between the corresponding pixels of the input images. @@ -537,6 +538,10 @@ The boolean operation can be one of the following: * `or` performs a bitwise or operation, like the c-operator `|`. * `eor` performs a bitwise exclusive or operation, like the c-operator `^`. +`options`, if present, is an Object with the following optional attributes: + +* `raw` an Object containing `width`, `height` and `channels` when providing uncompressed data. + ### Output #### toFile(path, [callback]) diff --git a/docs/changelog.md b/docs/changelog.md index f6825bde..c06efe7a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -21,11 +21,16 @@ Requires libvips v8.3.2 [#511](https://github.com/lovell/sharp/pull/511) [@mhirsch](https://github.com/mhirsch) +* Add support for raw pixel data with boolean and withOverlay operations. + [#516](https://github.com/lovell/sharp/pull/516) + [@mhirsch](https://github.com/mhirsch) + * Ensure ICC profiles are removed from PNG output unless withMetadata used. [#521](https://github.com/lovell/sharp/issues/521) [@ChrisPinewood](https://github.com/ChrisPinewood) * Remove deprecated interpolateWith method - use resize(w, h, { interpolator: ... }) + [#310](https://github.com/lovell/sharp/issues/310) ### v0.15 - "*outfit*" diff --git a/index.js b/index.js index eb6160b8..5f85fee2 100644 --- a/index.js +++ b/index.js @@ -42,14 +42,8 @@ var Sharp = function(input, options) { stream.Duplex.call(this); this.options = { // input options - bufferIn: [], - streamIn: false, sequentialRead: false, limitInputPixels: maximum.pixels, - density: 72, - rawWidth: 0, - rawHeight: 0, - rawChannels: 0, // ICC profiles iccProfilePath: path.join(__dirname, 'icc') + path.sep, // resize options @@ -93,8 +87,6 @@ var Sharp = function(input, options) { booleanBufferIn: null, booleanFileIn: '', // overlay - overlayFileIn: '', - overlayBufferIn: null, overlayGravity: 0, overlayXOffset : -1, overlayYOffset : -1, @@ -122,19 +114,7 @@ var Sharp = function(input, options) { module.exports.queue.emit('change', queueLength); } }; - if (isString(input)) { - // input=file - this.options.fileIn = input; - } else if (isBuffer(input)) { - // input=buffer - this.options.bufferIn = input; - } else if (!isDefined(input)) { - // input=stream - this.options.streamIn = true; - } else { - throw new Error('Unsupported input ' + typeof input); - } - this._inputOptions(options); + this.options.input = this._createInputDescriptor(input, options, { allowStream: true }); return this; }; module.exports = Sharp; @@ -187,37 +167,50 @@ var contains = function(val, list) { }; /* - Set input-related options - density: DPI at which to load vector images via libmagick + Create Object containing input and input-related options */ -Sharp.prototype._inputOptions = function(options) { - if (isObject(options)) { +Sharp.prototype._createInputDescriptor = function(input, inputOptions, containerOptions) { + var inputDescriptor = {}; + if (isString(input)) { + // filesystem + inputDescriptor.file = input; + } else if (isBuffer(input)) { + // Buffer + inputDescriptor.buffer = input; + } else if (!isDefined(input) && isObject(containerOptions) && containerOptions.allowStream) { + // Stream + inputDescriptor.buffer = []; + } else { + throw new Error('Unsupported input ' + typeof input); + } + if (isObject(inputOptions)) { // Density - if (isDefined(options.density)) { - if (isInteger(options.density) && inRange(options.density, 1, 2400)) { - this.options.density = options.density; + if (isDefined(inputOptions.density)) { + if (isInteger(inputOptions.density) && inRange(inputOptions.density, 1, 2400)) { + inputDescriptor.density = inputOptions.density; } else { - throw new Error('Invalid density (1 to 2400) ' + options.density); + throw new Error('Invalid density (1 to 2400) ' + inputOptions.density); } } // Raw pixel input - if (isDefined(options.raw)) { + if (isDefined(inputOptions.raw)) { if ( - isObject(options.raw) && - isInteger(options.raw.width) && inRange(options.raw.width, 1, maximum.width) && - isInteger(options.raw.height) && inRange(options.raw.height, 1, maximum.height) && - isInteger(options.raw.channels) && inRange(options.raw.channels, 1, 4) + isObject(inputOptions.raw) && + isInteger(inputOptions.raw.width) && inRange(inputOptions.raw.width, 1, maximum.width) && + isInteger(inputOptions.raw.height) && inRange(inputOptions.raw.height, 1, maximum.height) && + isInteger(inputOptions.raw.channels) && inRange(inputOptions.raw.channels, 1, 4) ) { - this.options.rawWidth = options.raw.width; - this.options.rawHeight = options.raw.height; - this.options.rawChannels = options.raw.channels; + inputDescriptor.rawWidth = inputOptions.raw.width; + inputDescriptor.rawHeight = inputOptions.raw.height; + inputDescriptor.rawChannels = inputOptions.raw.channels; } else { throw new Error('Expected width, height and channels for raw pixel input'); } } - } else if (isDefined(options)) { - throw new Error('Invalid input options ' + options); + } else if (isDefined(inputOptions)) { + throw new Error('Invalid input options ' + inputOptions); } + return inputDescriptor; }; /* @@ -225,9 +218,9 @@ Sharp.prototype._inputOptions = function(options) { */ Sharp.prototype._write = function(chunk, encoding, callback) { /*jslint unused: false */ - if (this.options.streamIn) { + if (Array.isArray(this.options.input.buffer)) { if (isBuffer(chunk)) { - this.options.bufferIn.push(chunk); + this.options.input.buffer.push(chunk); callback(); } else { callback(new Error('Non-Buffer data on Writable Stream')); @@ -238,13 +231,16 @@ Sharp.prototype._write = function(chunk, encoding, callback) { }; /* - Flattens the array of chunks in bufferIn + Flattens the array of chunks accumulated in input.buffer */ Sharp.prototype._flattenBufferIn = function() { - if (Array.isArray(this.options.bufferIn)) { - this.options.bufferIn = Buffer.concat(this.options.bufferIn); + if (this._isStreamInput()) { + this.options.input.buffer = Buffer.concat(this.options.input.buffer); } }; +Sharp.prototype._isStreamInput = function() { + return Array.isArray(this.options.input.buffer); +}; // Weighting to apply to image crop module.exports.gravity = { @@ -369,14 +365,8 @@ Sharp.prototype.negate = function(negate) { /* Bitwise boolean operations between images */ -Sharp.prototype.boolean = function(operand, operator) { - if (isString(operand)) { - this.options.booleanFileIn = operand; - } else if (isBuffer(operand)) { - this.options.booleanBufferIn = operand; - } else { - throw new Error('Unsupported boolean operand ' + typeof operand); - } +Sharp.prototype.boolean = function(operand, operator, options) { + this.options.boolean = this._createInputDescriptor(operand, options); if (isString(operator) && contains(operator, ['and', 'or', 'eor'])) { this.options.booleanOp = operator; } else { @@ -389,13 +379,9 @@ Sharp.prototype.boolean = function(operand, operator) { Overlay with another image, using an optional gravity */ Sharp.prototype.overlayWith = function(overlay, options) { - if (isString(overlay)) { - this.options.overlayFileIn = overlay; - } else if (isBuffer(overlay)) { - this.options.overlayBufferIn = overlay; - } else { - throw new Error('Unsupported overlay ' + typeof overlay); - } + this.options.overlay = this._createInputDescriptor(overlay, options, { + allowStream: false + }); if (isObject(options)) { if (isDefined(options.tile)) { if (isBoolean(options.tile)) { @@ -909,7 +895,7 @@ Sharp.prototype.toFile = function(fileOut, callback) { return BluebirdPromise.reject(errOutputInvalid); } } else { - if (this.options.fileIn === fileOut) { + if (this.options.input.file === fileOut) { var errOutputIsInput = new Error('Cannot use same file for input and output'); if (typeof callback === 'function') { callback(errOutputIsInput); @@ -999,7 +985,7 @@ Sharp.prototype._pipeline = function(callback) { var that = this; if (typeof callback === 'function') { // output=file/buffer - if (this.options.streamIn) { + if (this._isStreamInput()) { // output=file/buffer, input=stream this.on('finish', function() { that._flattenBufferIn(); @@ -1012,7 +998,7 @@ Sharp.prototype._pipeline = function(callback) { return this; } else if (this.options.streamOut) { // output=stream - if (this.options.streamIn) { + if (this._isStreamInput()) { // output=stream, input=stream this.on('finish', function() { that._flattenBufferIn(); @@ -1041,7 +1027,7 @@ Sharp.prototype._pipeline = function(callback) { return this; } else { // output=promise - if (this.options.streamIn) { + if (this._isStreamInput()) { // output=promise, input=stream return new BluebirdPromise(function(resolve, reject) { that.on('finish', function() { @@ -1077,7 +1063,7 @@ Sharp.prototype._pipeline = function(callback) { Sharp.prototype.metadata = function(callback) { var that = this; if (typeof callback === 'function') { - if (this.options.streamIn) { + if (this._isStreamInput()) { this.on('finish', function() { that._flattenBufferIn(); sharp.metadata(that.options, callback); @@ -1087,7 +1073,7 @@ Sharp.prototype.metadata = function(callback) { } return this; } else { - if (this.options.streamIn) { + if (this._isStreamInput()) { return new BluebirdPromise(function(resolve, reject) { that.on('finish', function() { that._flattenBufferIn(); diff --git a/src/common.cc b/src/common.cc index baec435e..94786dd4 100644 --- a/src/common.cc +++ b/src/common.cc @@ -1,32 +1,53 @@ #include #include #include + +#include +#include #include +#include "nan.h" #include "common.h" -// Verify platform and compiler compatibility - -#if (VIPS_MAJOR_VERSION < 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 2)) -#error libvips version 8.2.0+ required - see sharp.dimens.io/page/install -#endif - -#if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6))) -#error GCC version 4.6+ is required for C++11 features - see sharp.dimens.io/page/install#prerequisites -#endif - -#if (defined(__clang__) && defined(__has_feature)) -#if (!__has_feature(cxx_range_for)) -#error clang version 3.0+ is required for C++11 features - see sharp.dimens.io/page/install#prerequisites -#endif -#endif - -#define EXIF_IFD0_ORIENTATION "exif-ifd0-Orientation" - using vips::VImage; namespace sharp { + // Convenience methods to access the attributes of a v8::Object + bool HasAttr(v8::Handle obj, std::string attr) { + return Nan::Has(obj, Nan::New(attr).ToLocalChecked()).FromJust(); + } + std::string AttrAsStr(v8::Handle obj, std::string attr) { + return *Nan::Utf8String(Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked()); + } + + // Create an InputDescriptor instance from a v8::Object describing an input image + InputDescriptor* CreateInputDescriptor( + v8::Handle input, std::vector> buffersToPersist + ) { + Nan::HandleScope(); + InputDescriptor *descriptor = new InputDescriptor; + if (HasAttr(input, "file")) { + descriptor->file = AttrAsStr(input, "file"); + } else { + v8::Local buffer = AttrAs(input, "buffer"); + descriptor->bufferLength = node::Buffer::Length(buffer); + descriptor->buffer = node::Buffer::Data(buffer); + buffersToPersist.push_back(buffer); + } + // Density for vector-based input + if (HasAttr(input, "density")) { + descriptor->density = AttrTo(input, "density"); + } + // Raw pixel input + if (HasAttr(input, "rawChannels")) { + descriptor->rawChannels = AttrTo(input, "rawChannels"); + descriptor->rawWidth = AttrTo(input, "rawWidth"); + descriptor->rawHeight = AttrTo(input, "rawHeight"); + } + return descriptor; + } + // How many tasks are in the queue? volatile int counterQueue = 0; @@ -149,6 +170,73 @@ namespace sharp { return imageType; } + /* + Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data) + */ + std::tuple OpenInput(InputDescriptor *descriptor, VipsAccess accessMethod) { + VImage image; + ImageType imageType; + if (descriptor->buffer != nullptr) { + // From buffer + if (descriptor->rawChannels > 0) { + // Raw, uncompressed pixel data + image = VImage::new_from_memory(descriptor->buffer, descriptor->bufferLength, + descriptor->rawWidth, descriptor->rawHeight, descriptor->rawChannels, VIPS_FORMAT_UCHAR); + if (descriptor->rawChannels < 3) { + image.get_image()->Type = VIPS_INTERPRETATION_B_W; + } else { + image.get_image()->Type = VIPS_INTERPRETATION_sRGB; + } + imageType = ImageType::RAW; + } else { + // Compressed data + imageType = DetermineImageType(descriptor->buffer, descriptor->bufferLength); + if (imageType != ImageType::UNKNOWN) { + try { + vips::VOption *option = VImage::option()->set("access", accessMethod); + if (imageType == ImageType::SVG || imageType == ImageType::PDF) { + option->set("dpi", static_cast(descriptor->density)); + } + if (imageType == ImageType::MAGICK) { + option->set("density", std::to_string(descriptor->density).data()); + } + image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option); + if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { + SetDensity(image, descriptor->density); + } + } catch (...) { + throw vips::VError("Input buffer has corrupt header"); + } + } else { + throw vips::VError("Input buffer contains unsupported image format"); + } + } + } else { + // From filesystem + imageType = DetermineImageType(descriptor->file.data()); + if (imageType != ImageType::UNKNOWN) { + try { + vips::VOption *option = VImage::option()->set("access", accessMethod); + if (imageType == ImageType::SVG || imageType == ImageType::PDF) { + option->set("dpi", static_cast(descriptor->density)); + } + if (imageType == ImageType::MAGICK) { + option->set("density", std::to_string(descriptor->density).data()); + } + image = VImage::new_from_file(descriptor->file.data(), option); + if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { + SetDensity(image, descriptor->density); + } + } catch (...) { + throw vips::VError("Input file has corrupt header"); + } + } else { + throw vips::VError("Input file is missing or of an unsupported image format"); + } + } + return std::make_tuple(image, imageType); + } + /* Does this image have an embedded profile? */ diff --git a/src/common.h b/src/common.h index e931e460..cd7598d9 100644 --- a/src/common.h +++ b/src/common.h @@ -4,12 +4,70 @@ #include #include +#include #include +#include "nan.h" + +// Verify platform and compiler compatibility + +#if (VIPS_MAJOR_VERSION < 8 || (VIPS_MAJOR_VERSION == 8 && VIPS_MINOR_VERSION < 3)) +#error libvips version 8.3.x required - see sharp.dimens.io/page/install +#endif + +#if ((!defined(__clang__)) && defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6))) +#error GCC version 4.6+ is required for C++11 features - see sharp.dimens.io/page/install#prerequisites +#endif + +#if (defined(__clang__) && defined(__has_feature)) +#if (!__has_feature(cxx_range_for)) +#error clang version 3.0+ is required for C++11 features - see sharp.dimens.io/page/install#prerequisites +#endif +#endif + +#define EXIF_IFD0_ORIENTATION "exif-ifd0-Orientation" + using vips::VImage; namespace sharp { + struct InputDescriptor { + std::string name; + std::string file; + char *buffer; + size_t bufferLength; + int density; + int rawChannels; + int rawWidth; + int rawHeight; + + InputDescriptor(): + buffer(nullptr), + bufferLength(0), + density(72), + rawChannels(0), + rawWidth(0), + rawHeight(0) {} + }; + + // Convenience methods to access the attributes of a v8::Object + bool HasAttr(v8::Handle obj, std::string attr); + std::string AttrAsStr(v8::Handle obj, std::string attr); + template v8::Local AttrAs(v8::Handle obj, std::string attr) { + return Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked().As(); + } + template T AttrTo(v8::Handle obj, std::string attr) { + return Nan::To(Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked()).FromJust(); + } + template T AttrTo(v8::Handle obj, int attr) { + return Nan::To(Nan::Get(obj, attr).ToLocalChecked()).FromJust(); + } + + // Create an InputDescriptor instance from a v8::Object describing an input image + InputDescriptor* CreateInputDescriptor( + v8::Handle input, std::vector> buffersToPersist + ); + enum class ImageType { JPEG, PNG, @@ -57,6 +115,11 @@ namespace sharp { */ ImageType DetermineImageType(char const *file); + /* + Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data) + */ + std::tuple OpenInput(InputDescriptor *descriptor, VipsAccess accessMethod); + /* Does this image have an embedded profile? */ diff --git a/src/metadata.cc b/src/metadata.cc index e88e8d1c..6c05bf05 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -1,135 +1,54 @@ +#include + #include #include #include "nan.h" - #include "common.h" #include "metadata.h" -using v8::Handle; -using v8::Local; -using v8::Value; -using v8::Object; -using v8::Number; -using v8::String; -using v8::Boolean; -using v8::Function; -using v8::Exception; - -using Nan::AsyncQueueWorker; -using Nan::AsyncWorker; -using Nan::Callback; -using Nan::HandleScope; -using Nan::Utf8String; -using Nan::Has; -using Nan::Get; -using Nan::Set; -using Nan::New; -using Nan::NewBuffer; -using Nan::Null; -using Nan::Error; - -using vips::VImage; -using vips::VError; - -using sharp::ImageType; -using sharp::ImageTypeId; -using sharp::DetermineImageType; -using sharp::HasProfile; -using sharp::HasAlpha; -using sharp::ExifOrientation; -using sharp::HasDensity; -using sharp::GetDensity; -using sharp::FreeCallback; -using sharp::counterQueue; - -struct MetadataBaton { - // Input - std::string fileIn; - char *bufferIn; - size_t bufferInLength; - // Output - std::string format; - int width; - int height; - std::string space; - int channels; - int density; - bool hasProfile; - bool hasAlpha; - int orientation; - char *exif; - size_t exifLength; - char *icc; - size_t iccLength; - std::string err; - - MetadataBaton(): - bufferInLength(0), - density(0), - orientation(0), - exifLength(0), - iccLength(0) {} -}; - -class MetadataWorker : public AsyncWorker { +class MetadataWorker : public Nan::AsyncWorker { public: - MetadataWorker(Callback *callback, MetadataBaton *baton, const Local &bufferIn) : - AsyncWorker(callback), baton(baton) { - if (baton->bufferInLength > 0) { - SaveToPersistent("bufferIn", bufferIn); + MetadataWorker( + Nan::Callback *callback, MetadataBaton *baton, + std::vector> const buffersToPersist + ) : Nan::AsyncWorker(callback), baton(baton), buffersToPersist(buffersToPersist) { + // Protect Buffer objects from GC, keyed on index + std::accumulate(buffersToPersist.begin(), buffersToPersist.end(), 0, + [this](uint32_t index, v8::Local const buffer) -> uint32_t { + SaveToPersistent(index, buffer); + return index + 1; } - } + ); + } ~MetadataWorker() {} void Execute() { // Decrement queued task counter - g_atomic_int_dec_and_test(&counterQueue); + g_atomic_int_dec_and_test(&sharp::counterQueue); - ImageType imageType = ImageType::UNKNOWN; - VImage image; - if (baton->bufferInLength > 0) { - // From buffer - imageType = DetermineImageType(baton->bufferIn, baton->bufferInLength); - if (imageType != ImageType::UNKNOWN) { - try { - image = VImage::new_from_buffer(baton->bufferIn, baton->bufferInLength, nullptr); - } catch (...) { - (baton->err).append("Input buffer has corrupt header"); - imageType = ImageType::UNKNOWN; - } - } else { - (baton->err).append("Input buffer contains unsupported image format"); - } - } else { - // From file - imageType = DetermineImageType(baton->fileIn.data()); - if (imageType != ImageType::UNKNOWN) { - try { - image = VImage::new_from_file(baton->fileIn.data()); - } catch (...) { - (baton->err).append("Input file has corrupt header"); - imageType = ImageType::UNKNOWN; - } - } else { - (baton->err).append("Input file is missing or of an unsupported image format"); - } + vips::VImage image; + sharp::ImageType imageType = sharp::ImageType::UNKNOWN; + try { + std::tie(image, imageType) = OpenInput(baton->input, VIPS_ACCESS_SEQUENTIAL); + } catch (vips::VError const &err) { + (baton->err).append(err.what()); } - if (imageType != ImageType::UNKNOWN) { + if (imageType != sharp::ImageType::UNKNOWN) { // Image type - baton->format = ImageTypeId(imageType); + baton->format = sharp::ImageTypeId(imageType); // VipsImage attributes baton->width = image.width(); baton->height = image.height(); baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image.interpretation()); baton->channels = image.bands(); - if (HasDensity(image)) { - baton->density = GetDensity(image); + if (sharp::HasDensity(image)) { + baton->density = sharp::GetDensity(image); } - baton->hasProfile = HasProfile(image); + baton->hasProfile = sharp::HasProfile(image); // Derived attributes - baton->hasAlpha = HasAlpha(image); - baton->orientation = ExifOrientation(image); + baton->hasAlpha = sharp::HasAlpha(image); + baton->orientation = sharp::ExifOrientation(image); // EXIF if (image.get_typeof(VIPS_META_EXIF_NAME) == VIPS_TYPE_BLOB) { size_t exifLength; @@ -147,53 +66,59 @@ class MetadataWorker : public AsyncWorker { baton->iccLength = iccLength; } } + // Clean up vips_error_clear(); vips_thread_shutdown(); } void HandleOKCallback () { - HandleScope(); + using Nan::New; + using Nan::Set; + Nan::HandleScope(); - Local argv[2] = { Null(), Null() }; + v8::Local argv[2] = { Nan::Null(), Nan::Null() }; if (!baton->err.empty()) { - // Error - argv[0] = Error(baton->err.data()); + argv[0] = Nan::Error(baton->err.data()); } else { // Metadata Object - Local info = New(); - Set(info, New("format").ToLocalChecked(), New(baton->format).ToLocalChecked()); - Set(info, New("width").ToLocalChecked(), New(baton->width)); - Set(info, New("height").ToLocalChecked(), New(baton->height)); - Set(info, New("space").ToLocalChecked(), New(baton->space).ToLocalChecked()); - Set(info, New("channels").ToLocalChecked(), New(baton->channels)); + v8::Local info = New(); + Set(info, New("format").ToLocalChecked(), New(baton->format).ToLocalChecked()); + Set(info, New("width").ToLocalChecked(), New(baton->width)); + Set(info, New("height").ToLocalChecked(), New(baton->height)); + Set(info, New("space").ToLocalChecked(), New(baton->space).ToLocalChecked()); + Set(info, New("channels").ToLocalChecked(), New(baton->channels)); if (baton->density > 0) { - Set(info, New("density").ToLocalChecked(), New(baton->density)); + Set(info, New("density").ToLocalChecked(), New(baton->density)); } - Set(info, New("hasProfile").ToLocalChecked(), New(baton->hasProfile)); - Set(info, New("hasAlpha").ToLocalChecked(), New(baton->hasAlpha)); + Set(info, New("hasProfile").ToLocalChecked(), New(baton->hasProfile)); + Set(info, New("hasAlpha").ToLocalChecked(), New(baton->hasAlpha)); if (baton->orientation > 0) { - Set(info, New("orientation").ToLocalChecked(), New(baton->orientation)); + Set(info, New("orientation").ToLocalChecked(), New(baton->orientation)); } if (baton->exifLength > 0) { Set(info, New("exif").ToLocalChecked(), - NewBuffer(baton->exif, baton->exifLength, FreeCallback, nullptr).ToLocalChecked() + Nan::NewBuffer(baton->exif, baton->exifLength, sharp::FreeCallback, nullptr).ToLocalChecked() ); } if (baton->iccLength > 0) { Set(info, New("icc").ToLocalChecked(), - NewBuffer(baton->icc, baton->iccLength, FreeCallback, nullptr).ToLocalChecked() + Nan::NewBuffer(baton->icc, baton->iccLength, sharp::FreeCallback, nullptr).ToLocalChecked() ); } argv[1] = info; } - // Dispose of Persistent wrapper around input Buffer so it can be garbage collected - if (baton->bufferInLength > 0) { - GetFromPersistent("bufferIn"); - } + // Dispose of Persistent wrapper around input Buffers so they can be garbage collected + std::accumulate(buffersToPersist.begin(), buffersToPersist.end(), 0, + [this](uint32_t index, v8::Local const buffer) -> uint32_t { + GetFromPersistent(index); + return index + 1; + } + ); + delete baton->input; delete baton; // Return to JavaScript @@ -202,32 +127,27 @@ class MetadataWorker : public AsyncWorker { private: MetadataBaton* baton; + std::vector> buffersToPersist; }; /* metadata(options, callback) */ NAN_METHOD(metadata) { - HandleScope(); + // Input Buffers must not undergo GC compaction during processing + std::vector> buffersToPersist; // V8 objects are converted to non-V8 types held in the baton struct MetadataBaton *baton = new MetadataBaton; - Local options = info[0].As(); + v8::Local options = info[0].As(); - // Input filename - baton->fileIn = *Utf8String(Get(options, New("fileIn").ToLocalChecked()).ToLocalChecked()); - // Input Buffer object - Local bufferIn; - if (node::Buffer::HasInstance(Get(options, New("bufferIn").ToLocalChecked()).ToLocalChecked())) { - bufferIn = Get(options, New("bufferIn").ToLocalChecked()).ToLocalChecked().As(); - baton->bufferInLength = node::Buffer::Length(bufferIn); - baton->bufferIn = node::Buffer::Data(bufferIn); - } + // Input + baton->input = sharp::CreateInputDescriptor(sharp::AttrAs(options, "input"), buffersToPersist); // Join queue for worker thread - Callback *callback = new Callback(info[1].As()); - AsyncQueueWorker(new MetadataWorker(callback, baton, bufferIn)); + Nan::Callback *callback = new Nan::Callback(info[1].As()); + Nan::AsyncQueueWorker(new MetadataWorker(callback, baton, buffersToPersist)); // Increment queued task counter - g_atomic_int_inc(&counterQueue); + g_atomic_int_inc(&sharp::counterQueue); } diff --git a/src/metadata.h b/src/metadata.h index 5fa86520..a69f74bf 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -2,6 +2,41 @@ #define SRC_METADATA_H_ #include "nan.h" +#include "common.h" + +struct MetadataBaton { + // Input + sharp::InputDescriptor *input; + // Output + std::string format; + int width; + int height; + std::string space; + int channels; + int density; + bool hasProfile; + bool hasAlpha; + int orientation; + char *exif; + size_t exifLength; + char *icc; + size_t iccLength; + std::string err; + + MetadataBaton(): + input(nullptr), + width(0), + height(0), + channels(0), + density(0), + hasProfile(false), + hasAlpha(false), + orientation(0), + exif(nullptr), + exifLength(0), + icc(nullptr), + iccLength(0) {} +}; NAN_METHOD(metadata); diff --git a/src/pipeline.cc b/src/pipeline.cc index ce89bce8..6811a843 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -6,94 +6,22 @@ #include #include - #include -#include #include "nan.h" - #include "common.h" #include "operations.h" #include "pipeline.h" -using v8::Handle; -using v8::Local; -using v8::Value; -using v8::Object; -using v8::Integer; -using v8::Uint32; -using v8::String; -using v8::Array; -using v8::Function; -using v8::Exception; - -using Nan::AsyncQueueWorker; -using Nan::AsyncWorker; -using Nan::Callback; -using Nan::HandleScope; -using Nan::Utf8String; -using Nan::Has; -using Nan::Get; -using Nan::Set; -using Nan::To; -using Nan::New; -using Nan::NewBuffer; -using Nan::Null; -using Nan::Equals; - -using vips::VImage; -using vips::VInterpolate; -using vips::VOption; -using vips::VError; - -using sharp::Composite; -using sharp::Cutout; -using sharp::Normalize; -using sharp::Gamma; -using sharp::Blur; -using sharp::Convolve; -using sharp::Sharpen; -using sharp::EntropyCrop; -using sharp::TileCache; -using sharp::Threshold; -using sharp::Bandbool; -using sharp::Boolean; -using sharp::Trim; - -using sharp::ImageType; -using sharp::ImageTypeId; -using sharp::DetermineImageType; -using sharp::HasProfile; -using sharp::HasAlpha; -using sharp::ExifOrientation; -using sharp::SetExifOrientation; -using sharp::RemoveExifOrientation; -using sharp::SetDensity; -using sharp::IsJpeg; -using sharp::IsPng; -using sharp::IsWebp; -using sharp::IsTiff; -using sharp::IsDz; -using sharp::IsDzZip; -using sharp::IsV; -using sharp::FreeCallback; -using sharp::CalculateCrop; -using sharp::Is16Bit; -using sharp::MaximumImageAlpha; -using sharp::GetBooleanOperation; - -using sharp::counterProcess; -using sharp::counterQueue; - -class PipelineWorker : public AsyncWorker { +class PipelineWorker : public Nan::AsyncWorker { public: PipelineWorker( - Callback *callback, PipelineBaton *baton, Callback *queueListener, - std::vector> const buffersToPersist - ) : AsyncWorker(callback), baton(baton), queueListener(queueListener), buffersToPersist(buffersToPersist) { + Nan::Callback *callback, PipelineBaton *baton, Nan::Callback *queueListener, + std::vector> const buffersToPersist + ) : Nan::AsyncWorker(callback), baton(baton), queueListener(queueListener), buffersToPersist(buffersToPersist) { // Protect Buffer objects from GC, keyed on index std::accumulate(buffersToPersist.begin(), buffersToPersist.end(), 0, - [this](uint32_t index, Local const buffer) -> uint32_t { + [this](uint32_t index, v8::Local const buffer) -> uint32_t { SaveToPersistent(index, buffer); return index + 1; } @@ -101,102 +29,32 @@ class PipelineWorker : public AsyncWorker { } ~PipelineWorker() {} - /* - libuv worker - */ + // libuv worker void Execute() { + using sharp::HasAlpha; + using sharp::ImageType; + // Decrement queued task counter - g_atomic_int_dec_and_test(&counterQueue); + g_atomic_int_dec_and_test(&sharp::counterQueue); // Increment processing task counter - g_atomic_int_inc(&counterProcess); + g_atomic_int_inc(&sharp::counterProcess); // Default sRGB ICC profile from https://packages.debian.org/sid/all/icc-profiles-free/filelist std::string srgbProfile = baton->iccProfilePath + "sRGB.icc"; - // Input - ImageType inputImageType = ImageType::UNKNOWN; - VImage image; - if (baton->bufferInLength > 0) { - // From buffer - if (baton->rawWidth > 0 && baton->rawHeight > 0 && baton->rawChannels > 0) { - // Raw, uncompressed pixel data - try { - image = VImage::new_from_memory(baton->bufferIn, baton->bufferInLength, - baton->rawWidth, baton->rawHeight, baton->rawChannels, VIPS_FORMAT_UCHAR); - if (baton->rawChannels < 3) { - image.get_image()->Type = VIPS_INTERPRETATION_B_W; - } else { - image.get_image()->Type = VIPS_INTERPRETATION_sRGB; - } - inputImageType = ImageType::RAW; - } catch(VError const &err) { - (baton->err).append(err.what()); - inputImageType = ImageType::UNKNOWN; - } - } else { - // Compressed data - inputImageType = DetermineImageType(baton->bufferIn, baton->bufferInLength); - if (inputImageType != ImageType::UNKNOWN) { - try { - VOption *option = VImage::option()->set("access", baton->accessMethod); - if (inputImageType == ImageType::SVG || inputImageType == ImageType::PDF) { - option->set("dpi", static_cast(baton->density)); - } - if (inputImageType == ImageType::MAGICK) { - option->set("density", std::to_string(baton->density).data()); - } - image = VImage::new_from_buffer(baton->bufferIn, baton->bufferInLength, nullptr, option); - if (inputImageType == ImageType::SVG || - inputImageType == ImageType::PDF || - inputImageType == ImageType::MAGICK) { - SetDensity(image, baton->density); - } - } catch (...) { - (baton->err).append("Input buffer has corrupt header"); - inputImageType = ImageType::UNKNOWN; - } - } else { - (baton->err).append("Input buffer contains unsupported image format"); - } - } - } else { - // From file - inputImageType = DetermineImageType(baton->fileIn.data()); - if (inputImageType != ImageType::UNKNOWN) { - try { - VOption *option = VImage::option()->set("access", baton->accessMethod); - if (inputImageType == ImageType::SVG || inputImageType == ImageType::PDF) { - option->set("dpi", static_cast(baton->density)); - } - if (inputImageType == ImageType::MAGICK) { - option->set("density", std::to_string(baton->density).data()); - } - image = VImage::new_from_file(baton->fileIn.data(), option); - if (inputImageType == ImageType::SVG || - inputImageType == ImageType::PDF || - inputImageType == ImageType::MAGICK) { - SetDensity(image, baton->density); - } - } catch (...) { - (baton->err).append("Input file has corrupt header"); - inputImageType = ImageType::UNKNOWN; - } - } else { - (baton->err).append("Input file is missing or of an unsupported image format"); - } - } - if (inputImageType == ImageType::UNKNOWN) { - return Error(); - } - - // Limit input images to a given number of pixels, where pixels = width * height - // Ignore if 0 - if (baton->limitInputPixels > 0 && image.width() * image.height() > baton->limitInputPixels) { - (baton->err).append("Input image exceeds pixel limit"); - return Error(); - } - try { + // Open input + vips::VImage image; + ImageType inputImageType; + std::tie(image, inputImageType) = sharp::OpenInput(baton->input, baton->accessMethod); + + // Limit input images to a given number of pixels, where pixels = width * height + // Ignore if 0 + if (baton->limitInputPixels > 0 && image.width() * image.height() > baton->limitInputPixels) { + (baton->err).append("Input image exceeds pixel limit"); + return Error(); + } + // Calculate angle of rotation VipsAngle rotation; bool flip; @@ -214,12 +72,12 @@ class PipelineWorker : public AsyncWorker { // Rotate pre-extract if (baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) { image = image.rot(rotation); - RemoveExifOrientation(image); + sharp::RemoveExifOrientation(image); } // Trim if(baton->trimTolerance != 0) { - image = Trim(image, baton->trimTolerance); + image = sharp::Trim(image, baton->trimTolerance); } // Pre extraction @@ -362,9 +220,9 @@ class PipelineWorker : public AsyncWorker { } if (shrink_on_load > 1) { // Reload input using shrink-on-load - VOption *option = VImage::option()->set("shrink", shrink_on_load); - if (baton->bufferInLength > 1) { - VipsBlob *blob = vips_blob_new(nullptr, baton->bufferIn, baton->bufferInLength); + vips::VOption *option = VImage::option()->set("shrink", shrink_on_load); + if (baton->input->buffer != nullptr) { + VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength); if (inputImageType == ImageType::JPEG) { // Reload JPEG buffer image = VImage::jpegload_buffer(blob, option); @@ -376,10 +234,10 @@ class PipelineWorker : public AsyncWorker { } else { if (inputImageType == ImageType::JPEG) { // Reload JPEG file - image = VImage::jpegload(const_cast((baton->fileIn).data()), option); + image = VImage::jpegload(const_cast(baton->input->file.data()), option); } else { // Reload WebP file - image = VImage::webpload(const_cast((baton->fileIn).data()), option); + image = VImage::webpload(const_cast(baton->input->file.data()), option); } } // Recalculate integral shrink and double residual @@ -405,7 +263,7 @@ class PipelineWorker : public AsyncWorker { } // Ensure we're using a device-independent colour space - if (HasProfile(image)) { + if (sharp::HasProfile(image)) { // Convert to sRGB using embedded profile try { image = image.icc_transform(const_cast(srgbProfile.data()), VImage::option() @@ -425,12 +283,12 @@ class PipelineWorker : public AsyncWorker { } // Calculate maximum alpha value based on input image pixel depth - double const maxAlpha = MaximumImageAlpha(image.interpretation()); + double const maxAlpha = sharp::MaximumImageAlpha(image.interpretation()); // Flatten image to remove alpha channel if (baton->flatten && HasAlpha(image)) { // Scale up 8-bit values to match 16-bit input image - double const multiplier = Is16Bit(image.interpretation()) ? 256.0 : 1.0; + double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0; // Background colour std::vector background { baton->background[0] * multiplier, @@ -450,7 +308,7 @@ class PipelineWorker : public AsyncWorker { // Gamma encoding (darken) if (baton->gamma >= 1 && baton->gamma <= 3) { - image = Gamma(image, 1.0 / baton->gamma); + image = sharp::Gamma(image, 1.0 / baton->gamma); } // Convert to greyscale (linear, therefore after gamma encoding, if any) @@ -484,9 +342,9 @@ class PipelineWorker : public AsyncWorker { } // Ensure image has an alpha channel when there is an overlay - bool hasOverlay = baton->overlayBufferInLength > 0 || !baton->overlayFileIn.empty(); + bool hasOverlay = baton->overlay != nullptr; if (hasOverlay && !HasAlpha(image)) { - double const multiplier = Is16Bit(image.interpretation()) ? 256.0 : 1.0; + double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0; image = image.bandjoin( VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier) ); @@ -511,7 +369,7 @@ class PipelineWorker : public AsyncWorker { if (shouldAffineTransform) { // Insert tile cache to prevent over-computation of previous operations if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL) { - image = TileCache(image, yresidual); + image = sharp::TileCache(image, yresidual); } // Perform kernel-based reduction if (yresidual < 1.0 || xresidual < 1.0) { @@ -519,7 +377,7 @@ class PipelineWorker : public AsyncWorker { vips_enum_from_nick(nullptr, VIPS_TYPE_KERNEL, baton->kernel.data()) ); if (kernel != VIPS_KERNEL_CUBIC && kernel != VIPS_KERNEL_LANCZOS2 && kernel != VIPS_KERNEL_LANCZOS3) { - throw VError("Unknown kernel"); + throw vips::VError("Unknown kernel"); } if (yresidual < 1.0) { image = image.reducev(1.0 / yresidual, VImage::option()->set("kernel", kernel)); @@ -530,7 +388,7 @@ class PipelineWorker : public AsyncWorker { } // Perform affine enlargement if (yresidual > 1.0 || xresidual > 1.0) { - VInterpolate interpolator = VInterpolate::new_from_name(baton->interpolator.data()); + vips::VInterpolate interpolator = vips::VInterpolate::new_from_name(baton->interpolator.data()); if (yresidual > 1.0) { image = image.affine({1.0, 0.0, 0.0, yresidual}, VImage::option() ->set("interpolate", interpolator) @@ -547,26 +405,26 @@ class PipelineWorker : public AsyncWorker { // Rotate if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) { image = image.rot(rotation); - RemoveExifOrientation(image); + sharp::RemoveExifOrientation(image); } // Flip (mirror about Y axis) if (baton->flip) { image = image.flip(VIPS_DIRECTION_VERTICAL); - RemoveExifOrientation(image); + sharp::RemoveExifOrientation(image); } // Flop (mirror about X axis) if (baton->flop) { image = image.flip(VIPS_DIRECTION_HORIZONTAL); - RemoveExifOrientation(image); + sharp::RemoveExifOrientation(image); } // Crop/embed if (image.width() != baton->width || image.height() != baton->height) { if (baton->canvas == Canvas::EMBED) { // Scale up 8-bit values to match 16-bit input image - double const multiplier = Is16Bit(image.interpretation()) ? 256.0 : 1.0; + double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0; // Create background colour std::vector background; if (image.bands() > 2) { @@ -606,12 +464,12 @@ class PipelineWorker : public AsyncWorker { int top; if (baton->crop < 9) { // Gravity-based crop - std::tie(left, top) = CalculateCrop( + std::tie(left, top) = sharp::CalculateCrop( image.width(), image.height(), baton->width, baton->height, baton->crop ); } else { // Entropy-based crop - std::tie(left, top) = EntropyCrop(image, baton->width, baton->height); + std::tie(left, top) = sharp::EntropyCrop(image, baton->width, baton->height); } int width = std::min(image.width(), baton->width); int height = std::min(image.height(), baton->height); @@ -629,7 +487,7 @@ class PipelineWorker : public AsyncWorker { // Extend edges if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) { // Scale up 8-bit values to match 16-bit input image - double const multiplier = Is16Bit(image.interpretation()) ? 256.0 : 1.0; + double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0; // Create background colour std::vector background { baton->background[0] * multiplier, @@ -656,17 +514,17 @@ class PipelineWorker : public AsyncWorker { // Threshold - must happen before blurring, due to the utility of blurring after thresholding if (baton->threshold != 0) { - image = Threshold(image, baton->threshold, baton->thresholdGrayscale); + image = sharp::Threshold(image, baton->threshold, baton->thresholdGrayscale); } // Blur if (shouldBlur) { - image = Blur(image, baton->blurSigma); + image = sharp::Blur(image, baton->blurSigma); } // Convolve if (shouldConv) { - image = Convolve(image, + image = sharp::Convolve(image, baton->convKernelWidth, baton->convKernelHeight, baton->convKernelScale, baton->convKernelOffset, baton->convKernel @@ -675,71 +533,40 @@ class PipelineWorker : public AsyncWorker { // Sharpen if (shouldSharpen) { - image = Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged); + image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged); } // Composite with overlay, if present if (hasOverlay) { VImage overlayImage; ImageType overlayImageType = ImageType::UNKNOWN; - if (baton->overlayBufferInLength > 0) { - // Overlay with image from buffer - overlayImageType = DetermineImageType(baton->overlayBufferIn, baton->overlayBufferInLength); - if (overlayImageType != ImageType::UNKNOWN) { - try { - overlayImage = VImage::new_from_buffer(baton->overlayBufferIn, baton->overlayBufferInLength, - nullptr, VImage::option()->set("access", baton->accessMethod)); - } catch (...) { - (baton->err).append("Overlay buffer has corrupt header"); - overlayImageType = ImageType::UNKNOWN; - } - } else { - (baton->err).append("Overlay buffer contains unsupported image format"); - } - } else { - // Overlay with image from file - overlayImageType = DetermineImageType(baton->overlayFileIn.data()); - if (overlayImageType != ImageType::UNKNOWN) { - try { - overlayImage = VImage::new_from_file(baton->overlayFileIn.data(), - VImage::option()->set("access", baton->accessMethod)); - } catch (...) { - (baton->err).append("Overlay file has corrupt header"); - overlayImageType = ImageType::UNKNOWN; - } - } - } - if (overlayImageType == ImageType::UNKNOWN) { - return Error(); - } + std::tie(overlayImage, overlayImageType) = OpenInput(baton->overlay, baton->accessMethod); // Check if overlay is tiled if (baton->overlayTile) { - int overlayImageWidth = overlayImage.width(); - int overlayImageHeight = overlayImage.height(); + int const overlayImageWidth = overlayImage.width(); + int const overlayImageHeight = overlayImage.height(); int across = 0; int down = 0; - - // use gravity in ovelay - if(overlayImageWidth <= baton->width) { + // Use gravity in overlay + if (overlayImageWidth <= baton->width) { across = static_cast(ceil(static_cast(image.width()) / overlayImageWidth)); } - if(overlayImageHeight <= baton->height) { + if (overlayImageHeight <= baton->height) { down = static_cast(ceil(static_cast(image.height()) / overlayImageHeight)); } - if(across != 0 || down != 0) { + if (across != 0 || down != 0) { int left; int top; overlayImage = overlayImage.replicate(across, down); - - if(baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) { + if (baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) { // the overlayX/YOffsets will now be used to CalculateCrop for extract_area - std::tie(left, top) = CalculateCrop( + std::tie(left, top) = sharp::CalculateCrop( overlayImage.width(), overlayImage.height(), image.width(), image.height(), baton->overlayXOffset, baton->overlayYOffset ); } else { // the overlayGravity will now be used to CalculateCrop for extract_area - std::tie(left, top) = CalculateCrop( + std::tie(left, top) = sharp::CalculateCrop( overlayImage.width(), overlayImage.height(), image.width(), image.height(), baton->overlayGravity ); } @@ -750,18 +577,18 @@ class PipelineWorker : public AsyncWorker { // the overlayGravity was used for extract_area, therefore set it back to its default value of 0 baton->overlayGravity = 0; } - if(shouldCutout) { + if (shouldCutout) { // 'cut out' the image, premultiplication is not required - image = Cutout(overlayImage, image, baton->overlayGravity); + image = sharp::Cutout(overlayImage, image, baton->overlayGravity); } else { // Ensure overlay is premultiplied sRGB overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply(); - if(baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) { + if (baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) { // Composite images with given offsets - image = Composite(overlayImage, image, baton->overlayXOffset, baton->overlayYOffset); + image = sharp::Composite(overlayImage, image, baton->overlayXOffset, baton->overlayYOffset); } else { // Composite images with given gravity - image = Composite(overlayImage, image, baton->overlayGravity); + image = sharp::Composite(overlayImage, image, baton->overlayGravity); } } } @@ -770,7 +597,7 @@ class PipelineWorker : public AsyncWorker { if (shouldPremultiplyAlpha) { image = image.unpremultiply(VImage::option()->set("max_alpha", maxAlpha)); // Cast pixel values to integer - if (Is16Bit(image.interpretation())) { + if (sharp::Is16Bit(image.interpretation())) { image = image.cast(VIPS_FORMAT_USHORT); } else { image = image.cast(VIPS_FORMAT_UCHAR); @@ -779,55 +606,25 @@ class PipelineWorker : public AsyncWorker { // Gamma decoding (brighten) if (baton->gamma >= 1 && baton->gamma <= 3) { - image = Gamma(image, baton->gamma); + image = sharp::Gamma(image, baton->gamma); } // Apply normalization - stretch luminance to cover full dynamic range if (baton->normalize) { - image = Normalize(image); + image = sharp::Normalize(image); } // Apply bitwise boolean operation between images - if (baton->booleanOp != VIPS_OPERATION_BOOLEAN_LAST && - (baton->booleanBufferInLength > 0 || !baton->booleanFileIn.empty())) { + if (baton->boolean != nullptr) { VImage booleanImage; ImageType booleanImageType = ImageType::UNKNOWN; - if (baton->booleanBufferInLength > 0) { - // Buffer input for boolean operation - booleanImageType = DetermineImageType(baton->booleanBufferIn, baton->booleanBufferInLength); - if (booleanImageType != ImageType::UNKNOWN) { - try { - booleanImage = VImage::new_from_buffer(baton->booleanBufferIn, baton->booleanBufferInLength, - nullptr, VImage::option()->set("access", baton->accessMethod)); - } catch (...) { - (baton->err).append("Boolean operation buffer has corrupt header"); - booleanImageType = ImageType::UNKNOWN; - } - } else { - (baton->err).append("Boolean operation buffer contains unsupported image format"); - } - } else if (!baton->booleanFileIn.empty()) { - // File input for boolean operation - booleanImageType = DetermineImageType(baton->booleanFileIn.data()); - if (booleanImageType != ImageType::UNKNOWN) { - try { - booleanImage = VImage::new_from_file(baton->booleanFileIn.data(), - VImage::option()->set("access", baton->accessMethod)); - } catch (...) { - (baton->err).append("Boolean operation file has corrupt header"); - } - } - } - if (booleanImageType == ImageType::UNKNOWN) { - return Error(); - } - // Apply the boolean operation - image = Boolean(image, booleanImage, baton->booleanOp); + std::tie(booleanImage, booleanImageType) = sharp::OpenInput(baton->boolean, baton->accessMethod); + image = sharp::Boolean(image, booleanImage, baton->booleanOp); } // Apply per-channel Bandbool bitwise operations after all other operations if (baton->bandBoolOp >= VIPS_OPERATION_BOOLEAN_AND && baton->bandBoolOp < VIPS_OPERATION_BOOLEAN_LAST) { - image = Bandbool(image, baton->bandBoolOp); + image = sharp::Bandbool(image, baton->bandBoolOp); } // Extract an image channel (aka vips band) @@ -840,13 +637,13 @@ class PipelineWorker : public AsyncWorker { } // Convert image to sRGB, if not already - if (Is16Bit(image.interpretation())) { + if (sharp::Is16Bit(image.interpretation())) { image = image.cast(VIPS_FORMAT_USHORT); } if (image.interpretation() != VIPS_INTERPRETATION_sRGB) { image = image.colourspace(VIPS_INTERPRETATION_sRGB); // Transform colours from embedded profile to sRGB profile - if (baton->withMetadata && HasProfile(image)) { + if (baton->withMetadata && sharp::HasProfile(image)) { image = image.icc_transform(const_cast(srgbProfile.data()), VImage::option() ->set("embedded", TRUE) ); @@ -855,7 +652,7 @@ class PipelineWorker : public AsyncWorker { // Override EXIF Orientation tag if (baton->withMetadata && baton->withMetadataOrientation != -1) { - SetExifOrientation(image, baton->withMetadataOrientation); + sharp::SetExifOrientation(image, baton->withMetadataOrientation); } // Number of channels used in output image @@ -940,13 +737,13 @@ class PipelineWorker : public AsyncWorker { } } else { // File output - bool isJpeg = IsJpeg(baton->fileOut); - bool isPng = IsPng(baton->fileOut); - bool isWebp = IsWebp(baton->fileOut); - bool isTiff = IsTiff(baton->fileOut); - bool isDz = IsDz(baton->fileOut); - bool isDzZip = IsDzZip(baton->fileOut); - bool isV = IsV(baton->fileOut); + bool isJpeg = sharp::IsJpeg(baton->fileOut); + bool isPng = sharp::IsPng(baton->fileOut); + bool isWebp = sharp::IsWebp(baton->fileOut); + bool isTiff = sharp::IsTiff(baton->fileOut); + bool isDz = sharp::IsDz(baton->fileOut); + bool isDzZip = sharp::IsDzZip(baton->fileOut); + bool isV = sharp::IsV(baton->fileOut); bool matchInput = baton->formatOut == "input" && !(isJpeg || isPng || isWebp || isTiff || isDz || isDzZip || isV); if (baton->formatOut == "jpeg" || isJpeg || (matchInput && inputImageType == ImageType::JPEG)) { @@ -1017,7 +814,7 @@ class PipelineWorker : public AsyncWorker { return Error(); } } - } catch (VError const &err) { + } catch (vips::VError const &err) { (baton->err).append(err.what()); } // Clean up libvips' per-request data and threads @@ -1026,9 +823,11 @@ class PipelineWorker : public AsyncWorker { } void HandleOKCallback () { - HandleScope(); + using Nan::New; + using Nan::Set; + Nan::HandleScope(); - Local argv[3] = { Null(), Null(), Null() }; + v8::Local argv[3] = { Nan::Null(), Nan::Null(), Nan::Null() }; if (!baton->err.empty()) { // Error argv[0] = Nan::Error(baton->err.data()); @@ -1044,41 +843,44 @@ class PipelineWorker : public AsyncWorker { height = baton->heightPost; } // Info Object - Local info = New(); - Set(info, New("format").ToLocalChecked(), New(baton->formatOut).ToLocalChecked()); - Set(info, New("width").ToLocalChecked(), New(static_cast(width))); - Set(info, New("height").ToLocalChecked(), New(static_cast(height))); - Set(info, New("channels").ToLocalChecked(), New(static_cast(baton->channels))); + v8::Local info = New(); + Set(info, New("format").ToLocalChecked(), New(baton->formatOut).ToLocalChecked()); + Set(info, New("width").ToLocalChecked(), New(static_cast(width))); + Set(info, New("height").ToLocalChecked(), New(static_cast(height))); + Set(info, New("channels").ToLocalChecked(), New(static_cast(baton->channels))); if (baton->bufferOutLength > 0) { // Pass ownership of output data to Buffer instance - argv[1] = NewBuffer( - static_cast(baton->bufferOut), baton->bufferOutLength, FreeCallback, nullptr + argv[1] = Nan::NewBuffer( + static_cast(baton->bufferOut), baton->bufferOutLength, sharp::FreeCallback, nullptr ).ToLocalChecked(); // Add buffer size to info - Set(info, New("size").ToLocalChecked(), New(static_cast(baton->bufferOutLength))); + Set(info, New("size").ToLocalChecked(), New(static_cast(baton->bufferOutLength))); argv[2] = info; } else { // Add file size to info GStatBuf st; g_stat(baton->fileOut.data(), &st); - Set(info, New("size").ToLocalChecked(), New(static_cast(st.st_size))); + Set(info, New("size").ToLocalChecked(), New(static_cast(st.st_size))); argv[1] = info; } } // Dispose of Persistent wrapper around input Buffers so they can be garbage collected std::accumulate(buffersToPersist.begin(), buffersToPersist.end(), 0, - [this](uint32_t index, Local const buffer) -> uint32_t { + [this](uint32_t index, v8::Local const buffer) -> uint32_t { GetFromPersistent(index); return index + 1; } ); + delete baton->input; + delete baton->overlay; + delete baton->boolean; delete baton; // Decrement processing task counter - g_atomic_int_dec_and_test(&counterProcess); - Local queueLength[1] = { New(counterQueue) }; + g_atomic_int_dec_and_test(&sharp::counterProcess); + v8::Local queueLength[1] = { New(sharp::counterQueue) }; queueListener->Call(1, queueLength); delete queueListener; @@ -1088,8 +890,8 @@ class PipelineWorker : public AsyncWorker { private: PipelineBaton *baton; - Callback *queueListener; - std::vector> buffersToPersist; + Nan::Callback *queueListener; + std::vector> buffersToPersist; /* Calculate the angle of rotation and need-to-flip for the output image. @@ -1099,12 +901,12 @@ class PipelineWorker : public AsyncWorker { 3. Otherwise default to zero, i.e. no rotation */ std::tuple - CalculateRotationAndFlip(int const angle, VImage image) { + CalculateRotationAndFlip(int const angle, vips::VImage image) { VipsAngle rotate = VIPS_ANGLE_D0; bool flip = FALSE; bool flop = FALSE; if (angle == -1) { - switch(ExifOrientation(image)) { + switch(sharp::ExifOrientation(image)) { case 6: rotate = VIPS_ANGLE_D90; break; case 3: rotate = VIPS_ANGLE_D180; break; case 8: rotate = VIPS_ANGLE_D270; break; @@ -1135,63 +937,46 @@ class PipelineWorker : public AsyncWorker { } }; -// Convenience methods to access the attributes of a V8::Object -template T attrAs(Handle obj, std::string attr) { - return To(Get(obj, New(attr).ToLocalChecked()).ToLocalChecked()).FromJust(); -} -static std::string attrAsStr(Handle obj, std::string attr) { - return *Utf8String(Get(obj, New(attr).ToLocalChecked()).ToLocalChecked()); -} - /* pipeline(options, output, callback) */ NAN_METHOD(pipeline) { - HandleScope(); + using sharp::HasAttr; + using sharp::AttrTo; + using sharp::AttrAs; + using sharp::AttrAsStr; + using sharp::CreateInputDescriptor; + + // Input Buffers must not undergo GC compaction during processing + std::vector> buffersToPersist; // V8 objects are converted to non-V8 types held in the baton struct PipelineBaton *baton = new PipelineBaton; - Local options = info[0].As(); + v8::Local options = info[0].As(); - // Input Buffers must not undergo GC compaction during processing - std::vector> buffersToPersist; + // Input + baton->input = CreateInputDescriptor(AttrAs(options, "input"), buffersToPersist); - // Input filename - baton->fileIn = attrAsStr(options, "fileIn"); - baton->accessMethod = attrAs(options, "sequentialRead") ? - VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; - // Input Buffer object - Local bufferIn; - if (node::Buffer::HasInstance(Get(options, New("bufferIn").ToLocalChecked()).ToLocalChecked())) { - bufferIn = Get(options, New("bufferIn").ToLocalChecked()).ToLocalChecked().As(); - baton->bufferInLength = node::Buffer::Length(bufferIn); - baton->bufferIn = node::Buffer::Data(bufferIn); - buffersToPersist.push_back(bufferIn); - } // ICC profile to use when input CMYK image has no embedded profile - baton->iccProfilePath = attrAsStr(options, "iccProfilePath"); + baton->iccProfilePath = AttrAsStr(options, "iccProfilePath"); + baton->accessMethod = AttrTo(options, "sequentialRead") ? + VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; // Limit input images to a given number of pixels, where pixels = width * height - baton->limitInputPixels = attrAs(options, "limitInputPixels"); - // Density/DPI at which to load vector images via libmagick - baton->density = attrAs(options, "density"); - // Raw pixel input - baton->rawWidth = attrAs(options, "rawWidth"); - baton->rawHeight = attrAs(options, "rawHeight"); - baton->rawChannels = attrAs(options, "rawChannels"); + baton->limitInputPixels = AttrTo(options, "limitInputPixels"); // Extract image options - baton->topOffsetPre = attrAs(options, "topOffsetPre"); - baton->leftOffsetPre = attrAs(options, "leftOffsetPre"); - baton->widthPre = attrAs(options, "widthPre"); - baton->heightPre = attrAs(options, "heightPre"); - baton->topOffsetPost = attrAs(options, "topOffsetPost"); - baton->leftOffsetPost = attrAs(options, "leftOffsetPost"); - baton->widthPost = attrAs(options, "widthPost"); - baton->heightPost = attrAs(options, "heightPost"); + baton->topOffsetPre = AttrTo(options, "topOffsetPre"); + baton->leftOffsetPre = AttrTo(options, "leftOffsetPre"); + baton->widthPre = AttrTo(options, "widthPre"); + baton->heightPre = AttrTo(options, "heightPre"); + baton->topOffsetPost = AttrTo(options, "topOffsetPost"); + baton->leftOffsetPost = AttrTo(options, "leftOffsetPost"); + baton->widthPost = AttrTo(options, "widthPost"); + baton->heightPost = AttrTo(options, "heightPost"); // Output image dimensions - baton->width = attrAs(options, "width"); - baton->height = attrAs(options, "height"); + baton->width = AttrTo(options, "width"); + baton->height = AttrTo(options, "height"); // Canvas option - std::string canvas = attrAsStr(options, "canvas"); + std::string canvas = AttrAsStr(options, "canvas"); if (canvas == "crop") { baton->canvas = Canvas::CROP; } else if (canvas == "embed") { @@ -1204,87 +989,93 @@ NAN_METHOD(pipeline) { baton->canvas = Canvas::IGNORE_ASPECT; } // Background colour - Local background = Get(options, New("background").ToLocalChecked()).ToLocalChecked().As(); - for (int i = 0; i < 4; i++) { - baton->background[i] = To(Get(background, i).ToLocalChecked()).FromJust(); + v8::Local background = AttrAs(options, "background"); + for (unsigned int i = 0; i < 4; i++) { + baton->background[i] = AttrTo(background, i); } // Overlay options - baton->overlayFileIn = attrAsStr(options, "overlayFileIn"); - Local overlayBufferIn; - if (node::Buffer::HasInstance(Get(options, New("overlayBufferIn").ToLocalChecked()).ToLocalChecked())) { - overlayBufferIn = Get(options, New("overlayBufferIn").ToLocalChecked()).ToLocalChecked().As(); - baton->overlayBufferInLength = node::Buffer::Length(overlayBufferIn); - baton->overlayBufferIn = node::Buffer::Data(overlayBufferIn); - buffersToPersist.push_back(overlayBufferIn); - } - baton->overlayGravity = attrAs(options, "overlayGravity"); - baton->overlayXOffset = attrAs(options, "overlayXOffset"); - baton->overlayYOffset = attrAs(options, "overlayYOffset"); - baton->overlayTile = attrAs(options, "overlayTile"); - baton->overlayCutout = attrAs(options, "overlayCutout"); - // Boolean options - baton->booleanFileIn = attrAsStr(options, "booleanFileIn"); - Local booleanBufferIn; - if (node::Buffer::HasInstance(Get(options, New("booleanBufferIn").ToLocalChecked()).ToLocalChecked())) { - booleanBufferIn = Get(options, New("booleanBufferIn").ToLocalChecked()).ToLocalChecked().As(); - baton->booleanBufferInLength = node::Buffer::Length(booleanBufferIn); - baton->booleanBufferIn = node::Buffer::Data(booleanBufferIn); - buffersToPersist.push_back(booleanBufferIn); + if (HasAttr(options, "overlay")) { + baton->overlay = CreateInputDescriptor(AttrAs(options, "overlay"), buffersToPersist); + baton->overlayGravity = AttrTo(options, "overlayGravity"); + baton->overlayXOffset = AttrTo(options, "overlayXOffset"); + baton->overlayYOffset = AttrTo(options, "overlayYOffset"); + baton->overlayTile = AttrTo(options, "overlayTile"); + baton->overlayCutout = AttrTo(options, "overlayCutout"); } // Resize options - baton->withoutEnlargement = attrAs(options, "withoutEnlargement"); - baton->crop = attrAs(options, "crop"); - baton->kernel = attrAsStr(options, "kernel"); - baton->interpolator = attrAsStr(options, "interpolator"); + baton->withoutEnlargement = AttrTo(options, "withoutEnlargement"); + baton->crop = AttrTo(options, "crop"); + baton->kernel = AttrAsStr(options, "kernel"); + baton->interpolator = AttrAsStr(options, "interpolator"); // Operators - baton->flatten = attrAs(options, "flatten"); - baton->negate = attrAs(options, "negate"); - baton->blurSigma = attrAs(options, "blurSigma"); - baton->sharpenSigma = attrAs(options, "sharpenSigma"); - baton->sharpenFlat = attrAs(options, "sharpenFlat"); - baton->sharpenJagged = attrAs(options, "sharpenJagged"); - baton->threshold = attrAs(options, "threshold"); - baton->thresholdGrayscale = attrAs(options, "thresholdGrayscale"); - baton->trimTolerance = attrAs(options, "trimTolerance"); + baton->flatten = AttrTo(options, "flatten"); + baton->negate = AttrTo(options, "negate"); + baton->blurSigma = AttrTo(options, "blurSigma"); + baton->sharpenSigma = AttrTo(options, "sharpenSigma"); + baton->sharpenFlat = AttrTo(options, "sharpenFlat"); + baton->sharpenJagged = AttrTo(options, "sharpenJagged"); + baton->threshold = AttrTo(options, "threshold"); + baton->thresholdGrayscale = AttrTo(options, "thresholdGrayscale"); + baton->trimTolerance = AttrTo(options, "trimTolerance"); if(baton->accessMethod == VIPS_ACCESS_SEQUENTIAL && baton->trimTolerance != 0) { baton->accessMethod = VIPS_ACCESS_RANDOM; } - baton->gamma = attrAs(options, "gamma"); - baton->greyscale = attrAs(options, "greyscale"); - baton->normalize = attrAs(options, "normalize"); - baton->angle = attrAs(options, "angle"); - baton->rotateBeforePreExtract = attrAs(options, "rotateBeforePreExtract"); - baton->flip = attrAs(options, "flip"); - baton->flop = attrAs(options, "flop"); - baton->extendTop = attrAs(options, "extendTop"); - baton->extendBottom = attrAs(options, "extendBottom"); - baton->extendLeft = attrAs(options, "extendLeft"); - baton->extendRight = attrAs(options, "extendRight"); - baton->extractChannel = attrAs(options, "extractChannel"); + baton->gamma = AttrTo(options, "gamma"); + baton->greyscale = AttrTo(options, "greyscale"); + baton->normalize = AttrTo(options, "normalize"); + baton->angle = AttrTo(options, "angle"); + baton->rotateBeforePreExtract = AttrTo(options, "rotateBeforePreExtract"); + baton->flip = AttrTo(options, "flip"); + baton->flop = AttrTo(options, "flop"); + baton->extendTop = AttrTo(options, "extendTop"); + baton->extendBottom = AttrTo(options, "extendBottom"); + baton->extendLeft = AttrTo(options, "extendLeft"); + baton->extendRight = AttrTo(options, "extendRight"); + baton->extractChannel = AttrTo(options, "extractChannel"); + if (HasAttr(options, "boolean")) { + baton->boolean = CreateInputDescriptor(AttrAs(options, "boolean"), buffersToPersist); + baton->booleanOp = sharp::GetBooleanOperation(AttrAsStr(options, "booleanOp")); + } + if (HasAttr(options, "bandBoolOp")) { + baton->bandBoolOp = sharp::GetBooleanOperation(AttrAsStr(options, "bandBoolOp")); + } + if (HasAttr(options, "convKernel")) { + v8::Local kernel = AttrAs(options, "convKernel"); + baton->convKernelWidth = AttrTo(kernel, "width"); + baton->convKernelHeight = AttrTo(kernel, "height"); + baton->convKernelScale = AttrTo(kernel, "scale"); + baton->convKernelOffset = AttrTo(kernel, "offset"); + size_t const kernelSize = static_cast(baton->convKernelWidth * baton->convKernelHeight); + baton->convKernel = std::unique_ptr(new double[kernelSize]); + v8::Local kdata = AttrAs(kernel, "kernel"); + for (unsigned int i = 0; i < kernelSize; i++) { + baton->convKernel[i] = AttrTo(kdata, i); + } + } // Output options - baton->progressive = attrAs(options, "progressive"); - baton->quality = attrAs(options, "quality"); - baton->compressionLevel = attrAs(options, "compressionLevel"); - baton->withoutAdaptiveFiltering = attrAs(options, "withoutAdaptiveFiltering"); - baton->withoutChromaSubsampling = attrAs(options, "withoutChromaSubsampling"); - baton->trellisQuantisation = attrAs(options, "trellisQuantisation"); - baton->overshootDeringing = attrAs(options, "overshootDeringing"); - baton->optimiseScans = attrAs(options, "optimiseScans"); - baton->withMetadata = attrAs(options, "withMetadata"); - baton->withMetadataOrientation = attrAs(options, "withMetadataOrientation"); + baton->progressive = AttrTo(options, "progressive"); + baton->quality = AttrTo(options, "quality"); + baton->compressionLevel = AttrTo(options, "compressionLevel"); + baton->withoutAdaptiveFiltering = AttrTo(options, "withoutAdaptiveFiltering"); + baton->withoutChromaSubsampling = AttrTo(options, "withoutChromaSubsampling"); + baton->trellisQuantisation = AttrTo(options, "trellisQuantisation"); + baton->overshootDeringing = AttrTo(options, "overshootDeringing"); + baton->optimiseScans = AttrTo(options, "optimiseScans"); + baton->withMetadata = AttrTo(options, "withMetadata"); + baton->withMetadataOrientation = AttrTo(options, "withMetadataOrientation"); // Output - baton->formatOut = attrAsStr(options, "formatOut"); - baton->fileOut = attrAsStr(options, "fileOut"); + baton->formatOut = AttrAsStr(options, "formatOut"); + baton->fileOut = AttrAsStr(options, "fileOut"); // Tile output - baton->tileSize = attrAs(options, "tileSize"); - baton->tileOverlap = attrAs(options, "tileOverlap"); - std::string tileContainer = attrAsStr(options, "tileContainer"); + baton->tileSize = AttrTo(options, "tileSize"); + baton->tileOverlap = AttrTo(options, "tileOverlap"); + std::string tileContainer = AttrAsStr(options, "tileContainer"); if (tileContainer == "zip") { baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP; } else { baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_FS; } - std::string tileLayout = attrAsStr(options, "tileLayout"); + std::string tileLayout = AttrAsStr(options, "tileLayout"); if (tileLayout == "google") { baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_GOOGLE; } else if (tileLayout == "zoomify") { @@ -1292,42 +1083,16 @@ NAN_METHOD(pipeline) { } else { baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ; } - // Convolution Kernel - if(Has(options, New("convKernel").ToLocalChecked()).FromJust()) { - Local kernel = Get(options, New("convKernel").ToLocalChecked()).ToLocalChecked().As(); - baton->convKernelWidth = attrAs(kernel, "width"); - baton->convKernelHeight = attrAs(kernel, "height"); - baton->convKernelScale = attrAs(kernel, "scale"); - baton->convKernelOffset = attrAs(kernel, "offset"); - - size_t const kernelSize = static_cast(baton->convKernelWidth * baton->convKernelHeight); - baton->convKernel = std::unique_ptr(new double[kernelSize]); - Local kdata = Get(kernel, New("kernel").ToLocalChecked()).ToLocalChecked().As(); - for(unsigned int i = 0; i < kernelSize; i++) { - baton->convKernel[i] = To(Get(kdata, i).ToLocalChecked()).FromJust(); - } - } - // Bandbool operation - if(Has(options, New("bandBoolOp").ToLocalChecked()).FromJust()) { - baton->bandBoolOp = GetBooleanOperation(attrAsStr(options, "bandBoolOp")); - } - - // Boolean operation - if(Has(options, New("booleanOp").ToLocalChecked()).FromJust()) { - baton->booleanOp = GetBooleanOperation(attrAsStr(options, "booleanOp")); - } // Function to notify of queue length changes - Callback *queueListener = new Callback( - Get(options, New("queueListener").ToLocalChecked()).ToLocalChecked().As() - ); + Nan::Callback *queueListener = new Nan::Callback(AttrAs(options, "queueListener")); // Join queue for worker thread - Callback *callback = new Callback(info[1].As()); - AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, buffersToPersist)); + Nan::Callback *callback = new Nan::Callback(info[1].As()); + Nan::AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, buffersToPersist)); // Increment queued task counter - g_atomic_int_inc(&counterQueue); - Local queueLength[1] = { New(counterQueue) }; + g_atomic_int_inc(&sharp::counterQueue); + v8::Local queueLength[1] = { Nan::New(sharp::counterQueue) }; queueListener->Call(1, queueLength); } diff --git a/src/pipeline.h b/src/pipeline.h index 68961fd5..2a20dfca 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -6,6 +6,7 @@ #include #include "nan.h" +#include "common.h" NAN_METHOD(pipeline); @@ -18,30 +19,19 @@ enum class Canvas { }; struct PipelineBaton { - std::string fileIn; - char *bufferIn; - size_t bufferInLength; + sharp::InputDescriptor *input; std::string iccProfilePath; int limitInputPixels; - int density; - int rawWidth; - int rawHeight; - int rawChannels; std::string formatOut; std::string fileOut; void *bufferOut; size_t bufferOutLength; - std::string overlayFileIn; - char *overlayBufferIn; - size_t overlayBufferInLength; + sharp::InputDescriptor *overlay; int overlayGravity; int overlayXOffset; int overlayYOffset; bool overlayTile; bool overlayCutout; - std::string booleanFileIn; - char *booleanBufferIn; - size_t booleanBufferInLength; int topOffsetPre; int leftOffsetPre; int widthPre; @@ -96,8 +86,9 @@ struct PipelineBaton { int convKernelHeight; double convKernelScale; double convKernelOffset; - VipsOperationBoolean bandBoolOp; + sharp::InputDescriptor *boolean; VipsOperationBoolean booleanOp; + VipsOperationBoolean bandBoolOp; int extractChannel; int tileSize; int tileOverlap; @@ -105,22 +96,15 @@ struct PipelineBaton { VipsForeignDzLayout tileLayout; PipelineBaton(): - bufferInLength(0), + input(nullptr), limitInputPixels(0), - density(72), - rawWidth(0), - rawHeight(0), - rawChannels(0), - formatOut(""), - fileOut(""), bufferOutLength(0), - overlayBufferInLength(0), + overlay(nullptr), overlayGravity(0), overlayXOffset(-1), overlayYOffset(-1), overlayTile(false), overlayCutout(false), - booleanBufferInLength(0), topOffsetPre(-1), topOffsetPost(-1), channels(0), @@ -160,8 +144,9 @@ struct PipelineBaton { convKernelHeight(0), convKernelScale(0.0), convKernelOffset(0.0), - bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST), + boolean(nullptr), booleanOp(VIPS_OPERATION_BOOLEAN_LAST), + bandBoolOp(VIPS_OPERATION_BOOLEAN_LAST), extractChannel(-1), tileSize(256), tileOverlap(0), diff --git a/test/unit/boolean.js b/test/unit/boolean.js index eae65095..57538688 100644 --- a/test/unit/boolean.js +++ b/test/unit/boolean.js @@ -40,6 +40,23 @@ describe('Boolean operation between two images', function() { }); }); + it(op + ' operation, raw', function(done) { + sharp(fixtures.inputJpgBooleanTest) + .raw() + .toBuffer(function(err, data, info) { + if (err) throw err; + sharp(fixtures.inputJpg) + .resize(320, 240) + .boolean(data, op, { raw: info }) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('boolean_' + op + '_result.jpg'), data, done); + }); + }); + }); + }); it('Invalid operation', function() { diff --git a/test/unit/overlay.js b/test/unit/overlay.js index 4b8d6f70..e7425387 100644 --- a/test/unit/overlay.js +++ b/test/unit/overlay.js @@ -528,4 +528,28 @@ describe('Overlays', function() { }); }); + it('Composite RGBA raw buffer onto JPEG', function(done) { + sharp(fixtures.inputPngOverlayLayer1) + .raw() + .toBuffer(function(err, data, info) { + if (err) throw err; + sharp(fixtures.inputJpg) + .resize(2048, 1536) + .overlayWith(data, { raw: info }) + .toBuffer(function(err, data) { + if (err) throw err; + fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-rgb.jpg'), data, done); + }); + }); + }); + + it('Throws an error when called with an invalid file', function(done) { + sharp(fixtures.inputJpg) + .overlayWith('notfound.png') + .toBuffer(function(err) { + assert(err instanceof Error); + done(); + }); + }); + });