mirror of
https://github.com/lovell/sharp.git
synced 2025-07-10 11:00:14 +02:00
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.
This commit is contained in:
parent
36bfbdee0d
commit
e6bfa52b0b
11
docs/api.md
11
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])
|
||||
|
@ -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*"
|
||||
|
||||
|
116
index.js
116
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();
|
||||
|
124
src/common.cc
124
src/common.cc
@ -1,32 +1,53 @@
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <string.h>
|
||||
|
||||
#include <node.h>
|
||||
#include <node_buffer.h>
|
||||
#include <vips/vips8>
|
||||
|
||||
#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<v8::Object> obj, std::string attr) {
|
||||
return Nan::Has(obj, Nan::New(attr).ToLocalChecked()).FromJust();
|
||||
}
|
||||
std::string AttrAsStr(v8::Handle<v8::Object> 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<v8::Object> input, std::vector<v8::Local<v8::Object>> buffersToPersist
|
||||
) {
|
||||
Nan::HandleScope();
|
||||
InputDescriptor *descriptor = new InputDescriptor;
|
||||
if (HasAttr(input, "file")) {
|
||||
descriptor->file = AttrAsStr(input, "file");
|
||||
} else {
|
||||
v8::Local<v8::Object> buffer = AttrAs<v8::Object>(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<uint32_t>(input, "density");
|
||||
}
|
||||
// Raw pixel input
|
||||
if (HasAttr(input, "rawChannels")) {
|
||||
descriptor->rawChannels = AttrTo<uint32_t>(input, "rawChannels");
|
||||
descriptor->rawWidth = AttrTo<uint32_t>(input, "rawWidth");
|
||||
descriptor->rawHeight = AttrTo<uint32_t>(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<VImage, ImageType> 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<double>(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<double>(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?
|
||||
*/
|
||||
|
63
src/common.h
63
src/common.h
@ -4,12 +4,70 @@
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
#include <node.h>
|
||||
#include <vips/vips8>
|
||||
|
||||
#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<v8::Object> obj, std::string attr);
|
||||
std::string AttrAsStr(v8::Handle<v8::Object> obj, std::string attr);
|
||||
template<typename T> v8::Local<T> AttrAs(v8::Handle<v8::Object> obj, std::string attr) {
|
||||
return Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked().As<T>();
|
||||
}
|
||||
template<typename T> T AttrTo(v8::Handle<v8::Object> obj, std::string attr) {
|
||||
return Nan::To<T>(Nan::Get(obj, Nan::New(attr).ToLocalChecked()).ToLocalChecked()).FromJust();
|
||||
}
|
||||
template<typename T> T AttrTo(v8::Handle<v8::Object> obj, int attr) {
|
||||
return Nan::To<T>(Nan::Get(obj, attr).ToLocalChecked()).FromJust();
|
||||
}
|
||||
|
||||
// Create an InputDescriptor instance from a v8::Object describing an input image
|
||||
InputDescriptor* CreateInputDescriptor(
|
||||
v8::Handle<v8::Object> input, std::vector<v8::Local<v8::Object>> 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<VImage, ImageType> OpenInput(InputDescriptor *descriptor, VipsAccess accessMethod);
|
||||
|
||||
/*
|
||||
Does this image have an embedded profile?
|
||||
*/
|
||||
|
206
src/metadata.cc
206
src/metadata.cc
@ -1,135 +1,54 @@
|
||||
#include <numeric>
|
||||
|
||||
#include <node.h>
|
||||
#include <vips/vips8>
|
||||
|
||||
#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<Object> &bufferIn) :
|
||||
AsyncWorker(callback), baton(baton) {
|
||||
if (baton->bufferInLength > 0) {
|
||||
SaveToPersistent("bufferIn", bufferIn);
|
||||
MetadataWorker(
|
||||
Nan::Callback *callback, MetadataBaton *baton,
|
||||
std::vector<v8::Local<v8::Object>> 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<v8::Object> 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<Value> argv[2] = { Null(), Null() };
|
||||
v8::Local<v8::Value> 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<Object> info = New<Object>();
|
||||
Set(info, New("format").ToLocalChecked(), New<String>(baton->format).ToLocalChecked());
|
||||
Set(info, New("width").ToLocalChecked(), New<Number>(baton->width));
|
||||
Set(info, New("height").ToLocalChecked(), New<Number>(baton->height));
|
||||
Set(info, New("space").ToLocalChecked(), New<String>(baton->space).ToLocalChecked());
|
||||
Set(info, New("channels").ToLocalChecked(), New<Number>(baton->channels));
|
||||
v8::Local<v8::Object> info = New<v8::Object>();
|
||||
Set(info, New("format").ToLocalChecked(), New<v8::String>(baton->format).ToLocalChecked());
|
||||
Set(info, New("width").ToLocalChecked(), New<v8::Uint32>(baton->width));
|
||||
Set(info, New("height").ToLocalChecked(), New<v8::Uint32>(baton->height));
|
||||
Set(info, New("space").ToLocalChecked(), New<v8::String>(baton->space).ToLocalChecked());
|
||||
Set(info, New("channels").ToLocalChecked(), New<v8::Uint32>(baton->channels));
|
||||
if (baton->density > 0) {
|
||||
Set(info, New("density").ToLocalChecked(), New<Number>(baton->density));
|
||||
Set(info, New("density").ToLocalChecked(), New<v8::Uint32>(baton->density));
|
||||
}
|
||||
Set(info, New("hasProfile").ToLocalChecked(), New<Boolean>(baton->hasProfile));
|
||||
Set(info, New("hasAlpha").ToLocalChecked(), New<Boolean>(baton->hasAlpha));
|
||||
Set(info, New("hasProfile").ToLocalChecked(), New<v8::Boolean>(baton->hasProfile));
|
||||
Set(info, New("hasAlpha").ToLocalChecked(), New<v8::Boolean>(baton->hasAlpha));
|
||||
if (baton->orientation > 0) {
|
||||
Set(info, New("orientation").ToLocalChecked(), New<Number>(baton->orientation));
|
||||
Set(info, New("orientation").ToLocalChecked(), New<v8::Uint32>(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<v8::Object> 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<v8::Local<v8::Object>> buffersToPersist;
|
||||
};
|
||||
|
||||
/*
|
||||
metadata(options, callback)
|
||||
*/
|
||||
NAN_METHOD(metadata) {
|
||||
HandleScope();
|
||||
// Input Buffers must not undergo GC compaction during processing
|
||||
std::vector<v8::Local<v8::Object>> buffersToPersist;
|
||||
|
||||
// V8 objects are converted to non-V8 types held in the baton struct
|
||||
MetadataBaton *baton = new MetadataBaton;
|
||||
Local<Object> options = info[0].As<Object>();
|
||||
v8::Local<v8::Object> options = info[0].As<v8::Object>();
|
||||
|
||||
// Input filename
|
||||
baton->fileIn = *Utf8String(Get(options, New("fileIn").ToLocalChecked()).ToLocalChecked());
|
||||
// Input Buffer object
|
||||
Local<Object> bufferIn;
|
||||
if (node::Buffer::HasInstance(Get(options, New("bufferIn").ToLocalChecked()).ToLocalChecked())) {
|
||||
bufferIn = Get(options, New("bufferIn").ToLocalChecked()).ToLocalChecked().As<Object>();
|
||||
baton->bufferInLength = node::Buffer::Length(bufferIn);
|
||||
baton->bufferIn = node::Buffer::Data(bufferIn);
|
||||
}
|
||||
// Input
|
||||
baton->input = sharp::CreateInputDescriptor(sharp::AttrAs<v8::Object>(options, "input"), buffersToPersist);
|
||||
|
||||
// Join queue for worker thread
|
||||
Callback *callback = new Callback(info[1].As<Function>());
|
||||
AsyncQueueWorker(new MetadataWorker(callback, baton, bufferIn));
|
||||
Nan::Callback *callback = new Nan::Callback(info[1].As<v8::Function>());
|
||||
Nan::AsyncQueueWorker(new MetadataWorker(callback, baton, buffersToPersist));
|
||||
|
||||
// Increment queued task counter
|
||||
g_atomic_int_inc(&counterQueue);
|
||||
g_atomic_int_inc(&sharp::counterQueue);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
651
src/pipeline.cc
651
src/pipeline.cc
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@
|
||||
#include <vips/vips8>
|
||||
|
||||
#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),
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user