diff --git a/docs/api-constructor.md b/docs/api-constructor.md index 371407d9..9db6b792 100644 --- a/docs/api-constructor.md +++ b/docs/api-constructor.md @@ -37,6 +37,8 @@ Implements the [stream.Duplex][1] class. * `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.raw.premultiplied` **[boolean][7]?** specifies that the raw input has already been premultiplied, set to `true` + to avoid sharp premultiplying the image. (optional, default `false`) * `options.create` **[Object][6]?** describes a new image to be created. * `options.create.width` **[number][8]?** integral number of pixels wide. diff --git a/lib/constructor.js b/lib/constructor.js index 618d1910..85f7284c 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -139,6 +139,8 @@ const debuglog = util.debuglog('sharp'); * @param {number} [options.raw.width] - integral number of pixels wide. * @param {number} [options.raw.height] - integral number of pixels high. * @param {number} [options.raw.channels] - integral number of channels, between 1 and 4. + * @param {boolean} [options.raw.premultiplied] - specifies that the raw input has already been premultiplied, set to `true` + * to avoid sharp premultiplying the image. (optional, default `false`) * @param {Object} [options.create] - describes a new image to be created. * @param {number} [options.create.width] - integral number of pixels wide. * @param {number} [options.create.height] - integral number of pixels high. diff --git a/lib/input.js b/lib/input.js index 7364f503..f986e999 100644 --- a/lib/input.js +++ b/lib/input.js @@ -103,6 +103,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { inputDescriptor.rawWidth = inputOptions.raw.width; inputDescriptor.rawHeight = inputOptions.raw.height; inputDescriptor.rawChannels = inputOptions.raw.channels; + inputDescriptor.rawPremultiplied = !!inputOptions.raw.premultiplied; } else { throw new Error('Expected width, height and channels for raw pixel input'); } diff --git a/package.json b/package.json index 9530d040..7ccfd17a 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,8 @@ "Manan Jadhav ", "Leon Radley ", "alza54 ", - "Jacob Smith " + "Jacob Smith ", + "Michael Nutt " ], "scripts": { "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)", diff --git a/src/common.cc b/src/common.cc index feef01da..2f315a9c 100644 --- a/src/common.cc +++ b/src/common.cc @@ -95,6 +95,7 @@ namespace sharp { descriptor->rawChannels = AttrAsUint32(input, "rawChannels"); descriptor->rawWidth = AttrAsUint32(input, "rawWidth"); descriptor->rawHeight = AttrAsUint32(input, "rawHeight"); + descriptor->rawPremultiplied = AttrAsBool(input, "rawPremultiplied"); } // Multi-page input (GIF, TIFF, PDF) if (HasAttr(input, "pages")) { @@ -302,6 +303,9 @@ namespace sharp { } else { image.get_image()->Type = VIPS_INTERPRETATION_sRGB; } + if (descriptor->rawPremultiplied) { + image = image.unpremultiply(); + } imageType = ImageType::RAW; } else { // Compressed data diff --git a/src/common.h b/src/common.h index a5707cb8..d8ff0ecd 100644 --- a/src/common.h +++ b/src/common.h @@ -57,6 +57,7 @@ namespace sharp { int rawChannels; int rawWidth; int rawHeight; + bool rawPremultiplied; int pages; int page; int level; @@ -80,6 +81,7 @@ namespace sharp { rawChannels(0), rawWidth(0), rawHeight(0), + rawPremultiplied(false), pages(1), page(0), level(0), diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 6d3ff5c0..1aa2dfae 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -90,6 +90,7 @@ module.exports = { inputPngEmbed: getPath('embedgravitybird.png'), // Released to sharp under a CC BY 4.0 inputPngRGBWithAlpha: getPath('2569067123_aca715a2ee_o.png'), // http://www.flickr.com/photos/grizdave/2569067123/ (same as inputJpg) inputPngImageInAlpha: getPath('image-in-alpha.png'), // https://github.com/lovell/sharp/issues/1597 + inputPngSolidAlpha: getPath('with-alpha.png'), // https://github.com/lovell/sharp/issues/1599 inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp diff --git a/test/fixtures/with-alpha.png b/test/fixtures/with-alpha.png new file mode 100644 index 00000000..723dd585 Binary files /dev/null and b/test/fixtures/with-alpha.png differ diff --git a/test/unit/raw.js b/test/unit/raw.js index b4727b69..eed85708 100644 --- a/test/unit/raw.js +++ b/test/unit/raw.js @@ -107,6 +107,52 @@ describe('Raw pixel data', function () { }); }); + it('RGBA premultiplied', function (done) { + // Convert to raw pixel data + sharp(fixtures.inputPngSolidAlpha) + .resize(256) + .raw() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(256, info.width); + assert.strictEqual(192, info.height); + assert.strictEqual(4, info.channels); + + const originalData = Buffer.from(data); + + // Premultiply image data + for (let i = 0; i < data.length; i += 4) { + const alpha = data[i + 3]; + const norm = alpha / 255; + + if (alpha < 255) { + data[i] = Math.round(data[i] * norm); + data[i + 1] = Math.round(data[i + 1] * norm); + data[i + 2] = Math.round(data[i + 2] * norm); + } + } + + // Convert back to PNG + sharp(data, { + raw: { + width: info.width, + height: info.height, + channels: info.channels, + premultiplied: true + } + }) + .raw() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(256, info.width); + assert.strictEqual(192, info.height); + assert.strictEqual(4, info.channels); + assert.equal(data.compare(originalData), 0, 'output buffer matches unpremultiplied input buffer'); + done(); + }); + }); + }); + it('JPEG to raw Stream and back again', function (done) { const width = 32; const height = 24;