From 7f64453e5fbe6daa08cc9021dd9ee3f4e041b18b Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Fri, 11 Aug 2023 22:40:30 -0500 Subject: [PATCH] GIF: basic parsing of header Had to move a lot of stuff out of PNG-land and into common utilities. --- public/{png => common}/explorer.js | 6 +- public/common/global.css | 95 ++++++++++++++++++++++++++ public/{png => common}/nodePath.js | 0 public/gif/constants.js | 13 ++++ public/gif/getNodeUi.js | 70 +++++++++++++++++++ public/gif/gif.js | 18 ++++- public/gif/index.html | 1 - public/gif/parseGif.js | 30 +++++++++ public/png/index.html | 1 - public/png/png.css | 97 --------------------------- public/png/png.js | 2 +- test/{png => common}/nodePath.test.ts | 6 +- types/common.d.ts | 19 ++++++ types/gif.d.ts | 4 ++ types/png.d.ts | 21 +----- 15 files changed, 258 insertions(+), 125 deletions(-) rename public/{png => common}/explorer.js (97%) rename public/{png => common}/nodePath.js (100%) create mode 100644 public/gif/constants.js create mode 100644 public/gif/getNodeUi.js create mode 100644 public/gif/parseGif.js delete mode 100644 public/png/png.css rename test/{png => common}/nodePath.test.ts (85%) create mode 100644 types/common.d.ts create mode 100644 types/gif.d.ts diff --git a/public/png/explorer.js b/public/common/explorer.js similarity index 97% rename from public/png/explorer.js rename to public/common/explorer.js index d72d590..29f3332 100644 --- a/public/png/explorer.js +++ b/public/common/explorer.js @@ -1,8 +1,8 @@ // @ts-check -import crel, { fragment } from "../common/crel.js"; -import formatBytes from "../common/formatBytes.js"; -import allChildrenOf from "../common/allChildrenOf.js"; +import crel, { fragment } from "./crel.js"; +import formatBytes from "./formatBytes.js"; +import allChildrenOf from "./allChildrenOf.js"; import * as nodePath from "./nodePath.js"; /** @typedef {import("./nodePath.js").NodePath} NodePath */ /** @typedef {import("../../types/png.d.ts").PngNode} PngNode */ diff --git a/public/common/global.css b/public/common/global.css index 43d6172..9c22313 100644 --- a/public/common/global.css +++ b/public/common/global.css @@ -31,6 +31,7 @@ body { line-height: 1.5em; color: var(--foreground-color); background: var(--background-color); + margin: 1em; } h1, @@ -63,4 +64,98 @@ ol { input, textarea { font: inherit; +} + +#explorer { + display: flex; + overflow: hidden; +} + +#explorer .bytes, +#explorer .tree { + flex: 1; + overflow: auto; +} + +#explorer .bytes { + max-width: 15em; + font-family: Inconsolata, Consolas, Monaco, monospace; + font-size: 80%; + line-height: 1.5em; + white-space: pre-wrap; +} + +#explorer .bytes span { + cursor: pointer; +} + +#explorer .bytes span.activated { + background: var(--activated-color); + color: var(--background-color); +} + +#explorer .tree { + padding-left: 0.5rem; + scrollbar-gutter: stable; +} + +#explorer .tree details.activated { + outline-width: 3px; + outline-color: var(--tree-activated-border-color); +} + +#explorer .tree details { + outline: 1px solid var(--tree-border-color); + border-radius: 10px; + padding: 0 0.5rem 0 0.5rem; + transition: outline ease-out 0.1s; + margin: 3px 3px 0.5rem 3px; +} + +#explorer .tree details[open] { + padding-bottom: 6px; +} + +#explorer .tree details p:last-child { + margin-bottom: 0; +} + +#explorer .tree details .children { + margin-left: 0.5rem; +} + +#explorer .tree details summary { + cursor: pointer; + user-select: none; + display: flex; +} + +#explorer .tree details summary:before { + content: "▶"; + margin-right: 0.2em; + transition: transform ease-out 0.1s; +} + +#explorer .tree details[open]>summary:before { + transform: rotate(90deg); +} + +#explorer .tree details summary:focus { + outline: none; +} + +#explorer .tree details summary .title { + flex: 1; + font-weight: bold; +} + +#explorer .tree details summary .bytecount { + margin-left: 0.5em; + font-size: 75%; + opacity: 0.5; +} + +#explorer .tree details:not(.activated) { + background: var(--background-color); + color: var(--foreground-color); } \ No newline at end of file diff --git a/public/png/nodePath.js b/public/common/nodePath.js similarity index 100% rename from public/png/nodePath.js rename to public/common/nodePath.js diff --git a/public/gif/constants.js b/public/gif/constants.js new file mode 100644 index 0000000..fb45fdd --- /dev/null +++ b/public/gif/constants.js @@ -0,0 +1,13 @@ +// @ts-check + +/** @enum {string} */ +export const GifNodeType = [ + "root", + + "header", + "headerSignature", + "headerVersion", +].reduce((result, id) => { + result[id] = id; + return result; +}, Object.create(null)); diff --git a/public/gif/getNodeUi.js b/public/gif/getNodeUi.js new file mode 100644 index 0000000..9cebf81 --- /dev/null +++ b/public/gif/getNodeUi.js @@ -0,0 +1,70 @@ +// @ts-check + +import { GifNodeType } from "./constants.js"; +import crel, { fragment } from "../common/crel.js"; +import getOwn from "../common/getOwn.js"; +import { areBytesEqual } from "../common/bytes.js"; +/** @typedef {import("../../types/gif.d.ts").GifNode} GifNode */ + +const textEncoder = new TextEncoder(); + +const VERSION_87 = textEncoder.encode("87a"); +const VERSION_89 = textEncoder.encode("89a"); + +/** + * @param {(string | Node)[]} children + */ +const p = (...children) => crel("p", {}, ...children); + +/** + * @typedef {object} NodeUi + * @prop {string} title + * @prop {HTMLElement | DocumentFragment} description + */ + +/** @type {Record NodeUi>} */ +const NODE_UI_FNS = { + [GifNodeType.root]: () => ({ + title: "GIF file", + description: fragment(), + }), + + // Header + + [GifNodeType.header]: () => ({ + title: "GIF header", + description: fragment( + p('GIFs start with a 6-byte header. This is typically the string "GIF87a" or "GIF89a", encoded as ASCII.'), + ), + }), + [GifNodeType.headerSignature]: () => ({ + title: "GIF signature", + description: p('GIFs start with the string "GIF", encoded as ASCII.'), + }), + [GifNodeType.headerVersion]: ({ bytes }) => { + /** @type {string} */ let end; + if (areBytesEqual(bytes, VERSION_87)) { + end = "87a"; + } else if (areBytesEqual(bytes, VERSION_89)) { + end = "89a"; + } else { + end = "something unexpected! This might not be a valid GIF file"; + } + return { + title: "GIF version", + description: p( + `The version of the GIF format. This is typically "87a" or "89a". In this case, the version is ${end}.`, + ), + }; + }, +}; + +/** + * @param {GifNode} node + * @returns {NodeUi} + */ +export default (node) => { + const uiFn = getOwn(NODE_UI_FNS, node.type); + if (!uiFn) throw new Error("Found a node with no matching UI function"); + return uiFn(node); +}; diff --git a/public/gif/gif.js b/public/gif/gif.js index 9a9ed74..b565249 100644 --- a/public/gif/gif.js +++ b/public/gif/gif.js @@ -1,6 +1,13 @@ // @ts-check import * as base64 from "../common/vendor/base64-js.js"; +import parseGif from "./parseGif.js"; +import getNodeUi from "./getNodeUi.js"; +import explorer from "../common/explorer.js"; + +const errorEl = document.getElementById("error"); +const explorerEl = document.getElementById("explorer"); +if (!errorEl || !explorerEl) throw new Error("HTML is not set up correctly"); const main = () => { // TODO: We may want a better UI here. @@ -12,7 +19,16 @@ const main = () => { } const bytes = base64.toByteArray(fileDataBase64); - console.log(bytes); + const rootNode = parseGif(bytes); + if (!rootNode) { + // TODO: Is there better UI than this? + errorEl.removeAttribute("hidden"); + return; + } + + explorerEl.innerHTML = ""; + explorerEl.append(explorer(rootNode, getNodeUi)); + explorerEl.removeAttribute("hidden"); }; main(); diff --git a/public/gif/index.html b/public/gif/index.html index 35d14ec..4d23842 100644 --- a/public/gif/index.html +++ b/public/gif/index.html @@ -7,7 +7,6 @@ - diff --git a/public/gif/parseGif.js b/public/gif/parseGif.js new file mode 100644 index 0000000..d707524 --- /dev/null +++ b/public/gif/parseGif.js @@ -0,0 +1,30 @@ +// @ts-check + +import { GifNodeType } from "./constants.js"; +/** @typedef {import("../../types/gif.d.ts").GifNode} GifNode */ + +/** + * @param {Uint8Array} bytes + * @returns {null | GifNode} The root node of the PNG tree, or null if the PNG is invalid. + */ +export default (bytes) => { + /** @type {GifNode[]} */ + const children = [ + { + type: GifNodeType.header, + bytes: bytes.subarray(0, 6), + children: [ + { + type: GifNodeType.headerSignature, + bytes: bytes.subarray(0, 3), + }, + { + type: GifNodeType.headerVersion, + bytes: bytes.subarray(3, 6), + }, + ], + }, + ]; + + return { type: GifNodeType.root, bytes, children }; +}; diff --git a/public/png/index.html b/public/png/index.html index 54e829b..41ba595 100644 --- a/public/png/index.html +++ b/public/png/index.html @@ -7,7 +7,6 @@ - diff --git a/public/png/png.css b/public/png/png.css deleted file mode 100644 index 9af152f..0000000 --- a/public/png/png.css +++ /dev/null @@ -1,97 +0,0 @@ -body { - margin: 1rem; -} - -#explorer { - display: flex; - overflow: hidden; -} - -#explorer .bytes, -#explorer .tree { - flex: 1; - overflow: auto; -} - -#explorer .bytes { - max-width: 15em; - font-family: Inconsolata, Consolas, Monaco, monospace; - font-size: 80%; - line-height: 1.5em; - white-space: pre-wrap; -} - -#explorer .bytes span { - cursor: pointer; -} - -#explorer .bytes span.activated { - background: var(--activated-color); - color: var(--background-color); -} - -#explorer .tree { - padding-left: 0.5rem; - scrollbar-gutter: stable; -} - -#explorer .tree details.activated { - outline-width: 3px; - outline-color: var(--tree-activated-border-color); -} - -#explorer .tree details { - outline: 1px solid var(--tree-border-color); - border-radius: 10px; - padding: 0 0.5rem 0 0.5rem; - transition: outline ease-out 0.1s; - margin: 3px 3px 0.5rem 3px; -} - -#explorer .tree details[open] { - padding-bottom: 6px; -} - -#explorer .tree details p:last-child { - margin-bottom: 0; -} - -#explorer .tree details .children { - margin-left: 0.5rem; -} - -#explorer .tree details summary { - cursor: pointer; - user-select: none; - display: flex; -} - -#explorer .tree details summary:before { - content: "▶"; - margin-right: 0.2em; - transition: transform ease-out 0.1s; -} - -#explorer .tree details[open]>summary:before { - transform: rotate(90deg); -} - -#explorer .tree details summary:focus { - outline: none; -} - -#explorer .tree details summary .title { - flex: 1; - font-weight: bold; -} - -#explorer .tree details summary .bytecount { - margin-left: 0.5em; - font-size: 75%; - opacity: 0.5; -} - -#explorer .tree details:not(.activated) { - background: var(--background-color); - color: var(--foreground-color); -} \ No newline at end of file diff --git a/public/png/png.js b/public/png/png.js index 7896d4a..8cf7691 100644 --- a/public/png/png.js +++ b/public/png/png.js @@ -3,7 +3,7 @@ import * as base64 from "../common/vendor/base64-js.js"; import parsePng from "./parsePng.js"; import getNodeUi from "./getNodeUi.js"; -import explorer from "./explorer.js"; +import explorer from "../common/explorer.js"; const errorEl = document.getElementById("error"); const explorerEl = document.getElementById("explorer"); diff --git a/test/png/nodePath.test.ts b/test/common/nodePath.test.ts similarity index 85% rename from test/png/nodePath.test.ts rename to test/common/nodePath.test.ts index 06ec4fb..70a1cac 100644 --- a/test/png/nodePath.test.ts +++ b/test/common/nodePath.test.ts @@ -1,5 +1,9 @@ import { assert, assertEquals } from "assert"; -import { isEqualTo, isSupersetOf, parse } from "../../public/png/nodePath.js"; +import { + isEqualTo, + isSupersetOf, + parse, +} from "../../public/common/nodePath.js"; Deno.test("parse", () => { assertEquals(parse(""), []); diff --git a/types/common.d.ts b/types/common.d.ts new file mode 100644 index 0000000..e36223c --- /dev/null +++ b/types/common.d.ts @@ -0,0 +1,19 @@ +export type Node = { + /** + * The type of this node. Typically an enum specific to the format. + */ + type: T; + + /** + * The bytes that make up this node. + * + * It is highly encouraged to use `Uint8Array.prototype.subarray`, + * not `.slice`, to avoid copying the data. + */ + bytes: Uint8Array; + + /** + * Child nodes. + */ + children?: ReadonlyArray>; +}; diff --git a/types/gif.d.ts b/types/gif.d.ts new file mode 100644 index 0000000..4e26fdf --- /dev/null +++ b/types/gif.d.ts @@ -0,0 +1,4 @@ +import { GifNodeType } from "../public/gif/constants.js"; +import { Node } from "./common.d.ts"; + +export type GifNode = Node; diff --git a/types/png.d.ts b/types/png.d.ts index 2a51325..7f8ffd8 100644 --- a/types/png.d.ts +++ b/types/png.d.ts @@ -1,23 +1,4 @@ import { PngNodeType } from "../public/png/constants.js"; - -type Node = { - /** - * The type of this node. Typically an enum specific to the format. - */ - type: T; - - /** - * The bytes that make up this node. - * - * It is highly encouraged to use `Uint8Array.prototype.subarray`, - * not `.slice`, to avoid copying the data. - */ - bytes: Uint8Array; - - /** - * Child nodes. - */ - children?: ReadonlyArray>; -}; +import { Node } from "./common.d.ts"; export type PngNode = Node;