PNG: parsing PNG data

No UI yet, but the parser seems mostly done.
This commit is contained in:
Evan Hahn 2023-08-02 11:43:02 -05:00
parent 79498239c4
commit cfc4bc1996
188 changed files with 699 additions and 26 deletions

28
public/common/bytes.js Normal file
View File

@ -0,0 +1,28 @@
// @ts-check
/**
* @param {Uint8Array} a
* @param {Uint8Array} b
* @returns {boolean}
*/
export const areBytesEqual = (a, b) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
};
/**
* @param {Uint8Array} bytes
* @param {number} size
* @returns {Uint8Array[]}
*/
export const chunkBytes = (bytes, size) => {
/** @type {Uint8Array[]} */
const result = [];
for (let i = 0; i < bytes.byteLength; i += size) {
result.push(bytes.subarray(i, i + size));
}
return result;
};

99
public/png/constants.js Normal file
View File

@ -0,0 +1,99 @@
// @ts-check
/** @enum {string} */
export const PngNodeType = [
"root",
"signature",
"unknownChunk",
"chunkLength",
"chunkType",
"chunkData",
"chunkCrc",
"ihdr",
"ihdrChunkData",
"ihdrWidth",
"ihdrHeight",
"ihdrBitDepth",
"ihdrColourType",
"ihdrCompressionMethod",
"ihdrFilterMethod",
"ihdrInterlaceMethod",
"plte",
"plteChunkData",
"plteColor",
"idat",
"idatChunkData",
"iend",
"iendChunkLength",
"trns",
"chrm",
"gama",
"iccp",
"sbit",
"srgb",
"cicp",
"text",
"textData",
"textKeyword",
"textNullSeparator",
"textString",
"ztxt",
"ztxtData",
"ztxtCompressionMethod",
"ztxtString",
"itxt",
"bkgd",
"hist",
"phys",
"splt",
"exif",
"time",
"actl",
"fctl",
"fdat",
"offs",
"pcal",
"scal",
"gifg",
"gifx",
"gift",
"ster",
"dsig",
"idot",
].reduce((result, id) => {
result[id] = id;
return result;
}, Object.create(null));

28
public/png/crc32.js Normal file
View File

@ -0,0 +1,28 @@
// @ts-check
/** * @type {number[]} */
const crcTable = [];
for (let i = 0; i < 256; i++) {
let b = i;
for (let j = 0; j < 8; j++) {
b = b & 1 ? 0xedb88320 ^ (b >>> 1) : b >>> 1;
}
crcTable[i] = b >>> 0;
}
/**
* Compute the CRC32 checksum of some `Uint8Array`s.
* @param {Uint8Array[]} uint8arrays
* @returns {number}
*/
export default (...uint8arrays) => {
let crc = -1;
for (const bytes of uint8arrays) {
for (let i = 0; i < bytes.length; i++) {
crc = crcTable[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
}
}
return (crc ^ -1) >>> 0;
};

View File

@ -11,7 +11,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
</head> </head>
<body> <body>
<div id="error" hidden></div> <div id="error" hidden>Failed to parse PNG</div>
<div id="explorer" hidden> <div id="explorer" hidden>
<div id="bytesEl"></div> <div id="bytesEl"></div>
<div id="treeEl"></div> <div id="treeEl"></div>

305
public/png/parsePng.js Normal file
View File

@ -0,0 +1,305 @@
// @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").Node<PngNodeType>} 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 };
};

View File

@ -1,5 +1,7 @@
// @ts-check // @ts-check
import parsePng from "./parsePng.js";
import parseHash from "./parseHash.js"; import parseHash from "./parseHash.js";
const errorEl = document.getElementById("error"); const errorEl = document.getElementById("error");
@ -14,8 +16,17 @@ const main = () => {
return; return;
} }
const { bytes } = parsedHash;
const rootNode = parsePng(bytes);
if (!rootNode) {
// TODO: Is there better UI than this?
errorEl.removeAttribute("hidden");
return;
}
// TODO: Actually do something! // TODO: Actually do something!
console.log(parsedHash); console.log(rootNode);
}; };
main(); main();

View File

@ -1,11 +1,7 @@
import { assertEquals } from "assert"; import { assertEquals } from "assert";
import { b } from "../helpers.ts";
import * as base64url from "../../public/common/base64url.js"; import * as base64url from "../../public/common/base64url.js";
/**
* A shorthand for `new Uint8Array()`.
*/
const b = (...bytes: number[]) => new Uint8Array(bytes);
Deno.test('encodes no bytes as ""', () => { Deno.test('encodes no bytes as ""', () => {
assertEquals(base64url.stringify(b()), ""); assertEquals(base64url.stringify(b()), "");
}); });

20
test/common/bytes.test.ts Normal file
View File

@ -0,0 +1,20 @@
import { assert, assertEquals } from "assert";
import { b } from "../helpers.ts";
import { areBytesEqual, chunkBytes } from "../../public/common/bytes.js";
Deno.test("areBytesEqual", () => {
const x = b(1, 2);
const y = b(1, 2, 3);
const z = b(1, 2, 3);
assert(areBytesEqual(x, x));
assert(areBytesEqual(y, z));
assert(!areBytesEqual(x, y));
});
Deno.test("chunkBytes", () => {
assertEquals(
chunkBytes(b(1, 2, 3, 4, 5, 6, 7, 8), 3),
[b(1, 2, 3), b(4, 5, 6), b(7, 8)],
);
});

25
test/helpers.ts Normal file
View File

@ -0,0 +1,25 @@
import { stub } from "mock";
/**
* A shorthand for `new Uint8Array()`.
*/
export const b = (...bytes: number[]) => new Uint8Array(bytes);
/**
* @example
* Deno.test("works", stubbingWarn(() => {
* // ...
* }));
*/
export const stubbingWarn = (
fn: (t: Deno.TestContext) => void | Promise<void>,
): (t: Deno.TestContext) => Promise<void> => (
async (t) => {
const warnStub = stub(console, "warn");
try {
return await fn(t);
} finally {
warnStub.restore();
}
}
);

21
test/png/crc32.test.ts Normal file
View File

@ -0,0 +1,21 @@
import { assertEquals } from "assert";
import { b } from "../helpers.ts";
import crc32 from "../../public/png/crc32.js";
Deno.test("computes CRC32s", () => {
// These tests are lifted from the following Go 1.19.3 code:
//
// ```
// crc := crc32.NewIEEE()
// fmt.Println(crc.Sum32())
// crc.Write([]byte{})
// fmt.Println(crc.Sum32())
// crc.Write([]byte{1, 2, 3})
// fmt.Println(crc.Sum32())
// crc.Write([]byte{4, 5, 6})
// fmt.Println(crc.Sum32())
// ```
assertEquals(crc32(), 0);
assertEquals(crc32(b(1, 2, 3)), 1438416925);
assertEquals(crc32(b(1, 2, 3, 4, 5, 6)), 2180413220);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Some files were not shown because too many files have changed in this diff Show More