UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

727 lines (580 loc) • 20.1 kB
/*global Uint8Array:true ArrayBuffer:true */ "use strict"; import zlib from 'pako'; import { BinaryBuffer } from "../../../../../core/binary/BinaryBuffer.js"; import { EndianType } from "../../../../../core/binary/EndianType.js"; import { isArrayEqualStrict } from "../../../../../core/collection/array/isArrayEqualStrict.js"; import { crc32 } from "./crc32.js"; import { PNG } from './PNG.js'; import { PNG_HEADER_BYTES } from "./PNG_HEADER_BYTES.js"; /** * * @param {Uint8Array} encoded_chunk * @returns {ArrayBuffer} */ function inflate(encoded_chunk) { const inflator = new zlib.Inflate(); inflator.push(encoded_chunk); if (inflator.err) { throw new Error(inflator.err); } return inflator.result.buffer; } /** * * @param {Uint8Array} buffer * @param {number} offset * @return {number} */ function readUInt32(buffer, offset) { return (buffer[offset] << 24) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 3]); } /** * * @param {Uint8Array} buffer * @param {number} offset * @return {number} */ function readUInt8(buffer, offset) { return buffer[offset]; } /** * * @param {ArrayBuffer} bytes * @constructor */ export function PNGReader(bytes) { /** * current pointer * @type {number} */ this.i = 0; /** * bytes buffer * @type {Uint8Array} */ this.bytes = new Uint8Array(bytes); /** * Output object * @type {PNG} */ this.png = new PNG(); this.dataChunks = []; /** * * @type {BinaryBuffer} */ this.buffer = new BinaryBuffer(); // see https://www.w3.org/TR/2003/REC-PNG-20031110/#7Integers-and-byte-order this.buffer.endianness = EndianType.BigEndian; this.buffer.fromArrayBuffer(bytes); /** * Whether CRC should be performed or not * @type {boolean} */ this.crc_enabled = false; /** * * @type {Uint8Array} */ this.header = new Uint8Array(8); } /** * * @param {number} length * @return {Uint8Array} */ PNGReader.prototype.readBytes = function (length) { const result = new Uint8Array(length); this.buffer.readBytes(result, 0, length); return result; }; /** * 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'); } const buffer = this.buffer; const header = this.header; buffer.readBytes(header, 0, 8) // see https://www.w3.org/TR/PNG-Structure.html if (!isArrayEqualStrict(header, PNG_HEADER_BYTES)) { throw new Error('invalid PNGReader file (bad signature)'); } }; /** * 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 * * @returns {string} chunk type */ PNGReader.prototype.decodeChunk = function () { const buffer = this.buffer; const length = buffer.readUint32(); if (length < 0) { throw new Error('Bad chunk length ' + (0xFFFFFFFF & length)); } const chunk_address = buffer.position; const type = buffer.readASCIICharacters(4); /** * * @type {Uint8Array} */ const chunk = this.readBytes(length); // checksum const crc_expected = buffer.readUint32(); if (this.crc_enabled) { // NOTE: CRC includes the "type" tag, not just the chunk bytes const crc_actual = crc32(buffer.raw_bytes, chunk_address, length + 4); if (crc_actual !== crc_expected) { console.warn(`CRC (Cyclic Redundancy Check) error at block '${type}' at address ${chunk_address}`); } } // console.log(`chunk: ${type}`); switch (type) { case 'IHDR': this.decodeIHDR(chunk); break; case 'PLTE': this.decodePLTE(chunk); break; case 'IDAT': this.decodeIDAT(chunk); break; case 'tRNS': this.decodeTRNS(chunk); break; case 'IEND': this.decodeIEND(chunk); break; case 'sRGB': this.decodesRGB(chunk); break; case 'tIME': this.decodetIME(chunk); break; case 'zTXt': this.decodezTXt(chunk); break; case 'iTXt': this.decodeiTXt(chunk); break; default: // skip unknown block // console.warn(`Unsupported block ${type}`); break; } return type; }; /** * https://www.w3.org/TR/2003/REC-PNG-20031110/#11sRGB * @param {Uint8Array} chunk */ PNGReader.prototype.decodesRGB = function (chunk) { const rendering_intent = readUInt8(chunk, 0); // TODO add metadata to the PNG representation } /** * https://www.w3.org/TR/2003/REC-PNG-20031110/#11tIME * @param {Uint8Array} chunk */ PNGReader.prototype.decodetIME = function (chunk) { const year_high = readUInt8(chunk, 0); const year_low = readUInt8(chunk, 1); const year = (year_high << 8) | year_low; const month = readUInt8(chunk, 2); const day = readUInt8(chunk, 3); const hour = readUInt8(chunk, 4); const minute = readUInt8(chunk, 5); const second = readUInt8(chunk, 6); } /** * International textual data * @see https://www.w3.org/TR/2003/REC-PNG-20031110/ * @param {Uint8Array} chunk */ PNGReader.prototype.decodeiTXt = function (chunk) { const buffer = BinaryBuffer.fromArrayBuffer(chunk.buffer); const keyword = buffer.readASCIICharacters(79, true); const compression_flag = buffer.readUint8(); const compression_method = buffer.readUint8(); const language_tag = buffer.readASCIICharacters(Infinity, true); const translated_keyword = buffer.readASCIICharacters(Infinity, true); const remaining_bytes = buffer.data.length - buffer.position; let text; if (compression_flag === 0) { text = buffer.readASCIICharacters(remaining_bytes); } else if (compression_flag === 1) { const decoded = inflate(new Uint8Array(buffer.data, buffer.position, remaining_bytes)); buffer.fromArrayBuffer(decoded); text = buffer.readASCIICharacters(decoded.byteLength); } else { throw new Error(`Invalid compression flag value '${compression_flag}'`); } return { keyword, language_tag, translated_keyword, text } } /** * Compressed textual data * @see https://www.w3.org/TR/2003/REC-PNG-20031110/#11zTXt * @param {Uint8Array} chunk */ PNGReader.prototype.decodezTXt = function (chunk) { const buffer = BinaryBuffer.fromArrayBuffer(chunk.buffer); const keyword = buffer.readASCIICharacters(79, true); const compression_method = buffer.readUint8(); let value; if (compression_method === 0) { // deflate method const encoded_chunk = new Uint8Array(chunk.buffer, buffer.position); const decompressed_data = inflate(encoded_chunk); buffer.fromArrayBuffer(decompressed_data); value = buffer.readASCIICharacters(decompressed_data.length); } else { throw new Error(`Unsupported compression method '${compression_method}'`); } return { keyword: keyword, text: value } } /** * https://www.w3.org/TR/PNG/#11tEXt * @param {Uint8Array} chunk */ PNGReader.prototype.decodetEXt = function (chunk) { const buff = BinaryBuffer.fromArrayBuffer(chunk.buffer); const keyword = buff.readASCIICharacters(Number.POSITIVE_INFINITY, true); const value = buff.readASCIICharacters(keyword.length - 1, false); this.png.text[keyword] = value; } /** * NOTE: untested * https://www.w3.org/TR/PNG/#11iEXt * @param {Uint8Array} chunk */ PNGReader.prototype.decodeiEXt = function (chunk) { const buff = BinaryBuffer.fromArrayBuffer(chunk.buffer); const keyword = buff.readASCIICharacters(Number.POSITIVE_INFINITY, true); const compression_flag = buff.readUint8(); const compression_method = buff.readUint8(); const language_tag = buff.readASCIICharacters(Number.POSITIVE_INFINITY, true); const translated_keyword = buff.readUTF8String(); const separator = buff.readUint8(); if (separator !== 0) { throw new Error('Expected Null Separator after Translated keyword'); } const text = buff.readUTF8String(); this.png.text[keyword] = text; } /** * 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) { const 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); }; /** * https://www.w3.org/TR/PNG/#11tRNS * @param {Uint8Array} chunk */ PNGReader.prototype.decodeTRNS = function (chunk) { this.png.setTRNS(chunk); }; /** * http://www.w3.org/TR/2003/REC-PNG-20031110/#11IEND */ PNGReader.prototype.decodeIEND = function () { }; /** * Uncompress IDAT chunks */ PNGReader.prototype.decodePixels = function () { const png = this.png; const inflator = new zlib.Inflate(); const chunks = this.dataChunks; const chunk_count = chunks.length; for (let i = 0; i < chunk_count; i++) { inflator.push(chunks[i]); if (inflator.err) { throw new Error(inflator.err); } } const decompressed_data = inflator.result; if (png.getInterlaceMethod() === 0) { this.interlaceNone(decompressed_data); } else { this.interlaceAdam7(decompressed_data); } }; // Different interlace methods /** * @param {Uint8Array} data */ PNGReader.prototype.interlaceNone = function (data) { const png = this.png; const depth = png.bitDepth; const bits_per_line = png.colors * depth; const bytes_per_pixel = bits_per_line / 8; const width = png.width; const height = png.height; // color bytes per row const color_bytes_per_row = Math.ceil(bytes_per_pixel * width); const output_bytes_per_row = Math.ceil(bytes_per_pixel) * width; const pixels = new Uint8Array(output_bytes_per_row * height); let offset = 0; const data_length = data.length; for (let i = 0; i < data_length; i += color_bytes_per_row + 1) { const scanline_address = i + 1; // scanline = slice.call(data, scanline_address, scanline_address + cpr); const header = readUInt8(data, i); switch (header) { case 0: // NONE if (depth === 1) { for (let x = 0; x < output_bytes_per_row; x++) { const q = x >>> 4; const datum = data[q + scanline_address]; const shift = ((x) & 0x7); const out_value = (datum >>> shift) & 0x1; pixels[offset + x] = out_value; } } else if (depth === 2) { for (let x = 0; x < output_bytes_per_row; x++) { const q = x >>> 2; const datum = data[q + scanline_address]; const shift = ((~x) & 0x3) << 1; const out_value = (datum >>> shift) & 0x3; pixels[offset + x] = out_value; } } else if (depth === 4) { for (let x = 0; x < output_bytes_per_row; x++) { const q = x >>> 1; const datum = data[q + scanline_address]; const shift = ((~x) & 0x1) << 2; const out_value = (datum >>> shift) & 0xF; pixels[offset + x] = out_value; } } else if (depth === 8) { for (let x = 0; x < output_bytes_per_row; x++) { pixels[offset + x] = data[x + scanline_address]; } } else { throw new Error(`unsupported bit depth ${depth}`) } break; case 1: this.unFilterSub(data, scanline_address, pixels, bytes_per_pixel, offset, color_bytes_per_row); break; case 2: this.unFilterUp(data, scanline_address, pixels, bytes_per_pixel, offset, color_bytes_per_row); break; case 3: this.unFilterAverage(data, scanline_address, pixels, bytes_per_pixel, offset, color_bytes_per_row); break; case 4: this.unFilterPaeth(data, scanline_address, pixels, bytes_per_pixel, offset, color_bytes_per_row); break; default: throw new Error(`unknown filtered scanline type '${header}'`); } offset += output_bytes_per_row; } png.pixels = pixels; }; /** * De-interlace image according to Adam 7 scheme * @param {Uint8Array} data */ PNGReader.prototype.interlaceAdam7 = function (data) { throw new Error("Adam7 interlacing is not implemented yet"); }; // Unfiltering /** * 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, scanline_offset, pixels, bpp, of, length) { let i = 0; for (; i < bpp; i++) { pixels[of + i] = scanline[i + scanline_offset]; } for (; i < length; i++) { // Raw(x) + Raw(x - bpp) const of_i = of + i; pixels[of_i] = (scanline[i + scanline_offset] + 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, scanline_offset, pixels, bpp, of, length) { let 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 + scanline_offset]; } else for (; i < length; i++) { // Raw(x) byte = scanline[i + scanline_offset]; // 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, scanline_offset, pixels, bpp, of, length) { let 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 + scanline_offset]; } // Prior(x) == 0 && Raw(x - bpp) != 0 (right shift, prevent doubles) for (; i < length; i++) { const of_i = of + i; pixels[of_i] = (scanline[i + scanline_offset] + (pixels[of_i - bpp] >> 1)) & 0xFF; } } else { // Prior(x) != 0 && Raw(x - bpp) == 0 for (; i < bpp; i++) { const of_i = of + i; pixels[of_i] = (scanline[i + scanline_offset] + (pixels[of_i - length] >> 1)) & 0xFF; } // Prior(x) != 0 && Raw(x - bpp) != 0 for (; i < length; i++) { byte = scanline[i + scanline_offset]; const of_i = of + 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, scanline_offset, pixels, bpp, of, length) { let 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 + scanline_offset]; } // Prior(x) == 0 && Raw(x - bpp) != 0 // paethPredictor(x, 0, 0) is always x for (; i < length; i++) { pixels[of + i] = (scanline[i + scanline_offset] + pixels[of + i - bpp]) & 0xFF; } } else { const of1 = of - length; // Prior(x) != 0 && Raw(x - bpp) == 0 // paethPredictor(x, 0, 0) is always x for (; i < bpp; i++) { pixels[of + i] = (scanline[i + scanline_offset] + pixels[of1 + i]) & 0xFF; } // Prior(x) != 0 && Raw(x - bpp) != 0 for (; i < length; i++) { raw = scanline[i + scanline_offset]; const offset_0 = of + i; c = pixels[offset_0 - length - bpp]; b = pixels[offset_0 - length]; a = pixels[offset_0 - 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[offset_0] = (raw + pr) & 0xFF; } } }; /** * Parse the PNG file * @returns {PNG} */ PNGReader.prototype.parse = function () { this.decodeHeader(); for (; ;) { const type = this.decodeChunk(); if (type === 'IEND') { // reached the end break; } } this.decodePixels(); return this.png; };