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
|
||||
|
||||
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 */
|
|
@ -31,6 +31,7 @@ body {
|
|||
line-height: 1.5em;
|
||||
color: var(--foreground-color);
|
||||
background: var(--background-color);
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
h1,
|
||||
|
@ -64,3 +65,97 @@ 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);
|
||||
}
|
|
@ -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
|
||||
|
||||
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();
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
<meta http-equiv="refresh" content="0; url=.." />
|
||||
</noscript>
|
||||
<link rel="stylesheet" href="../common/global.css">
|
||||
<link rel="stylesheet" href="gif.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<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=.." />
|
||||
</noscript>
|
||||
<link rel="stylesheet" href="../common/global.css">
|
||||
<link rel="stylesheet" href="png.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<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 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");
|
||||
|
|
|
@ -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(""), []);
|
|
@ -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";
|
||||
|
||||
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>>;
|
||||
};
|
||||
import { Node } from "./common.d.ts";
|
||||
|
||||
export type PngNode = Node<PngNodeType>;
|
||||
|
|
Loading…
Reference in New Issue