Add support for bit depth with raw input and output (#2762)

* Determine input raw pixel depth from the given typed array
* Allow pixel depth to be set on raw output
This commit is contained in:
Mart 2021-08-03 15:52:54 +02:00 committed by GitHub
parent eabb671b10
commit b7add480c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 123 additions and 16 deletions

View File

@ -92,8 +92,9 @@ const debuglog = util.debuglog('sharp');
* } * }
* }).toFile('noise.png'); * }).toFile('noise.png');
* *
* @param {(Buffer|Uint8Array|Uint8ClampedArray|string)} [input] - if present, can be * @param {(Buffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string)} [input] - if present, can be
* a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or * a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image data, or
* a Uint8Array / Uint8ClampedArray / Int8Array / Uint16Array / Int16Array / Uint32Array / Int32Array / Float32Array / Float64Array containing raw pixel image data, or
* a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file. * a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file.
* JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present. * JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
* @param {Object} [options] - if present, is an Object with optional attributes. * @param {Object} [options] - if present, is an Object with optional attributes.
@ -254,6 +255,7 @@ const Sharp = function (input, options) {
heifCompression: 'av1', heifCompression: 'av1',
heifSpeed: 5, heifSpeed: 5,
heifChromaSubsampling: '4:4:4', heifChromaSubsampling: '4:4:4',
rawDepth: 'uchar',
tileSize: 256, tileSize: 256,
tileOverlap: 0, tileOverlap: 0,
tileContainer: 'fs', tileContainer: 'fs',

View File

@ -34,8 +34,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
throw Error('Input Buffer is empty'); throw Error('Input Buffer is empty');
} }
inputDescriptor.buffer = input; inputDescriptor.buffer = input;
} else if (is.uint8Array(input)) { } else if (is.typedArray(input)) {
// Uint8Array or Uint8ClampedArray
if (input.length === 0) { if (input.length === 0) {
throw Error('Input Bit Array is empty'); throw Error('Input Bit Array is empty');
} }
@ -104,6 +103,37 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
inputDescriptor.rawHeight = inputOptions.raw.height; inputDescriptor.rawHeight = inputOptions.raw.height;
inputDescriptor.rawChannels = inputOptions.raw.channels; inputDescriptor.rawChannels = inputOptions.raw.channels;
inputDescriptor.rawPremultiplied = !!inputOptions.raw.premultiplied; inputDescriptor.rawPremultiplied = !!inputOptions.raw.premultiplied;
switch (input.constructor) {
case Uint8Array:
case Uint8ClampedArray:
inputDescriptor.rawDepth = 'uchar';
break;
case Int8Array:
inputDescriptor.rawDepth = 'char';
break;
case Uint16Array:
inputDescriptor.rawDepth = 'ushort';
break;
case Int16Array:
inputDescriptor.rawDepth = 'short';
break;
case Uint32Array:
inputDescriptor.rawDepth = 'uint';
break;
case Int32Array:
inputDescriptor.rawDepth = 'int';
break;
case Float32Array:
inputDescriptor.rawDepth = 'float';
break;
case Float64Array:
inputDescriptor.rawDepth = 'double';
break;
default:
inputDescriptor.rawDepth = 'uchar';
break;
}
} else { } else {
throw new Error('Expected width, height and channels for raw pixel input'); throw new Error('Expected width, height and channels for raw pixel input');
} }

View File

