Skip to content

Commit dbcb7e6

Browse files
committed
Add toUint8Array for output backed by transferable ArrayBuffer #4355
1 parent e1bad54 commit dbcb7e6

File tree

10 files changed

+161
-5
lines changed

10 files changed

+161
-5
lines changed

docs/src/content/docs/api-output.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,38 @@ await sharp(pixelArray, { raw: { width, height, channels } })
117117
```
118118

119119

120+
## toUint8Array
121+
> toUint8Array() ⇒ <code>Promise.&lt;{data: Uint8Array, info: Object}&gt;</code>
122+
123+
Write output to a `Uint8Array` backed by a transferable `ArrayBuffer`.
124+
JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.
125+
126+
Use [toFormat](#toformat) or one of the format-specific functions such as [jpeg](#jpeg), [png](#png) etc. to set the output format.
127+
128+
If no explicit format is set, the output format will match the input image, except SVG input which becomes PNG output.
129+
130+
By default all metadata will be removed, which includes EXIF-based orientation.
131+
See [keepExif](#keepexif) and similar methods for control over this.
132+
133+
Resolves with an `Object` containing:
134+
- `data` is the output image as a `Uint8Array` backed by a transferable `ArrayBuffer`.
135+
- `info` contains properties relating to the output image such as `width` and `height`.
136+
137+
138+
**Since**: v0.35.0
139+
**Example**
140+
```js
141+
const { data, info } = await sharp(input).toUint8Array();
142+
```
143+
**Example**
144+
```js
145+
const { data } = await sharp(input)
146+
.avif()
147+
.toUint8Array();
148+
const base64String = data.toBase64();
149+
```
150+
151+
120152
## keepExif
121153
> keepExif() ⇒ <code>Sharp</code>
122154

docs/src/content/docs/changelog/v0.35.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ slug: changelog/v0.35.0
1212

1313
* Add `withGainMap` to process HDR JPEG images with embedded gain maps.
1414
[#4314](https://github.com/lovell/sharp/issues/4314)
15+
16+
* Add `toUint8Array` for output image as a `TypedArray` backed by a transferable `ArrayBuffer`.
17+
[#4355](https://github.com/lovell/sharp/issues/4355)

lib/constructor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ const Sharp = function (input, options) {
306306
fileOut: '',
307307
formatOut: 'input',
308308
streamOut: false,
309+
typedArrayOut: false,
309310
keepMetadata: 0,
310311
withMetadataOrientation: -1,
311312
withMetadataDensity: 0,

lib/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,13 @@ declare namespace sharp {
693693
*/
694694
toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer; info: OutputInfo }>;
695695

696+
/**
697+
* Write output to a Uint8Array backed by a transferable ArrayBuffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
698+
* By default, the format will match the input image, except SVG input which becomes PNG output.
699+
* @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
700+
*/
701+
toUint8Array(): Promise<{ data: Uint8Array; info: OutputInfo }>;
702+
696703
/**
697704
* Keep all EXIF metadata from the input image in the output image.
698705
* EXIF metadata is unsupported for TIFF output.

lib/output.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,41 @@ function toBuffer (options, callback) {
164164
return this._pipeline(is.fn(options) ? options : callback, stack);
165165
}
166166

167+
/**
168+
* Write output to a `Uint8Array` backed by a transferable `ArrayBuffer`.
169+
* JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.
170+
*
171+
* 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.
172+
*
173+
* If no explicit format is set, the output format will match the input image, except SVG input which becomes PNG output.
174+
*
175+
* By default all metadata will be removed, which includes EXIF-based orientation.
176+
* See {@link #keepexif keepExif} and similar methods for control over this.
177+
*
178+
* Resolves with an `Object` containing:
179+
* - `data` is the output image as a `Uint8Array` backed by a transferable `ArrayBuffer`.
180+
* - `info` contains properties relating to the output image such as `width` and `height`.
181+
*
182+
* @since v0.35.0
183+
*
184+
* @example
185+
* const { data, info } = await sharp(input).toUint8Array();
186+
*
187+
* @example
188+
* const { data } = await sharp(input)
189+
* .avif()
190+
* .toUint8Array();
191+
* const base64String = data.toBase64();
192+
*
193+
* @returns {Promise<{ data: Uint8Array, info: Object }>}
194+
*/
195+
function toUint8Array () {
196+
this.options.resolveWithObject = true;
197+
this.options.typedArrayOut = true;
198+
const stack = Error();
199+
return this._pipeline(null, stack);
200+
}
201+
167202
/**
168203
* Keep all EXIF metadata from the input image in the output image.
169204
*
@@ -1659,6 +1694,7 @@ module.exports = (Sharp) => {
16591694
// Public
16601695
toFile,
16611696
toBuffer,
1697+
toUint8Array,
16621698
keepExif,
16631699
withExif,
16641700
withExifMerge,

src/pipeline.cc

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,12 +1345,21 @@ class PipelineWorker : public Napi::AsyncWorker {
13451345
}
13461346

13471347
if (baton->bufferOutLength > 0) {
1348-
// Add buffer size to info
13491348
info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));
1350-
// Pass ownership of output data to Buffer instance
1351-
Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
1352-
baton->bufferOutLength, sharp::FreeCallback);
1353-
Callback().Call(Receiver().Value(), { env.Null(), data, info });
1349+
if (baton->typedArrayOut) {
1350+
// ECMAScript ArrayBuffer with Uint8Array view
1351+
Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, baton->bufferOutLength);
1352+
memcpy(ab.Data(), baton->bufferOut, baton->bufferOutLength);
1353+
sharp::FreeCallback(static_cast<char*>(baton->bufferOut), nullptr);
1354+
Napi::TypedArrayOf<uint8_t> data = Napi::TypedArrayOf<uint8_t>::New(env,
1355+
baton->bufferOutLength, ab, 0, napi_uint8_array);
1356+
Callback().Call(Receiver().Value(), { env.Null(), data, info });
1357+
} else {
1358+
// Node.js Buffer
1359+
Napi::Buffer<char> data = Napi::Buffer<char>::NewOrCopy(env, static_cast<char*>(baton->bufferOut),
1360+
baton->bufferOutLength, sharp::FreeCallback);
1361+
Callback().Call(Receiver().Value(), { env.Null(), data, info });
1362+
}
13541363
} else {
13551364
// Add file size to info
13561365
if (baton->formatOut != "dz" || sharp::IsDzZip(baton->fileOut)) {
@@ -1700,6 +1709,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
17001709
// Output
17011710
baton->formatOut = sharp::AttrAsStr(options, "formatOut");
17021711
baton->fileOut = sharp::AttrAsStr(options, "fileOut");
1712+
baton->typedArrayOut = sharp::AttrAsBool(options, "typedArrayOut");
17031713
baton->keepMetadata = sharp::AttrAsUint32(options, "keepMetadata");
17041714
baton->withMetadataOrientation = sharp::AttrAsUint32(options, "withMetadataOrientation");
17051715
baton->withMetadataDensity = sharp::AttrAsDouble(options, "withMetadataDensity");

src/pipeline.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ struct PipelineBaton {
4848
size_t bufferOutLength;
4949
int pageHeightOut;
5050
int pagesOut;
51+
bool typedArrayOut;
5152
std::vector<Composite *> composite;
5253
std::vector<sharp::InputDescriptor *> joinChannelIn;
5354
int topOffsetPre;
@@ -243,6 +244,7 @@ struct PipelineBaton {
243244
bufferOutLength(0),
244245
pageHeightOut(0),
245246
pagesOut(0),
247+
typedArrayOut(false),
246248
topOffsetPre(-1),
247249
topOffsetPost(-1),
248250
channels(0),

test/bench/perf.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,19 @@ async.series({
228228
}
229229
});
230230
}
231+
}).add('sharp-buffer-uint8array', {
232+
defer: true,
233+
fn: (deferred) => {
234+
sharp(inputJpgBuffer)
235+
.resize(width, height)
236+
.toUint8Array()
237+
.then(() => {
238+
deferred.resolve();
239+
})
240+
.catch((err) => {
241+
throw err;
242+
});
243+
}
231244
}).add('sharp-file-file', {
232245
defer: true,
233246
fn: (deferred) => {
@@ -266,6 +279,19 @@ async.series({
266279
}
267280
});
268281
}
282+
}).add('sharp-file-uint8array', {
283+
defer: true,
284+
fn: (deferred) => {
285+
sharp(fixtures.inputJpg)
286+
.resize(width, height)
287+
.toUint8Array()
288+
.then(() => {
289+
deferred.resolve();
290+
})
291+
.catch((err) => {
292+
throw err;
293+
});
294+
}
269295
}).add('sharp-promise', {
270296
defer: true,
271297
fn: (deferred) => {

test/types/sharp.test-d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ let transformer = sharp()
8686
});
8787
readableStream.pipe(transformer).pipe(writableStream);
8888

89+
sharp().toUint8Array();
90+
sharp().toUint8Array().then(({ data }) => data.byteLength);
91+
8992
console.log(sharp.format);
9093
console.log(sharp.versions);
9194

test/unit/io.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const fs = require('node:fs');
77
const path = require('node:path');
88
const { afterEach, beforeEach, describe, it } = require('node:test');
99
const assert = require('node:assert');
10+
const { isMarkedAsUntransferable } = require('node:worker_threads');
1011

1112
const sharp = require('../../');
1213
const fixtures = require('../fixtures');
@@ -1092,4 +1093,39 @@ describe('Input/output', () => {
10921093
assert.strictEqual(channels, 3);
10931094
assert.strictEqual(format, 'jpeg');
10941095
});
1096+
1097+
it('toBuffer resolves with an untransferable Buffer', async () => {
1098+
const data = await sharp(fixtures.inputJpg)
1099+
.resize({ width: 8, height: 8 })
1100+
.toBuffer();
1101+
1102+
if (isMarkedAsUntransferable) {
1103+
assert.strictEqual(isMarkedAsUntransferable(data.buffer), true);
1104+
}
1105+
assert.strictEqual(ArrayBuffer.isView(data), true);
1106+
assert.strictEqual(ArrayBuffer.isView(data.buffer), false);
1107+
});
1108+
1109+
it('toUint8Array resolves with a transferable Uint8Array', async () => {
1110+
const { data, info } = await sharp(fixtures.inputJpg)
1111+
.resize({ width: 8, height: 8 })
1112+
.toUint8Array();
1113+
1114+
assert.strictEqual(data instanceof Uint8Array, true);
1115+
if (isMarkedAsUntransferable) {
1116+
assert.strictEqual(isMarkedAsUntransferable(data.buffer), false);
1117+
}
1118+
assert.strictEqual(ArrayBuffer.isView(data), true);
1119+
assert.strictEqual(info.format, 'jpeg');
1120+
assert.strictEqual(info.width, 8);
1121+
assert.strictEqual(info.height, 8);
1122+
assert.strictEqual(data.byteLength, info.size);
1123+
assert.strictEqual(data[0], 0xFF);
1124+
assert.strictEqual(data[1], 0xD8);
1125+
1126+
const metadata = await sharp(data).metadata();
1127+
assert.strictEqual(metadata.format, 'jpeg');
1128+
assert.strictEqual(metadata.width, 8);
1129+
assert.strictEqual(metadata.height, 8);
1130+
});
10951131
});

0 commit comments

Comments
 (0)