Add toArrayBuffer to generate transferable output #4355

This commit is contained in:
Lovell Fuller
2025-12-22 15:05:33 +00:00
parent e1bad5470e
commit 6176f53f83
11 changed files with 142 additions and 7 deletions

View File

@@ -117,6 +117,42 @@ await sharp(pixelArray, { raw: { width, height, channels } })
```
## toArrayBuffer
> toArrayBuffer() ⇒ <code>Promise.&lt;{data: ArrayBuffer, info: Object}&gt;</code>
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() ⇒ <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 `toArrayBuffer` to generate output image as 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,
sharedOut: true,
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 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.

View File

@@ -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,

View File

@@ -786,7 +786,7 @@ namespace sharp {
/*
Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows
*/
std::function<void(void*, char*)> FreeCallback = [](void*, char* data) {
std::function<void(void*, void*)> FreeCallback = [](void*, void* data) {
g_free(data);
};

View File

@@ -318,7 +318,7 @@ namespace sharp {
/*
Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows
*/
extern std::function<void(void*, char*)> FreeCallback;
extern std::function<void(void*, void*)> FreeCallback;
/*
Called with warnings from the glib-registered "VIPS" domain

View File

@@ -1345,12 +1345,20 @@ 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->sharedOut) {
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 {
try {
Napi::ArrayBuffer data = Napi::ArrayBuffer::New(env, static_cast<char*>(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");

View File

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

View File

@@ -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);

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,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);
});
});