formats.exposed/public/gif/getNodeUi.js

486 lines
14 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";
import { parseLogicalScreenDescriptorPackedField } from "./parseGif.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 {(string | Node)[]} children
*/
const li = (...children) => crel("li", {}, ...children);
/**
* @param {number} byte
* @returns {string}
*/
const formatByte = (byte) => byte.toString(16).padStart(2, "0");
/**
* @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 block",
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]: ({ bytes }) => {
const byte = bytes[0];
const packedField = parseLogicalScreenDescriptorPackedField(byte);
return {
title: "Logical Screen Descriptor packed fields",
description: fragment(
p(
"This byte contains several flags that control how the GIF is displayed. In binary, it looks like this:",
),
p(bytes[0].toString(2).padStart(8, "0")),
crel(
"ul",
{},
li(
packedField.globalColorTableFlag
? "The first bit is 1, which means that a global color table follows the logical screen descriptor. See below for more information about this table."
: "The first bit is 0, which means that this GIF has no global color table. This means that each frame has its own local color table.",
),
li(
`The next three bits (${
packedField.colorResolution
.toString(2)
.padStart(
3,
"0",
)
}) encode the color resolution. The bigger this number, the more colors this GIF can represent. This value is decoded as binary and then incremented by one, so the color resolution is ${packedField.colorResolution}.`,
),
li(
`The next bit is ${
packedField.sortFlag ? 1 : 0
}, which means that the colors in the global color table are${
packedField.sortFlag ? "" : "n't"
} sorted. This used to be important when memory was more scarce, but it's not really important anymore.`,
),
li(
`The last three bits (${
(byte & 0b111)
.toString(2)
.padStart(
3,
"0",
)
}) encode the number of colors of the global color table. To decode this field, you add 1 and then raise that to the power of 2, so the size of the global color table is ${
pluralize(
packedField.sizeOfGlobalColorTable,
"color",
)
}. `,
),
),
),
};
},
[GifNodeType.logicalScreenBackgroundColorIndex]: () => ({
title: "Background Color Index",
description: p("TODO"),
}),
[GifNodeType.logicalScreenPixelAspectRatio]: () => ({
title: "Pixel Aspect Ratio",
description: p("TODO"),
}),
// Global color table
[GifNodeType.globalColorTable]: ({ children }) => ({
title: "Global Color Table",
description: p(
`The Global Color Table is a list of colors. Each color is encoded as three bytes: one for red, one for green, and one for blue. The number of colors in the table is ${
pluralize(
children?.length || 0,
"color",
)
}.`,
),
}),
[GifNodeType.globalColorTableColor]: ({ bytes }) => ({
title: "Global Color Table color",
description: p(
"A color. It looks like this: ",
crel(
"span",
{
style:
`display:inline-block;width:1em;height:1em;vertical-align:middle;color:transparent;background:rgb(${
bytes.join(
",",
)
})`,
},
`#${formatByte(bytes[0])}${formatByte(bytes[1])}${
formatByte(bytes[2])
}`,
),
),
}),
// Image descriptor
[GifNodeType.imageSection]: () => ({
title: "Image",
description: p("A single frame of this GIF."),
}),
[GifNodeType.imageDescriptor]: () => ({
title: "Image Descriptor",
description: p("TODO"),
}),
[GifNodeType.imageSeparator]: () => ({
title: "Image Separator",
description: p("TODO"),
}),
[GifNodeType.imageDescriptorLeftPosition]: () => ({
title: "Image Left Position",
description: p("The X coordinate of the top left corner of this frame."),
}),
[GifNodeType.imageDescriptorTopPosition]: () => ({
title: "Image Top Position",
description: p("The Y coordinate of the top left corner of this frame."),
}),
[GifNodeType.imageDescriptorWidth]: () => ({
title: "Image Width",
description: p("The width of this frame."),
}),
[GifNodeType.imageDescriptorHeight]: () => ({
title: "Image Height",
description: p("The height of this frame."),
}),
[GifNodeType.imageDescriptorPackedFields]: () => ({
title: "Image Descriptor packed fields",
description: p("TODO"),
}),
// Local color table
[GifNodeType.localColorTable]: ({ children }) => ({
title: "Local Color Table",
description: p(
`The Local Color Table is a list of colors for this frame. Each color is encoded as three bytes: one for red, one for green, and one for blue. The number of colors in the table is ${
pluralize(
children?.length || 0,
"color",
)
}.`,
),
}),
[GifNodeType.localColorTableColor]: ({ bytes }) => ({
// TODO: DRY this out with the global color above? Or maybe just use a single node type?
title: "Local Color Table color",
description: p(
"A color. It looks like this: ",
crel(
"span",
{
style:
`display:inline-block;width:1em;height:1em;vertical-align:middle;color:transparent;background:rgb(${
bytes.join(
",",
)
})`,
},
`#${formatByte(bytes[0])}${formatByte(bytes[1])}${
formatByte(bytes[2])
}`,
),
),
}),
// Image data
[GifNodeType.imageData]: () => ({
title: "Image Data",
description: p("TODO"),
}),
[GifNodeType.imageDataLzwMinimumCodeSize]: () => ({
title: "LZW Minimum Code Size",
description: p("TODO"),
}),
[GifNodeType.imageDataSubBlock]: () => ({
title: "Image Data Sub-Block",
description: p("TODO"),
}),
[GifNodeType.imageDataSubBlockSize]: () => ({
title: "Image Data Sub-Block Size",
description: p("TODO"),
}),
[GifNodeType.imageDataSubBlockData]: () => ({
title: "Image Data Sub-Block Data",
description: p("TODO"),
}),
[GifNodeType.imageDataTerminator]: () => ({
title: "Image Data Terminator",
description: p("TODO"),
}),
// Generic extension stuff
[GifNodeType.extensionBlockIntroducer]: () => ({
title: "Extension Block Introducer",
description: p("TODO"),
}),
[GifNodeType.extensionBlockSize]: () => ({
title: "Extension Block Size",
description: p("TODO"),
}),
[GifNodeType.extensionBlockTerminator]: () => ({
title: "Extension Block Terminator",
description: p("A zero byte indicating the end of this extension block."),
}),
// Application extension block
[GifNodeType.applicationExtensionBlock]: () => ({
title: "Application Extension Block",
description: p(
'A block for application-specific extensions. A very common extension is the "NETSCAPE2.0" extension which specifies the number of loops.',
),
}),
[GifNodeType.applicationIdentifier]: ({ bytes }) => ({
title: "Application Identifier",
description: p(
"An 8-byte string identifying the application. In this case, the identifier is ",
crel("code", {}, new TextDecoder().decode(bytes)),
),
}),
[GifNodeType.applicationAuthenticationCode]: () => ({
title: "Application Authentication Code",
description: p("Three bytes identifying the application."),
}),
[GifNodeType.applicationSubBlock]: ({ bytes }) => {
const isTerminator = areBytesEqual(bytes, new Uint8Array([0]));
return {
title: "Application Sub-Block",
description: p(
"A sub-block of data for this application." +
(isTerminator
? " This byte is empty, meaning it's the end of the sub-blocks and this application extension block."
: ""),
),
};
},
[GifNodeType.applicationSubBlockSize]: ({ bytes }) => ({
title: "Application Sub-Block Size",
description: p(`The size of this application sub-block: ${bytes[0]}.`),
}),
[GifNodeType.applicationSubBlockData]: () => ({
title: "Application Sub-Block Data",
description: p("The data for this application sub-block."),
}),
// Comment extension block
[GifNodeType.commentSubBlock]: () => ({
title: "Comment Sub-Block",
description: p("TODO"),
}),
[GifNodeType.commentSubBlockSize]: () => ({
title: "Comment Sub-Block Size",
description: p("TODO"),
}),
[GifNodeType.commentSubBlockData]: () => ({
title: "Comment Sub-Block Data",
description: p("TODO"),
}),
// Graphic control extension block
[GifNodeType.graphicControlExtensionBlock]: () => ({
title: "Graphic Control Extension Block",
description: p("TODO"),
}),
[GifNodeType.graphicControlExtensionPackedFields]: () => ({
title: "Graphic Control Extension packed fields",
description: p("TODO"),
}),
[GifNodeType.graphicControlExtensionDelayTime]: () => ({
title: "Graphic Control Extension Delay Time",
description: p("TODO"),
}),
[GifNodeType.graphicControlExtensionTransparentColorIndex]: () => ({
title: "Graphic Control Extension Transparent Color Index",
description: p("TODO"),
}),
// Plain text extension block
[GifNodeType.plainTextExtensionBlock]: () => ({
title: "Plain Text Extension Block",
description: p("TODO"),
}),
[GifNodeType.plainTextExtensionBlockTextGridLeftPosition]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextExtensionBlockTextGridTopPosition]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextExtensionBlockTextGridWidth]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextExtensionBlockTextGridHeight]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextExtensionBlockCharacterCellWidth]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextExtensionBlockCharacterCellHeight]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextExtensionBlockTextForegroundColorIndex]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextExtensionBlockTextBackgroundColorIndex]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextSubBlock]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextSubBlockSize]: () => ({
title: "TODO",
description: p("TODO"),
}),
[GifNodeType.plainTextSubBlockData]: () => ({
title: "TODO",
description: p("TODO"),
}),
// Unknown extension block
[GifNodeType.unknownExtensionBlock]: () => ({
title: "Unknown Extension Block",
description: p("TODO"),
}),
[GifNodeType.unknownExtensionBlockLabel]: () => ({
title: "Unknown Extension Block Label",
description: p("TODO"),
}),
[GifNodeType.unknownExtensionBlockData]: () => ({
title: "Unknown Extension Block Data",
description: p("TODO"),
}),
// Terminator
[GifNodeType.gifTerminator]: () => ({
title: "GIF Terminator",
description: p(
"The terminator is a one-byte block indicating the end of our GIF. We're done!",
),
}),
};
/**
* @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);
};