From 2c37ebcbf27e0b228e341a360556080b747eb6f3 Mon Sep 17 00:00:00 2001 From: Jesus Nuevo Date: Sat, 5 Aug 2017 18:41:47 +1000 Subject: [PATCH] Add writing 16 bit channels and support for grayscale input (#82) * Add support for writing 16 bit channels * Fix for quick exit from the bitpacker * Add example and describe bitDepth option in README --- README.md | 5 +- examples/16bit_write.js | 51 ++++++++++++++++ lib/bitpacker.js | 127 ++++++++++++++++++++++++++++------------ lib/filter-pack.js | 2 + lib/packer.js | 12 +++- 5 files changed, 156 insertions(+), 41 deletions(-) create mode 100644 examples/16bit_write.js diff --git a/README.md b/README.md index f252110..d9ca348 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,10 @@ As input any color type is accepted (grayscale, rgb, palette, grayscale with alp - `deflateFactory` - deflate stream factory (default: `zlib.createDeflate`) - `filterType` - png filtering method for scanlines (default: -1 => auto, accepts array of numbers 0-4) - `colorType` - the output colorType - see constants. 0 = grayscale, no alpha, 2 = color, no alpha, 4 = grayscale & alpha, 6 = color & alpha. Default currently 6, but in the future may calculate best mode. -- `inputHasAlpha` - whether the input bitmap has 4 bits per pixel (rgb and alpha) or 3 (rgb - no alpha). +- `inputColorType` - the input colorType - see constants. Default is 6 (RGBA) +- `bitDepth` - the bitDepth of the output, 8 or 16 bits. Input data is expected to have this bit depth. +16 bit data is expected in the system endianness (Default: 8) +- `inputHasAlpha` - whether the input bitmap has 4 bytes per pixel (rgb and alpha) or 3 (rgb - no alpha). - `bgColor` - an object containing red, green, and blue values between 0 and 255 that is used when packing a PNG if alpha is not to be included (default: 255,255,255) diff --git a/examples/16bit_write.js b/examples/16bit_write.js new file mode 100644 index 0000000..d5b0241 --- /dev/null +++ b/examples/16bit_write.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node + +var fs = require('fs'); +var PNG = require("../lib/png").PNG; +var w = 32; +var h = 64; + +/// RGBA input (color type 6) +var buffer = new Buffer(2 * w * h * 4); +var bitmap = new Uint16Array(buffer.buffer); +for (var i = 0; i < h; i++) { + for (var j = 0; j < w; j++) { + bitmap[i * 4 * w + 4*j] = i * 65535 / h; + bitmap[i * 4 * w + 4*j + 1] = j * 65535 / w; + bitmap[i * 4 * w + 4*j + 2] = (h-i) * 65535 / h; + bitmap[i * 4 * w + 4*j + 3] = 65535; + } +} + +var png = new PNG({ + width: w, + height:h, + bitDepth: 16, + colorType: 6, + inputColorType: 6, + inputHasAlpha: true +}); + +png.data = buffer; +png.pack().pipe(fs.createWriteStream('colortype6.png')); + +//////// Grayscale 16 bits/////// + +var buffer = new Buffer(2 * w * h); +var bitmap = new Uint16Array(buffer.buffer); +for (var i = 0; i < h; i++) { + for (var j = 0; j < w; j++) + bitmap[i * w + j] = i * 65535 / h; +} + +png = new PNG({ + width: w, + height:h, + bitDepth: 16, + colorType: 0, + inputColorType: 0, + inputHasAlpha: false +}); + +png.data = buffer; +png.pack().pipe(fs.createWriteStream('colortype0.png')); \ No newline at end of file diff --git a/lib/bitpacker.js b/lib/bitpacker.js index b8b2d16..219380f 100644 --- a/lib/bitpacker.js +++ b/lib/bitpacker.js @@ -2,73 +2,124 @@ var constants = require('./constants'); -module.exports = function(data, width, height, options) { +module.exports = function(dataIn, width, height, options) { var outHasAlpha = [constants.COLORTYPE_COLOR_ALPHA, constants.COLORTYPE_ALPHA].indexOf(options.colorType) !== -1; - - if ([constants.COLORTYPE_GRAYSCALE, constants.COLORTYPE_ALPHA].indexOf(options.colorType) === -1) { + if (options.colorType === options.inputColorType) { + var bigEndian = (function() { + var buffer = new ArrayBuffer(2); + new DataView(buffer).setInt16(0, 256, true /* littleEndian */); + // Int16Array uses the platform's endianness. + return new Int16Array(buffer)[0] !== 256; + })(); // If no need to convert to grayscale and alpha is present/absent in both, take a fast route - if (options.inputHasAlpha && outHasAlpha) { - return data; - } - if (!options.inputHasAlpha && !outHasAlpha) { - return data; + if (options.bitDepth === 8 || (options.bitDepth === 16 && bigEndian)){ + return dataIn; } } + // map to a UInt16 array if data is 16bit, fix endianness below + var data = options.bitDepth !== 16 ? dataIn : new Uint16Array(dataIn.buffer); + + var maxValue = 255; + var inBpp = constants.COLORTYPE_TO_BPP_MAP[options.inputColorType]; var outBpp = constants.COLORTYPE_TO_BPP_MAP[options.colorType]; + if (options.bitDepth === 16) { + maxValue = 65535; + outBpp *= 2; + } var outData = new Buffer(width * height * outBpp); - var inBpp = options.inputHasAlpha ? 4 : 3; + var inIndex = 0; var outIndex = 0; var bgColor = options.bgColor || {}; if (bgColor.red === undefined) { - bgColor.red = 255; + bgColor.red = maxValue; } if (bgColor.green === undefined) { - bgColor.green = 255; + bgColor.green = maxValue; } if (bgColor.blue === undefined) { - bgColor.blue = 255; + bgColor.blue = maxValue; + } + + function getRGBA(data, inIndex) { + var red, green, blue, alpha = maxValue; + switch (options.inputColorType) { + case constants.COLORTYPE_COLOR_ALPHA: + alpha = data[inIndex + 3]; + red = data[inIndex]; + green = data[inIndex+1]; + blue = data[inIndex+2]; + break; + case constants.COLORTYPE_COLOR: + red = data[inIndex]; + green = data[inIndex+1]; + blue = data[inIndex+2]; + break; + case constants.COLORTYPE_ALPHA: + alpha = data[inIndex + 1]; + red = data[inIndex]; + green = red; + blue = red; + break; + case constants.COLORTYPE_GRAYSCALE: + red = data[inIndex]; + green = red; + blue = red; + break; + default: + throw new Error('input color type:' + options.inputColorType + ' is not supported at present'); + } + + if (options.inputHasAlpha) { + if (!outHasAlpha) { + alpha /= maxValue; + red = Math.min(Math.max(Math.round((1 - alpha) * bgColor.red + alpha * red), 0), maxValue); + green = Math.min(Math.max(Math.round((1 - alpha) * bgColor.green + alpha * green), 0), maxValue); + blue = Math.min(Math.max(Math.round((1 - alpha) * bgColor.blue + alpha * blue), 0), maxValue); + } + } + return {red, green, blue, alpha}; } for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { - var red = data[inIndex]; - var green = data[inIndex + 1]; - var blue = data[inIndex + 2]; - - var alpha; - if (options.inputHasAlpha) { - alpha = data[inIndex + 3]; - if (!outHasAlpha) { - alpha /= 255; - red = Math.min(Math.max(Math.round((1 - alpha) * bgColor.red + alpha * red), 0), 255); - green = Math.min(Math.max(Math.round((1 - alpha) * bgColor.green + alpha * green), 0), 255); - blue = Math.min(Math.max(Math.round((1 - alpha) * bgColor.blue + alpha * blue), 0), 255); - } - } - else { - alpha = 255; - } + var rgba = getRGBA(data, inIndex); switch (options.colorType) { case constants.COLORTYPE_COLOR_ALPHA: case constants.COLORTYPE_COLOR: - outData[outIndex] = red; - outData[outIndex + 1] = green; - outData[outIndex + 2] = blue; - if (outHasAlpha) { - outData[outIndex + 3] = alpha; + if (options.bitDepth === 8) { + outData[outIndex] = rgba.red; + outData[outIndex + 1] = rgba.green; + outData[outIndex + 2] = rgba.blue; + if (outHasAlpha) { + outData[outIndex + 3] = rgba.alpha; + } + } else { + outData.writeUInt16BE(rgba.red, outIndex); + outData.writeUInt16BE(rgba.green, outIndex + 2); + outData.writeUInt16BE(rgba.blue, outIndex + 4); + if (outHasAlpha) { + outData.writeUInt16BE(rgba.alpha, outIndex + 6); + } } break; case constants.COLORTYPE_ALPHA: case constants.COLORTYPE_GRAYSCALE: // Convert to grayscale and alpha - var grayscale = (red + green + blue) / 3; - outData[outIndex] = grayscale; - if (outHasAlpha) { - outData[outIndex + 1] = alpha; + var grayscale = (rgba.red + rgba.green + rgba.blue) / 3; + if (options.bitDepth === 8) { + outData[outIndex] = grayscale; + if (outHasAlpha) { + outData[outIndex + 1] = rgba.alpha; + } + } else { + outData.writeUInt16BE(grayscale, outIndex); + if (outHasAlpha) { + outData.writeUInt16BE(rgba.alpha, outIndex + 2); + } } break; } diff --git a/lib/filter-pack.js b/lib/filter-pack.js index c39ae78..24a7c45 100644 --- a/lib/filter-pack.js +++ b/lib/filter-pack.js @@ -152,10 +152,12 @@ module.exports = function(pxData, width, height, options, bpp) { throw new Error('unrecognised filter types'); } + if (options.bitDepth === 16) bpp *= 2; var byteWidth = width * bpp; var rawPos = 0; var pxPos = 0; var rawData = new Buffer((byteWidth + 1) * height); + var sel = filterTypes[0]; for (var y = 0; y < height; y++) { diff --git a/lib/packer.js b/lib/packer.js index 6085164..b3f435d 100644 --- a/lib/packer.js +++ b/lib/packer.js @@ -17,7 +17,7 @@ var Packer = module.exports = function(options) { options.bitDepth = options.bitDepth || 8; // This is outputColorType options.colorType = (typeof options.colorType === 'number') ? options.colorType : constants.COLORTYPE_COLOR_ALPHA; - options.inputColorType = options.inputColorType || constants.COLORTYPE_COLOR_ALPHA; + options.inputColorType = (typeof options.inputColorType === 'number') ? options.inputColorType : constants.COLORTYPE_COLOR_ALPHA; if ([ constants.COLORTYPE_GRAYSCALE, @@ -27,7 +27,15 @@ var Packer = module.exports = function(options) { ].indexOf(options.colorType) === -1) { throw new Error('option color type:' + options.colorType + ' is not supported at present'); } - if (options.bitDepth !== 8) { + if ([ + constants.COLORTYPE_GRAYSCALE, + constants.COLORTYPE_COLOR, + constants.COLORTYPE_COLOR_ALPHA, + constants.COLORTYPE_ALPHA + ].indexOf(options.inputColorType) === -1) { + throw new Error('option input color type:' + options.inputColorType + ' is not supported at present'); + } + if (options.bitDepth !== 8 && options.bitDepth !== 16) { throw new Error('option bit depth:' + options.bitDepth + ' is not supported at present'); } };