Add toUint8Array for output backed by transferable ArrayBuffer #4355

This commit is contained in:
Lovell Fuller
2025-12-22 15:05:33 +00:00
parent e1bad5470e
commit dbcb7e60bd
10 changed files with 161 additions and 5 deletions

View File

@@ -117,6 +117,38 @@ await sharp(pixelArray, { raw: { width, height, channels } })
```
## toUint8Array
> toUint8Array() ⇒ <code>Promise.&lt;{data: Uint8Array, info: Object}&gt;</code>
Write output to a `Uint8Array` backed by a transferable `ArrayBuffer`.
JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.
Use [toFormat](#toformat) or one of the format-specific functions such as [jpeg](#jpeg), [png](#png) etc. to set the output format.
If no explicit format is set, the output format will match the input image, except SVG input which becomes PNG output.
By default all metadata will be removed, which includes EXIF-based orientation.
See [keepExif](#keepexif) and similar methods for control over this.
Resolves with an `Object` containing:
- `data` is the output image as a `Uint8Array` backed by a transferable `ArrayBuffer`.
- `info` contains properties relating to the output image such as `width` and `height`.
**Since**: v0.35.0
**Example**
```js
const { data, info } = await sharp(input).toUint8Array();
```
**Example**
```js
const { data } = await sharp(input)
.avif()
.toUint8Array();
const base64String = data.toBase64();
```
## keepExif
> keepExif() ⇒ <code>Sharp</code>

View File

@@ -12,3 +12,6 @@ slug: changelog/v0.35.0
* Add `withGainMap` to process HDR JPEG images with embedded gain maps.
[#4314](https://github.com/lovell/sharp/issues/4314)
* Add `toUint8Array` for output image as a `TypedArray` backed by a transferable `ArrayBuffer`.
[#4355](https://github.com/lovell/sharp/issues/4355)

View File

@@ -306,6 +306,7 @@ const Sharp = function (input, options) {
fileOut: '',
formatOut: 'input',
streamOut: false,
typedArrayOut: false,
keepMetadata: 0,
withMetadataOrientation: -1,
withMetadataDensity: 0,

7
lib/index.d.ts vendored
View File

@@ -693,6 +693,13 @@ declare namespace sharp {
*/
toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer; info: OutputInfo }>;
/**
* Write output to a Uint8Array backed by a transferable ArrayBuffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* @returns A promise that resolves with an object containing the Uint8Array data and an info object containing the output image format, size (bytes), width, height and channels
*/
toUint8Array(): Promise<{ data: Uint8Array; info: OutputInfo }>;
/**
* Keep all EXIF metadata from the input image in the output image.
* EXIF metadata is unsupported for TIFF output.

View File

@@ -164,6 +164,41 @@ function toBuffer (options, callback) {
return this._pipeline(is.fn(options) ? options : callback, stack);
}
/**
* Write output to a `Uint8Array` backed by a transferable `ArrayBuffer`.
* JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.
*
* Use {@link #toformat toFormat} or one of the format-specific functions such as {@link #jpeg jpeg}, {@link #png png} etc. to set the output format.
*
* If no explicit format is set, the output format will match the input image, except SVG input which becomes PNG output.
*
* By default all metadata will be removed, which includes EXIF-based orientation.
* See {@link #keepexif keepExif} and similar methods for control over this.
*
* Resolves with an `Object` containing:
* - `data` is the output image as a `Uint8Array` backed by a transferable `ArrayBuffer`.
* - `info` contains properties relating to the output image such as `width` and `height`.
*
* @since v0.35.0
*
* @example
* const { data, info } = await sharp(input).toUint8Array();
*
* @example
* const { data } = await sharp(input)
* .avif()
* .toUint8Array();
* const base64String = data.toBase64();
*
* @returns {Promise<{ data: Uint8Array, info: Object }>}
*/
function toUint8Array () {
this.options.resolveWithObject = true;
this.options.typedArrayOut = true;
const stack = Error();
return this._pipeline(null, stack);
}
/**
* Keep all EXIF metadata from the input image in the output image.
*
@@ -1659,6 +1694,7 @@ module.exports = (Sharp) => {
// Public
toFile,
toBuffer,
toUint8Array,
keepExif,
withExif,
withExifMerge,

View File

@@ -1345,12 +1345,21 @@ class PipelineWorker : public Napi::AsyncWorker {
}
if (baton->bufferOutLength > 0) {
// Add buffer size to info
info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));
// Pass ownership of output data to Buffer instance
Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
baton->bufferOutLength, sharp::FreeCallback);
Callback().Call(Receiver().Value(), { env.Null(), data, info });
if (baton->typedArrayOut) {
// ECMAScript ArrayBuffer with Uint8Array view
Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, baton->bufferOutLength);
memcpy(ab.Data(), baton->bufferOut, baton->bufferOutLength);
sharp::FreeCallback(static_cast<char*>(baton->bufferOut), nullptr);
Napi::TypedArrayOf<uint8_t> data = Napi::TypedArrayOf<uint8_t>::New(env,
baton->bufferOutLength, ab, 0, napi_uint8_array);
Callback().Call(Receiver().Value(), { env.Null(), data, info });
} else {
// Node.js Buffer
Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
baton->bufferOutLength, sharp::FreeCallback);
Callback().Call(Receiver().Value(), { env.Null(), data, info });
}
} else {
// Add file size to info
if (baton->formatOut != "dz" || sharp::IsDzZip(baton->fileOut)) {
@@ -1700,6 +1709,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
// Output
baton->formatOut = sharp::AttrAsStr(options, "formatOut");
baton->fileOut = sharp::AttrAsStr(options, "fileOut");
baton->typedArrayOut = sharp::AttrAsBool(options, "typedArrayOut");
baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata");
baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");

View File

@@ -48,6 +48,7 @@ struct PipelineBaton {
size_t bufferOutLength;
int pageHeightOut;
int pagesOut;
bool typedArrayOut;
std::vector<Composite *> composite;
std::vector<sharp::InputDescriptor *> joinChannelIn;
int topOffsetPre;
@@ -243,6 +244,7 @@ struct PipelineBaton {
bufferOutLength(0),
pageHeightOut(0),
pagesOut(0),
typedArrayOut(false),
topOffsetPre(-1),
topOffsetPost(-1),
channels(0),

View File

@@ -228,6 +228,19 @@ async.series({
}
});
}
}).add('sharp-buffer-uint8array', {
defer: true,
fn: (deferred) => {
sharp(inputJpgBuffer)
.resize(width, height)
.toUint8Array()
.then(() => {
deferred.resolve();
})
.catch((err) => {
throw err;
});
}
}).add('sharp-file-file', {
defer: true,
fn: (deferred) => {
@@ -266,6 +279,19 @@ async.series({
}
});
}
}).add('sharp-file-uint8array', {
defer: true,
fn: (deferred) => {
sharp(fixtures.inputJpg)
.resize(width, height)
.toUint8Array()
.then(() => {
deferred.resolve();
})
.catch((err) => {
throw err;
});
}
}).add('sharp-promise', {
defer: true,
fn: (deferred) => {

View File

@@ -86,6 +86,9 @@ let transformer = sharp()
});
readableStream.pipe(transformer).pipe(writableStream);
sharp().toUint8Array();
sharp().toUint8Array().then(({ data }) => data.byteLength);
console.log(sharp.format);
console.log(sharp.versions);

View File

@@ -7,6 +7,7 @@ const fs = require('node:fs');
const path = require('node:path');
const { afterEach, beforeEach, describe, it } = require('node:test');
const assert = require('node:assert');
const { isMarkedAsUntransferable } = require('node:worker_threads');
const sharp = require('../../');
const fixtures = require('../fixtures');
@@ -1092,4 +1093,39 @@ describe('Input/output', () => {
assert.strictEqual(channels, 3);
assert.strictEqual(format, 'jpeg');
});
it('toBuffer resolves with an untransferable Buffer', async () => {
const data = await sharp(fixtures.inputJpg)
.resize({ width: 8, height: 8 })
.toBuffer();
if (isMarkedAsUntransferable) {
assert.strictEqual(isMarkedAsUntransferable(data.buffer), true);
}
assert.strictEqual(ArrayBuffer.isView(data), true);
assert.strictEqual(ArrayBuffer.isView(data.buffer), false);
});
it('toUint8Array resolves with a transferable Uint8Array', async () => {
const { data, info } = await sharp(fixtures.inputJpg)
.resize({ width: 8, height: 8 })
.toUint8Array();
assert.strictEqual(data instanceof Uint8Array, true);
if (isMarkedAsUntransferable) {
assert.strictEqual(isMarkedAsUntransferable(data.buffer), false);
}
assert.strictEqual(ArrayBuffer.isView(data), true);
assert.strictEqual(info.format, 'jpeg');
assert.strictEqual(info.width, 8);
assert.strictEqual(info.height, 8);
assert.strictEqual(data.byteLength, info.size);
assert.strictEqual(data[0], 0xFF);
assert.strictEqual(data[1], 0xD8);
const metadata = await sharp(data).metadata();
assert.strictEqual(metadata.format, 'jpeg');
assert.strictEqual(metadata.width, 8);
assert.strictEqual(metadata.height, 8);
});
});