mirror of
https://github.com/lovell/sharp.git
synced 2025-07-13 12:20:13 +02:00
Add support for raw, uncompressed pixel Buffer/Stream input
This commit is contained in:
parent
cf7664a854
commit
e380576da2
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
48
index.js
48
index.js
@ -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);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
@ -17,7 +17,8 @@ namespace sharp {
|
||||
MAGICK,
|
||||
OPENSLIDE,
|
||||
PPM,
|
||||
FITS
|
||||
FITS,
|
||||
RAW
|
||||
};
|
||||
|
||||
// How many tasks are in the queue?
|
||||
|
@ -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
|
||||
|
@ -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,20 +207,33 @@ class PipelineWorker : public AsyncWorker {
|
||||
VImage image;
|
||||
if (baton->bufferInLength > 0) {
|
||||
// From buffer
|
||||
inputImageType = DetermineImageType(baton->bufferIn, baton->bufferInLength);
|
||||
if (inputImageType != ImageType::UNKNOWN) {
|
||||
try {
|
||||
image = VImage::new_from_buffer(
|
||||
baton->bufferIn, baton->bufferInLength, nullptr, VImage::option()
|
||||
->set("access", baton->accessMethod)
|
||||
->set("density", baton->density.data())
|
||||
);
|
||||
} catch (...) {
|
||||
(baton->err).append("Input buffer has corrupt header");
|
||||
inputImageType = ImageType::UNKNOWN;
|
||||
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 {
|
||||
(baton->err).append("Input buffer contains unsupported image format");
|
||||
// Compressed data
|
||||
inputImageType = DetermineImageType(baton->bufferIn, baton->bufferInLength);
|
||||
if (inputImageType != ImageType::UNKNOWN) {
|
||||
try {
|
||||
image = VImage::new_from_buffer(
|
||||
baton->bufferIn, baton->bufferInLength, nullptr, VImage::option()
|
||||
->set("access", baton->accessMethod)
|
||||
->set("density", baton->density.data())
|
||||
);
|
||||
} 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
|
||||
@ -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");
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user