diff --git a/docs/api-constructor.md b/docs/api-constructor.md index 2e2c3b54..c8bf3d43 100644 --- a/docs/api-constructor.md +++ b/docs/api-constructor.md @@ -13,32 +13,32 @@ Implements the [stream.Duplex][1] class. ### Parameters -- `input` **([Buffer][2] \| [string][3])?** if present, can be - a Buffer containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or +- `input` **([Buffer][2] \| [Uint8Array][3] \| [Uint8ClampedArray][4] \| [string][5])?** if present, can be + a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file. JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present. -- `options` **[Object][4]?** if present, is an Object with optional attributes. - - `options.failOnError` **[boolean][5]** by default halt processing and raise an error when loading invalid images. +- `options` **[Object][6]?** if present, is an Object with optional attributes. + - `options.failOnError` **[boolean][7]** by default halt processing and raise an error when loading invalid images. Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid. (optional, default `true`) - - `options.limitInputPixels` **([number][6] \| [boolean][5])** Do not process input images where the number of pixels + - `options.limitInputPixels` **([number][8] \| [boolean][7])** Do not process input images where the number of pixels (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted. An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF). (optional, default `268402689`) - - `options.sequentialRead` **[boolean][5]** Set this to `true` to use sequential rather than random access where possible. + - `options.sequentialRead` **[boolean][7]** Set this to `true` to use sequential rather than random access where possible. This can reduce memory usage and might improve performance on some systems. (optional, default `false`) - - `options.density` **[number][6]** number representing the DPI for vector images in the range 1 to 100000. (optional, default `72`) - - `options.pages` **[number][6]** number of pages to extract for multi-page input (GIF, WebP, AVIF, TIFF, PDF), use -1 for all pages. (optional, default `1`) - - `options.page` **[number][6]** page number to start extracting from for multi-page input (GIF, WebP, AVIF, TIFF, PDF), zero based. (optional, default `0`) - - `options.level` **[number][6]** level to extract from a multi-level input (OpenSlide), zero based. (optional, default `0`) - - `options.animated` **[boolean][5]** Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`). (optional, default `false`) - - `options.raw` **[Object][4]?** describes raw pixel input image data. See `raw()` for pixel ordering. - - `options.raw.width` **[number][6]?** integral number of pixels wide. - - `options.raw.height` **[number][6]?** integral number of pixels high. - - `options.raw.channels` **[number][6]?** integral number of channels, between 1 and 4. - - `options.create` **[Object][4]?** describes a new image to be created. - - `options.create.width` **[number][6]?** integral number of pixels wide. - - `options.create.height` **[number][6]?** integral number of pixels high. - - `options.create.channels` **[number][6]?** integral number of channels, either 3 (RGB) or 4 (RGBA). - - `options.create.background` **([string][3] \| [Object][4])?** parsed by the [color][7] module to extract values for red, green, blue and alpha. + - `options.density` **[number][8]** number representing the DPI for vector images in the range 1 to 100000. (optional, default `72`) + - `options.pages` **[number][8]** number of pages to extract for multi-page input (GIF, WebP, AVIF, TIFF, PDF), use -1 for all pages. (optional, default `1`) + - `options.page` **[number][8]** page number to start extracting from for multi-page input (GIF, WebP, AVIF, TIFF, PDF), zero based. (optional, default `0`) + - `options.level` **[number][8]** level to extract from a multi-level input (OpenSlide), zero based. (optional, default `0`) + - `options.animated` **[boolean][7]** Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`). (optional, default `false`) + - `options.raw` **[Object][6]?** describes raw pixel input image data. See `raw()` for pixel ordering. + - `options.raw.width` **[number][8]?** integral number of pixels wide. + - `options.raw.height` **[number][8]?** integral number of pixels high. + - `options.raw.channels` **[number][8]?** integral number of channels, between 1 and 4. + - `options.create` **[Object][6]?** describes a new image to be created. + - `options.create.width` **[number][8]?** integral number of pixels wide. + - `options.create.height` **[number][8]?** integral number of pixels high. + - `options.create.channels` **[number][8]?** integral number of channels, either 3 (RGB) or 4 (RGBA). + - `options.create.background` **([string][5] \| [Object][6])?** parsed by the [color][9] module to extract values for red, green, blue and alpha. ### Examples @@ -84,9 +84,24 @@ sharp({ await sharp('in.gif', { animated: true }).toFile('out.webp'); ``` -- Throws **[Error][8]** Invalid parameters +```javascript +// Read a raw array of pixels and save it to a png +const input = Uint8Array.from([255, 255, 255, 0, 0, 0]); // or Uint8ClampedArray +const image = sharp(input, { + // because the input does not contain its dimensions or how many channels it has + // we need to specify it in the constructor options + raw: { + width: 2, + height: 1, + channels: 3 + } +}); +await image.toFile('my-two-pixels.png'); +``` -Returns **[Sharp][9]** +- Throws **[Error][10]** Invalid parameters + +Returns **[Sharp][11]** ## clone @@ -154,22 +169,26 @@ Promise.all(promises) }); ``` -Returns **[Sharp][9]** +Returns **[Sharp][11]** [1]: http://nodejs.org/api/stream.html#stream_class_stream_duplex [2]: https://nodejs.org/api/buffer.html -[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array -[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray -[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[7]: https://www.npmjs.org/package/color +[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean -[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error +[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number -[9]: #sharp +[9]: https://www.npmjs.org/package/color + +[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error + +[11]: #sharp diff --git a/docs/api-output.md b/docs/api-output.md index c0cd43dd..8d820be7 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -86,6 +86,21 @@ sharp(input) .catch(err => { ... }); ``` +```javascript +const data = await sharp('my-image.jpg') + // output the raw pixels + .raw() + .toBuffer(); + +// create a more type safe way to work with the raw pixel data +// this will not copy the data, instead it will change `data`s underlying ArrayBuffer +// so `data` and `pixelArray` point to the same memory location +const pixelArray = new Uint8ClampedArray(data.buffer); + +// When you are done changing the pixelArray, sharp takes the `pixelArray` as an input +await sharp(pixelArray).toFile('my-changed-image.jpg'); +``` + Returns **[Promise][5]<[Buffer][8]>** when no callback is provided ## withMetadata diff --git a/lib/constructor.js b/lib/constructor.js index 0d58053c..078dcbc3 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -90,8 +90,22 @@ const debuglog = util.debuglog('sharp'); * // Convert an animated GIF to an animated WebP * await sharp('in.gif', { animated: true }).toFile('out.webp'); * - * @param {(Buffer|string)} [input] - if present, can be - * a Buffer containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or + * @example + * // Read a raw array of pixels and save it to a png + * const input = Uint8Array.from([255, 255, 255, 0, 0, 0]); // or Uint8ClampedArray + * const image = sharp(input, { + * // because the input does not contain its dimensions or how many channels it has + * // we need to specify it in the constructor options + * raw: { + * width: 2, + * height: 1, + * channels: 3 + * } + * }); + * await image.toFile('my-two-pixels.png'); + * + * @param {(Buffer|Uint8Array|Uint8ClampedArray|string)} [input] - if present, can be + * a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or * a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file. * JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present. * @param {Object} [options] - if present, is an Object with optional attributes. diff --git a/lib/input.js b/lib/input.js index ecab8ffb..c9c284e8 100644 --- a/lib/input.js +++ b/lib/input.js @@ -31,6 +31,9 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { } else if (is.buffer(input)) { // Buffer inputDescriptor.buffer = input; + } else if (is.uint8Array(input)) { + // Uint8Array or Uint8ClampedArray + inputDescriptor.buffer = Buffer.from(input.buffer); } else if (is.plainObject(input) && !is.defined(inputOptions)) { // Plain Object descriptor, e.g. create inputOptions = input; diff --git a/lib/is.js b/lib/is.js index 762d8452..7662c6e7 100644 --- a/lib/is.js +++ b/lib/is.js @@ -48,6 +48,15 @@ const buffer = function (val) { return val instanceof Buffer; }; +/** + * Is this value a Uint8Array or Uint8ClampedArray object? + * @private + */ +const uint8Array = function (val) { + // allow both since Uint8ClampedArray simply clamps the values between 0-255 + return val instanceof Uint8Array || val instanceof Uint8ClampedArray; +}; + /** * Is this value a non-empty string? * @private @@ -110,6 +119,7 @@ module.exports = { fn: fn, bool: bool, buffer: buffer, + uint8Array: uint8Array, string: string, number: number, integer: integer, diff --git a/lib/output.js b/lib/output.js index 2331a249..858c21a6 100644 --- a/lib/output.js +++ b/lib/output.js @@ -104,6 +104,20 @@ function toFile (fileOut, callback) { * .then(({ data, info }) => { ... }) * .catch(err => { ... }); * + * @example + * const data = await sharp('my-image.jpg') + * // output the raw pixels + * .raw() + * .toBuffer(); + * + * // create a more type safe way to work with the raw pixel data + * // this will not copy the data, instead it will change `data`s underlying ArrayBuffer + * // so `data` and `pixelArray` point to the same memory location + * const pixelArray = new Uint8ClampedArray(data.buffer); + * + * // When you are done changing the pixelArray, sharp takes the `pixelArray` as an input + * await sharp(pixelArray).toFile('my-changed-image.jpg'); + * * @param {Object} [options] * @param {boolean} [options.resolveWithObject] Resolve the Promise with an Object containing `data` and `info` properties instead of resolving only with `data`. * @param {Function} [callback] diff --git a/package.json b/package.json index b9b804e8..f14ebdb8 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,8 @@ "Robert O'Rourke ", "Guillermo Alfonso Varela ChouciƱo ", "Christian Flintrup ", - "Manan Jadhav " + "Manan Jadhav ", + "Leon Radley " ], "scripts": { "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", diff --git a/test/unit/io.js b/test/unit/io.js index eb723e40..9b1b9255 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -147,6 +147,38 @@ describe('Input/output', function () { readable.pipe(pipeline).pipe(writable); }); + it('Read from Uint8Array and write to Buffer', async () => { + const uint8array = Uint8Array.from([255, 255, 255, 0, 0, 0]); + const { data, info } = await sharp(uint8array, { + raw: { + width: 2, + height: 1, + channels: 3 + } + }).toBuffer({ resolveWithObject: true }); + + assert.deepStrictEqual(uint8array, new Uint8Array(data)); + assert.strictEqual(info.width, 2); + assert.strictEqual(info.height, 1); + }); + + it('Read from Uint8ClampedArray and output to Buffer', async () => { + // since a Uint8ClampedArray is the same as Uint8Array but clamps the values + // between 0-255 it seemed good to add this also + const uint8array = Uint8ClampedArray.from([255, 255, 255, 0, 0, 0]); + const { data, info } = await sharp(uint8array, { + raw: { + width: 2, + height: 1, + channels: 3 + } + }).toBuffer({ resolveWithObject: true }); + + assert.deepStrictEqual(uint8array, new Uint8ClampedArray(data)); + assert.strictEqual(info.width, 2); + assert.strictEqual(info.height, 1); + }); + it('Stream should emit info event', function (done) { const readable = fs.createReadStream(fixtures.inputJpg); const writable = fs.createWriteStream(fixtures.outputJpg);