Comparing version 0.2.0-alpha to 0.3.0-alpha
@@ -27,3 +27,2 @@ #!/usr/bin/env node | ||
png.pipe(fs.createWriteStream('bg.png')); | ||
png.pack(); | ||
png.pack().pipe(fs.createWriteStream(__dirname + '/bg.png')); |
@@ -9,8 +9,8 @@ | ||
for (var i = 0; i < files.length; i++) { | ||
files.forEach(function(file) { | ||
if (!files[i].match(/\.png$/i)) | ||
continue; | ||
if (!file.match(/\.png$/i)) | ||
return; | ||
fs.createReadStream(__dirname + '/img/' + files[i]) | ||
fs.createReadStream(__dirname + '/img/' + file) | ||
.pipe(new PNG()) | ||
@@ -33,5 +33,8 @@ .on('parsed', function() { | ||
this.pack(); | ||
}).pipe(fs.createWriteStream(__dirname + '/out/' + files[i])); | ||
} | ||
this.pack() | ||
.pipe(fs.createWriteStream(__dirname + '/out/' + file)); | ||
}); | ||
}); | ||
}); |
@@ -25,14 +25,18 @@ // Copyright (c) 2012 Kuba Niegowski | ||
zlib = require('zlib'), | ||
events = require('events'); | ||
ChunkStream = require('./chunkstream'); | ||
var Filter = module.exports = function(options) { | ||
events.EventEmitter.call(this); | ||
var Filter = module.exports = function(width, height, Bpp, data, options) { | ||
ChunkStream.call(this); | ||
this._width = width; | ||
this._height = height; | ||
this._Bpp = Bpp; | ||
this._data = data; | ||
this._options = options; | ||
this._line = 0; | ||
options.filterType = 'filterType' in options ? options.filterType : -1; | ||
this._width = 0; | ||
this._height = 0; | ||
this._filters = { | ||
@@ -46,87 +50,109 @@ 0: this._filterNone.bind(this), | ||
this.read(this._width * Bpp + 1, this._reverseFilterLine.bind(this)); | ||
}; | ||
util.inherits(Filter, events.EventEmitter); | ||
util.inherits(Filter, ChunkStream); | ||
Filter.prototype.prepare = function(width, height, type) { | ||
if (type != 0) | ||
throw new Error('Unsupported filter method'); | ||
this.width = width; | ||
this.height = height; | ||
var pixelBppMap = { | ||
1: { // L | ||
0: 0, | ||
1: 0, | ||
2: 0, | ||
3: 0xff, | ||
}, | ||
2: { // LA | ||
0: 0, | ||
1: 0, | ||
2: 0, | ||
3: 1 | ||
}, | ||
3: { // RGB | ||
0: 0, | ||
1: 1, | ||
2: 2, | ||
3: 0xff | ||
}, | ||
4: { // RGBA | ||
0: 0, | ||
1: 1, | ||
2: 2, | ||
3: 3 | ||
} | ||
}; | ||
Filter.prototype.unfilter = function(rawData, Bpp) { | ||
Filter.prototype._reverseFilterLine = function(rawData) { | ||
var pxLineLength = this.width << 2, | ||
rawLineLength = this.width * Bpp + 1, | ||
pxData = new Buffer(pxLineLength * this.height); | ||
var pxData = this._data, | ||
pxLineLength = this._width << 2, | ||
pxRowPos = this._line * pxLineLength, | ||
filter = rawData[0]; | ||
for (var y = 0; y < this.height; y++) { | ||
if (filter == 0) { | ||
for (var x = 0; x < this._width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = 1 + x * this._Bpp; | ||
var rawRowPos = rawLineLength * y + 1, | ||
pxRowPos = y * pxLineLength, | ||
filter = rawData[rawRowPos - 1]; | ||
for (var i = 0; i < 4; i++) { | ||
var idx = pixelBppMap[this._Bpp][i]; | ||
pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] : 0xff; | ||
} | ||
} | ||
} else if (filter == 1) { | ||
for (var x = 0; x < this._width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = 1 + x * this._Bpp; | ||
if (filter == 0) { | ||
for (var x = 0; x < this.width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = rawRowPos + x * Bpp; | ||
for (var i = 0; i < 4; i++) { | ||
var idx = pixelBppMap[this._Bpp][i], | ||
left = x > 0 ? pxData[pxPos + i - 4] : 0; | ||
for (var i = 0; i < Bpp; i++) | ||
pxData[pxPos + i] = rawData[rawPos + i]; | ||
pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + left : 0xff; | ||
} | ||
} | ||
} else if (filter == 1) { | ||
for (var x = 0; x < this.width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = rawRowPos + x * Bpp; | ||
} else if (filter == 2) { | ||
for (var x = 0; x < this._width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = 1 + x * this._Bpp; | ||
for (var i = 0; i < Bpp; i++) { | ||
var left = x > 0 ? pxData[pxPos + i - 4] : 0; | ||
pxData[pxPos + i] = rawData[rawPos + i] + left; | ||
} | ||
for (var i = 0; i < 4; i++) { | ||
var idx = pixelBppMap[this._Bpp][i], | ||
up = this._line > 0 ? pxData[pxPos - pxLineLength + i] : 0; | ||
pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + up : 0xff; | ||
} | ||
} else if (filter == 2) { | ||
for (var x = 0; x < this.width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = rawRowPos + x * Bpp; | ||
} | ||
for (var i = 0; i < Bpp; i++) { | ||
var up = y > 0 ? pxData[pxPos - pxLineLength + i] : 0; | ||
pxData[pxPos + i] = rawData[rawPos + i] + up; | ||
} | ||
} | ||
} else if (filter == 3) { | ||
for (var x = 0; x < this._width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = 1 + x * this._Bpp; | ||
} else if (filter == 3) { | ||
for (var x = 0; x < this.width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = rawRowPos + x * Bpp; | ||
for (var i = 0; i < 4; i++) { | ||
var idx = pixelBppMap[this._Bpp][i], | ||
left = x > 0 ? pxData[pxPos + i - 4] : 0, | ||
up = this._line > 0 ? pxData[pxPos - pxLineLength + i] : 0, | ||
add = Math.floor((left + up) / 2); | ||
for (var i = 0; i < Bpp; i++) { | ||
var left = x > 0 ? pxData[pxPos + i - 4] : 0, | ||
up = y > 0 ? pxData[pxPos - pxLineLength + i] : 0; | ||
pxData[pxPos + i] = rawData[rawPos + i] | ||
+ Math.floor((left + up) / 2); | ||
} | ||
pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + add : 0xff; | ||
} | ||
} else if (filter == 4) { | ||
for (var x = 0; x < this.width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = rawRowPos + x * Bpp; | ||
} | ||
for (var i = 0; i < Bpp; i++) { | ||
var left = x > 0 ? pxData[pxPos + i - 4] : 0, | ||
up = y > 0 ? pxData[pxPos - pxLineLength + i] : 0, | ||
upLeft = x > 0 && y > 0 | ||
? pxData[pxPos - pxLineLength + i - 4] : 0; | ||
} else if (filter == 4) { | ||
for (var x = 0; x < this._width; x++) { | ||
var pxPos = pxRowPos + (x << 2), | ||
rawPos = 1 + x * this._Bpp; | ||
pxData[pxPos + i] = rawData[rawPos + i] | ||
+ PaethPredictor(left, up, upLeft) | ||
} | ||
for (var i = 0; i < 4; i++) { | ||
var idx = pixelBppMap[this._Bpp][i], | ||
left = x > 0 ? pxData[pxPos + i - 4] : 0, | ||
up = this._line > 0 ? pxData[pxPos - pxLineLength + i] : 0, | ||
upLeft = x > 0 && this._line > 0 | ||
? pxData[pxPos - pxLineLength + i - 4] : 0, | ||
add = PaethPredictor(left, up, upLeft); | ||
pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + add : 0xff; | ||
} | ||
@@ -136,42 +162,21 @@ } | ||
// expand data to 32 bit | ||
for (var y = 0; y < this.height; y++) { | ||
var pxRowPos = y * pxLineLength; | ||
if (Bpp == 1) { // L | ||
for (var x = 0; x < this.width; x++) { | ||
var pxPos = pxRowPos + (x << 2); | ||
this._line++; | ||
pxData[pxPos + 1] = pxData[pxPos + 2] = pxData[pxPos]; | ||
pxData[pxPos + 3] = 0xff; | ||
} | ||
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); | ||
}; | ||
} else if (Bpp == 2) { // LA | ||
for (var x = 0; x < this.width; x++) { | ||
var pxPos = pxRowPos + (x << 2); | ||
pxData[pxPos + 3] = pxData[pxPos + 1]; | ||
pxData[pxPos + 1] = pxData[pxPos + 2] = pxData[pxPos]; | ||
} | ||
} else if (Bpp == 3) { // RGB | ||
for (var x = 0; x < this.width; x++) { | ||
var pxPos = pxRowPos + (x << 2); | ||
pxData[pxPos + 3] = 0xff; | ||
} | ||
Filter.prototype.filter = function() { | ||
} // else RGBA | ||
} | ||
var pxData = this._data, | ||
rawData = new Buffer(((this._width << 2) + 1) * this._height); | ||
return pxData; | ||
}; | ||
for (var y = 0; y < this._height; y++) { | ||
Filter.prototype.filter = function(pxData, width, height) { | ||
var rawData = new Buffer(((width << 2) + 1) * height); | ||
for (var y = 0; y < height; y++) { | ||
// find best filter for this line (with lowest sum of values) | ||
@@ -183,3 +188,3 @@ if (this._options.filterType == -1) { | ||
for (var f in this._filters) { | ||
var sum = this._filters[f](pxData, y, width, height, null); | ||
var sum = this._filters[f](pxData, y, null); | ||
if (sum < min) { | ||
@@ -194,3 +199,3 @@ sel = f; | ||
} | ||
this._filters[sel](pxData, y, width, height, rawData); | ||
this._filters[sel](pxData, y, rawData); | ||
} | ||
@@ -200,5 +205,5 @@ return rawData; | ||
Filter.prototype._filterNone = function(pxData, y, width, height, rawData) { | ||
Filter.prototype._filterNone = function(pxData, y, rawData) { | ||
var pxRowLength = width << 2, | ||
var pxRowLength = this._width << 2, | ||
rawRowLength = pxRowLength + 1, | ||
@@ -219,5 +224,5 @@ sum = 0; | ||
Filter.prototype._filterSub = function(pxData, y, width, height, rawData) { | ||
Filter.prototype._filterSub = function(pxData, y, rawData) { | ||
var pxRowLength = width << 2, | ||
var pxRowLength = this._width << 2, | ||
rawRowLength = pxRowLength + 1, | ||
@@ -240,5 +245,5 @@ sum = 0; | ||
Filter.prototype._filterUp = function(pxData, y, width, height, rawData) { | ||
Filter.prototype._filterUp = function(pxData, y, rawData) { | ||
var pxRowLength = width << 2, | ||
var pxRowLength = this._width << 2, | ||
rawRowLength = pxRowLength + 1, | ||
@@ -261,5 +266,5 @@ sum = 0; | ||
Filter.prototype._filterAvg = function(pxData, y, width, height, rawData) { | ||
Filter.prototype._filterAvg = function(pxData, y, rawData) { | ||
var pxRowLength = width << 2, | ||
var pxRowLength = this._width << 2, | ||
rawRowLength = pxRowLength + 1, | ||
@@ -283,5 +288,5 @@ sum = 0; | ||
Filter.prototype._filterPaeth = function(pxData, y, width, height, rawData) { | ||
Filter.prototype._filterPaeth = function(pxData, y, rawData) { | ||
var pxRowLength = width << 2, | ||
var pxRowLength = this._width << 2, | ||
rawRowLength = pxRowLength + 1, | ||
@@ -288,0 +293,0 @@ sum = 0; |
@@ -25,23 +25,11 @@ // Copyright (c) 2012 Kuba Niegowski | ||
var util = require('util'), | ||
Stream = require('stream'), | ||
Compress = require('./compress'), | ||
zlib = require('zlib'), | ||
CrcStream = require('./crc'), | ||
ChunkStream = require('./chunkstream'), | ||
constants = require('./constants'), | ||
Filter = require('./filter'); | ||
var signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; | ||
var TYPE_IHDR = 0x49484452; | ||
var TYPE_IEND = 0x49454e44; | ||
var TYPE_IDAT = 0x49444154; | ||
var TYPE_PLTE = 0x504c5445; | ||
var TYPE_tRNS = 0x74524e53; | ||
var TYPE_gAMA = 0x67414d41; | ||
var COLOR_PALETTE = 1; | ||
var COLOR_COLOR = 2; | ||
var COLOR_ALPHA = 4; | ||
var Parser = module.exports = function(options) { | ||
Stream.call(this); | ||
ChunkStream.call(this); | ||
@@ -54,3 +42,7 @@ this._options = options; | ||
// input flags | ||
this._inflate = null; | ||
this._filter = null; | ||
this._crc = null; | ||
// input flags/metadata | ||
this._palette = []; | ||
@@ -60,181 +52,122 @@ this._colorType = 0; | ||
this._chunks = {}; | ||
this._chunks[TYPE_IHDR] = this._parseIHDR.bind(this); | ||
this._chunks[TYPE_IEND] = this._parseIEND.bind(this); | ||
this._chunks[TYPE_IDAT] = this._parseIDAT.bind(this); | ||
this._chunks[TYPE_PLTE] = this._parsePLTE.bind(this); | ||
this._chunks[TYPE_tRNS] = this._parseTRNS.bind(this); | ||
this._chunks[TYPE_gAMA] = this._parseGAMA.bind(this); | ||
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._compress = new Compress(options); | ||
this._filter = new Filter(options); | ||
this.writable = true; | ||
this._initCompress(); | ||
this.on('error', this._handleError.bind(this)); | ||
this._handleSignature(); | ||
}; | ||
util.inherits(Parser, Stream); | ||
util.inherits(Parser, ChunkStream); | ||
Parser.prototype._initCompress = function() { | ||
this._compress.on('error', this.emit.bind(this, 'error')); | ||
Parser.prototype._handleError = function() { | ||
this._compress.on('deflated', this._packData.bind(this)); | ||
this._compress.on('inflated', this._unfilter.bind(this)); | ||
}; | ||
this.writable = false; | ||
Parser.prototype._parse = function(data) { | ||
this.destroy(); | ||
var idx = 0; | ||
try { | ||
// check PNG file signature | ||
while (idx < signature.length) { | ||
if (data[idx] != signature[idx]) { | ||
throw new Error('Invalid file signature'); | ||
} | ||
idx++; | ||
} | ||
//console.log('Signature is ok'); | ||
// iterate chunks | ||
while (idx < data.length) { | ||
idx = this._parseChunk(data, idx); | ||
} | ||
} | ||
catch(err) { | ||
this.emit('error', err); | ||
} | ||
if (this._inflate) | ||
this._inflate.destroy(); | ||
}; | ||
Parser.prototype._pack = function(width, height, data) { | ||
// Signature | ||
this.emit('data', new Buffer(signature)); | ||
this.emit('data', this._packIHDR(width, height)); | ||
// filter pixel data | ||
var data = this._filter.filter(data, width, height); | ||
// compress it | ||
this._compress.deflate(data); | ||
Parser.prototype._handleSignature = function() { | ||
this.read(constants.PNG_SIGNATURE.length, | ||
this._parseSignature.bind(this) | ||
); | ||
}; | ||
Parser.prototype._packData = function(data) { | ||
Parser.prototype._parseSignature = function(data) { | ||
// console.log('deflate', data.length); | ||
var signature = constants.PNG_SIGNATURE; | ||
this.emit('data', this._packIDAT(data)); | ||
this.emit('data', this._packIEND()); | ||
this.emit('end'); | ||
}; | ||
Parser.prototype._unfilter = function(data) { | ||
// expand data to 32 bit depending on colorType | ||
if (this._colorType == 0) { // L | ||
data = this._filter.unfilter(data, 1); // 1 Bpp | ||
} else if (this._colorType == 2) { // RGB | ||
data = this._filter.unfilter(data, 3); // 3 Bpp | ||
} else if (this._colorType == 3) { // I | ||
data = this._filter.unfilter(data, 1); // 1 Bpp | ||
// use values fom palette | ||
var pxLineLength = this.width << 2; | ||
for (var y = 0; y < this.height; y++) { | ||
var pxRowPos = y * pxLineLength; | ||
for (var x = 0; x < this.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 < signature.length; i++) { | ||
if (data[i] != signature[i]) { | ||
this.emit('error', new Error('Invalid file signature')); | ||
return; | ||
} | ||
} else if (this._colorType == 4) { // LA | ||
data = this._filter.unfilter(data, 2); // 2 Bpp | ||
} else if (this._colorType == 6) { // RGBA | ||
data = this._filter.unfilter(data, 4); // 4 Bpp | ||
} else throw new Error('Unsupported color type'); | ||
this.emit('parsed', data); | ||
} | ||
this.read(8, this._parseChunkBegin.bind(this)); | ||
}; | ||
Parser.prototype._parseChunkBegin = function(data) { | ||
// chunk content length | ||
var length = data.readUInt32BE(0); | ||
Parser.prototype._parseChunk = function(data, idx) { | ||
if (this._hasIEND) | ||
throw new Error('Not expected chunk after IEND'); | ||
// chunk size (only content) | ||
var length = data.readUInt32BE(idx); | ||
idx += 4; | ||
// chunk type | ||
var type = data.readUInt32BE(idx), | ||
ancillary = !!(data[idx] & 0x20), // or critical | ||
priv = !!(data[idx+1] & 0x20), // or public | ||
safeToCopy = !!(data[idx+3] & 0x20), // or unsafe | ||
var type = data.readUInt32BE(4), | ||
name = ''; | ||
for (var i = 0; i < 4; i++) | ||
name += String.fromCharCode(data[idx+i]); | ||
idx += 4; | ||
for (var i = 4; i < 8; i++) | ||
name += String.fromCharCode(data[i]); | ||
// console.log('chunk ', name, length); | ||
// calc CRC (of chunk type and content) | ||
var calcCrc = crc32(data.slice(idx - 4, idx + length)), | ||
content = data.slice(idx, idx + length); | ||
idx += length; | ||
// chunk flags | ||
var ancillary = !!(data[4] & 0x20), // or critical | ||
priv = !!(data[5] & 0x20), // or public | ||
safeToCopy = !!(data[7] & 0x20); // or unsafe | ||
// read CRC | ||
var fileCrc = data.readInt32BE(idx); | ||
idx += 4; | ||
if (!this._hasIHDR && type != constants.TYPE_IHDR) { | ||
this.emit('error', new Error('Expected IHDR on beggining')); | ||
return; | ||
} | ||
// and check CRC | ||
if (this._options.checkCRC && calcCrc != fileCrc) | ||
throw new Error('Crc error'); | ||
this._crc = new CrcStream(); | ||
this._crc.write(new Buffer(name)); | ||
if (!this._hasIHDR && type != TYPE_IHDR) | ||
throw new Error('Expected IHDR on beggining'); | ||
if (this._chunks[type]) { | ||
this._chunks[type](content); | ||
return this._chunks[type](length); | ||
} else if (!ancillary) | ||
throw new Error('Unsupported critical chunk type ' + name); | ||
// else | ||
// console.log('Ignoring chunk', name, type.toString(16)); | ||
} else if (!ancillary) { | ||
this.emit('error', new Error('Unsupported critical chunk type ' + name)); | ||
return; | ||
} else { | ||
this.read(length + 4, this._skipChunk.bind(this)); | ||
} | ||
}; | ||
return idx; | ||
Parser.prototype._skipChunk = function(data) { | ||
this.read(8, this._parseChunkBegin.bind(this)); | ||
}; | ||
Parser.prototype._packChunk = function(type, data) { | ||
Parser.prototype._handleChunkEnd = function() { | ||
this.read(4, this._parseChunkEnd.bind(this)); | ||
}; | ||
var len = (data ? data.length : 0), | ||
buf = new Buffer(len + 12); | ||
Parser.prototype._parseChunkEnd = function(data) { | ||
buf.writeUInt32BE(len, 0); | ||
buf.writeUInt32BE(type, 4); | ||
var fileCrc = data.readInt32BE(0), | ||
calcCrc = this._crc.crc32(); | ||
if (data) data.copy(buf, 8); | ||
// check CRC | ||
if (this._options.checkCRC && calcCrc != fileCrc) { | ||
this.emit('error', new Error('Crc error')); | ||
return; | ||
} | ||
buf.writeInt32BE(crc32(buf.slice(4, buf.length - 4)), buf.length - 4); | ||
return buf; | ||
if (this._hasIEND) { | ||
this.destroySoon(); | ||
} else { | ||
this.read(8, this._parseChunkBegin.bind(this)); | ||
} | ||
}; | ||
Parser.prototype._handleIHDR = function(length) { | ||
this.read(length, this._parseIHDR.bind(this)); | ||
}; | ||
Parser.prototype._parseIHDR = function(data) { | ||
this._crc.write(data); | ||
var width = data.readUInt32BE(0), | ||
height = data.readUInt32BE(4), | ||
depth = data[8], | ||
colorType = data[9], // 1 palette, 2 color, 4 alpha | ||
colorType = data[9], // bits: 1 palette, 2 color, 4 alpha | ||
compr = data[10], | ||
@@ -249,34 +182,55 @@ filter = data[11], | ||
if (depth != 8) | ||
throw new Error('Unsupported bit depth ' + depth); | ||
if (interlace != 0) | ||
throw new Error('Unsupported interlace method'); | ||
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; | ||
} | ||
this._colorType = colorType; | ||
this._compress.prepareInflate(compr); | ||
this._filter.prepare(width, height, filter); | ||
this._data = new Buffer(width * height * 4); | ||
this._filter = new Filter( | ||
width, height, | ||
colorTypeToBppMap[this._colorType], | ||
this._data, | ||
this._options | ||
); | ||
this._hasIHDR = true; | ||
this.emit('metadata', width, height); | ||
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._handleChunkEnd(); | ||
}; | ||
Parser.prototype._packIHDR = function(width, height) { | ||
var buf = new Buffer(13); | ||
buf.writeUInt32BE(width, 0); | ||
buf.writeUInt32BE(height, 4); | ||
buf[8] = 8; | ||
buf[9] = 6; // colorType | ||
buf[10] = 0; // compression | ||
buf[11] = 0; // filter | ||
buf[12] = 0; // interlace | ||
return this._packChunk(TYPE_IHDR, buf); | ||
Parser.prototype._handlePLTE = function(length) { | ||
this.read(length, this._parsePLTE.bind(this)); | ||
}; | ||
Parser.prototype._parsePLTE = function(data) { | ||
this._crc.write(data); | ||
Parser.prototype._parsePLTE = function(data) { | ||
var entries = Math.floor(data.length / 3); | ||
@@ -293,16 +247,26 @@ // console.log('Palette:', entries); | ||
} | ||
this._handleChunkEnd(); | ||
}; | ||
Parser.prototype._handleTRNS = function(length) { | ||
this.read(length, this._parseTRNS.bind(this)); | ||
}; | ||
Parser.prototype._parseTRNS = function(data) { | ||
this._crc.write(data); | ||
// palette | ||
if (this._colorType == 3) { | ||
if (this._palette.length == 0) | ||
throw new Error('Transparency chunk must be after palette'); | ||
if (data.length > this._palette.length) | ||
throw new Error('More transparent colors than palette size'); | ||
for (var i = 0; i < this._palette.length; i++) | ||
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; | ||
} | ||
} | ||
@@ -312,63 +276,90 @@ | ||
// there might be one gray/color defined as transparent | ||
this._handleChunkEnd(); | ||
}; | ||
Parser.prototype._handleGAMA = function(length) { | ||
this.read(length, this._parseGAMA.bind(this)); | ||
}; | ||
Parser.prototype._parseGAMA = function(data) { | ||
this._crc.write(data); | ||
this.emit('gamma', data.readUInt32BE(0) / 100000); | ||
this._handleChunkEnd(); | ||
}; | ||
Parser.prototype._parseIDAT = function(data) { | ||
Parser.prototype._handleIDAT = function(length) { | ||
this.read(-length, this._parseIDAT.bind(this, length)); | ||
}; | ||
Parser.prototype._parseIDAT = function(length, data) { | ||
this._crc.write(data); | ||
if (this._colorType == 3 && this._palette.length == 0) | ||
throw new Error('Expected palette not found'); | ||
this._compress.writeInflate(data); | ||
}; | ||
if (!this._inflate) { | ||
this._inflate = zlib.createInflate(); | ||
Parser.prototype._packIDAT = function(data) { | ||
return this._packChunk(TYPE_IDAT, data); | ||
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(); | ||
}; | ||
Parser.prototype._handleIEND = function(length) { | ||
this.read(length, this._parseIEND.bind(this)); | ||
}; | ||
Parser.prototype._parseIEND = function(data) { | ||
this._crc.write(data); | ||
// no more data to inflate | ||
this._compress.endInflate(); | ||
this._inflate.end(); | ||
this._hasIEND = true; | ||
this._handleChunkEnd(); | ||
}; | ||
Parser.prototype._packIEND = function() { | ||
return this._packChunk(TYPE_IEND, null); | ||
var colorTypeToBppMap = { | ||
0: 1, | ||
2: 3, | ||
3: 1, | ||
4: 2, | ||
6: 4 | ||
}; | ||
Parser.prototype._reverseFiltered = function(data, width, height) { | ||
if (this._colorType == 3) { // paletted | ||
// use values from palette | ||
var pxLineLength = width << 2; | ||
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]]; | ||
// prepare crc table as in PNG Specification | ||
var crcTable = []; | ||
for (var i = 0; i < 256; i++) { | ||
var c = i; | ||
for (var j = 0; j < 8; j++) { | ||
if (c & 1) { | ||
c = 0xedb88320 ^ (c >>> 1); | ||
} else { | ||
c = c >>> 1; | ||
for (var i = 0; i < 4; i++) | ||
data[pxPos + i] = color[i]; | ||
} | ||
} | ||
} | ||
crcTable[i] = c; | ||
} | ||
function crc32(buf) { | ||
var crc = -1; | ||
for (var i = 0; i < buf.length; i++) { | ||
crc = crcTable[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); | ||
} | ||
return crc ^ -1; | ||
} | ||
this.emit('parsed', data); | ||
}; |
@@ -25,8 +25,12 @@ // Copyright (c) 2012 Kuba Niegowski | ||
var util = require('util'), | ||
Parser = require('./parser'); | ||
Stream = require('stream'), | ||
Parser = require('./parser'), | ||
Packer = require('./packer'); | ||
var PNG = exports.PNG = function(options) { | ||
Parser.call(this, options = options || {}); | ||
Stream.call(this); | ||
options = options || {}; | ||
this.width = options.width || 0; | ||
@@ -39,19 +43,31 @@ this.height = options.height || 0; | ||
this.gamma = 0; | ||
this.readable = this.writable = true; | ||
this._buffers = []; | ||
this._buffLen = 0; | ||
this.on('metadata', this._metadata.bind(this)); | ||
this.on('gamma', this._gamma.bind(this)); | ||
this._parser = new Parser(options || {}); | ||
this.on('parsed', function(data) { | ||
this._parser.on('error', this.emit.bind(this, 'error')); | ||
this._parser.on('close', this._handleClose.bind(this)); | ||
this._parser.on('metadata', this._metadata.bind(this)); | ||
this._parser.on('gamma', this._gamma.bind(this)); | ||
this._parser.on('parsed', function(data) { | ||
this.data = data; | ||
this.emit('parsed', data); | ||
}.bind(this)); | ||
this._packer = new Packer(options); | ||
this._packer.on('data', this.emit.bind(this, 'data')); | ||
this._packer.on('end', this.emit.bind(this, 'end')); | ||
this._parser.on('close', this._handleClose.bind(this)); | ||
this._packer.on('error', this.emit.bind(this, 'error')); | ||
}; | ||
util.inherits(PNG, Parser); | ||
util.inherits(PNG, Stream); | ||
PNG.prototype.pack = function() { | ||
this._pack(this.width, this.height, this.data); | ||
process.nextTick(function() { | ||
this._packer.pack(this.data, this.width, this.height); | ||
}.bind(this)); | ||
return this; | ||
@@ -81,3 +97,3 @@ }; | ||
this._parse(data); | ||
this.end(data); | ||
return this; | ||
@@ -87,4 +103,3 @@ }; | ||
PNG.prototype.write = function(data) { | ||
this._buffers.push(data); | ||
this._buffLen += data.length; | ||
this._parser.write(data); | ||
return true; | ||
@@ -94,12 +109,12 @@ }; | ||
PNG.prototype.end = function(data) { | ||
if (data) this.write(data); | ||
this._parse(Buffer.concat(this._buffers, this._buffLen)); | ||
this._buffers = []; | ||
this._buffLen = 0; | ||
this._parser.end(data); | ||
}; | ||
PNG.prototype._metadata = function(width, height) { | ||
this.width = width; | ||
this.height = height; | ||
this.data = null; | ||
PNG.prototype._metadata = function(metadata) { | ||
this.width = metadata.width; | ||
this.height = metadata.height; | ||
this.data = metadata.data; | ||
delete metadata.data; | ||
this.emit('metadata', metadata); | ||
}; | ||
@@ -111,2 +126,8 @@ | ||
PNG.prototype._handleClose = function() { | ||
if (!this._parser.writable && !this._packer.readable) | ||
this.emit('close'); | ||
}; | ||
PNG.prototype.bitblt = function(dst, sx, sy, w, h, dx, dy) { | ||
@@ -113,0 +134,0 @@ |
{ | ||
"name": "pngjs", | ||
"version": "0.2.0-alpha", | ||
"version": "0.3.0-alpha", | ||
"description": "Simple PNG encoder/decoder", | ||
@@ -5,0 +5,0 @@ "author": "Kuba Niegowski", |
@@ -17,28 +17,24 @@ About | ||
var png = new PNG({ | ||
filterType: 4 | ||
}); | ||
fs.createReadStream('in.png') | ||
.pipe(new PNG({ | ||
filterType: 4 | ||
})) | ||
.on('parsed', function() { | ||
png.on('parsed', function() { | ||
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 y = 0; y < png.height; y++) { | ||
for (var x = 0; x < png.width; x++) { | ||
var idx = (png.width * y + x) << 2; | ||
// invert color | ||
this.data[idx] = 255 - this.data[idx]; | ||
this.data[idx+1] = 255 - this.data[idx+1]; | ||
this.data[idx+2] = 255 - this.data[idx+2]; | ||
// invert color | ||
png.data[idx] = 255 - png.data[idx]; | ||
png.data[idx+1] = 255 - png.data[idx+1]; | ||
png.data[idx+2] = 255 - png.data[idx+2]; | ||
// and reduce opacity | ||
png.data[idx+3] = png.data[idx+3] >> 1; | ||
// and reduce opacity | ||
this.data[idx+3] = this.data[idx+3] >> 1; | ||
} | ||
} | ||
} | ||
png.pipe(fs.createWriteStream('out.png')); | ||
png.pack(); | ||
}); | ||
fs.createReadStream('in.png').pipe(png); | ||
this.pack().pipe(fs.createWriteStream('out.png')); | ||
}); | ||
``` | ||
@@ -58,20 +54,35 @@ For more examples see `examples` folder. | ||
## Class: PNG | ||
`PNG` is readable and writeable `Stream`. | ||
`PNG` is readable and writable `Stream`. | ||
### Options | ||
- `width` - `int` | ||
- `height` - `int` | ||
- `checkCRC` - `boolean` default: `true` | ||
- `deflateChunkSize` - `int` default: 32 kB | ||
- `deflateLevel` - `int` default: 9 | ||
- `filterType` - `int` default: -1 (auto) | ||
- `width` - use this with `height` if you want to create png from scratch | ||
- `height` - as above | ||
- `checkCRC` - whether parser should be strict about checksums in source stream (default: `true`) | ||
- `deflateChunkSize` - chunk size used for deflating data chunks, this should be power of 2 and must not be less than 256 and more than 32*1024 (default: 32 kB) | ||
- `deflateLevel` - compression level for delate (default: 9) | ||
- `filterType` - png filtering method for scanlines (default: -1 => auto) | ||
### Event "metadata" | ||
`function(metadata) { }` | ||
Image's header has been parsed, metadata contains this information: | ||
- `width` image size in pixels | ||
- `height` image size in pixels | ||
- `palette` image is paletted | ||
- `color` image is not grayscale | ||
- `alpha` image contains alpha channel | ||
### Event: "parsed" | ||
`function(data) { }` | ||
Input image has been completly parsed, `data` is complete and ready for modification. | ||
### png.pack() | ||
Starts converting data to PNG file Stream | ||
### Event: "error" | ||
`function(error) { }` | ||
### png.parse(data, [callback]) | ||
Parses PNG file data. Alternatively you can stream data to PNG. | ||
Parses PNG file data. Alternatively you can stream data to instance of PNG. | ||
@@ -81,10 +92,31 @@ Optional `callback` is once called on `error` or `parsed`. The callback gets | ||
Returns `this` for method chaining. | ||
### png.pack() | ||
Starts converting data to PNG file Stream. | ||
Returns `this` for method chaining. | ||
### png.bitblt(dst, sx, sy, w, h, dx, dy) | ||
Helper for image manipulation, copies rectangle of pixels from current image (`sx`, `sy`, `w`, `h`) to `dst` image (at `dx`, `dy`). | ||
Returns `this` for method chaining. | ||
### Property: width | ||
Width of image in pixels | ||
### Property: height | ||
Height of image in pixels | ||
### Property: data | ||
Buffer of image pixel data. Every pixel consists 4 bytes: R, G, B, A (opacity). | ||
### Property: gamma | ||
Gamma of image (0 if not specified) | ||
@@ -94,2 +126,5 @@ Changelog | ||
### 0.3.0-alpha - 23 Aug 2012 | ||
- Processing data as Streams, not complete Buffers of data | ||
### 0.2.0-alpha - 21 Aug 2012 | ||
@@ -96,0 +131,0 @@ - Input added palette, grayscale, no alpha support |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
65326
40
1023
157
1