@ -49,12 +49,26 @@ const buffer = function (val) {
}; };
/** /**
* Is this value a Uint8Array or Uint8ClampedArray object? * Is this value a typed array object?. E.g. Uint8Array or Uint8ClampedArray?
* @private * @private
*/ */
const uint8Array = function (val) { const typedArray = function (val) {
// allow both since Uint8ClampedArray simply clamps the values between 0-255 if (defined(val)) {
return val instanceof Uint8Array || val instanceof Uint8ClampedArray; switch (val.constructor) {
case Uint8Array:
case Uint8ClampedArray:
case Int8Array:
case Uint16Array:
case Int16Array:
case Uint32Array:
case Int32Array:
case Float32Array:
case Float64Array:
return true;
}
}
return false;
}; };
/** /**
@ -119,7 +133,7 @@ module.exports = {
fn: fn, fn: fn,
bool: bool, bool: bool,
buffer: buffer, buffer: buffer,
uint8Array: uint8Array, typedArray: typedArray,
string: string, string: string,
number: number, number: number,
integer: integer, integer: integer,

View File

@ -748,7 +748,16 @@ function heif (options) {
* *
* @returns {Sharp} * @returns {Sharp}
*/ */
function raw () { function raw (options) {
if (is.object(options)) {
if (is.defined(options.depth)) {
if (is.string(options.depth) && is.inArray(options.depth, ['char', 'uchar', 'short', 'ushort', 'int', 'uint', 'float', 'complex', 'double', 'dpcomplex'])) {
this.options.rawDepth = options.depth;
} else {
throw is.invalidParameterError('depth', 'one of: char, uchar, short, ushort, int, uint, float, complex, double, dpcomplex', options.depth);
}
}
}
return this._updateFormatOut('raw'); return this._updateFormatOut('raw');
} }

View File

