mirror of
https://github.com/kettek/png-js
synced 2025-03-14 14:34:30 -07:00
445 lines
13 KiB
JavaScript
445 lines
13 KiB
JavaScript
|
import fs from 'fs';
|
||
|
import zlib from 'zlib';
|
||
|
import { promisify } from 'util';
|
||
|
|
||
|
var range = function range(left, right, inclusive) {
|
||
|
var range = [];
|
||
|
var ascending = left < right;
|
||
|
var end = !inclusive ? right : ascending ? right + 1 : right - 1;
|
||
|
|
||
|
for (var i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
|
||
|
range.push(i);
|
||
|
}
|
||
|
|
||
|
return range;
|
||
|
};
|
||
|
|
||
|
var classCallCheck = function (instance, Constructor) {
|
||
|
if (!(instance instanceof Constructor)) {
|
||
|
throw new TypeError("Cannot call a class as a function");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var createClass = function () {
|
||
|
function defineProperties(target, props) {
|
||
|
for (var i = 0; i < props.length; i++) {
|
||
|
var descriptor = props[i];
|
||
|
descriptor.enumerable = descriptor.enumerable || false;
|
||
|
descriptor.configurable = true;
|
||
|
if ("value" in descriptor) descriptor.writable = true;
|
||
|
Object.defineProperty(target, descriptor.key, descriptor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return function (Constructor, protoProps, staticProps) {
|
||
|
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
||
|
if (staticProps) defineProperties(Constructor, staticProps);
|
||
|
return Constructor;
|
||
|
};
|
||
|
}();
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
var toConsumableArray = function (arr) {
|
||
|
if (Array.isArray(arr)) {
|
||
|
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
|
||
|
|
||
|
return arr2;
|
||
|
} else {
|
||
|
return Array.from(arr);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var inflateAsync = promisify(zlib.inflate);
|
||
|
var readFileAsync = promisify(fs.readFile);
|
||
|
|
||
|
var IndexedPNG = function () {
|
||
|
function IndexedPNG(data) {
|
||
|
classCallCheck(this, IndexedPNG);
|
||
|
|
||
|
this.data = data;
|
||
|
this.pos = 8; // Skip the default header
|
||
|
|
||
|
this.palette = [];
|
||
|
this.imgData = [];
|
||
|
this.transparency = {};
|
||
|
this.text = {};
|
||
|
|
||
|
this.process();
|
||
|
}
|
||
|
|
||
|
createClass(IndexedPNG, [{
|
||
|
key: 'process',
|
||
|
value: function process() {
|
||
|
var _this = this;
|
||
|
|
||
|
var i = void 0;
|
||
|
while (true) {
|
||
|
var end;
|
||
|
var chunkSize = this.readUInt32();
|
||
|
var section = function () {
|
||
|
var result = [];
|
||
|
for (i = 0; i < 4; i++) {
|
||
|
result.push(String.fromCharCode(_this.data[_this.pos++]));
|
||
|
}
|
||
|
return result;
|
||
|
}().join('');
|
||
|
|
||
|
switch (section) {
|
||
|
case 'IHDR':
|
||
|
// we can grab interesting values from here (like width, height, etc)
|
||
|
this.width = this.readUInt32();
|
||
|
this.height = this.readUInt32();
|
||
|
this.bits = this.data[this.pos++];
|
||
|
this.colorType = this.data[this.pos++];
|
||
|
this.compressionMethod = this.data[this.pos++];
|
||
|
this.filterMethod = this.data[this.pos++];
|
||
|
this.interlaceMethod = this.data[this.pos++];
|
||
|
break;
|
||
|
|
||
|
case 'PLTE':
|
||
|
this.palette = this.read(chunkSize);
|
||
|
break;
|
||
|
|
||
|
case 'IDAT':
|
||
|
for (i = 0, end = chunkSize; i < end; i++) {
|
||
|
this.imgData.push(this.data[this.pos++]);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'tRNS':
|
||
|
// This chunk can only occur once and it must occur after the
|
||
|
// PLTE chunk and before the IDAT chunk.
|
||
|
this.transparency = {};
|
||
|
switch (this.colorType) {
|
||
|
case 3:
|
||
|
// Indexed color, RGB. Each byte in this chunk is an alpha for
|
||
|
// the palette index in the PLTE ("palette") chunk up until the
|
||
|
// last non-opaque entry. Set up an array, stretching over all
|
||
|
// palette entries which will be 0 (opaque) or 1 (transparent).
|
||
|
this.transparency.indexed = this.read(chunkSize);
|
||
|
//var short = 255 - this.transparency.indexed.length;
|
||
|
var short = this.transparency.indexed.length - 1;
|
||
|
if (short > 0) {
|
||
|
var asc, end1;
|
||
|
for (i = 0, end1 = short, asc = 0 <= end1; asc ? i < end1 : i > end1; asc ? i++ : i--) {
|
||
|
this.transparency.indexed.push(255);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 0:
|
||
|
// Greyscale. Corresponding to entries in the PLTE chunk.
|
||
|
// Grey is two bytes, range 0 .. (2 ^ bit-depth) - 1
|
||
|
this.transparency.grayscale = this.read(chunkSize)[0];
|
||
|
break;
|
||
|
case 2:
|
||
|
// True color with proper alpha channel.
|
||
|
this.transparency.rgb = this.read(chunkSize);
|
||
|
break;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'tEXt':
|
||
|
var text = this.read(chunkSize);
|
||
|
var index = text.indexOf(0);
|
||
|
var key = String.fromCharCode.apply(String, toConsumableArray(Array.from(text.slice(0, index) || [])));
|
||
|
this.text[key] = String.fromCharCode.apply(String, toConsumableArray(Array.from(text.slice(index + 1) || [])));
|
||
|
break;
|
||
|
|
||
|
case 'IEND':
|
||
|
// we've got everything we need!
|
||
|
this.colors = function () {
|
||
|
switch (_this.colorType) {
|
||
|
case 0:
|
||
|
case 3:
|
||
|
case 4:
|
||
|
return 1;
|
||
|
case 2:
|
||
|
case 6:
|
||
|
return 3;
|
||
|
}
|
||
|
}();
|
||
|
|
||
|
this.hasAlphaChannel = [4, 6].includes(this.colorType);
|
||
|
var colors = this.colors + (this.hasAlphaChannel ? 1 : 0);
|
||
|
this.pixelBitlength = this.bits * colors;
|
||
|
|
||
|
this.colorSpace = function () {
|
||
|
switch (_this.colors) {
|
||
|
case 1:
|
||
|
return 'DeviceGray';
|
||
|
case 3:
|
||
|
return 'DeviceRGB';
|
||
|
}
|
||
|
}();
|
||
|
|
||
|
this.imgData = Buffer.from(this.imgData);
|
||
|
return;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
// unknown (or unimportant) section, skip it
|
||
|
this.pos += chunkSize;
|
||
|
}
|
||
|
|
||
|
this.pos += 4; // Skip the CRC
|
||
|
|
||
|
if (this.pos > this.data.length) {
|
||
|
throw new Error("Incomplete or corrupt IndexedPNG file");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}, {
|
||
|
key: 'read',
|
||
|
value: function read(bytes) {
|
||
|
var _this2 = this;
|
||
|
|
||
|
return range(0, bytes, false).map(function (i) {
|
||
|
return _this2.data[_this2.pos++];
|
||
|
});
|
||
|
}
|
||
|
}, {
|
||
|
key: 'readUInt32',
|
||
|
value: function readUInt32() {
|
||
|
var b1 = this.data[this.pos++] << 24;
|
||
|
var b2 = this.data[this.pos++] << 16;
|
||
|
var b3 = this.data[this.pos++] << 8;
|
||
|
var b4 = this.data[this.pos++];
|
||
|
return b1 | b2 | b3 | b4;
|
||
|
}
|
||
|
}, {
|
||
|
key: 'readUInt16',
|
||
|
value: function readUInt16() {
|
||
|
var b1 = this.data[this.pos++] << 8;
|
||
|
var b2 = this.data[this.pos++];
|
||
|
return b1 | b2;
|
||
|
}
|
||
|
}, {
|
||
|
key: 'decodePixels',
|
||
|
value: async function decodePixels() {
|
||
|
var data = void 0;
|
||
|
try {
|
||
|
data = await inflateAsync(this.imgData);
|
||
|
} catch (err) {
|
||
|
throw err;
|
||
|
}
|
||
|
var pixelBytes = this.pixelBitlength / 8;
|
||
|
var scanlineLength = pixelBytes * this.width;
|
||
|
|
||
|
var pixels = Buffer.allocUnsafe(scanlineLength * this.height);
|
||
|
var _data = data,
|
||
|
length = _data.length;
|
||
|
|
||
|
var row = 0;
|
||
|
var pos = 0;
|
||
|
var c = 0;
|
||
|
|
||
|
while (pos < length) {
|
||
|
var byte, col, i, left, upper;
|
||
|
var end;
|
||
|
var end1;
|
||
|
var end2;
|
||
|
var end3;
|
||
|
var end4;
|
||
|
switch (data[pos++]) {
|
||
|
case 0:
|
||
|
// None
|
||
|
for (i = 0, end = scanlineLength; i < end; i++) {
|
||
|
pixels[c++] = data[pos++];
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 1:
|
||
|
// Sub
|
||
|
for (i = 0, end1 = scanlineLength; i < end1; i++) {
|
||
|
byte = data[pos++];
|
||
|
left = i < pixelBytes ? 0 : pixels[c - pixelBytes];
|
||
|
pixels[c++] = (byte + left) % 256;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 2:
|
||
|
// Up
|
||
|
for (i = 0, end2 = scanlineLength; i < end2; i++) {
|
||
|
byte = data[pos++];
|
||
|
col = (i - i % pixelBytes) / pixelBytes;
|
||
|
upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + i % pixelBytes];
|
||
|
pixels[c++] = (upper + byte) % 256;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 3:
|
||
|
// Average
|
||
|
for (i = 0, end3 = scanlineLength; i < end3; i++) {
|
||
|
byte = data[pos++];
|
||
|
col = (i - i % pixelBytes) / pixelBytes;
|
||
|
left = i < pixelBytes ? 0 : pixels[c - pixelBytes];
|
||
|
upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + i % pixelBytes];
|
||
|
pixels[c++] = (byte + Math.floor((left + upper) / 2)) % 256;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 4:
|
||
|
// Paeth
|
||
|
for (i = 0, end4 = scanlineLength; i < end4; i++) {
|
||
|
var paeth, upperLeft;
|
||
|
byte = data[pos++];
|
||
|
col = (i - i % pixelBytes) / pixelBytes;
|
||
|
left = i < pixelBytes ? 0 : pixels[c - pixelBytes];
|
||
|
|
||
|
if (row === 0) {
|
||
|
upper = upperLeft = 0;
|
||
|
} else {
|
||
|
upper = pixels[(row - 1) * scanlineLength + col * pixelBytes + i % pixelBytes];
|
||
|
upperLeft = col && pixels[(row - 1) * scanlineLength + (col - 1) * pixelBytes + i % pixelBytes];
|
||
|
}
|
||
|
|
||
|
var p = left + upper - upperLeft;
|
||
|
var pa = Math.abs(p - left);
|
||
|
var pb = Math.abs(p - upper);
|
||
|
var pc = Math.abs(p - upperLeft);
|
||
|
|
||
|
if (pa <= pb && pa <= pc) {
|
||
|
paeth = left;
|
||
|
} else if (pb <= pc) {
|
||
|
paeth = upper;
|
||
|
} else {
|
||
|
paeth = upperLeft;
|
||
|
}
|
||
|
|
||
|
pixels[c++] = (byte + paeth) % 256;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
throw new Error('Invalid filter algorithm: ' + data[pos - 1]);
|
||
|
}
|
||
|
|
||
|
row++;
|
||
|
}
|
||
|
|
||
|
return pixels;
|
||
|
}
|
||
|
}, {
|
||
|
key: 'decodePalette',
|
||
|
value: function decodePalette() {
|
||
|
var palette = this.palette;
|
||
|
|
||
|
var transparency = this.transparency.indexed || [];
|
||
|
var ret = Buffer.allocUnsafe(palette.length / 3 * 4);
|
||
|
var pos = 0;
|
||
|
var length = palette.length;
|
||
|
|
||
|
var c = 0;
|
||
|
|
||
|
for (var i = 0, end = palette.length; i < end; i += 3) {
|
||
|
var left;
|
||
|
ret[pos++] = palette[i];
|
||
|
ret[pos++] = palette[i + 1];
|
||
|
ret[pos++] = palette[i + 2];
|
||
|
ret[pos++] = (left = transparency[c++]) != null ? left : 255;
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
}, {
|
||
|
key: 'toPNGData',
|
||
|
value: async function toPNGData(options) {
|
||
|
var palette = options.palette || this.decodedPalette;
|
||
|
if (!this.decodedPixels) {
|
||
|
await this.decode();
|
||
|
}
|
||
|
if (options.clip) {
|
||
|
// Ensure some sane defaults
|
||
|
if (options.clip.x == undefined) options.clip.x = 0;
|
||
|
if (options.clip.y == undefined) options.clip.y = 0;
|
||
|
if (options.clip.w == undefined) options.clip.w = this.width - options.clip.x;
|
||
|
if (options.clip.h == undefined) options.clip.h = this.height - options.clip.y;
|
||
|
// Now check for user errors.
|
||
|
if (options.clip.x < 0 || options.clip.x >= this.width) throw new Error("clip.x is out of bounds");
|
||
|
if (options.clip.y < 0 || options.clip.y >= this.height) throw new Error("clip.y is out of bounds");
|
||
|
if (options.clip.w <= 0 || options.clip.w > this.width) throw new Error("clip.w is out of bounds");
|
||
|
if (options.clip.h <= 0 || options.clip.h > this.height) throw new Error("clip.h is out of bounds");
|
||
|
// Now we can get our clipped array.
|
||
|
var pixels = new Uint8ClampedArray(options.clip.w * options.clip.h * 4);
|
||
|
for (var x = 0; x < options.clip.w; x++) {
|
||
|
for (var y = 0; y < options.clip.h; y++) {
|
||
|
var i = (x + y * options.clip.w) * 4;
|
||
|
var index = this.decodedPixels[x + options.clip.x + (y + options.clip.y) * this.width] * 4;
|
||
|
pixels[i++] = palette[index];
|
||
|
pixels[i++] = palette[index + 1];
|
||
|
pixels[i++] = palette[index + 2];
|
||
|
pixels[i++] = palette[index + 3];
|
||
|
}
|
||
|
}
|
||
|
return { pixels: pixels, width: options.clip.w };
|
||
|
} else {
|
||
|
// Allocate RGBA buffer
|
||
|
var _pixels = new Uint8ClampedArray(this.decodedPixels.length * 4);
|
||
|
var j = 0;
|
||
|
for (var _i = 0; _i < this.decodedPixels.length; _i++) {
|
||
|
var _index = this.decodedPixels[_i] * 4;
|
||
|
_pixels[j++] = palette[_index]; // R
|
||
|
_pixels[j++] = palette[_index + 1]; // G
|
||
|
_pixels[j++] = palette[_index + 2]; // B
|
||
|
_pixels[j++] = palette[_index + 3]; // A
|
||
|
}
|
||
|
return { pixels: _pixels, width: this.width };
|
||
|
}
|
||
|
}
|
||
|
}, {
|
||
|
key: 'toImageData',
|
||
|
value: async function toImageData(options) {
|
||
|
var data = await this.toPNGData(options);
|
||
|
return new ImageData(data.pixels, data.width);
|
||
|
}
|
||
|
}, {
|
||
|
key: 'decode',
|
||
|
value: async function decode() {
|
||
|
this.decodedPalette = this.decodePalette();
|
||
|
this.decodedPixels = await this.decodePixels();
|
||
|
}
|
||
|
}]);
|
||
|
return IndexedPNG;
|
||
|
}();
|
||
|
|
||
|
export default IndexedPNG;
|