@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
1,048 lines (839 loc) • 27.6 kB
JavaScript
/*global Uint8Array:true ArrayBuffer:true */
"use strict";
import zlib from 'pako';
import { assert } from "../../../../../core/assert.js";
import { BinaryBuffer } from "../../../../../core/binary/BinaryBuffer.js";
import { EndianType } from "../../../../../core/binary/EndianType.js";
import { platform_compute_endianness } from "../../../../../core/binary/platform_compute_endianness.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();
/**
*
* @type {Uint8Array[]}
*/
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 buffer = this.buffer;
// Reference instead of a copy
const result = new Uint8Array(buffer.data, buffer.position, length);
buffer.skip(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) {
if(compression_method !== 0){
throw new Error('only compression_method 0 is supported');
}
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
if(compression_method !== 0){
throw new Error('only compression_method 0 is supported');
}
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();
// TODO process compression properly
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
* @param {Uint8Array} chunk
*/
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.transparency_lookup = 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;
// https://www.w3.org/TR/png-3/#8InterlaceMethods
if (png.getInterlaceMethod() === 0) {
this.png.pixels = this.interlaceNone(decompressed_data);
} else {
this.png.pixels = 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 pixels = new Uint8Array(color_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 filter_type = readUInt8(data, i);
this.unFilter(
filter_type,
data,
scanline_address,
pixels,
depth,
offset,
offset - color_bytes_per_row,
color_bytes_per_row
);
offset += color_bytes_per_row;
}
return pixels;
};
/**
* De-interlace image according to Adam 7 scheme
* @param {Uint8Array} data
*/
PNGReader.prototype.interlaceAdam7 = function (data) {
const png = this.png;
const depth = png.bitDepth;
const bytes_per_pixel = png.colors * depth / 8;
const pixels = new Uint8Array(bytes_per_pixel * png.width * png.height);
// Adam7 interlacing pattern
const passes = [
{ x: 0, y: 0, xStep: 8, yStep: 8 }, // Pass 1
{ x: 4, y: 0, xStep: 8, yStep: 8 }, // Pass 2
{ x: 0, y: 4, xStep: 4, yStep: 8 }, // Pass 3
{ x: 2, y: 0, xStep: 4, yStep: 4 }, // Pass 4
{ x: 0, y: 2, xStep: 2, yStep: 4 }, // Pass 5
{ x: 1, y: 0, xStep: 2, yStep: 2 }, // Pass 6
{ x: 0, y: 1, xStep: 1, yStep: 2 }, // Pass 7
];
const width = png.width;
const height = png.height;
let offset = 0;
// Create a new line for the unfiltered data
const line_bytes_buffer = new Uint8Array(width * bytes_per_pixel);
// Process each pass
for (let passIndex = 0; passIndex < 7; passIndex++) {
const pass = passes[passIndex];
// Calculate pass dimensions
const pass_width = Math.ceil((width - pass.x) / pass.xStep);
const pass_height = Math.ceil((height - pass.y) / pass.yStep);
if (pass_width <= 0 || pass_height <= 0) {
continue;
}
const passLineBytes = pass_width * bytes_per_pixel;
// Indicate to unfiltering that this is the first line (no previous line)
let previous_line_offset = -1;
// Process each scanline in this pass
for (let y = 0; y < pass_height; y++) {
// First byte is the filter type
const filter_type = data[offset++];
// we use alternating line offsets to save on memory allocations
const line_offset = (y % 2) * passLineBytes;
// Apply the appropriate unfilter
this.unFilter(
filter_type,
data,
offset,
line_bytes_buffer,
bytes_per_pixel,
line_offset,
previous_line_offset,
passLineBytes,
);
offset += passLineBytes;
previous_line_offset = line_offset;
for (let x = 0; x < pass_width; x++) {
const outputX = pass.x + x * pass.xStep;
const outputY = pass.y + y * pass.yStep;
if (outputX >= width || outputY >= height) {
continue;
}
for (let i = 0; i < bytes_per_pixel; i++) {
const out_offset = (outputY * width + outputX) * bytes_per_pixel + i;
pixels[out_offset] = line_bytes_buffer[line_offset + x * bytes_per_pixel + i];
}
}
}
}
function swap16(val) {
return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
}
if (depth === 16) {
const uint16Data = new Uint16Array(pixels.buffer);
const osIsLittleEndian = platform_compute_endianness() === EndianType.LittleEndian;
if (osIsLittleEndian) {
for (let k = 0; k < uint16Data.length; k++) {
// PNG is always big endian. Swap the bytes.
uint16Data[k] = swap16(uint16Data[k]);
}
}
return uint16Data;
} else {
return pixels;
}
};
// Unfiltering
/**
*
* @param {number} filter_type
* @param {Uint8Array} data
* @param {number} scanline_address
* @param {Uint8Array} output
* @param {number} bytes_per_pixel
* @param {number} output_offset
* @param {number} output_offset_previous where does result of previous scanline begin in the output? Needed for various filters such as `Up`
* @param {number} length
*/
PNGReader.prototype.unFilter = function (
filter_type,
data,
scanline_address,
output,
bytes_per_pixel,
output_offset,
output_offset_previous,
length
) {
assert.isInteger(filter_type, 'filter_type');
assert.isNonNegativeInteger(scanline_address, 'scanline_address');
assert.isNonNegativeInteger(bytes_per_pixel, 'bytes_per_pixel');
assert.isNonNegativeInteger(output_offset, 'output_offset');
assert.isNonNegativeInteger(length, 'length');
switch (filter_type) {
case 0:
this.unFilterNone(
data,
scanline_address,
output,
bytes_per_pixel,
output_offset,
output_offset_previous,
length
);
break;
case 1:
this.unFilterSub(
data,
scanline_address,
output,
bytes_per_pixel,
output_offset,
output_offset_previous,
length
);
break;
case 2:
this.unFilterUp(
data,
scanline_address,
output,
bytes_per_pixel,
output_offset,
output_offset_previous,
length
);
break;
case 3:
this.unFilterAverage(
data,
scanline_address,
output,
bytes_per_pixel,
output_offset,
output_offset_previous,
length
);
break;
case 4:
this.unFilterPaeth(
data,
scanline_address,
output,
bytes_per_pixel,
output_offset,
output_offset_previous,
length
);
break;
default:
throw new Error(`unknown filtered scanline type '${filter_type}'`);
}
}
/**
*
* @param {Uint8Array} data
* @param {number} scanline_address
* @param {Uint8Array} output
* @param {number} bytes_per_pixel
* @param {number} output_offset
* @param {number} output_offset_previous
* @param {number} length
*/
PNGReader.prototype.unFilterNone = function (
data,
scanline_address,
output,
bytes_per_pixel,
output_offset,
output_offset_previous,
length
) {
const png = this.png;
const depth = png.bitDepth;
if (depth === 1) {
for (let x = 0; x < length; x++) {
const q = x >>> 4;
const datum = data[q + scanline_address];
const shift = ((x) & 0x7);
const out_value = (datum >>> shift) & 0x1;
output[output_offset + x] = out_value;
}
} else if (depth === 2) {
for (let x = 0; x < length; x++) {
const q = x >>> 2;
const datum = data[q + scanline_address];
const shift = ((~x) & 0x3) << 1;
const out_value = (datum >>> shift) & 0x3;
output[output_offset + x] = out_value;
}
} else if (depth === 4) {
for (let x = 0; x < length; x++) {
const q = x >>> 1;
const datum = data[q + scanline_address];
const shift = ((~x) & 0x1) << 2;
const out_value = (datum >>> shift) & 0xF;
output[output_offset + x] = out_value;
}
} else if (depth === 8) {
for (let x = 0; x < length; x++) {
// straight copy
output[output_offset + x] = data[x + scanline_address];
}
} else {
throw new Error(`unsupported bit depth ${depth}`)
}
};
/**
* 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)
*
* @param {Uint8Array} scanline raw data
* @param {number} scanline_offset
* @param {Uint8Array} pixels processed output
* @param {number} bpp bytes-per-pixel
* @param {number} offset
* @param {number} output_offset_previous
* @param {number} length
*/
PNGReader.prototype.unFilterSub = function (
scanline,
scanline_offset,
pixels,
bpp,
offset,
output_offset_previous,
length
) {
let i = 0;
for (; i < bpp; i++) {
pixels[offset + i] = scanline[i + scanline_offset];
}
for (; i < length; i++) {
// Raw(x) + Raw(x - bpp)
const of_i = offset + 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)
*
* @param {Uint8Array} scanline raw data
* @param {number} scanline_offset
* @param {Uint8Array} pixels processed output
* @param {number} bpp bytes-per-pixel
* @param {number} offset
* @param {number} output_offset_previous
* @param {number} length
*/
PNGReader.prototype.unFilterUp = function (
scanline,
scanline_offset,
pixels,
bpp,
offset,
output_offset_previous,
length
) {
let i = 0, byte, prev;
// Prior(x) is 0 for all x on the first scanline
if (output_offset_previous < 0) {
for (; i < length; i++) {
pixels[offset + i] = scanline[i + scanline_offset];
}
} else {
for (; i < length; i++) {
// Raw(x)
byte = scanline[i + scanline_offset];
// Prior(x)
prev = pixels[output_offset_previous + i];
pixels[offset + 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)
*
* @param {Uint8Array} scanline raw data
* @param {number} scanline_offset
* @param {Uint8Array} pixels processed output
* @param {number} bpp bytes-per-pixel
* @param {number} offset
* @param {number} output_offset_previous
* @param {number} length
*/
PNGReader.prototype.unFilterAverage = function (
scanline,
scanline_offset,
pixels,
bpp,
offset,
output_offset_previous,
length
) {
let i = 0, byte, prev, prior;
if (output_offset_previous < 0) {
// Prior(x) == 0 && Raw(x - bpp) == 0
for (; i < bpp; i++) {
pixels[offset + i] = scanline[i + scanline_offset];
}
// Prior(x) == 0 && Raw(x - bpp) != 0 (right shift, prevent doubles)
for (; i < length; i++) {
const of_i = offset + 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++) {
pixels[offset + i] = (scanline[i + scanline_offset] + (pixels[output_offset_previous + i] >> 1)) & 0xFF;
}
// Prior(x) != 0 && Raw(x - bpp) != 0
for (; i < length; i++) {
byte = scanline[i + scanline_offset];
prev = pixels[offset + i - bpp];
prior = pixels[output_offset_previous + i];
pixels[offset + 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
*
* @param {Uint8Array} scanline raw data
* @param {number} scanline_offset
* @param {Uint8Array} pixels processed output
* @param {number} bpp bytes-per-pixel
* @param {number} offset
* @param {number} output_offset_previous
* @param {number} length
*/
PNGReader.prototype.unFilterPaeth = function (
scanline,
scanline_offset,
pixels,
bpp,
offset,
output_offset_previous,
length
) {
let i = 0, raw, a, b, c, p, pa, pb, pc, pr;
if (output_offset_previous < 0) {
// Prior(x) == 0 && Raw(x - bpp) == 0
for (; i < bpp; i++) {
pixels[offset + i] = scanline[i + scanline_offset];
}
// Prior(x) == 0 && Raw(x - bpp) != 0
// paethPredictor(x, 0, 0) is always x
for (; i < length; i++) {
pixels[offset + i] = (scanline[i + scanline_offset] + pixels[offset + i - bpp]) & 0xFF;
}
} else {
// Prior(x) != 0 && Raw(x - bpp) == 0
// paethPredictor(x, 0, 0) is always x
for (; i < bpp; i++) {
pixels[offset + i] = (scanline[i + scanline_offset] + pixels[output_offset_previous + i]) & 0xFF;
}
// Prior(x) != 0 && Raw(x - bpp) != 0
for (; i < length; i++) {
raw = scanline[i + scanline_offset];
c = pixels[output_offset_previous + i - bpp];
b = pixels[output_offset_previous + i];
a = pixels[offset + i - 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 + i] = (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;
};