formats.exposed/public/gif/getNodeUi.js

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);
};