Add support for raw, uncompressed pixel Buffer/Stream input

This commit is contained in:
Lovell Fuller 2016-02-03 19:21:37 +00:00
parent cf7664a854
commit e380576da2
7 changed files with 177 additions and 31 deletions

View File

@ -12,15 +12,16 @@ Constructor to which further methods are chained.
`input`, if present, can be one of:
* Buffer containing JPEG, PNG, WebP, GIF, SVG or TIFF 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, with most major formats supported.
JPEG, PNG, WebP, GIF, SVG or TIFF format image data
JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data
can be streamed into the object when `input` is `null` or `undefined`.
`options`, if present, is an Object with the following optional attributes:
* `density` an integral number representing the DPI for vector images, defaulting to 72.
* `raw` an Object containing `width`, `height` and `channels` when providing uncompressed data. See `raw()` for pixel ordering.
The object returned by the constructor implements the
[stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
@ -400,7 +401,7 @@ sharp('input.png')
`callback`, if present, is called with two arguments `(err, info)` where:
* `err` contains an error message, if any.
* `info` contains the output image `format`, `size` (bytes), `width` and `height`.
* `info` contains the output image `format`, `size` (bytes), `width`, `height` and `channels`.
A Promises/A+ promise is returned when `callback` is not provided.
@ -412,7 +413,7 @@ Write image data to a Buffer, the format of which will match the input image by
* `err` is an error message, if any.
* `buffer` is the output image data.
* `info` contains the output image `format`, `size` (bytes), `width` and `height`.
* `info` contains the output image `format`, `size` (bytes), `width`, `height` and `channels`.
A Promises/A+ promise is returned when `callback` is not provided.

View File

@ -9,6 +9,10 @@
[#110](https://github.com/lovell/sharp/issues/110)
[@bradisbell](https://github.com/bradisbell)
* Add support for raw, uncompressed pixel Buffer/Stream input.
[#220](https://github.com/lovell/sharp/issues/220)
[@mikemorris](https://github.com/mikemorris)
* Switch from libvips' C to C++ bindings, requires upgrade to v8.2.2.
[#299](https://github.com/lovell/sharp/issues/299)

View File

@ -47,6 +47,9 @@ var Sharp = function(input, options) {
sequentialRead: false,
limitInputPixels: maximum.pixels,
density: '72',
rawWidth: 0,
rawHeight: 0,
rawChannels: 0,
// ICC profiles
iccProfilePath: path.join(__dirname, 'icc') + path.sep,
// resize options
@ -135,23 +138,52 @@ module.exports.format = sharp.format();
*/
module.exports.versions = versions;
/*
Validation helpers
*/
var isDefined = function(val) {
return typeof val !== 'undefined' && val !== null;
};
var isObject = function(val) {
return typeof val === 'object';
};
var isInteger = function(val) {
return typeof val === 'number' && !Number.isNaN(val) && val % 1 === 0;
};
var inRange = function(val, min, max) {
return val >= min && val <= max;
};
/*
Set input-related options
density: DPI at which to load vector images via libmagick
*/
Sharp.prototype._inputOptions = function(options) {
if (typeof options === 'object') {
if (typeof options.density !== 'undefined') {
if (
typeof options.density === 'number' && !Number.isNaN(options.density) &&
options.density % 1 === 0 && options.density > 0 && options.density <= 2400
) {
if (isObject(options)) {
// Density
if (isDefined(options.density)) {
if (isInteger(options.density) && inRange(options.density, 1, 2400)) {
this.options.density = options.density.toString();
} else {
throw new Error('Invalid density (1 to 2400) ' + options.density);
}
}
} else if (typeof options !== 'undefined' && options !== null) {
// Raw pixel input
if (isDefined(options.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)
) {
this.options.rawWidth = options.raw.width;
this.options.rawHeight = options.raw.height;
this.options.rawChannels = options.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);
}
};

View File

@ -17,7 +17,8 @@ namespace sharp {
MAGICK,
OPENSLIDE,
PPM,
FITS
FITS,
RAW
};
// How many tasks are in the queue?

View File

@ -122,6 +122,7 @@ class MetadataWorker : public AsyncWorker {
case ImageType::OPENSLIDE: baton->format = "openslide"; break;
case ImageType::PPM: baton->format = "ppm"; break;
case ImageType::FITS: baton->format = "fits"; break;
case ImageType::RAW: baton->format = "raw"; break;
case ImageType::UNKNOWN: break;
}
// VipsImage attributes

View File

@ -78,6 +78,9 @@ struct PipelineBaton {
std::string iccProfilePath;
int limitInputPixels;
std::string density;
int rawWidth;
int rawHeight;
int rawChannels;
std::string output;
std::string outputFormat;
void *bufferOut;
@ -92,6 +95,7 @@ struct PipelineBaton {
int heightPost;
int width;
int height;
int channels;
Canvas canvas;
int gravity;
std::string interpolator;
@ -131,10 +135,14 @@ struct PipelineBaton {
bufferInLength(0),
limitInputPixels(0),
density(""),
rawWidth(0),
rawHeight(0),
rawChannels(0),
outputFormat(""),
bufferOutLength(0),
topOffsetPre(-1),
topOffsetPost(-1),
channels(0),
canvas(Canvas::CROP),
gravity(0),
flatten(false),
@ -199,6 +207,18 @@ class PipelineWorker : public AsyncWorker {
VImage image;
if (baton->bufferInLength > 0) {
// From buffer
if (baton->rawWidth > 0 && baton->rawHeight > 0 && baton->rawChannels > 0) {
// Raw, uncompressed pixel data
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;
} else {
// Compressed data
inputImageType = DetermineImageType(baton->bufferIn, baton->bufferInLength);
if (inputImageType != ImageType::UNKNOWN) {
try {
@ -214,6 +234,7 @@ class PipelineWorker : public AsyncWorker {
} else {
(baton->err).append("Input buffer contains unsupported image format");
}
}
} else {
// From file
inputImageType = DetermineImageType(baton->fileIn.data());
@ -667,12 +688,9 @@ class PipelineWorker : public AsyncWorker {
// Convert image to sRGB, if not already
if (image.interpretation() == VIPS_INTERPRETATION_RGB16) {
// Cast to integer and fast 8-bit conversion by discarding LSB
image = image.cast(VIPS_FORMAT_USHORT).msb();
// Explicitly set interpretation to sRGB
image.get_image()->Type = VIPS_INTERPRETATION_sRGB;
} else if (image.interpretation() != VIPS_INTERPRETATION_sRGB) {
// Switch interpretation to sRGB
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)) {
@ -791,6 +809,8 @@ class PipelineWorker : public AsyncWorker {
return Error();
}
}
// Number of channels used in output image
baton->channels = image.bands();
} catch (VError const &err) {
(baton->err).append(err.what());
}
@ -822,6 +842,7 @@ class PipelineWorker : public AsyncWorker {
Set(info, New("format").ToLocalChecked(), New<String>(baton->outputFormat).ToLocalChecked());
Set(info, New("width").ToLocalChecked(), New<Uint32>(static_cast<uint32_t>(width)));
Set(info, New("height").ToLocalChecked(), New<Uint32>(static_cast<uint32_t>(height)));
Set(info, New("channels").ToLocalChecked(), New<Uint32>(static_cast<uint32_t>(baton->channels)));
if (baton->bufferOutLength > 0) {
// Pass ownership of output data to Buffer instance
@ -1003,6 +1024,10 @@ NAN_METHOD(pipeline) {
baton->limitInputPixels = attrAs<int32_t>(options, "limitInputPixels");
// Density/DPI at which to load vector images via libmagick
baton->density = attrAsStr(options, "density");
// Raw pixel input
baton->rawWidth = attrAs<int32_t>(options, "rawWidth");
baton->rawHeight = attrAs<int32_t>(options, "rawHeight");
baton->rawChannels = attrAs<int32_t>(options, "rawChannels");
// Extract image options
baton->topOffsetPre = attrAs<int32_t>(options, "topOffsetPre");
baton->leftOffsetPre = attrAs<int32_t>(options, "leftOffsetPre");

View File

@ -862,6 +862,88 @@ describe('Input/output', function() {
});
});
describe('Raw pixel input', function() {
it('Missing options', function() {
assert.throws(function() {
sharp(null, { raw: {} } );
});
});
it('Incomplete options', function() {
assert.throws(function() {
sharp(null, { raw: { width: 1, height: 1} } );
});
});
it('Invalid channels', function() {
assert.throws(function() {
sharp(null, { raw: { width: 1, height: 1, channels: 5} } );
});
});
it('Invalid height', function() {
assert.throws(function() {
sharp(null, { raw: { width: 1, height: 0, channels: 4} } );
});
});
it('Invalid width', function() {
assert.throws(function() {
sharp(null, { raw: { width: 'zoinks', height: 1, channels: 4} } );
});
});
it('RGB', function(done) {
// Convert to raw pixel data
sharp(fixtures.inputJpg)
.resize(256)
.raw()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(256, info.width);
assert.strictEqual(209, info.height);
assert.strictEqual(3, info.channels);
// Convert back to JPEG
sharp(data, {
raw: {
width: info.width,
height: info.height,
channels: info.channels
}})
.jpeg()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(256, info.width);
assert.strictEqual(209, info.height);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(fixtures.inputJpg, data, done);
});
});
});
it('RGBA', function(done) {
// Convert to raw pixel data
sharp(fixtures.inputPngOverlayLayer1)
.resize(256)
.raw()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(256, info.width);
assert.strictEqual(192, info.height);
assert.strictEqual(4, info.channels);
// Convert back to PNG
sharp(data, {
raw: {
width: info.width,
height: info.height,
channels: info.channels
}})
.png()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(256, info.width);
assert.strictEqual(192, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.inputPngOverlayLayer1, data, done);
});
});
});
});
it('Queue length change events', function(done) {
var eventCounter = 0;
var queueListener = function(queueLength) {