120 lines
3.3 KiB
JavaScript
120 lines
3.3 KiB
JavaScript
// @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";
|
|
import pluralize from "../common/pluralize.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);
|
|
|
|
/**
|
|
* @param {Uint8Array} bytes
|
|
* @returns {number}
|
|
*/
|
|
const readGifUint = (bytes) => {
|
|
if (bytes.length !== 2) throw new Error("Expected 2 bytes");
|
|
return (bytes[1] << 8) | bytes[0];
|
|
};
|
|
|
|
/**
|
|
* @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}.`,
|
|
),
|
|
};
|
|
},
|
|
|
|
// Logical screen descriptor
|
|
|
|
[GifNodeType.logicalScreenDescriptor]: () => ({
|
|
title: "Logical Screen Descriptor",
|
|
description: p(
|
|
"The logical screen descriptor contains information about the overall GIF, such as its dimensions.",
|
|
),
|
|
}),
|
|
[GifNodeType.logicalScreenWidth]: ({ bytes }) => ({
|
|
title: "Logical Screen Width",
|
|
description: p(
|
|
`The width of the image stored as a 16-bit little endian unsigned integer. In this case, the width is ${
|
|
pluralize(readGifUint(bytes), "byte")
|
|
}.`,
|
|
),
|
|
}),
|
|
[GifNodeType.logicalScreenHeight]: ({ bytes }) => ({
|
|
title: "Logical Screen Height",
|
|
description: p(
|
|
`The height of the image stored as a 16-bit little endian unsigned integer. In this case, the height is ${
|
|
pluralize(readGifUint(bytes), "byte")
|
|
}.`,
|
|
),
|
|
}),
|
|
[GifNodeType.logicalScreenDescriptorPackedFields]: () => ({
|
|
title: "Logical Screen Descriptor packed fields",
|
|
description: p("TODO"),
|
|
}),
|
|
[GifNodeType.logicalScreenBackgroundColorIndex]: () => ({
|
|
title: "Background Color Index",
|
|
description: p("TODO"),
|
|
}),
|
|
[GifNodeType.logicalScreenPixelAspectRatio]: () => ({
|
|
title: "Pixel Aspect Ratio",
|
|
description: p("TODO"),
|
|
}),
|
|
};
|
|
|
|
/**
|
|
* @param {GifNode} node
|
|
* @returns {NodeUi}
|
|
*/
|
|
export default (node) => {
|
|
const uiFn = getOwn(NODE_UI_FNS, node.type);
|
|
if (!uiFn) {
|
|
throw new Error(`Found a node (${node.type}) with no matching UI function`);
|
|
}
|
|
return uiFn(node);
|
|
};
|