diff --git a/docs/src/content/docs/api-output.md b/docs/src/content/docs/api-output.md
index f070c980..5902912e 100644
--- a/docs/src/content/docs/api-output.md
+++ b/docs/src/content/docs/api-output.md
@@ -117,6 +117,38 @@ await sharp(pixelArray, { raw: { width, height, channels } })
```
+## toUint8Array
+> toUint8Array() ⇒ Promise.<{data: Uint8Array, info: Object}>
+
+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() ⇒ Sharp
diff --git a/docs/src/content/docs/changelog/v0.35.0.md b/docs/src/content/docs/changelog/v0.35.0.md
index 03fb139b..29b43f6b 100644
--- a/docs/src/content/docs/changelog/v0.35.0.md
+++ b/docs/src/content/docs/changelog/v0.35.0.md
@@ -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)
diff --git a/lib/constructor.js b/lib/constructor.js
index d4a1263a..915e02df 100644
--- a/lib/constructor.js
+++ b/lib/constructor.js
@@ -306,6 +306,7 @@ const Sharp = function (input, options) {
fileOut: '',
formatOut: 'input',
streamOut: false,
+ typedArrayOut: false,
keepMetadata: 0,
withMetadataOrientation: -1,
withMetadataDensity: 0,
diff --git a/lib/index.d.ts b/lib/index.d.ts
index 4952b532..3d6f840c 100644
--- a/lib/index.d.ts
+++ b/lib/index.d.ts
@@ -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.
diff --git a/lib/output.js b/lib/output.js
index 8700847e..d1383f19 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -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,
diff --git a/src/pipeline.cc b/src/pipeline.cc
index f3cf2743..66ceb6b5 100644
--- a/src/pipeline.cc
+++ b/src/pipeline.cc
@@ -1345,12 +1345,21 @@ class PipelineWorker : public Napi::AsyncWorker {
}
if (baton->bufferOutLength > 0) {
- // Add buffer size to info
info.Set("size", static_cast(baton->bufferOutLength));
- // Pass ownership of output data to Buffer instance
- Napi::Buffer data = Napi::Buffer::NewOrCopy(env, static_cast(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(baton->bufferOut), nullptr);
+ Napi::TypedArrayOf data = Napi::TypedArrayOf::New(env,
+ baton->bufferOutLength, ab, 0, napi_uint8_array);
+ Callback().Call(Receiver().Value(), { env.Null(), data, info });
+ } else {
+ // Node.js Buffer
+ Napi::Buffer data = Napi::Buffer::NewOrCopy(env, static_cast(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");
diff --git a/src/pipeline.h b/src/pipeline.h
index fdbdd84e..c9ff7102 100644
--- a/src/pipeline.h
+++ b/src/pipeline.h
@@ -48,6 +48,7 @@ struct PipelineBaton {
size_t bufferOutLength;
int pageHeightOut;
int pagesOut;
+ bool typedArrayOut;
std::vector composite;
std::vector joinChannelIn;
int topOffsetPre;
@@ -243,6 +244,7 @@ struct PipelineBaton {
bufferOutLength(0),
pageHeightOut(0),
pagesOut(0),
+ typedArrayOut(false),
topOffsetPre(-1),
topOffsetPost(-1),
channels(0),
diff --git a/test/bench/perf.js b/test/bench/perf.js
index 375006ef..bb1ebc5b 100644
--- a/test/bench/perf.js
+++ b/test/bench/perf.js
@@ -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) => {
diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts
index 9364c08c..f854e3d5 100644
--- a/test/types/sharp.test-d.ts
+++ b/test/types/sharp.test-d.ts
@@ -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);
diff --git a/test/unit/io.js b/test/unit/io.js
index ad13f48b..874e9bf2 100644
--- a/test/unit/io.js
+++ b/test/unit/io.js
@@ -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);
+ });
});