mirror of
https://github.com/lovell/sharp.git
synced 2025-07-13 20:30:14 +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:
|
`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.
|
* 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`.
|
can be streamed into the object when `input` is `null` or `undefined`.
|
||||||
|
|
||||||
`options`, if present, is an Object with the following optional attributes:
|
`options`, if present, is an Object with the following optional attributes:
|
||||||
|
|
||||||
* `density` an integral number representing the DPI for vector images, defaulting to 72.
|
* `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
|
The object returned by the constructor implements the
|
||||||
[stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
|
[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:
|
`callback`, if present, is called with two arguments `(err, info)` where:
|
||||||
|
|
||||||
* `err` contains an error message, if any.
|
* `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.
|
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.
|
* `err` is an error message, if any.
|
||||||
* `buffer` is the output image data.
|
* `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.
|
A Promises/A+ promise is returned when `callback` is not provided.
|
||||||
|
|
||||||
|
@ -9,6 +9,10 @@
|
|||||||
[#110](https://github.com/lovell/sharp/issues/110)
|
[#110](https://github.com/lovell/sharp/issues/110)
|
||||||
[@bradisbell](https://github.com/bradisbell)
|
[@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.
|
* Switch from libvips' C to C++ bindings, requires upgrade to v8.2.2.
|
||||||
[#299](https://github.com/lovell/sharp/issues/299)
|
[#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,
|
sequentialRead: false,
|
||||||
limitInputPixels: maximum.pixels,
|
limitInputPixels: maximum.pixels,
|
||||||
density: '72',
|
density: '72',
|
||||||
|
rawWidth: 0,
|
||||||
|
rawHeight: 0,
|
||||||
|
rawChannels: 0,
|
||||||
// ICC profiles
|
// ICC profiles
|
||||||
iccProfilePath: path.join(__dirname, 'icc') + path.sep,
|
iccProfilePath: path.join(__dirname, 'icc') + path.sep,
|
||||||
// resize options
|
// resize options
|
||||||
@ -135,23 +138,52 @@ module.exports.format = sharp.format();
|
|||||||
*/
|
*/
|
||||||
module.exports.versions = versions;
|
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
|
Set input-related options
|
||||||
density: DPI at which to load vector images via libmagick
|
density: DPI at which to load vector images via libmagick
|
||||||
*/
|
*/
|
||||||
Sharp.prototype._inputOptions = function(options) {
|
Sharp.prototype._inputOptions = function(options) {
|
||||||
if (typeof options === 'object') {
|
if (isObject(options)) {
|
||||||
if (typeof options.density !== 'undefined') {
|
// Density
|
||||||
if (
|
if (isDefined(options.density)) {
|
||||||
typeof options.density === 'number' && !Number.isNaN(options.density) &&
|
if (isInteger(options.density) && inRange(options.density, 1, 2400)) {
|
||||||
options.density % 1 === 0 && options.density > 0 && options.density <= 2400
|
|
||||||
) {
|
|
||||||
this.options.density = options.density.toString();
|
this.options.density = options.density.toString();
|
||||||
} else {
|
} 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);
|
throw new Error('Invalid input options ' + options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,8 @@ namespace sharp {
|
|||||||
MAGICK,
|
MAGICK,
|
||||||
OPENSLIDE,
|
OPENSLIDE,
|
||||||
PPM,
|
PPM,
|
||||||
FITS
|
FITS,
|
||||||
|
RAW
|
||||||
};
|
};
|
||||||
|
|
||||||
// How many tasks are in the queue?
|
// How many tasks are in the queue?
|
||||||
|
@ -122,6 +122,7 @@ class MetadataWorker : public AsyncWorker {
|
|||||||
case ImageType::OPENSLIDE: baton->format = "openslide"; break;
|
case ImageType::OPENSLIDE: baton->format = "openslide"; break;
|
||||||
case ImageType::PPM: baton->format = "ppm"; break;
|
case ImageType::PPM: baton->format = "ppm"; break;
|
||||||
case ImageType::FITS: baton->format = "fits"; break;
|
case ImageType::FITS: baton->format = "fits"; break;
|
||||||
|
case ImageType::RAW: baton->format = "raw"; break;
|
||||||
case ImageType::UNKNOWN: break;
|
case ImageType::UNKNOWN: break;
|
||||||
}
|
}
|
||||||
// VipsImage attributes
|
// VipsImage attributes
|
||||||
|
@ -78,6 +78,9 @@ struct PipelineBaton {
|
|||||||
std::string iccProfilePath;
|
std::string iccProfilePath;
|
||||||
int limitInputPixels;
|
int limitInputPixels;
|
||||||
std::string density;
|
std::string density;
|
||||||
|
int rawWidth;
|
||||||
|
int rawHeight;
|
||||||
|
int rawChannels;
|
||||||
std::string output;
|
std::string output;
|
||||||
std::string outputFormat;
|
std::string outputFormat;
|
||||||
void *bufferOut;
|
void *bufferOut;
|
||||||
@ -92,6 +95,7 @@ struct PipelineBaton {
|
|||||||
int heightPost;
|
int heightPost;
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
|
int channels;
|
||||||
Canvas canvas;
|
Canvas canvas;
|
||||||
int gravity;
|
int gravity;
|
||||||
std::string interpolator;
|
std::string interpolator;
|
||||||
@ -131,10 +135,14 @@ struct PipelineBaton {
|
|||||||
bufferInLength(0),
|
bufferInLength(0),
|
||||||
limitInputPixels(0),
|
limitInputPixels(0),
|
||||||
density(""),
|
density(""),
|
||||||
|
rawWidth(0),
|
||||||
|
rawHeight(0),
|
||||||
|
rawChannels(0),
|
||||||
outputFormat(""),
|
outputFormat(""),
|
||||||
bufferOutLength(0),
|
bufferOutLength(0),
|
||||||
topOffsetPre(-1),
|
topOffsetPre(-1),
|
||||||
topOffsetPost(-1),
|
topOffsetPost(-1),
|
||||||
|
channels(0),
|
||||||
canvas(Canvas::CROP),
|
canvas(Canvas::CROP),
|
||||||
gravity(0),
|
gravity(0),
|
||||||
flatten(false),
|
flatten(false),
|
||||||
@ -199,20 +207,33 @@ class PipelineWorker : public AsyncWorker {
|
|||||||
VImage image;
|
VImage image;
|
||||||
if (baton->bufferInLength > 0) {
|
if (baton->bufferInLength > 0) {
|
||||||
// From buffer
|
// From buffer
|
||||||
inputImageType = DetermineImageType(baton->bufferIn, baton->bufferInLength);
|
if (baton->rawWidth > 0 && baton->rawHeight > 0 && baton->rawChannels > 0) {
|
||||||
if (inputImageType != ImageType::UNKNOWN) {
|
// Raw, uncompressed pixel data
|
||||||
try {
|
image = VImage::new_from_memory(baton->bufferIn, baton->bufferInLength,
|
||||||
image = VImage::new_from_buffer(
|
baton->rawWidth, baton->rawHeight, baton->rawChannels, VIPS_FORMAT_UCHAR);
|
||||||
baton->bufferIn, baton->bufferInLength, nullptr, VImage::option()
|
if (baton->rawChannels < 3) {
|
||||||
->set("access", baton->accessMethod)
|
image.get_image()->Type = VIPS_INTERPRETATION_B_W;
|
||||||
->set("density", baton->density.data())
|
} else {
|
||||||
);
|
image.get_image()->Type = VIPS_INTERPRETATION_sRGB;
|
||||||
} catch (...) {
|
|
||||||
(baton->err).append("Input buffer has corrupt header");
|
|
||||||
inputImageType = ImageType::UNKNOWN;
|
|
||||||
}
|
}
|
||||||
|
inputImageType = ImageType::RAW;
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
// From file
|
// From file
|
||||||
@ -667,12 +688,9 @@ class PipelineWorker : public AsyncWorker {
|
|||||||
|
|
||||||
// Convert image to sRGB, if not already
|
// Convert image to sRGB, if not already
|
||||||
if (image.interpretation() == VIPS_INTERPRETATION_RGB16) {
|
if (image.interpretation() == VIPS_INTERPRETATION_RGB16) {
|
||||||
// Cast to integer and fast 8-bit conversion by discarding LSB
|
image = image.cast(VIPS_FORMAT_USHORT);
|
||||||
image = image.cast(VIPS_FORMAT_USHORT).msb();
|
}
|
||||||
// Explicitly set interpretation to sRGB
|
if (image.interpretation() != VIPS_INTERPRETATION_sRGB) {
|
||||||
image.get_image()->Type = VIPS_INTERPRETATION_sRGB;
|
|
||||||
} else if (image.interpretation() != VIPS_INTERPRETATION_sRGB) {
|
|
||||||
// Switch interpretation to sRGB
|
|
||||||
image = image.colourspace(VIPS_INTERPRETATION_sRGB);
|
image = image.colourspace(VIPS_INTERPRETATION_sRGB);
|
||||||
// Transform colours from embedded profile to sRGB profile
|
// Transform colours from embedded profile to sRGB profile
|
||||||
if (baton->withMetadata && HasProfile(image)) {
|
if (baton->withMetadata && HasProfile(image)) {
|
||||||
@ -791,6 +809,8 @@ class PipelineWorker : public AsyncWorker {
|
|||||||
return Error();
|
return Error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Number of channels used in output image
|
||||||
|
baton->channels = image.bands();
|
||||||
} catch (VError const &err) {
|
} catch (VError const &err) {
|
||||||
(baton->err).append(err.what());
|
(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("format").ToLocalChecked(), New<String>(baton->outputFormat).ToLocalChecked());
|
||||||
Set(info, New("width").ToLocalChecked(), New<Uint32>(static_cast<uint32_t>(width)));
|
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("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) {
|
if (baton->bufferOutLength > 0) {
|
||||||
// Pass ownership of output data to Buffer instance
|
// Pass ownership of output data to Buffer instance
|
||||||
@ -1003,6 +1024,10 @@ NAN_METHOD(pipeline) {
|
|||||||
baton->limitInputPixels = attrAs<int32_t>(options, "limitInputPixels");
|
baton->limitInputPixels = attrAs<int32_t>(options, "limitInputPixels");
|
||||||
// Density/DPI at which to load vector images via libmagick
|
// Density/DPI at which to load vector images via libmagick
|
||||||
baton->density = attrAsStr(options, "density");
|
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
|
// Extract image options
|
||||||
baton->topOffsetPre = attrAs<int32_t>(options, "topOffsetPre");
|
baton->topOffsetPre = attrAs<int32_t>(options, "topOffsetPre");
|
||||||
baton->leftOffsetPre = attrAs<int32_t>(options, "leftOffsetPre");
|
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) {
|
it('Queue length change events', function(done) {
|
||||||
var eventCounter = 0;
|
var eventCounter = 0;
|
||||||
var queueListener = function(queueLength) {
|
var queueListener = function(queueLength) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user