diff --git a/docs/src/content/docs/api-output.md b/docs/src/content/docs/api-output.md
index f070c980..b1887be4 100644
--- a/docs/src/content/docs/api-output.md
+++ b/docs/src/content/docs/api-output.md
@@ -117,6 +117,42 @@ await sharp(pixelArray, { raw: { width, height, channels } })
```
+## toArrayBuffer
+> toArrayBuffer() ⇒ Promise.<{data: ArrayBuffer, info: Object}>
+
+Write output to 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 transferable `ArrayBuffer`.
+- `info` contains properties relating to the output image such as `width` and `height`.
+
+This method does not work with the Electron V8 memory cage
+and will reject with an error if used in that environment.
+Use [toBuffer](#tobuffer) instead.
+
+
+**Since**: v0.35.0
+**Example**
+```js
+const { data, info } = await sharp(input).toArrayBuffer();
+```
+**Example**
+```js
+const { data } = await sharp(input)
+ .avif()
+ .toArrayBuffer();
+const uint8Array = new Uint8Array(data);
+```
+
+
## 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..25985cfc 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 `toArrayBuffer` to generate output image as a transferable `ArrayBuffer`.
+ [#4355](https://github.com/lovell/sharp/issues/4355)
diff --git a/lib/constructor.js b/lib/constructor.js
index d4a1263a..102fab1b 100644
--- a/lib/constructor.js
+++ b/lib/constructor.js
@@ -306,6 +306,7 @@ const Sharp = function (input, options) {
fileOut: '',
formatOut: 'input',
streamOut: false,
+ sharedOut: true,
keepMetadata: 0,
withMetadataOrientation: -1,
withMetadataDensity: 0,
diff --git a/lib/index.d.ts b/lib/index.d.ts
index 4952b532..7fb5df9f 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 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 ArrayBuffer data and an info object containing the output image format, size (bytes), width, height and channels
+ */
+ toArrayBuffer(): Promise<{ data: ArrayBuffer; 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..afbdbde5 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -164,6 +164,45 @@ function toBuffer (options, callback) {
return this._pipeline(is.fn(options) ? options : callback, stack);
}
+/**
+ * Write output to 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 transferable `ArrayBuffer`.
+ * - `info` contains properties relating to the output image such as `width` and `height`.
+ *
+ * This method does not work with the Electron V8 memory cage
+ * and will reject with an error if used in that environment.
+ * Use {@link #tobuffer toBuffer} instead.
+ *
+ * @since v0.35.0
+ *
+ * @example
+ * const { data, info } = await sharp(input).toArrayBuffer();
+ *
+ * @example
+ * const { data } = await sharp(input)
+ * .avif()
+ * .toArrayBuffer();
+ * const uint8Array = new Uint8Array(data);
+ *
+ * @returns {Promise<{ data: ArrayBuffer, info: Object }>}
+ */
+function toArrayBuffer () {
+ this.options.resolveWithObject = true;
+ this.options.sharedOut = true;
+ const stack = Error();
+ return this._pipeline(null, stack);
+}
+
/**
* Keep all EXIF metadata from the input image in the output image.
*
@@ -1659,6 +1698,7 @@ module.exports = (Sharp) => {
// Public
toFile,
toBuffer,
+ toArrayBuffer,
keepExif,
withExif,
withExifMerge,
diff --git a/src/common.cc b/src/common.cc
index 22d74265..fa334527 100644
--- a/src/common.cc
+++ b/src/common.cc
@@ -786,7 +786,7 @@ namespace sharp {
/*
Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows
*/
- std::function FreeCallback = [](void*, char* data) {
+ std::function FreeCallback = [](void*, void* data) {
g_free(data);
};
diff --git a/src/common.h b/src/common.h
index 01fee215..f9165de5 100644
--- a/src/common.h
+++ b/src/common.h
@@ -318,7 +318,7 @@ namespace sharp {
/*
Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows
*/
- extern std::function FreeCallback;
+ extern std::function FreeCallback;
/*
Called with warnings from the glib-registered "VIPS" domain
diff --git a/src/pipeline.cc b/src/pipeline.cc
index f3cf2743..f6681c6f 100644
--- a/src/pipeline.cc
+++ b/src/pipeline.cc
@@ -1345,12 +1345,20 @@ 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->sharedOut) {
+ Napi::Buffer data = Napi::Buffer::NewOrCopy(env, static_cast(baton->bufferOut),
+ baton->bufferOutLength, sharp::FreeCallback);
+ Callback().Call(Receiver().Value(), { env.Null(), data, info });
+ } else {
+ try {
+ Napi::ArrayBuffer data = Napi::ArrayBuffer::New(env, static_cast(baton->bufferOut),
+ baton->bufferOutLength, sharp::FreeCallback);
+ Callback().Call(Receiver().Value(), { env.Null(), data, info });
+ } catch (const Napi::Error& e) {
+ Callback().Call(Receiver().Value(), { e.Value() });
+ }
+ }
} else {
// Add file size to info
if (baton->formatOut != "dz" || sharp::IsDzZip(baton->fileOut)) {
@@ -1700,6 +1708,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
// Output
baton->formatOut = sharp::AttrAsStr(options, "formatOut");
baton->fileOut = sharp::AttrAsStr(options, "fileOut");
+ baton->sharedOut = sharp::AttrAsBool(options, "sharedOut");
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..205b1e5b 100644
--- a/src/pipeline.h
+++ b/src/pipeline.h
@@ -48,6 +48,7 @@ struct PipelineBaton {
size_t bufferOutLength;
int pageHeightOut;
int pagesOut;
+ bool sharedOut;
std::vector composite;
std::vector joinChannelIn;
int topOffsetPre;
@@ -243,6 +244,7 @@ struct PipelineBaton {
bufferOutLength(0),
pageHeightOut(0),
pagesOut(0),
+ sharedOut(true),
topOffsetPre(-1),
topOffsetPost(-1),
channels(0),
diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts
index 9364c08c..4706610d 100644
--- a/test/types/sharp.test-d.ts
+++ b/test/types/sharp.test-d.ts
@@ -86,6 +86,10 @@ let transformer = sharp()
});
readableStream.pipe(transformer).pipe(writableStream);
+sharp().toArrayBuffer();
+sharp().toArrayBuffer().then(({ data }) => data);
+sharp().toArrayBuffer().then(({ data }) => new Uint8Array(data));
+
console.log(sharp.format);
console.log(sharp.versions);
diff --git a/test/unit/io.js b/test/unit/io.js
index ad13f48b..2bd8f480 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,36 @@ 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('toArrayBuffer resolves with a transferable ArrayBuffer', async () => {
+ const { data, info } = await sharp(fixtures.inputJpg)
+ .resize({ width: 8, height: 8 })
+ .toArrayBuffer();
+
+ if (isMarkedAsUntransferable) {
+ assert.strictEqual(isMarkedAsUntransferable(data), false);
+ }
+ assert.strictEqual(ArrayBuffer.isView(data), true);
+ assert.strictEqual(info.format, 'jpeg');
+ assert.strictEqual(info.width, 8);
+ assert.strictEqual(info.height, 8);
+
+ const uint8Array = new Uint8Array(data);
+ assert.strictEqual(ArrayBuffer.isView(uint8Array), true);
+ assert.strictEqual(uint8Array.length, info.size);
+ assert.strictEqual(uint8Array[0], 0xFF);
+ assert.strictEqual(uint8Array[1], 0xD8);
+ });
});