/*global Uint8Array:true ArrayBuffer:true */ "use strict"; var zlib = require('zlib'); var PNG = require('./PNG'); var inflate = function(data, callback){ return zlib.inflate(Buffer.from(data), callback); }; var slice = Array.prototype.slice; var toString = Object.prototype.toString; function equalBytes(a, b){ if (a.length != b.length) return false; for (var l = a.length; l--;) if (a[l] != b[l]) return false; return true; } function readUInt32(buffer, offset){ return (buffer[offset] << 24) + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + (buffer[offset + 3] << 0); } function readUInt16(buffer, offset){ return (buffer[offset + 1] << 8) + (buffer[offset] << 0); } function readUInt8(buffer, offset){ return buffer[offset] << 0; } function bufferToString(buffer){ var str = ''; for (var i = 0; i < buffer.length; i++){ str += String.fromCharCode(buffer[i]); } return str; } var PNGReader = function(bytes){ if (typeof bytes == 'string'){ var bts = bytes; bytes = new Array(bts.length); for (var i = 0, l = bts.length; i < l; i++){ bytes[i] = bts[i].charCodeAt(0); } } else { var type = toString.call(bytes).slice(8, -1); if (type == 'ArrayBuffer') bytes = new Uint8Array(bytes); } // current pointer this.i = 0; // bytes buffer this.bytes = bytes; // Output object this.png = new PNG(); this.dataChunks = []; }; PNGReader.prototype.readBytes = function(length){ var end = this.i + length; if (end > this.bytes.length){ throw new Error('Unexpectedly reached end of file'); } var bytes = slice.call(this.bytes, this.i, end); this.i = end; return bytes; }; /** * http://www.w3.org/TR/2003/REC-PNG-20031110/#5PNG-file-signature */ PNGReader.prototype.decodeHeader = function(){ if (this.i !== 0){ throw new Error('file pointer should be at 0 to read the header'); } var header = this.readBytes(8); if (!equalBytes(header, [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])){ throw new Error('invalid PNGReader file (bad signature)'); } this.header = header; }; /** * http://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-layout * * length = 4 bytes * type = 4 bytes (IHDR, PLTE, IDAT, IEND or others) * chunk = length bytes * crc = 4 bytes */ PNGReader.prototype.decodeChunk = function(){ var length = readUInt32(this.readBytes(4), 0); if (length < 0){ throw new Error('Bad chunk length ' + (0xFFFFFFFF & length)); } var type = bufferToString(this.readBytes(4)); var chunk = this.readBytes(length); var crc = this.readBytes(4); switch (type){ case 'IHDR': this.decodeIHDR(chunk); break; case 'PLTE': this.decodePLTE(chunk); break; case 'IDAT': this.decodeIDAT(chunk); break; case 'IEND': this.decodeIEND(chunk); break; } return type; }; /** * http://www.w3.org/TR/2003/REC-PNG-20031110/#11IHDR * http://www.libpng.org/pub/png/spec/1.2/png-1.2-pdg.html#C.IHDR * * Width 4 bytes * Height 4 bytes * Bit depth 1 byte * Colour type 1 byte * Compression method 1 byte * Filter method 1 byte * Interlace method 1 byte */ PNGReader.prototype.decodeIHDR = function(chunk){ var png = this.png; png.setWidth( readUInt32(chunk, 0)); png.setHeight( readUInt32(chunk, 4)); png.setBitDepth( readUInt8(chunk, 8)); png.setColorType( readUInt8(chunk, 9)); png.setCompressionMethod( readUInt8(chunk, 10)); png.setFilterMethod( readUInt8(chunk, 11)); png.setInterlaceMethod( readUInt8(chunk, 12)); }; /** * * http://www.w3.org/TR/PNG/#11PLTE */ PNGReader.prototype.decodePLTE = function(chunk){ this.png.setPalette(chunk); }; /** * http://www.w3.org/TR/2003/REC-PNG-20031110/#11IDAT */ PNGReader.prototype.decodeIDAT = function(chunk){ // multiple IDAT chunks will concatenated this.dataChunks.push(chunk); }; /** * http://www.w3.org/TR/2003/REC-PNG-20031110/#11IEND */ PNGReader.prototype.decodeIEND = function(){ }; /** * Uncompress IDAT chunks */ PNGReader.prototype.decodePixels = function(callback){ var png = this.png; var reader = this; var length = 0; var i, j, k, l; for (l = this.dataChunks.length; l--;) length += this.dataChunks[l].length; var data = Buffer.alloc(length); for (i = 0, k = 0, l = this.dataChunks.length; i < l; i++){ var chunk = this.dataChunks[i]; for (j = 0; j < chunk.length; j++) data[k++] = chunk[j]; } inflate(data, function(err, data){ if (err) return callback(err); try { if (png.getInterlaceMethod() === 0){ reader.interlaceNone(data); } else { reader.interlaceAdam7(data); } } catch (e){ return callback(e); } callback(); }); }; // Different interlace methods PNGReader.prototype.interlaceNone = function(data){ var png = this.png; // bytes per pixel var bpp = Math.max(1, png.colors * png.bitDepth / 8); // color bytes per row var cpr = bpp * png.width; var pixels = Buffer.alloc(bpp * png.width * png.height); var scanline; var offset = 0; for (var i = 0; i < data.length; i += cpr + 1){ scanline = slice.call(data, i + 1, i + cpr + 1); switch (readUInt8(data, i)){ case 0: this.unFilterNone( scanline, pixels, bpp, offset, cpr); break; case 1: this.unFilterSub( scanline, pixels, bpp, offset, cpr); break; case 2: this.unFilterUp( scanline, pixels, bpp, offset, cpr); break; case 3: this.unFilterAverage(scanline, pixels, bpp, offset, cpr); break; case 4: this.unFilterPaeth( scanline, pixels, bpp, offset, cpr); break; default: throw new Error("unkown filtered scanline"); } offset += cpr; } png.pixels = pixels; }; PNGReader.prototype.interlaceAdam7 = function(data){ throw new Error("Adam7 interlacing is not implemented yet"); }; // Unfiltering /** * No filtering, direct copy */ PNGReader.prototype.unFilterNone = function(scanline, pixels, bpp, of, length){ for (var i = 0, to = length; i < to; i++){ pixels[of + i] = scanline[i]; } }; /** * The Sub() filter transmits the difference between each byte and the value * of the corresponding byte of the prior pixel. * Sub(x) = Raw(x) + Raw(x - bpp) */ PNGReader.prototype.unFilterSub = function(scanline, pixels, bpp, of, length){ var i = 0; for (; i < bpp; i++) pixels[of + i] = scanline[i]; for (; i < length; i++){ // Raw(x) + Raw(x - bpp) pixels[of + i] = (scanline[i] + pixels[of + i - bpp]) & 0xFF; } }; /** * The Up() filter is just like the Sub() filter except that the pixel * immediately above the current pixel, rather than just to its left, is used * as the predictor. * Up(x) = Raw(x) + Prior(x) */ PNGReader.prototype.unFilterUp = function(scanline, pixels, bpp, of, length){ var i = 0, byte, prev; // Prior(x) is 0 for all x on the first scanline if ((of - length) < 0) for (; i < length; i++){ pixels[of + i] = scanline[i]; } else for (; i < length; i++){ // Raw(x) byte = scanline[i]; // Prior(x) prev = pixels[of + i - length]; pixels[of + i] = (byte + prev) & 0xFF; } }; /** * The Average() filter uses the average of the two neighboring pixels (left * and above) to predict the value of a pixel. * Average(x) = Raw(x) + floor((Raw(x-bpp)+Prior(x))/2) */ PNGReader.prototype.unFilterAverage = function(scanline, pixels, bpp, of, length){ var i = 0, byte, prev, prior; if ((of - length) < 0){ // Prior(x) == 0 && Raw(x - bpp) == 0 for (; i < bpp; i++){ pixels[of + i] = scanline[i]; } // Prior(x) == 0 && Raw(x - bpp) != 0 (right shift, prevent doubles) for (; i < length; i++){ pixels[of + i] = (scanline[i] + (pixels[of + i - bpp] >> 1)) & 0xFF; } } else { // Prior(x) != 0 && Raw(x - bpp) == 0 for (; i < bpp; i++){ pixels[of + i] = (scanline[i] + (pixels[of - length + i] >> 1)) & 0xFF; } // Prior(x) != 0 && Raw(x - bpp) != 0 for (; i < length; i++){ byte = scanline[i]; prev = pixels[of + i - bpp]; prior = pixels[of + i - length]; pixels[of + i] = (byte + (prev + prior >> 1)) & 0xFF; } } }; /** * The Paeth() filter computes a simple linear function of the three * neighboring pixels (left, above, upper left), then chooses as predictor * the neighboring pixel closest to the computed value. This technique is due * to Alan W. Paeth. * Paeth(x) = Raw(x) + * PaethPredictor(Raw(x-bpp), Prior(x), Prior(x-bpp)) * function PaethPredictor (a, b, c) * begin * ; a = left, b = above, c = upper left * p := a + b - c ; initial estimate * pa := abs(p - a) ; distances to a, b, c * pb := abs(p - b) * pc := abs(p - c) * ; return nearest of a,b,c, * ; breaking ties in order a,b,c. * if pa <= pb AND pa <= pc then return a * else if pb <= pc then return b * else return c * end */ PNGReader.prototype.unFilterPaeth = function(scanline, pixels, bpp, of, length){ var i = 0, raw, a, b, c, p, pa, pb, pc, pr; if ((of - length) < 0){ // Prior(x) == 0 && Raw(x - bpp) == 0 for (; i < bpp; i++){ pixels[of + i] = scanline[i]; } // Prior(x) == 0 && Raw(x - bpp) != 0 // paethPredictor(x, 0, 0) is always x for (; i < length; i++){ pixels[of + i] = (scanline[i] + pixels[of + i - bpp]) & 0xFF; } } else { // Prior(x) != 0 && Raw(x - bpp) == 0 // paethPredictor(x, 0, 0) is always x for (; i < bpp; i++){ pixels[of + i] = (scanline[i] + pixels[of + i - length]) & 0xFF; } // Prior(x) != 0 && Raw(x - bpp) != 0 for (; i < length; i++){ raw = scanline[i]; a = pixels[of + i - bpp]; b = pixels[of + i - length]; c = pixels[of + i - length - bpp]; p = a + b - c; pa = Math.abs(p - a); pb = Math.abs(p - b); pc = Math.abs(p - c); if (pa <= pb && pa <= pc) pr = a; else if (pb <= pc) pr = b; else pr = c; pixels[of + i] = (raw + pr) & 0xFF; } } }; /** * Parse the PNG file * * reader.parse(options, callback) * OR * reader.parse(callback) * * OPTIONS: * option | type | default * ---------------------------- * data boolean true should it read the pixel data */ PNGReader.prototype.parse = function(options, callback){ if (typeof options == 'function') callback = options; if (typeof options != 'object') options = {}; try { this.decodeHeader(); while (this.i < this.bytes.length){ var type = this.decodeChunk(); // stop after IHDR chunk, or after IEND if (type == 'IHDR' && options.data === false || type == 'IEND') break; } var png = this.png; this.decodePixels(function(err){ callback(err, png); }); } catch (e){ callback(e); } }; module.exports = PNGReader;