From b7add480c726adcc343838b3bff2c8df3d91784b Mon Sep 17 00:00:00 2001 From: Mart Date: Tue, 3 Aug 2021 15:52:54 +0200 Subject: [PATCH] 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 --- lib/constructor.js | 6 ++++-- lib/input.js | 34 ++++++++++++++++++++++++++++++++-- lib/is.js | 24 +++++++++++++++++++----- lib/output.js | 11 ++++++++++- src/common.cc | 5 ++++- src/common.h | 2 ++ src/pipeline.cc | 14 +++++++++++--- src/pipeline.h | 2 ++ test/unit/raw.js | 41 +++++++++++++++++++++++++++++++++++++++-- 9 files changed, 123 insertions(+), 16 deletions(-) diff --git a/lib/constructor.js b/lib/constructor.js index 373fa99b..fa5534b7 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -92,8 +92,9 @@ const debuglog = util.debuglog('sharp'); * } * }).toFile('noise.png'); * - * @param {(Buffer|Uint8Array|Uint8ClampedArray|string)} [input] - if present, can be - * a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or + * @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 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. * 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. @@ -254,6 +255,7 @@ const Sharp = function (input, options) { heifCompression: 'av1', heifSpeed: 5, heifChromaSubsampling: '4:4:4', + rawDepth: 'uchar', tileSize: 256, tileOverlap: 0, tileContainer: 'fs', diff --git a/lib/input.js b/lib/input.js index 7f4e8620..5dd2f0e9 100644 --- a/lib/input.js +++ b/lib/input.js @@ -34,8 +34,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw Error('Input Buffer is empty'); } inputDescriptor.buffer = input; - } else if (is.uint8Array(input)) { - // Uint8Array or Uint8ClampedArray + } else if (is.typedArray(input)) { if (input.length === 0) { throw Error('Input Bit Array is empty'); } @@ -104,6 +103,37 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { inputDescriptor.rawHeight = inputOptions.raw.height; inputDescriptor.rawChannels = inputOptions.raw.channels; 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 { throw new Error('Expected width, height and channels for raw pixel input'); } diff --git a/lib/is.js b/lib/is.js index 7662c6e7..24b8a957 100644 --- a/lib/is.js +++ b/lib/is.js @@ -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 */ -const uint8Array = function (val) { - // allow both since Uint8ClampedArray simply clamps the values between 0-255 - return val instanceof Uint8Array || val instanceof Uint8ClampedArray; +const typedArray = function (val) { + if (defined(val)) { + 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, bool: bool, buffer: buffer, - uint8Array: uint8Array, + typedArray: typedArray, string: string, number: number, integer: integer, diff --git a/lib/output.js b/lib/output.js index 4c7229ae..9d82f894 100644 --- a/lib/output.js +++ b/lib/output.js @@ -748,7 +748,16 @@ function heif (options) { * * @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'); } diff --git a/src/common.cc b/src/common.cc index 2f315a9c..3bfb4b0e 100644 --- a/src/common.cc +++ b/src/common.cc @@ -92,6 +92,9 @@ namespace sharp { } // Raw pixel input if (HasAttr(input, "rawChannels")) { + descriptor->rawDepth = static_cast( + vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT, + AttrAsStr(input, "rawDepth").data())); descriptor->rawChannels = AttrAsUint32(input, "rawChannels"); descriptor->rawWidth = AttrAsUint32(input, "rawWidth"); descriptor->rawHeight = AttrAsUint32(input, "rawHeight"); @@ -297,7 +300,7 @@ namespace sharp { 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); + descriptor->rawWidth, descriptor->rawHeight, descriptor->rawChannels, descriptor->rawDepth); if (descriptor->rawChannels < 3) { image.get_image()->Type = VIPS_INTERPRETATION_B_W; } else { diff --git a/src/common.h b/src/common.h index f19bfe7e..4d2d753f 100644 --- a/src/common.h +++ b/src/common.h @@ -54,6 +54,7 @@ namespace sharp { size_t bufferLength; bool isBuffer; double density; + VipsBandFormat rawDepth; int rawChannels; int rawWidth; int rawHeight; @@ -78,6 +79,7 @@ namespace sharp { bufferLength(0), isBuffer(FALSE), density(72.0), + rawDepth(VIPS_FORMAT_UCHAR), rawChannels(0), rawWidth(0), rawHeight(0), diff --git a/src/pipeline.cc b/src/pipeline.cc index 6df5891d..35eff31b 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -889,9 +889,9 @@ class PipelineWorker : public Napi::AsyncWorker { image = image[0]; baton->channels = 1; } - if (image.format() != VIPS_FORMAT_UCHAR) { - // Cast pixels to uint8 (unsigned char) - image = image.cast(VIPS_FORMAT_UCHAR); + if (image.format() != baton->rawDepth) { + // Cast pixels to requested format + image = image.cast(baton->rawDepth); } // Get raw image data baton->bufferOut = static_cast(image.write_to_memory(&baton->bufferOutLength)); @@ -1131,6 +1131,9 @@ class PipelineWorker : public Napi::AsyncWorker { info.Set("width", static_cast(width)); info.Set("height", static_cast(height)); info.Set("channels", static_cast(baton->channels)); + if (baton->formatOut == "raw") { + info.Set("depth", vips_enum_nick(VIPS_TYPE_BAND_FORMAT, baton->rawDepth)); + } info.Set("premultiplied", baton->premultiplied); if (baton->hasCropOffset) { info.Set("cropOffsetLeft", static_cast(baton->cropOffsetLeft)); @@ -1457,6 +1460,11 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) { baton->heifSpeed = sharp::AttrAsUint32(options, "heifSpeed"); baton->heifChromaSubsampling = sharp::AttrAsStr(options, "heifChromaSubsampling"); + // Raw output + baton->rawDepth = static_cast( + vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT, + sharp::AttrAsStr(options, "rawDepth").data())); + // Animated output if (sharp::HasAttr(options, "pageHeight")) { baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight"); diff --git a/src/pipeline.h b/src/pipeline.h index 1e9c5c82..d934ded2 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -169,6 +169,7 @@ struct PipelineBaton { int heifSpeed; std::string heifChromaSubsampling; bool heifLossless; + VipsBandFormat rawDepth; std::string err; bool withMetadata; int withMetadataOrientation; @@ -298,6 +299,7 @@ struct PipelineBaton { heifSpeed(5), heifChromaSubsampling("4:4:4"), heifLossless(false), + rawDepth(VIPS_FORMAT_UCHAR), withMetadata(false), withMetadataOrientation(-1), withMetadataDensity(0.0), diff --git a/test/unit/raw.js b/test/unit/raw.js index eed85708..6f8bef98 100644 --- a/test/unit/raw.js +++ b/test/unit/raw.js @@ -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) { sharp(fixtures.inputJpg) .greyscale() @@ -227,7 +227,7 @@ describe('Raw pixel data', function () { }); }); - it('extract A from RGBA', () => + it('Extract A from RGBA', () => sharp(fixtures.inputPngWithTransparency) .resize(32, 24) .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); + }) + ); + } + }); });