Replace CDN React/ReactDOM/Babel with local libs; remove Babel and inline scripts Build Tailwind locally, add safelist; switch to assets/tailwind.css Self-host Font Awesome and Inter (CSS + woff2); remove external font CDNs Implement strict CSP (no unsafe-inline/eval; scripts/styles/fonts from self) Extract inline handlers; move PWA scripts to external files Add local QR code generation (qrcode lib) and remove api.qrserver.com Improve SessionTypeSelector visual selection (highlighted background and ring) Keep PWA working with service worker and offline assets Refs: CSP hardening, offline-first, no external dependencies
291 lines
7.5 KiB
JavaScript
291 lines
7.5 KiB
JavaScript
"use strict";
|
|
|
|
let constants = require("./constants");
|
|
let CrcCalculator = require("./crc");
|
|
|
|
let Parser = (module.exports = function (options, dependencies) {
|
|
this._options = options;
|
|
options.checkCRC = options.checkCRC !== false;
|
|
|
|
this._hasIHDR = false;
|
|
this._hasIEND = false;
|
|
this._emittedHeadersFinished = false;
|
|
|
|
// 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.read = dependencies.read;
|
|
this.error = dependencies.error;
|
|
this.metadata = dependencies.metadata;
|
|
this.gamma = dependencies.gamma;
|
|
this.transColor = dependencies.transColor;
|
|
this.palette = dependencies.palette;
|
|
this.parsed = dependencies.parsed;
|
|
this.inflateData = dependencies.inflateData;
|
|
this.finished = dependencies.finished;
|
|
this.simpleTransparency = dependencies.simpleTransparency;
|
|
this.headersFinished = dependencies.headersFinished || function () {};
|
|
});
|
|
|
|
Parser.prototype.start = function () {
|
|
this.read(constants.PNG_SIGNATURE.length, this._parseSignature.bind(this));
|
|
};
|
|
|
|
Parser.prototype._parseSignature = function (data) {
|
|
let signature = constants.PNG_SIGNATURE;
|
|
|
|
for (let 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));
|
|
};
|
|
|
|
Parser.prototype._parseChunkBegin = function (data) {
|
|
// chunk content length
|
|
let length = data.readUInt32BE(0);
|
|
|
|
// chunk type
|
|
let type = data.readUInt32BE(4);
|
|
let name = "";
|
|
for (let i = 4; i < 8; i++) {
|
|
name += String.fromCharCode(data[i]);
|
|
}
|
|
|
|
//console.log('chunk ', name, length);
|
|
|
|
// chunk flags
|
|
let ancillary = Boolean(data[4] & 0x20); // or critical
|
|
// priv = Boolean(data[5] & 0x20), // or public
|
|
// safeToCopy = Boolean(data[7] & 0x20); // or unsafe
|
|
|
|
if (!this._hasIHDR && type !== constants.TYPE_IHDR) {
|
|
this.error(new Error("Expected IHDR on beggining"));
|
|
return;
|
|
}
|
|
|
|
this._crc = new CrcCalculator();
|
|
this._crc.write(Buffer.from(name));
|
|
|
|
if (this._chunks[type]) {
|
|
return this._chunks[type](length);
|
|
}
|
|
|
|
if (!ancillary) {
|
|
this.error(new Error("Unsupported critical chunk type " + name));
|
|
return;
|
|
}
|
|
|
|
this.read(length + 4, this._skipChunk.bind(this));
|
|
};
|
|
|
|
Parser.prototype._skipChunk = function (/*data*/) {
|
|
this.read(8, this._parseChunkBegin.bind(this));
|
|
};
|
|
|
|
Parser.prototype._handleChunkEnd = function () {
|
|
this.read(4, this._parseChunkEnd.bind(this));
|
|
};
|
|
|
|
Parser.prototype._parseChunkEnd = function (data) {
|
|
let fileCrc = data.readInt32BE(0);
|
|
let calcCrc = this._crc.crc32();
|
|
|
|
// check CRC
|
|
if (this._options.checkCRC && calcCrc !== fileCrc) {
|
|
this.error(new Error("Crc error - " + fileCrc + " - " + calcCrc));
|
|
return;
|
|
}
|
|
|
|
if (!this._hasIEND) {
|
|
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);
|
|
|
|
let width = data.readUInt32BE(0);
|
|
let height = data.readUInt32BE(4);
|
|
let depth = data[8];
|
|
let colorType = data[9]; // bits: 1 palette, 2 color, 4 alpha
|
|
let compr = data[10];
|
|
let filter = data[11];
|
|
let interlace = data[12];
|
|
|
|
// console.log(' width', width, 'height', height,
|
|
// 'depth', depth, 'colorType', colorType,
|
|
// 'compr', compr, 'filter', filter, 'interlace', interlace
|
|
// );
|
|
|
|
if (
|
|
depth !== 8 &&
|
|
depth !== 4 &&
|
|
depth !== 2 &&
|
|
depth !== 1 &&
|
|
depth !== 16
|
|
) {
|
|
this.error(new Error("Unsupported bit depth " + depth));
|
|
return;
|
|
}
|
|
if (!(colorType in constants.COLORTYPE_TO_BPP_MAP)) {
|
|
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 && interlace !== 1) {
|
|
this.error(new Error("Unsupported interlace method"));
|
|
return;
|
|
}
|
|
|
|
this._colorType = colorType;
|
|
|
|
let bpp = constants.COLORTYPE_TO_BPP_MAP[this._colorType];
|
|
|
|
this._hasIHDR = true;
|
|
|
|
this.metadata({
|
|
width: width,
|
|
height: height,
|
|
depth: depth,
|
|
interlace: Boolean(interlace),
|
|
palette: Boolean(colorType & constants.COLORTYPE_PALETTE),
|
|
color: Boolean(colorType & constants.COLORTYPE_COLOR),
|
|
alpha: Boolean(colorType & constants.COLORTYPE_ALPHA),
|
|
bpp: bpp,
|
|
colorType: colorType,
|
|
});
|
|
|
|
this._handleChunkEnd();
|
|
};
|
|
|
|
Parser.prototype._handlePLTE = function (length) {
|
|
this.read(length, this._parsePLTE.bind(this));
|
|
};
|
|
Parser.prototype._parsePLTE = function (data) {
|
|
this._crc.write(data);
|
|
|
|
let entries = Math.floor(data.length / 3);
|
|
// console.log('Palette:', entries);
|
|
|
|
for (let i = 0; i < entries; i++) {
|
|
this._palette.push([data[i * 3], data[i * 3 + 1], data[i * 3 + 2], 0xff]);
|
|
}
|
|
|
|
this.palette(this._palette);
|
|
|
|
this._handleChunkEnd();
|
|
};
|
|
|
|
Parser.prototype._handleTRNS = function (length) {
|
|
this.simpleTransparency();
|
|
this.read(length, this._parseTRNS.bind(this));
|
|
};
|
|
Parser.prototype._parseTRNS = function (data) {
|
|
this._crc.write(data);
|
|
|
|
// palette
|
|
if (this._colorType === constants.COLORTYPE_PALETTE_COLOR) {
|
|
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 (let i = 0; i < data.length; i++) {
|
|
this._palette[i][3] = data[i];
|
|
}
|
|
this.palette(this._palette);
|
|
}
|
|
|
|
// for colorType 0 (grayscale) and 2 (rgb)
|
|
// there might be one gray/color defined as transparent
|
|
if (this._colorType === constants.COLORTYPE_GRAYSCALE) {
|
|
// grey, 2 bytes
|
|
this.transColor([data.readUInt16BE(0)]);
|
|
}
|
|
if (this._colorType === constants.COLORTYPE_COLOR) {
|
|
this.transColor([
|
|
data.readUInt16BE(0),
|
|
data.readUInt16BE(2),
|
|
data.readUInt16BE(4),
|
|
]);
|
|
}
|
|
|
|
this._handleChunkEnd();
|
|
};
|
|
|
|
Parser.prototype._handleGAMA = function (length) {
|
|
this.read(length, this._parseGAMA.bind(this));
|
|
};
|
|
Parser.prototype._parseGAMA = function (data) {
|
|
this._crc.write(data);
|
|
this.gamma(data.readUInt32BE(0) / constants.GAMMA_DIVISION);
|
|
|
|
this._handleChunkEnd();
|
|
};
|
|
|
|
Parser.prototype._handleIDAT = function (length) {
|
|
if (!this._emittedHeadersFinished) {
|
|
this._emittedHeadersFinished = true;
|
|
this.headersFinished();
|
|
}
|
|
this.read(-length, this._parseIDAT.bind(this, length));
|
|
};
|
|
Parser.prototype._parseIDAT = function (length, data) {
|
|
this._crc.write(data);
|
|
|
|
if (
|
|
this._colorType === constants.COLORTYPE_PALETTE_COLOR &&
|
|
this._palette.length === 0
|
|
) {
|
|
throw new Error("Expected palette not found");
|
|
}
|
|
|
|
this.inflateData(data);
|
|
let leftOverLength = length - data.length;
|
|
|
|
if (leftOverLength > 0) {
|
|
this._handleIDAT(leftOverLength);
|
|
} else {
|
|
this._handleChunkEnd();
|
|
}
|
|
};
|
|
|
|
Parser.prototype._handleIEND = function (length) {
|
|
this.read(length, this._parseIEND.bind(this));
|
|
};
|
|
Parser.prototype._parseIEND = function (data) {
|
|
this._crc.write(data);
|
|
|
|
this._hasIEND = true;
|
|
this._handleChunkEnd();
|
|
|
|
if (this.finished) {
|
|
this.finished();
|
|
}
|
|
};
|