@ -92,6 +92,9 @@ namespace sharp {
} }
// Raw pixel input // Raw pixel input
if (HasAttr(input, "rawChannels")) { if (HasAttr(input, "rawChannels")) {
descriptor->rawDepth = static_cast<VipsBandFormat>(
vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT,
AttrAsStr(input, "rawDepth").data()));
descriptor->rawChannels = AttrAsUint32(input, "rawChannels"); descriptor->rawChannels = AttrAsUint32(input, "rawChannels");
descriptor->rawWidth = AttrAsUint32(input, "rawWidth"); descriptor->rawWidth = AttrAsUint32(input, "rawWidth");
descriptor->rawHeight = AttrAsUint32(input, "rawHeight"); descriptor->rawHeight = AttrAsUint32(input, "rawHeight");
@ -297,7 +300,7 @@ namespace sharp {
if (descriptor->rawChannels > 0) { if (descriptor->rawChannels > 0) {
// Raw, uncompressed pixel data // Raw, uncompressed pixel data
image = VImage::new_from_memory(descriptor->buffer, descriptor->bufferLength, image = VImage::new_from_memory(descriptor->buffer, descriptor->bufferLength,
descriptor->rawWidth, descriptor->rawHeight, descriptor->rawChannels, VIPS_FORMAT_UCHAR); descriptor->rawWidth, descriptor->rawHeight, descriptor->rawChannels, descriptor->rawDepth);
if (descriptor->rawChannels < 3) { if (descriptor->rawChannels < 3) {
image.get_image()->Type = VIPS_INTERPRETATION_B_W; image.get_image()->Type = VIPS_INTERPRETATION_B_W;
} else { } else {

View File

@ -54,6 +54,7 @@ namespace sharp {
size_t bufferLength; size_t bufferLength;
bool isBuffer; bool isBuffer;
double density; double density;
VipsBandFormat rawDepth;
int rawChannels; int rawChannels;
int rawWidth; int rawWidth;
int rawHeight; int rawHeight;
@ -78,6 +79,7 @@ namespace sharp {
bufferLength(0), bufferLength(0),
isBuffer(FALSE), isBuffer(FALSE),
density(72.0), density(72.0),
rawDepth(VIPS_FORMAT_UCHAR),
rawChannels(0), rawChannels(0),
rawWidth(0), rawWidth(0),
rawHeight(0), rawHeight(0),

View File

@ -889,9 +889,9 @@ class PipelineWorker : public Napi::AsyncWorker {
image = image[0]; image = image[0];
baton->channels = 1; baton->channels = 1;
} }
if (image.format() != VIPS_FORMAT_UCHAR) { if (image.format() != baton->rawDepth) {
// Cast pixels to uint8 (unsigned char) // Cast pixels to requested format
image = image.cast(VIPS_FORMAT_UCHAR); image = image.cast(baton->rawDepth);
} }
// Get raw image data // Get raw image data
baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength)); baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
@ -1131,6 +1131,9 @@ class PipelineWorker : public Napi::AsyncWorker {
info.Set("width", static_cast<uint32_t>(width)); info.Set("width", static_cast<uint32_t>(width));
info.Set("height", static_cast<uint32_t>(height)); info.Set("height", static_cast<uint32_t>(height));
info.Set("channels", static_cast<uint32_t>(baton->channels)); info.Set("channels", static_cast<uint32_t>(baton->channels));
if (baton->formatOut == "raw") {
info.Set("depth", vips_enum_nick(VIPS_TYPE_BAND_FORMAT, baton->rawDepth));
}
info.Set("premultiplied", baton->premultiplied); info.Set("premultiplied", baton->premultiplied);
if (baton->hasCropOffset) { if (baton->hasCropOffset) {
info.Set("cropOffsetLeft", static_cast<int32_t>(baton->cropOffsetLeft)); info.Set("cropOffsetLeft", static_cast<int32_t>(baton->cropOffsetLeft));
@ -1457,6 +1460,11 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed"); baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed");
baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling"); baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling");
// Raw output
baton->rawDepth = static_cast<VipsBandFormat>(
vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT,
sharp::AttrAsStr(options, "rawDepth").data()));
// Animated output // Animated output
if (sharp::HasAttr(options, "pageHeight")) { if (sharp::HasAttr(options, "pageHeight")) {
baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight"); baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight");

View File

@ -169,6 +169,7 @@ struct PipelineBaton {
int heifSpeed; int heifSpeed;
std::string heifChromaSubsampling; std::string heifChromaSubsampling;
bool heifLossless; bool heifLossless;
VipsBandFormat rawDepth;
std::string err; std::string err;
bool withMetadata; bool withMetadata;
int withMetadataOrientation; int withMetadataOrientation;
@ -298,6 +299,7 @@ struct PipelineBaton {
heifSpeed(5), heifSpeed(5),
heifChromaSubsampling("4:4:4"), heifChromaSubsampling("4:4:4"),
heifLossless(false), heifLossless(false),
rawDepth(VIPS_FORMAT_UCHAR),
withMetadata(false), withMetadata(false),
withMetadataOrientation(-1), withMetadataOrientation(-1),
withMetadataDensity(0.0), withMetadataDensity(0.0),

View File

@ -179,7 +179,7 @@ describe('Raw pixel data', function () {
}); });
}); });
describe('Ouput raw, uncompressed image data', function () { describe('Output raw, uncompressed image data', function () {
it('1 channel greyscale image', function (done) { it('1 channel greyscale image', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.greyscale() .greyscale()
@ -227,7 +227,7 @@ describe('Raw pixel data', function () {
}); });
}); });
it('extract A from RGBA', () => it('Extract A from RGBA', () =>
sharp(fixtures.inputPngWithTransparency) sharp(fixtures.inputPngWithTransparency)
.resize(32, 24) .resize(32, 24)
.extractChannel(3) .extractChannel(3)
@ -241,4 +241,41 @@ describe('Raw pixel data', function () {
}) })
); );
}); });
describe('Raw pixel depths', function () {
it('Invalid depth', function () {
assert.throws(function () {
sharp(Buffer.alloc(3), { raw: { width: 1, height: 1, channels: 3 } })
.raw({ depth: 'zoinks' });
});
});
for (const { constructor, depth, bits } of [
{ constructor: Uint8Array, depth: undefined, bits: 8 },
{ constructor: Uint8Array, depth: 'uchar', bits: 8 },
{ constructor: Uint8ClampedArray, depth: 'uchar', bits: 8 },
{ constructor: Int8Array, depth: 'char', bits: 8 },
{ constructor: Uint16Array, depth: 'ushort', bits: 16 },
{ constructor: Int16Array, depth: 'short', bits: 16 },
{ constructor: Uint32Array, depth: 'uint', bits: 32 },
{ constructor: Int32Array, depth: 'int', bits: 32 },
{ constructor: Float32Array, depth: 'float', bits: 32 },
{ constructor: Float64Array, depth: 'double', bits: 64 }
]) {
it(constructor.name, () =>
sharp(new constructor(3), { raw: { width: 1, height: 1, channels: 3 } })
.raw({ depth })
.toBuffer({ resolveWithObject: true })
.then(({ data, info }) => {
assert.strictEqual(1, info.width);
assert.strictEqual(1, info.height);
assert.strictEqual(3, info.channels);
if (depth !== undefined) {
assert.strictEqual(depth, info.depth);
}
assert.strictEqual(data.length / 3, bits / 8);
})
);
}
});
}); });