formats.exposed/public/gif/getNodeUi.js

201 lines
6.4 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. Sorting the color table can improve compression.${
packedField.globalColorTableFlag
? ""
: " However, there is no global color table in this GIF, so this bit is ignored."
}`,
),
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])
}`,
),
),
}),
};
/**
* @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);
};