formats.exposed/public/png/parsePng.js

305 lines
8.0 KiB
JavaScript

// @ts-check
import { PngNodeType } from "./constants.js";
import { areBytesEqual, chunkBytes } from "../common/bytes.js";
import crc32 from "./crc32.js";
/** @typedef {import("../../types/png.d.ts").PngNode} PngNode */
const PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
const textDecoder = new TextDecoder();
/**
* @param {Uint8Array} bytes
* @returns {null | PngNode} The root node of the PNG tree, or null if the PNG is invalid.
*/
export default (bytes) => {
if (!isSignatureValid(bytes)) {
console.warn("Invalid PNG signature");
return null;
}
/** @type {PngNode[]} */
const children = [{ type: PngNodeType.signature, bytes: PNG_SIGNATURE }];
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
for (let i = PNG_SIGNATURE.length; i < bytes.length;) {
const lengthBytes = bytes.subarray(i, i + 4);
if (lengthBytes.byteLength !== 4) {
console.warn("Invalid data. Did the file end too early?");
return null;
}
const length = view.getUint32(i);
i += 4;
const typeBytes = bytes.subarray(i, i + 4);
const type = parseType(typeBytes);
if (!type) {
console.warn("Invalid chunk type)");
return null;
}
i += 4;
const data = bytes.subarray(i, i + length);
if (data.length !== length) {
console.warn("Invalid data. Did the file end too early?");
return null;
}
i += length;
const actualCrcBytes = bytes.subarray(i, i + 4);
if (actualCrcBytes.length !== 4) {
console.warn("Not enough bytes for the CRC. Did the file end too early?");
return null;
}
const actualCrc = view.getUint32(i);
const expectedCrc = crc32(typeBytes, data);
if (actualCrc !== expectedCrc) {
console.warn("Invalid chunk CRC");
return null;
}
i += 4;
const allBytes = bytes.subarray(i - length - 12, i);
children.push(
formatChunk(allBytes, lengthBytes, typeBytes, data, actualCrcBytes),
);
}
return { type: PngNodeType.root, bytes, children };
};
/**
* @param {Uint8Array} bytes
* @returns {boolean}
*/
const isSignatureValid = (bytes) =>
areBytesEqual(bytes.subarray(0, 8), PNG_SIGNATURE);
/**
* @param {Uint8Array} bytes
* @returns {null | string} The chunk type, or null if the chunk type is invalid.
*/
const parseType = (bytes) => {
if (bytes.length !== 4) return null;
const result = textDecoder.decode(bytes);
if (!/^[A-Za-z]{4}$/.test(result)) return null;
return result;
};
/**
* @param {Uint8Array} allBytes All the bytes in the chunk.
* @param {Uint8Array} lengthBytes
* @param {Uint8Array} typeBytes
* @param {Uint8Array} data
* @param {Uint8Array} crcBytes
* @returns {PngNode}
*/
const formatChunk = (allBytes, lengthBytes, typeBytes, data, crcBytes) => {
/** @type {PngNodeType} */
let type = PngNodeType.unknownChunk;
/** @type {PngNode} */
let chunkLengthNode = { type: PngNodeType.chunkLength, bytes: lengthBytes };
/** @type {PngNode} */
const chunkTypeNode = { type: PngNodeType.chunkType, bytes: typeBytes };
/** @type {PngNode} */
let chunkDataNode = { type: PngNodeType.chunkData, bytes: data };
/** @type {PngNode} */
const chunkCrcNode = { type: PngNodeType.chunkCrc, bytes: crcBytes };
switch (textDecoder.decode(typeBytes)) {
case "IHDR":
type = PngNodeType.ihdr;
chunkDataNode = {
...chunkDataNode,
type: PngNodeType.ihdrChunkData,
children: [
{ type: PngNodeType.ihdrWidth, bytes: data.subarray(0, 4) },
{ type: PngNodeType.ihdrHeight, bytes: data.subarray(4, 8) },
{ type: PngNodeType.ihdrBitDepth, bytes: data.subarray(8, 9) },
{ type: PngNodeType.ihdrColourType, bytes: data.subarray(9, 10) },
{
type: PngNodeType.ihdrCompressionMethod,
bytes: data.subarray(10, 11),
},
{ type: PngNodeType.ihdrFilterMethod, bytes: data.subarray(11, 12) },
{
type: PngNodeType.ihdrInterlaceMethod,
bytes: data.subarray(12, 13),
},
],
};
break;
case "PLTE":
type = PngNodeType.plte;
chunkDataNode = {
...chunkDataNode,
type: PngNodeType.plteChunkData,
children: chunkBytes(data, 3).map((bytes) => ({
type: PngNodeType.plteColor,
bytes,
})),
};
break;
case "IDAT":
type = PngNodeType.idat;
chunkDataNode = { ...chunkDataNode, type: PngNodeType.idatChunkData };
break;
case "IEND":
type = PngNodeType.iend;
chunkLengthNode = {
...chunkLengthNode,
type: PngNodeType.iendChunkLength,
};
break;
case "tRNS":
type = PngNodeType.trns;
break;
case "cHRM":
type = PngNodeType.chrm;
break;
case "gAMA":
type = PngNodeType.gama;
break;
case "iCCP":
type = PngNodeType.iccp;
break;
case "sBIT":
type = PngNodeType.sbit;
break;
case "sRGB":
type = PngNodeType.srgb;
break;
case "cICP":
type = PngNodeType.cicp;
break;
case "tEXt": {
type = PngNodeType.text;
const nullSeparatorIndex = data.indexOf(0);
if (nullSeparatorIndex === -1) break;
chunkDataNode = {
...chunkDataNode,
type: PngNodeType.textData,
children: [
{
type: PngNodeType.textKeyword,
bytes: data.subarray(0, nullSeparatorIndex),
},
{
type: PngNodeType.textNullSeparator,
bytes: data.subarray(nullSeparatorIndex, nullSeparatorIndex + 1),
},
{
type: PngNodeType.textString,
bytes: data.subarray(nullSeparatorIndex + 1),
},
],
};
break;
}
case "zTXt": {
type = PngNodeType.ztxt;
const nullSeparatorIndex = data.indexOf(0);
if (nullSeparatorIndex === -1) break;
chunkDataNode = {
...chunkDataNode,
type: PngNodeType.ztxtData,
children: [
{
type: PngNodeType.textKeyword,
bytes: data.subarray(0, nullSeparatorIndex),
},
{
type: PngNodeType.textNullSeparator,
bytes: data.subarray(nullSeparatorIndex, nullSeparatorIndex + 1),
},
{
type: PngNodeType.ztxtCompressionMethod,
bytes: data.subarray(
nullSeparatorIndex + 1,
nullSeparatorIndex + 2,
),
},
{
type: PngNodeType.ztxtString,
bytes: data.subarray(nullSeparatorIndex + 2),
},
],
};
break;
}
case "iTXt":
type = PngNodeType.itxt;
break;
case "bKGD":
type = PngNodeType.bkgd;
break;
case "hIST":
type = PngNodeType.hist;
break;
case "pHYs":
type = PngNodeType.phys;
break;
case "sPLT":
type = PngNodeType.splt;
break;
case "eXIf":
type = PngNodeType.exif;
break;
case "tIME":
type = PngNodeType.time;
break;
case "acTL":
type = PngNodeType.actl;
break;
case "fcTL":
type = PngNodeType.fctl;
break;
case "fdAT":
type = PngNodeType.fdat;
break;
case "oFFs":
type = PngNodeType.offs;
break;
case "pCAL":
type = PngNodeType.pcal;
break;
case "sCAL":
type = PngNodeType.scal;
break;
case "gIFg":
type = PngNodeType.gifg;
break;
case "gIFx":
type = PngNodeType.gifx;
break;
case "gIFt":
type = PngNodeType.gift;
break;
case "sTER":
type = PngNodeType.ster;
break;
case "dSIG":
type = PngNodeType.dsig;
break;
case "iDOT":
type = PngNodeType.idot;
break;
default:
console.warn(`Unknown chunk type ${type}`);
break;
}
/** @type {PngNode[]} */
const children = [];
children.push(chunkLengthNode);
children.push(chunkTypeNode);
if (data.byteLength) children.push(chunkDataNode);
children.push(chunkCrcNode);
return { type, bytes: allBytes, children };
};