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