mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
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:
parent
eabb671b10
commit
b7add480c7
@ -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',
|
||||||
|
34
lib/input.js
34
lib/input.js
@ -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');
|
||||||
}
|
}
|
||||||
|
24
lib/is.js
24
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
|
* @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,
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
|
@ -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");
|
||||||
|
@ -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),
|
||||||
|
@ -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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user