GIF: basic parsing of header
Had to move a lot of stuff out of PNG-land and into common utilities.
This commit is contained in:
parent
69c66e7d99
commit
7f64453e5f
|
@ -1,8 +1,8 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import crel, { fragment } from "../common/crel.js";
|
import crel, { fragment } from "./crel.js";
|
||||||
import formatBytes from "../common/formatBytes.js";
|
import formatBytes from "./formatBytes.js";
|
||||||
import allChildrenOf from "../common/allChildrenOf.js";
|
import allChildrenOf from "./allChildrenOf.js";
|
||||||
import * as nodePath from "./nodePath.js";
|
import * as nodePath from "./nodePath.js";
|
||||||
/** @typedef {import("./nodePath.js").NodePath} NodePath */
|
/** @typedef {import("./nodePath.js").NodePath} NodePath */
|
||||||
/** @typedef {import("../../types/png.d.ts").PngNode} PngNode */
|
/** @typedef {import("../../types/png.d.ts").PngNode} PngNode */
|
|
@ -31,6 +31,7 @@ body {
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
color: var(--foreground-color);
|
color: var(--foreground-color);
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
|
margin: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
|
@ -63,4 +64,98 @@ ol {
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea {
|
||||||
font: inherit;
|
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);
|
||||||
}
|
}
|
|
@ -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));
|
|
@ -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<string, (node: GifNode) => 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);
|
||||||
|
};
|
|
@ -1,6 +1,13 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import * as base64 from "../common/vendor/base64-js.js";
|
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 = () => {
|
const main = () => {
|
||||||
// TODO: We may want a better UI here.
|
// TODO: We may want a better UI here.
|
||||||
|
@ -12,7 +19,16 @@ const main = () => {
|
||||||
}
|
}
|
||||||
const bytes = base64.toByteArray(fileDataBase64);
|
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();
|
main();
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<meta http-equiv="refresh" content="0; url=.." />
|
<meta http-equiv="refresh" content="0; url=.." />
|
||||||
</noscript>
|
</noscript>
|
||||||
<link rel="stylesheet" href="../common/global.css">
|
<link rel="stylesheet" href="../common/global.css">
|
||||||
<link rel="stylesheet" href="gif.css">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -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 };
|
||||||
|
};
|
|
@ -7,7 +7,6 @@
|
||||||
<meta http-equiv="refresh" content="0; url=.." />
|
<meta http-equiv="refresh" content="0; url=.." />
|
||||||
</noscript>
|
</noscript>
|
||||||
<link rel="stylesheet" href="../common/global.css">
|
<link rel="stylesheet" href="../common/global.css">
|
||||||
<link rel="stylesheet" href="png.css">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@
|
||||||
import * as base64 from "../common/vendor/base64-js.js";
|
import * as base64 from "../common/vendor/base64-js.js";
|
||||||
import parsePng from "./parsePng.js";
|
import parsePng from "./parsePng.js";
|
||||||
import getNodeUi from "./getNodeUi.js";
|
import getNodeUi from "./getNodeUi.js";
|
||||||
import explorer from "./explorer.js";
|
import explorer from "../common/explorer.js";
|
||||||
|
|
||||||
const errorEl = document.getElementById("error");
|
const errorEl = document.getElementById("error");
|
||||||
const explorerEl = document.getElementById("explorer");
|
const explorerEl = document.getElementById("explorer");
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { assert, assertEquals } from "assert";
|
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", () => {
|
Deno.test("parse", () => {
|
||||||
assertEquals(parse(""), []);
|
assertEquals(parse(""), []);
|
|
@ -0,0 +1,19 @@
|
||||||
|
export type Node<T> = {
|
||||||
|
/**
|
||||||
|
* 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<Node<T>>;
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { GifNodeType } from "../public/gif/constants.js";
|
||||||
|
import { Node } from "./common.d.ts";
|
||||||
|
|
||||||
|
export type GifNode = Node<GifNodeType>;
|
|
@ -1,23 +1,4 @@
|
||||||
import { PngNodeType } from "../public/png/constants.js";
|
import { PngNodeType } from "../public/png/constants.js";
|
||||||
|
import { Node } from "./common.d.ts";
|
||||||
type Node<T> = {
|
|
||||||
/**
|
|
||||||
* 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<Node<T>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PngNode = Node<PngNodeType>;
|
export type PngNode = Node<PngNodeType>;
|
||||||
|
|
Loading…
Reference in New Issue