diff --git a/lib/crc.js b/lib/crc.js index a3ac042..ef47f77 100755 --- a/lib/crc.js +++ b/lib/crc.js @@ -20,21 +20,11 @@ 'use strict'; -var util = require('util'), - Stream = require('stream'); - - -var CrcStream = module.exports = function() { - Stream.call(this); - +var CrcCalculator = module.exports = function() { this._crc = -1; - - this.writable = true; }; -util.inherits(CrcStream, Stream); - -CrcStream.prototype.write = function(data) { +CrcCalculator.prototype.write = function(data) { for (var i = 0; i < data.length; i++) { this._crc = crcTable[(this._crc ^ data[i]) & 0xff] ^ (this._crc >>> 8); @@ -42,18 +32,12 @@ CrcStream.prototype.write = function(data) { return true; }; -CrcStream.prototype.end = function(data) { - if (data) this.write(data); - - this.emit('crc', this.crc32()); -}; - -CrcStream.prototype.crc32 = function() { +CrcCalculator.prototype.crc32 = function() { return this._crc ^ -1; }; -CrcStream.crc32 = function(buf) { +CrcCalculator.crc32 = function(buf) { var crc = -1; for (var i = 0; i < buf.length; i++) { diff --git a/lib/filter-async.js b/lib/filter-async.js new file mode 100644 index 0000000..6fb7e68 --- /dev/null +++ b/lib/filter-async.js @@ -0,0 +1,38 @@ +// Copyright (c) 2012 Kuba Niegowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + +var util = require('util'), + ChunkStream = require('./chunkstream'), + Filter = require('./filter'); + + +var FilterAsync = module.exports = function(width, height, Bpp, data, options) { + ChunkStream.call(this); + + this._filter = new Filter(width, height, Bpp, data, options, { + read: this.read.bind(this), + complete: this.emit.bind(this, 'complete') + }); + + this._filter.start(); +}; +util.inherits(FilterAsync, ChunkStream); diff --git a/lib/filter-sync.js b/lib/filter-sync.js new file mode 100644 index 0000000..1ba810b --- /dev/null +++ b/lib/filter-sync.js @@ -0,0 +1,37 @@ +// Copyright (c) 2012 Kuba Niegowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + +var SyncReader = require('./sync-reader'), + Filter = require('./filter'); + + +exports.process = function(buffer, width, height, Bpp, outData, options) { + + var reader = new SyncReader(buffer); + var filter = new Filter(width, height, Bpp, outData, options, { + read: reader.read.bind(reader), + complete: function(){} + }); + + filter.start(); + reader.process(); +}; \ No newline at end of file diff --git a/lib/filter.js b/lib/filter.js index fbe60d2..83ad820 100755 --- a/lib/filter.js +++ b/lib/filter.js @@ -21,12 +21,10 @@ 'use strict'; var util = require('util'), - zlib = require('zlib'), ChunkStream = require('./chunkstream'); -var Filter = module.exports = function(width, height, Bpp, data, options) { - ChunkStream.call(this); +var Filter = module.exports = function(width, height, Bpp, data, options, dependencies) { this._width = width; this._height = height; @@ -50,10 +48,13 @@ var Filter = module.exports = function(width, height, Bpp, data, options) { 4: this._filterPaeth.bind(this) }; - this.read(this._width * Bpp + 1, this._reverseFilterLine.bind(this)); + this.read = dependencies.read; + this.complete = dependencies.complete; }; -util.inherits(Filter, ChunkStream); +Filter.prototype.start = function() { + this.read(this._width * this._Bpp + 1, this._reverseFilterLine.bind(this)); +}; var pixelBppMap = { 1: { // L @@ -167,7 +168,7 @@ Filter.prototype._reverseFilterLine = function(rawData) { if (this._line < this._height) this.read(this._width * this._Bpp + 1, this._reverseFilterLine.bind(this)); else - this.emit('complete', this._data, this._width, this._height); + this.complete(this._data, this._width, this._height); }; diff --git a/lib/packer.js b/lib/packer.js index 19046c8..c8b74a8 100755 --- a/lib/packer.js +++ b/lib/packer.js @@ -50,7 +50,7 @@ Packer.prototype.pack = function(data, width, height) { this.emit('data', this._packIHDR(width, height)); // filter pixel data - var filter = new Filter(width, height, 4, data, this._options); + var filter = new Filter(width, height, 4, data, this._options, {}); var data = filter.filter(); // compress it diff --git a/lib/parser-async.js b/lib/parser-async.js new file mode 100644 index 0000000..a9453b8 --- /dev/null +++ b/lib/parser-async.js @@ -0,0 +1,97 @@ +// Copyright (c) 2012 Kuba Niegowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + + +var util = require('util'), + zlib = require('zlib'), + ChunkStream = require('./chunkstream'), + FilterAsync = require('./filter-async'), + Parser = require('./parser'); + + +var ParserAsync = module.exports = function(options) { + ChunkStream.call(this); + + this._parser = new Parser(options, { + read: this.read.bind(this), + error: this.emit.bind(this, "error"), + metadata: this.emit.bind(this, "metadata"), + gamma: this.emit.bind(this, "gamma"), + finished: this._finished.bind(this), + inflateData: this._inflateData.bind(this), + createData: this._createData.bind(this) + }); + this._options = options; + this.writable = true; + + this.on('error', this._handleError.bind(this)); + this._parser.start(); +}; +util.inherits(ParserAsync, ChunkStream); + + +ParserAsync.prototype._handleError = function() { + + this.writable = false; + + this.destroy(); + + if (this._inflate && this._inflate.destroy) { + this._inflate.destroy(); + } +}; + +ParserAsync.prototype._inflateData = function(data) { + if (!this._inflate) { + this._inflate = zlib.createInflate(); + + this._inflate.on('error', this.emit.bind(this, 'error')); + this._filter.on('complete', this._complete.bind(this)); + + this._inflate.pipe(this._filter); + } + this._inflate.write(data); +}; + +ParserAsync.prototype._createData = function(width, height, bpp) { + this._data = new Buffer(width * height * 4); + this._filter = new FilterAsync( + width, height, + bpp, + this._data, + this._options + ); + return this._data; +}; + +ParserAsync.prototype._finished = function(data) { + // no more data to inflate + this._inflate.end(); + this.destroySoon(); +}; + +ParserAsync.prototype._complete = function(data, width, height) { + + data = this._parser.reverseFiltered(data, width, height); + + this.emit('parsed', data); +}; diff --git a/lib/parser-sync.js b/lib/parser-sync.js new file mode 100644 index 0000000..71bcbdf --- /dev/null +++ b/lib/parser-sync.js @@ -0,0 +1,87 @@ +// Copyright (c) 2012 Kuba Niegowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + + +var zlib = require('zlib'), + SyncReader = require('./sync-reader'), + FilterSync = require('./filter-sync'), + Parser = require('./parser'); + + +var ParserSync = module.exports = function(buffer, options) { + + var reader = new SyncReader(buffer); + + this._inflateDataList = []; + this._parser = new Parser(options, { + read: reader.read.bind(reader), + error: this._handleError.bind(this), + metadata: this._metaData.bind(this), + gamma: this._gamma.bind(this), + finished: function() {}, + inflateData: this._inflateData.bind(this), + createData: this._createData.bind(this) + }); + this._options = options; + + this._parser.start(); + reader.process(); + + //join together the inflate datas + var inflateData = Buffer.concat(this._inflateDataList); + + var data = zlib.inflateSync(inflateData); + + FilterSync.process( + data, + this._width, this._height, + this._bpp, + this._data, + this._options + ); + + this.data = this._parser.reverseFiltered(this._data, this._width, this._height); +}; + +ParserSync.prototype._handleError = function(err) { + this.err = err; +}; + +ParserSync.prototype._metaData = function(metaData) { + this.metaData = metaData; +}; + +ParserSync.prototype._gamma = function(gamma) { + this.gamma = gamma; +}; + +ParserSync.prototype._inflateData = function(data) { + this._inflateDataList.push(data); +}; + +ParserSync.prototype._createData = function(width, height, bpp) { + this._data = new Buffer(width * height * 4); + this._bpp = bpp; + this._width = width; + this._height = height; + return this._data; +}; diff --git a/lib/parser.js b/lib/parser.js old mode 100755 new mode 100644 index f4eef7b..680154f --- a/lib/parser.js +++ b/lib/parser.js @@ -21,339 +21,302 @@ 'use strict'; -var util = require('util'), - zlib = require('zlib'), - CrcStream = require('./crc'), - ChunkStream = require('./chunkstream'), - constants = require('./constants'), - Filter = require('./filter'); +var constants = require('./constants'), + CrcCalculator = require('./crc'); -var Parser = module.exports = function(options) { - ChunkStream.call(this); +var Parser = module.exports = function(options, dependencies) { - this._options = options; - options.checkCRC = options.checkCRC !== false; + this._options = options; + options.checkCRC = options.checkCRC !== false; - this._hasIHDR = false; - this._hasIEND = false; + this._hasIHDR = false; + this._hasIEND = false; - this._inflate = null; - this._filter = null; - this._crc = null; + // input flags/metadata + this._palette = []; + this._colorType = 0; - // input flags/metadata - this._palette = []; - this._colorType = 0; + this._chunks = {}; + this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this); + this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this); + this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this); + this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this); + this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this); + this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this); - this._chunks = {}; - this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this); - this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this); - this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this); - this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this); - this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this); - this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this); - - this.writable = true; - - this.on('error', this._handleError.bind(this)); - this._handleSignature(); -}; -util.inherits(Parser, ChunkStream); - - -Parser.prototype._handleError = function() { - - this.writable = false; - - this.destroy(); - - if (this._inflate) - this._inflate.destroy(); + this.read = dependencies.read; + this.error = dependencies.error; + this.metadata = dependencies.metadata; + this.gamma = dependencies.gamma; + this.parsed = dependencies.parsed; + this.createData = dependencies.createData; + this.inflateData = dependencies.inflateData; + this.finished = dependencies.finished; }; -Parser.prototype._handleSignature = function() { - this.read(constants.PNG_SIGNATURE.length, - this._parseSignature.bind(this) - ); +var colorTypeToBppMap = { + 0: 1, + 2: 3, + 3: 1, + 4: 2, + 6: 4 +}; + +Parser.prototype.start = function() { + this.read(constants.PNG_SIGNATURE.length, + this._parseSignature.bind(this) + ); }; Parser.prototype._parseSignature = function(data) { - var signature = constants.PNG_SIGNATURE; + var signature = constants.PNG_SIGNATURE; - for (var i = 0; i < signature.length; i++) { - if (data[i] != signature[i]) { - this.emit('error', new Error('Invalid file signature')); - return; - } + for (var i = 0; i < signature.length; i++) { + if (data[i] != signature[i]) { + this.error(new Error('Invalid file signature')); + return; } - this.read(8, this._parseChunkBegin.bind(this)); + } + this.read(8, this._parseChunkBegin.bind(this)); }; Parser.prototype._parseChunkBegin = function(data) { - // chunk content length - var length = data.readUInt32BE(0); + // chunk content length + var length = data.readUInt32BE(0); - // chunk type - var type = data.readUInt32BE(4), - name = ''; - for (var i = 4; i < 8; i++) - name += String.fromCharCode(data[i]); + // chunk type + var type = data.readUInt32BE(4), + name = ''; + for (var i = 4; i < 8; i++) { + name += String.fromCharCode(data[i]); + } - // console.log('chunk ', name, length); + //console.log('chunk ', name, length); - // chunk flags - var ancillary = !!(data[4] & 0x20), // or critical - priv = !!(data[5] & 0x20), // or public - safeToCopy = !!(data[7] & 0x20); // or unsafe + // chunk flags + var ancillary = !!(data[4] & 0x20), // or critical + priv = !!(data[5] & 0x20), // or public + safeToCopy = !!(data[7] & 0x20); // or unsafe - if (!this._hasIHDR && type != constants.TYPE_IHDR) { - this.emit('error', new Error('Expected IHDR on beggining')); - return; - } + if (!this._hasIHDR && type != constants.TYPE_IHDR) { + this.error(new Error('Expected IHDR on beggining')); + return; + } - this._crc = new CrcStream(); - this._crc.write(new Buffer(name)); + this._crc = new CrcCalculator(); + this._crc.write(new Buffer(name)); - if (this._chunks[type]) { - return this._chunks[type](length); + if (this._chunks[type]) { + return this._chunks[type](length); - } else if (!ancillary) { - this.emit('error', new Error('Unsupported critical chunk type ' + name)); - return; - } else { - this.read(length + 4, this._skipChunk.bind(this)); - } + } else if (!ancillary) { + this.error(new Error('Unsupported critical chunk type ' + name)); + return; + } else { + this.read(length + 4, this._skipChunk.bind(this)); + } }; Parser.prototype._skipChunk = function(data) { - this.read(8, this._parseChunkBegin.bind(this)); + this.read(8, this._parseChunkBegin.bind(this)); }; Parser.prototype._handleChunkEnd = function() { - this.read(4, this._parseChunkEnd.bind(this)); + this.read(4, this._parseChunkEnd.bind(this)); }; Parser.prototype._parseChunkEnd = function(data) { - var fileCrc = data.readInt32BE(0), - calcCrc = this._crc.crc32(); + var fileCrc = data.readInt32BE(0), + calcCrc = this._crc.crc32(); - // check CRC - if (this._options.checkCRC && calcCrc != fileCrc) { - this.emit('error', new Error('Crc error')); - return; - } + // check CRC + if (this._options.checkCRC && calcCrc != fileCrc) { + this.error(new Error('Crc error - ' + fileCrc + " - " + calcCrc)); + return; + } - if (this._hasIEND) { - this.destroySoon(); - - } else { - this.read(8, this._parseChunkBegin.bind(this)); - } + if (!this._hasIEND) { + this.read(8, this._parseChunkBegin.bind(this)); + } }; - Parser.prototype._handleIHDR = function(length) { - this.read(length, this._parseIHDR.bind(this)); + this.read(length, this._parseIHDR.bind(this)); }; Parser.prototype._parseIHDR = function(data) { - this._crc.write(data); + this._crc.write(data); - var width = data.readUInt32BE(0), - height = data.readUInt32BE(4), - depth = data[8], - colorType = data[9], // bits: 1 palette, 2 color, 4 alpha - compr = data[10], - filter = data[11], - interlace = data[12]; + var width = data.readUInt32BE(0), + height = data.readUInt32BE(4), + depth = data[8], + colorType = data[9], // bits: 1 palette, 2 color, 4 alpha + compr = data[10], + filter = data[11], + interlace = data[12]; - // console.log(' width', width, 'height', height, - // 'depth', depth, 'colorType', colorType, - // 'compr', compr, 'filter', filter, 'interlace', interlace - // ); + // console.log(' width', width, 'height', height, + // 'depth', depth, 'colorType', colorType, + // 'compr', compr, 'filter', filter, 'interlace', interlace + // ); - if (depth != 8) { - this.emit('error', new Error('Unsupported bit depth ' + depth)); - return; - } - if (!(colorType in colorTypeToBppMap)) { - this.emit('error', new Error('Unsupported color type')); - return; - } - if (compr != 0) { - this.emit('error', new Error('Unsupported compression method')); - return; - } - if (filter != 0) { - this.emit('error', new Error('Unsupported filter method')); - return; - } - if (interlace != 0) { - this.emit('error', new Error('Unsupported interlace method')); - return; - } + if (depth != 8) { + this.error(new Error('Unsupported bit depth ' + depth)); + return; + } + if (!(colorType in colorTypeToBppMap)) { + this.error(new Error('Unsupported color type')); + return; + } + if (compr != 0) { + this.error(new Error('Unsupported compression method')); + return; + } + if (filter != 0) { + this.error(new Error('Unsupported filter method')); + return; + } + if (interlace != 0) { + this.error(new Error('Unsupported interlace method')); + return; + } - this._colorType = colorType; + this._colorType = colorType; - this._data = new Buffer(width * height * 4); - this._filter = new Filter( - width, height, - colorTypeToBppMap[this._colorType], - this._data, - this._options - ); + this._data = this.createData(width, height, colorTypeToBppMap[this._colorType]); - this._hasIHDR = true; + this._hasIHDR = true; - this.emit('metadata', { - width: width, - height: height, - palette: !!(colorType & constants.COLOR_PALETTE), - color: !!(colorType & constants.COLOR_COLOR), - alpha: !!(colorType & constants.COLOR_ALPHA), - data: this._data - }); + this.metadata({ + width: width, + height: height, + palette: !!(colorType & constants.COLOR_PALETTE), + color: !!(colorType & constants.COLOR_COLOR), + alpha: !!(colorType & constants.COLOR_ALPHA), + data: this._data + }); - this._handleChunkEnd(); + this._handleChunkEnd(); }; Parser.prototype._handlePLTE = function(length) { - this.read(length, this._parsePLTE.bind(this)); + this.read(length, this._parsePLTE.bind(this)); }; Parser.prototype._parsePLTE = function(data) { - this._crc.write(data); + this._crc.write(data); - var entries = Math.floor(data.length / 3); - // console.log('Palette:', entries); + var entries = Math.floor(data.length / 3); + // console.log('Palette:', entries); - for (var i = 0; i < entries; i++) { - this._palette.push([ - data.readUInt8(i * 3), - data.readUInt8(i * 3 + 1), - data.readUInt8(i * 3 + 2 ), - 0xff - ]); - } + for (var i = 0; i < entries; i++) { + this._palette.push([ + data.readUInt8(i * 3), + data.readUInt8(i * 3 + 1), + data.readUInt8(i * 3 + 2 ), + 0xff + ]); + } - this._handleChunkEnd(); + this._handleChunkEnd(); }; Parser.prototype._handleTRNS = function(length) { - this.read(length, this._parseTRNS.bind(this)); + this.read(length, this._parseTRNS.bind(this)); }; Parser.prototype._parseTRNS = function(data) { - this._crc.write(data); + this._crc.write(data); - // palette - if (this._colorType == 3) { - if (this._palette.length == 0) { - this.emit('error', new Error('Transparency chunk must be after palette')); - return; - } - if (data.length > this._palette.length) { - this.emit('error', new Error('More transparent colors than palette size')); - return; - } - for (var i = 0; i < this._palette.length; i++) { - this._palette[i][3] = i < data.length ? data.readUInt8(i) : 0xff; - } + // palette + if (this._colorType == 3) { + if (this._palette.length == 0) { + this.error(new Error('Transparency chunk must be after palette')); + return; } + if (data.length > this._palette.length) { + this.error(new Error('More transparent colors than palette size')); + return; + } + for (var i = 0; i < this._palette.length; i++) { + this._palette[i][3] = i < data.length ? data.readUInt8(i) : 0xff; + } + } - // for colorType 0 (grayscale) and 2 (rgb) - // there might be one gray/color defined as transparent + // for colorType 0 (grayscale) and 2 (rgb) + // there might be one gray/color defined as transparent - this._handleChunkEnd(); + this._handleChunkEnd(); }; Parser.prototype._handleGAMA = function(length) { - this.read(length, this._parseGAMA.bind(this)); + this.read(length, this._parseGAMA.bind(this)); }; Parser.prototype._parseGAMA = function(data) { - this._crc.write(data); - this.emit('gamma', data.readUInt32BE(0) / 100000); + this._crc.write(data); + this.gamma(data.readUInt32BE(0) / 100000); - this._handleChunkEnd(); + this._handleChunkEnd(); }; Parser.prototype._handleIDAT = function(length) { - this.read(-length, this._parseIDAT.bind(this, length)); + this.read(-length, this._parseIDAT.bind(this, length)); }; Parser.prototype._parseIDAT = function(length, data) { - this._crc.write(data); + this._crc.write(data); - if (this._colorType == 3 && this._palette.length == 0) - throw new Error('Expected palette not found'); + if (this._colorType == 3 && this._palette.length == 0) + throw new Error('Expected palette not found'); - if (!this._inflate) { - this._inflate = zlib.createInflate(); + this.inflateData(data); + length -= data.length; - this._inflate.on('error', this.emit.bind(this, 'error')); - this._filter.on('complete', this._reverseFiltered.bind(this)); - - this._inflate.pipe(this._filter); - } - - this._inflate.write(data); - length -= data.length; - - if (length > 0) - this._handleIDAT(length); - else - this._handleChunkEnd(); + if (length > 0) + this._handleIDAT(length); + else + this._handleChunkEnd(); }; Parser.prototype._handleIEND = function(length) { - this.read(length, this._parseIEND.bind(this)); + this.read(length, this._parseIEND.bind(this)); }; Parser.prototype._parseIEND = function(data) { - this._crc.write(data); + this._crc.write(data); - // no more data to inflate - this._inflate.end(); + this._hasIEND = true; + this._handleChunkEnd(); - this._hasIEND = true; - this._handleChunkEnd(); + this.finished(); }; +Parser.prototype.reverseFiltered = function(data, width, height) { -var colorTypeToBppMap = { - 0: 1, - 2: 3, - 3: 1, - 4: 2, - 6: 4 -}; + if (this._colorType == 3) { // paletted -Parser.prototype._reverseFiltered = function(data, width, height) { + // use values from palette + var pxLineLength = width << 2; - if (this._colorType == 3) { // paletted + for (var y = 0; y < height; y++) { + var pxRowPos = y * pxLineLength; - // use values from palette - var pxLineLength = width << 2; + for (var x = 0; x < width; x++) { + var pxPos = pxRowPos + (x << 2), + color = this._palette[data[pxPos]]; - for (var y = 0; y < height; y++) { - var pxRowPos = y * pxLineLength; - - for (var x = 0; x < width; x++) { - var pxPos = pxRowPos + (x << 2), - color = this._palette[data[pxPos]]; - - for (var i = 0; i < 4; i++) - data[pxPos + i] = color[i]; - } - } + for (var i = 0; i < 4; i++) + data[pxPos + i] = color[i]; + } } - - this.emit('parsed', data); + } + return data; }; + diff --git a/lib/png-sync.js b/lib/png-sync.js new file mode 100644 index 0000000..3e10647 --- /dev/null +++ b/lib/png-sync.js @@ -0,0 +1,42 @@ +// Copyright (c) 2012 Kuba Niegowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + + +var Parser = require('./parser-sync'); + + +exports.read = function(buffer, options) { + + options = options || {}; + var parser = new Parser(buffer, options); + + if (parser.err) { + throw parser.err; + } + + return { + data: parser.data, + width: parser._width, + height: parser._height, + gamma: parser.gamma || 0 + }; +}; diff --git a/lib/png.js b/lib/png.js index c2c56c9..95b1542 100755 --- a/lib/png.js +++ b/lib/png.js @@ -23,8 +23,9 @@ var util = require('util'), Stream = require('stream'), - Parser = require('./parser'), - Packer = require('./packer'); + Parser = require('./parser-async'), + Packer = require('./packer'), + PNGSync = require('./png-sync'); var PNG = exports.PNG = function(options) { @@ -34,7 +35,7 @@ var PNG = exports.PNG = function(options) { this.width = options.width || 0; this.height = options.height || 0; - + this.data = this.width > 0 && this.height > 0 ? new Buffer(4 * this.width * this.height) : null; @@ -63,6 +64,7 @@ var PNG = exports.PNG = function(options) { }; util.inherits(PNG, Stream); +PNG.sync = PNGSync; PNG.prototype.pack = function() { @@ -125,25 +127,46 @@ PNG.prototype._handleClose = function() { this.emit('close'); }; - -PNG.prototype.bitblt = function(dst, sx, sy, w, h, dx, dy) { - - var src = this; +PNG.bitblt = function(src, dst, sx, sy, w, h, dx, dy) { if (sx > src.width || sy > src.height - || sx + w > src.width || sy + h > src.height) + || sx + w > src.width || sy + h > src.height) throw new Error('bitblt reading outside image'); if (dx > dst.width || dy > dst.height - || dx + w > dst.width || dy + h > dst.height) + || dx + w > dst.width || dy + h > dst.height) throw new Error('bitblt writing outside image'); for (var y = 0; y < h; y++) { src.data.copy(dst.data, - ((dy + y) * dst.width + dx) << 2, - ((sy + y) * src.width + sx) << 2, - ((sy + y) * src.width + sx + w) << 2 + ((dy + y) * dst.width + dx) << 2, + ((sy + y) * src.width + sx) << 2, + ((sy + y) * src.width + sx + w) << 2 ); } +}; + +PNG.prototype.bitblt = function(dst, sx, sy, w, h, dx, dy) { + + PNG.bitblt(this, dst, sx, sy, w, h, dx, dy); return this; }; + +PNG.adjustGamma = function(src) { + if (src.gamma) { + for (var y = 0; y < src.height; y++) { + for (var x = 0; x < src.width; x++) { + var idx = (src.width * y + x) << 2; + + for (var i = 0; i < 3; i++) { + var sample = src.data[idx + i] / 255; + sample = Math.pow(sample, 1 / 2.2 / src.gamma); + src.data[idx + i] = Math.round(sample * 255); + } + } + } + src.gamma = 0; + } +}; + +PNG.prototype.adjustGamma = function() { PNG.adjustGamma(this); }; \ No newline at end of file diff --git a/lib/sync-reader.js b/lib/sync-reader.js new file mode 100644 index 0000000..bfc8e09 --- /dev/null +++ b/lib/sync-reader.js @@ -0,0 +1,70 @@ +// Copyright (c) 2012 Kuba Niegowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + +var SyncReader = module.exports = function (buffer) { + + this._buffer = buffer; + this._reads = []; +}; + +SyncReader.prototype.read = function (length, callback) { + + this._reads.push({ + length: Math.abs(length), // if length < 0 then at most this length + allowLess: length < 0, + func: callback + }); +}; + +SyncReader.prototype.process = function () { + + // as long as there is any data and read requests + while (this._reads.length > 0 && this._buffer.length) { + + var read = this._reads[0]; + + if (this._buffer.length && (this._buffer.length >= read.length || read.allowLess)) { + + // ok there is any data so that we can satisfy this request + this._reads.shift(); // == read + + var buf = this._buffer; + + this._buffer = buf.slice(read.length); + + read.func.call(this, buf.slice(0, read.length)); + + } else { + break; + } + + } + + if (this._reads.length > 0) { + return new Error('There are some read requests waitng on finished stream'); + } + + if (this._buffer.length > 0) { + return new Error('unrecognised content at end of stream'); + } + +}; diff --git a/test/in/large.png b/test/in/large.png new file mode 100644 index 0000000..1f1f382 Binary files /dev/null and b/test/in/large.png differ diff --git a/test/list.html b/test/list.html index 392cf4a..a283102 100644 --- a/test/list.html +++ b/test/list.html @@ -11,175 +11,175 @@

Filtering

- filter changing per scanline, grayscale, 4 bit
- no filtering, colour, 8 bit
- no filtering, grayscale, 8 bit
- filter 3, colour, 8 bit
- filter 3, grayscale, 8 bit
- filter 2, colour, 8 bit
- filter 2, grayscale, 8 bit
- filter 1, colour, 8 bit
- filter 1, grayscale, 8 bit
- filter 0, colour, 8 bit
- filter 0, grayscale, 8 bit
+ filter changing per scanline, grayscale, 4 bit
+ no filtering, colour, 8 bit
+ no filtering, grayscale, 8 bit
+ filter 3, colour, 8 bit
+ filter 3, grayscale, 8 bit
+ filter 2, colour, 8 bit
+ filter 2, grayscale, 8 bit
+ filter 1, colour, 8 bit
+ filter 1, grayscale, 8 bit
+ filter 0, colour, 8 bit
+ filter 0, grayscale, 8 bit

Ancilary chunks

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Basic

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

ZLib

-
-
-
-
+
+
+
+

Transparency

-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Sizing

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Pallettes

-
-
-
-
-
-
+
+
+
+
+
+

Chunk Ordering

-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+

Gamma

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/outsync/.gitignore b/test/outsync/.gitignore new file mode 100644 index 0000000..e33609d --- /dev/null +++ b/test/outsync/.gitignore @@ -0,0 +1 @@ +*.png diff --git a/test/test.js b/test/test.js index dad6869..53b7a40 100644 --- a/test/test.js +++ b/test/test.js @@ -2,7 +2,6 @@ var fs = require('fs'), PNG = require('../lib/png').PNG; - fs.readdir(__dirname + '/in/', function(err, files) { if (err) throw err; @@ -20,6 +19,19 @@ fs.readdir(__dirname + '/in/', function(err, files) { expectedError = true; } + if (!expectedError) { + var data = fs.readFileSync(__dirname + '/in/' + file); + var png = PNG.sync.read(data); + + var outpng = new PNG(); + PNG.adjustGamma(png); + outpng.data = png.data; + outpng.width = png.width; + outpng.height = png.height; + outpng.pack() + .pipe(fs.createWriteStream(__dirname + '/outsync/' + file)); + } + fs.createReadStream(__dirname + '/in/' + file) .pipe(new PNG()) .on('error', function(err) { @@ -33,22 +45,10 @@ fs.readdir(__dirname + '/in/', function(err, files) { console.log("Error expected, parsed fine", file); } - if (this.gamma) { - for (var y = 0; y < this.height; y++) { - for (var x = 0; x < this.width; x++) { - var idx = (this.width * y + x) << 2; - - for (var i = 0; i < 3; i++) { - var sample = this.data[idx + i] / 255; - sample = Math.pow(sample, 1 / 2.2 / this.gamma); - this.data[idx + i] = Math.round(sample * 255); - } - } - } - } + this.adjustGamma(); this.pack() - .pipe(fs.createWriteStream(__dirname + '/out/' + file)); + .pipe(fs.createWriteStream(__dirname + '/out/' + file)); });