From 1aa053ce6f8aadba51010c56e0cfc563c952fd1b Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Sat, 11 Mar 2017 11:46:01 +0000 Subject: [PATCH] Create blank image (width, height, channels, background) #470 --- docs/api-composite.md | 5 ++ docs/api-constructor.md | 24 +++++++++- docs/changelog.md | 4 ++ lib/composite.js | 5 ++ lib/constructor.js | 23 ++++++++- lib/input.js | 25 ++++++++++ src/common.cc | 64 +++++++++++++++++-------- src/common.h | 14 +++++- src/pipeline.cc | 2 +- test/fixtures/expected/create-rgb.jpg | Bin 0 -> 271 bytes test/fixtures/expected/create-rgba.png | Bin 0 -> 105 bytes test/unit/io.js | 60 +++++++++++++++++++++++ 12 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 test/fixtures/expected/create-rgb.jpg create mode 100644 test/fixtures/expected/create-rgba.png diff --git a/docs/api-composite.md b/docs/api-composite.md index f1a47bac..f5caccc8 100644 --- a/docs/api-composite.md +++ b/docs/api-composite.md @@ -25,6 +25,11 @@ If both `top` and `left` options are provided, they take precedence over `gravit - `options.raw.width` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** - `options.raw.height` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** - `options.raw.channels` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** + - `options.create` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** describes a blank overlay to be created. + - `options.create.width` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** + - `options.create.height` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** + - `options.create.channels` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** 3-4 + - `options.create.background` **([String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object))?** parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. **Examples** diff --git a/docs/api-constructor.md b/docs/api-constructor.md index 09fac196..2d6e8e92 100644 --- a/docs/api-constructor.md +++ b/docs/api-constructor.md @@ -17,10 +17,15 @@ JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when null or undefined. - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** if present, is an Object with optional attributes. - `options.density` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** integral number representing the DPI for vector images. (optional, default `72`) - - `options.raw` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** describes raw pixel image data. See `raw()` for pixel ordering. + - `options.raw` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** describes raw pixel input image data. See `raw()` for pixel ordering. - `options.raw.width` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** - `options.raw.height` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** - - `options.raw.channels` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** + - `options.raw.channels` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** 1-4 + - `options.create` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** describes a new image to be created. + - `options.create.width` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** + - `options.create.height` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** + - `options.create.channels` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** 3-4 + - `options.create.background` **([String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object))?** parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. **Examples** @@ -46,6 +51,21 @@ var transformer = sharp() readableStream.pipe(transformer).pipe(writableStream); ``` +```javascript +// Create a blank 300x200 PNG image of semi-transluent red pixels +sharp(null, { + create: { + width: 300, + height: 200, + channels: 4, + background: { r: 255, g: 0, b: 0, alpha: 128 } + } +}) +.png() +.toBuffer() +.then( ... ); +``` + - Throws **[Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)** Invalid parameters Returns **[Sharp](#sharp)** diff --git a/docs/changelog.md b/docs/changelog.md index 3a8f2733..169e19bb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,10 @@ Requires libvips v8.4.2. [#143](https://github.com/lovell/sharp/issues/143) [@salzhrani](https://github.com/salzhrani) +* Create blank image of given width, height, channels and background. + [#470](https://github.com/lovell/sharp/issues/470) + [@pjarts](https://github.com/pjarts) + #### v0.17.2 - 11th February 2017 * Ensure Readable side of Stream can start flowing after Writable side has finished. diff --git a/lib/composite.js b/lib/composite.js index 6e2205c8..39d503ee 100644 --- a/lib/composite.js +++ b/lib/composite.js @@ -38,6 +38,11 @@ const is = require('./is'); * @param {Number} [options.raw.width] * @param {Number} [options.raw.height] * @param {Number} [options.raw.channels] + * @param {Object} [options.create] - describes a blank overlay to be created. + * @param {Number} [options.create.width] + * @param {Number} [options.create.height] + * @param {Number} [options.create.channels] - 3-4 + * @param {String|Object} [options.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. * @returns {Sharp} * @throws {Error} Invalid parameters */ diff --git a/lib/constructor.js b/lib/constructor.js index dc490751..50d3e105 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -54,16 +54,35 @@ let versions = { * }); * readableStream.pipe(transformer).pipe(writableStream); * + * @example + * // Create a blank 300x200 PNG image of semi-transluent red pixels + * sharp(null, { + * create: { + * width: 300, + * height: 200, + * channels: 4, + * background: { r: 255, g: 0, b: 0, alpha: 128 } + * } + * }) + * .png() + * .toBuffer() + * .then( ... ); + * * @param {(Buffer|String)} [input] - if present, can be * a Buffer containing JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data, or * a String containing the path to an JPEG, PNG, WebP, GIF, SVG or TIFF image file. * JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when null or undefined. * @param {Object} [options] - if present, is an Object with optional attributes. * @param {Number} [options.density=72] - integral number representing the DPI for vector images. - * @param {Object} [options.raw] - describes raw pixel image data. See `raw()` for pixel ordering. + * @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering. * @param {Number} [options.raw.width] * @param {Number} [options.raw.height] - * @param {Number} [options.raw.channels] + * @param {Number} [options.raw.channels] - 1-4 + * @param {Object} [options.create] - describes a new image to be created. + * @param {Number} [options.create.width] + * @param {Number} [options.create.height] + * @param {Number} [options.create.channels] - 3-4 + * @param {String|Object} [options.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. * @returns {Sharp} * @throws {Error} Invalid parameters */ diff --git a/lib/input.js b/lib/input.js index 4bd2de0b..36e94fb2 100644 --- a/lib/input.js +++ b/lib/input.js @@ -1,6 +1,7 @@ 'use strict'; const util = require('util'); +const color = require('color'); const is = require('./is'); const sharp = require('../build/Release/sharp.node'); @@ -46,6 +47,30 @@ const _createInputDescriptor = function _createInputDescriptor (input, inputOpti throw new Error('Expected width, height and channels for raw pixel input'); } } + // Create new image + if (is.defined(inputOptions.create)) { + if ( + is.object(inputOptions.create) && + is.integer(inputOptions.create.width) && is.inRange(inputOptions.create.width, 1, this.constructor.maximum.width) && + is.integer(inputOptions.create.height) && is.inRange(inputOptions.create.height, 1, this.constructor.maximum.height) && + is.integer(inputOptions.create.channels) && is.inRange(inputOptions.create.channels, 3, 4) && + is.defined(inputOptions.create.background) + ) { + inputDescriptor.createWidth = inputOptions.create.width; + inputDescriptor.createHeight = inputOptions.create.height; + inputDescriptor.createChannels = inputOptions.create.channels; + const background = color(inputOptions.create.background); + inputDescriptor.createBackground = [ + background.red(), + background.green(), + background.blue(), + Math.round(background.alpha() * 255) + ]; + delete inputDescriptor.buffer; + } else { + throw new Error('Expected width, height, channels and background to create a new input image'); + } + } } else if (is.defined(inputOptions)) { throw new Error('Invalid input options ' + inputOptions); } diff --git a/src/common.cc b/src/common.cc index 62f9e532..57e1d6b7 100644 --- a/src/common.cc +++ b/src/common.cc @@ -44,7 +44,7 @@ namespace sharp { InputDescriptor *descriptor = new InputDescriptor; if (HasAttr(input, "file")) { descriptor->file = AttrAsStr(input, "file"); - } else { + } else if (HasAttr(input, "buffer")) { v8::Local buffer = AttrAs(input, "buffer"); descriptor->bufferLength = node::Buffer::Length(buffer); descriptor->buffer = node::Buffer::Data(buffer); @@ -60,6 +60,16 @@ namespace sharp { descriptor->rawWidth = AttrTo(input, "rawWidth"); descriptor->rawHeight = AttrTo(input, "rawHeight"); } + // Create new image + if (HasAttr(input, "createChannels")) { + descriptor->createChannels = AttrTo(input, "createChannels"); + descriptor->createWidth = AttrTo(input, "createWidth"); + descriptor->createHeight = AttrTo(input, "createHeight"); + v8::Local createBackground = AttrAs(input, "createBackground"); + for (unsigned int i = 0; i < 4; i++) { + descriptor->createBackground[i] = AttrTo(createBackground, i); + } + } return descriptor; } @@ -192,7 +202,6 @@ namespace sharp { VImage image; ImageType imageType; if (descriptor->buffer != nullptr) { - // From buffer if (descriptor->rawChannels > 0) { // Raw, uncompressed pixel data image = VImage::new_from_memory(descriptor->buffer, descriptor->bufferLength, @@ -227,26 +236,41 @@ namespace sharp { } } } else { - // From filesystem - imageType = DetermineImageType(descriptor->file.data()); - if (imageType != ImageType::UNKNOWN) { - try { - vips::VOption *option = VImage::option()->set("access", accessMethod); - if (imageType == ImageType::SVG || imageType == ImageType::PDF) { - option->set("dpi", static_cast(descriptor->density)); - } - if (imageType == ImageType::MAGICK) { - option->set("density", std::to_string(descriptor->density).data()); - } - image = VImage::new_from_file(descriptor->file.data(), option); - if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { - SetDensity(image, descriptor->density); - } - } catch (...) { - throw vips::VError("Input file has corrupt header"); + if (descriptor->createChannels > 0) { + // Create new image + std::vector background = { + descriptor->createBackground[0], + descriptor->createBackground[1], + descriptor->createBackground[2] + }; + if (descriptor->createChannels == 4) { + background.push_back(descriptor->createBackground[3]); } + image = VImage::new_matrix(descriptor->createWidth, descriptor->createHeight).new_from_image(background); + image.get_image()->Type = VIPS_INTERPRETATION_sRGB; + imageType = ImageType::RAW; } else { - throw vips::VError("Input file is missing or of an unsupported image format"); + // From filesystem + imageType = DetermineImageType(descriptor->file.data()); + if (imageType != ImageType::UNKNOWN) { + try { + vips::VOption *option = VImage::option()->set("access", accessMethod); + if (imageType == ImageType::SVG || imageType == ImageType::PDF) { + option->set("dpi", static_cast(descriptor->density)); + } + if (imageType == ImageType::MAGICK) { + option->set("density", std::to_string(descriptor->density).data()); + } + image = VImage::new_from_file(descriptor->file.data(), option); + if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { + SetDensity(image, descriptor->density); + } + } catch (...) { + throw vips::VError("Input file has corrupt header"); + } + } else { + throw vips::VError("Input file is missing or of an unsupported image format"); + } } } return std::make_tuple(image, imageType); diff --git a/src/common.h b/src/common.h index 60e1f857..20790c5f 100644 --- a/src/common.h +++ b/src/common.h @@ -52,6 +52,10 @@ namespace sharp { int rawChannels; int rawWidth; int rawHeight; + int createChannels; + int createWidth; + int createHeight; + double createBackground[4]; InputDescriptor(): buffer(nullptr), @@ -59,7 +63,15 @@ namespace sharp { density(72), rawChannels(0), rawWidth(0), - rawHeight(0) {} + rawHeight(0), + createChannels(0), + createWidth(0), + createHeight(0) { + createBackground[0] = 0.0; + createBackground[1] = 0.0; + createBackground[2] = 0.0; + createBackground[3] = 255.0; + } }; // Convenience methods to access the attributes of a v8::Object diff --git a/src/pipeline.cc b/src/pipeline.cc index 37148c1a..ec202b9f 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -1100,7 +1100,7 @@ NAN_METHOD(pipeline) { // Background colour v8::Local background = AttrAs(options, "background"); for (unsigned int i = 0; i < 4; i++) { - baton->background[i] = AttrTo(background, i); + baton->background[i] = AttrTo(background, i); } // Overlay options if (HasAttr(options, "overlay")) { diff --git a/test/fixtures/expected/create-rgb.jpg b/test/fixtures/expected/create-rgb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..15c5741332d0e81f91d68db74484aeaf8da69c20 GIT binary patch literal 271 zcmb7-OAdlC6h+^ow1pR1xL7ghs&9{ F=Lg|2B!U0{ literal 0 HcmV?d00001 diff --git a/test/fixtures/expected/create-rgba.png b/test/fixtures/expected/create-rgba.png new file mode 100644 index 0000000000000000000000000000000000000000..0e1d966d0f25735fb8a582d47888af725768931b GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W=!3HF^gw{O+Qk(@Ik;M!Q%r8Kgacgx@Hc(L7 z)5S4FV`B2p^9Mf6Z|rL5oYmGVWs&tD!BBvqG@MB^apR@4KxGV`u6{1-oD!M<4xJsm literal 0 HcmV?d00001 diff --git a/test/unit/io.js b/test/unit/io.js index 894848a2..772d944e 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -1156,6 +1156,66 @@ describe('Input/output', function () { }); }); + describe('create new image', function () { + it('RGB', function (done) { + const create = { + width: 10, + height: 20, + channels: 3, + background: { r: 0, g: 255, b: 0 } + }; + sharp(null, { create: create }) + .jpeg() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(create.width, info.width); + assert.strictEqual(create.height, info.height); + assert.strictEqual(create.channels, info.channels); + assert.strictEqual('jpeg', info.format); + fixtures.assertSimilar(fixtures.expected('create-rgb.jpg'), data, done); + }); + }); + it('RGBA', function (done) { + const create = { + width: 20, + height: 10, + channels: 4, + background: { r: 255, g: 0, b: 0, alpha: 128 } + }; + sharp(null, { create: create }) + .png() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(create.width, info.width); + assert.strictEqual(create.height, info.height); + assert.strictEqual(create.channels, info.channels); + assert.strictEqual('png', info.format); + fixtures.assertSimilar(fixtures.expected('create-rgba.png'), data, done); + }); + }); + it('Invalid channels', function () { + const create = { + width: 10, + height: 20, + channels: 2, + background: { r: 0, g: 0, b: 0 } + }; + assert.throws(function () { + sharp(null, { create: create }); + }); + }); + it('Missing background', function () { + const create = { + width: 10, + height: 20, + channels: 3 + }; + assert.throws(function () { + sharp(null, { create: create }); + }); + }); + }); + it('Queue length change events', function (done) { let eventCounter = 0; const queueListener = function (queueLength) {