formats.exposed/public/png/getNodeUi.js

671 lines
18 KiB
JavaScript

// @ts-check
import { PngNodeType } from "./constants.js";
import crel, { fragment } from "../common/crel.js";
import formatBytes from "../common/formatBytes.js";
import pluralize from "../common/pluralize.js";
import getOwn from "../common/getOwn.js";
import parsePngUint from "./parsePngUint.js";
/** @typedef {import("../../types/png.d.ts").PngNode} PngNode */
/**
* @param {(string | Node)[]} children
*/
const p = (...children) => crel("p", {}, ...children);
/**
* @param {string} text
* @param {string} href
*/
const a = (text, href) => crel("a", { href, target: "_blank" }, text);
/**
* @param {number} byte
* @returns {string}
*/
const formatByte = (byte) => byte.toString(16).padStart(2, "0");
/**
* @param {Uint8Array} _bytes
* @returns {null | Uint8Array}
*/
const maybeInflate = (_bytes) => {
// TODO: Inflate the bytes
return null;
};
/**
* @typedef {object} NodeUi
* @prop {string} title
* @prop {HTMLElement | DocumentFragment} description
*/
/** @type {Record<string, (node: PngNode) => NodeUi>} */
const NODE_UI_FNS = {
// Generic parts of a PNG
[PngNodeType.root]: () => ({
title: "PNG file",
description: fragment(),
}),
[PngNodeType.signature]: () => ({
title: "PNG signature",
description: fragment(
p(
"Every PNG starts with the same 8 bytes: the ",
a("PNG signature", "https://www.w3.org/TR/png/#5PNG-file-signature"),
".",
),
p("PNG decoders use these bytes to ensure that they're reading a PNG image. Typically, they reject the file if it doesn't start with the signature. Data can get corrupted in various ways (ever had a file with the wrong extension?) and this helps address that."),
p('Fun fact: if you decode these bytes as ASCII, you\'ll see the letters "PNG" in there!'),
),
}),
[PngNodeType.chunkLength]: ({ bytes }) => ({
title: "Chunk length",
description: fragment(
p(
"This chunk has ",
pluralize(parsePngUint(bytes), "byte"),
" of data, as you see in the chunk data section below. This gets encoded as a 32-bit unsigned integer. Read more in the ",
a(
'"Chunk layout" section of the spec',
"https://www.w3.org/TR/png/#5Chunk-layout",
),
".",
),
),
}),
[PngNodeType.chunkType]: ({ bytes }) => ({
title: "Chunk type",
description: p(
"The chunk type, ",
new TextDecoder().decode(bytes),
", encoded as ASCII.",
),
}),
[PngNodeType.chunkData]: () => ({
title: "Chunk data",
description: p("The chunk's data."),
}),
[PngNodeType.chunkCrc]: () => ({
title: "Chunk checksum",
description: p(
"A four-byte checksum for this chunk. Checksums use the CRC32 algorithm on the type and data bytes in the chunk.",
),
}),
[PngNodeType.unknownChunk]: (node) => {
const children = node.children || [];
const chunkTypeNode = children.find((child) =>
child.type === PngNodeType.chunkType
);
const chunkTypeBytes = chunkTypeNode?.bytes;
const chunkType = chunkTypeBytes
? new TextDecoder().decode(chunkTypeBytes)
: null;
return {
title: chunkType ? `${chunkType} chunk` : "Unknown chunk",
description: p(
"This looks like a valid chunk type, but this tool doesn't know about it.",
),
};
},
// IHDR
[PngNodeType.ihdr]: () => ({
title: "IHDR: Image header",
description: p(
"Every PNG is made up of multiple chunks. The first chunk is always the image header, or ",
a("IHDR", "https://www.w3.org/TR/png/#11IHDR"),
". It contains metadata about the image, such as dimensions and color information.",
),
}),
[PngNodeType.ihdrChunkData]: () => ({
title: "Image header data",
description: fragment(),
}),
[PngNodeType.ihdrWidth]: ({ bytes }) => ({
title: "Image width",
description: p(
"This image has a width of ",
pluralize(parsePngUint(bytes), "pixel"),
". That gets encoded as a 32-bit unsigned integer.",
),
}),
[PngNodeType.ihdrHeight]: ({ bytes }) => ({
title: "Image height",
description: p(
"This image has a height of ",
pluralize(parsePngUint(bytes), "pixel"),
". That gets encoded as a 32-bit unsigned integer.",
),
}),
[PngNodeType.ihdrBitDepth]: ({ bytes }) => ({
title: "Image bit depth",
description: p(
`The number of bits per sample in the image. This image uses ${
pluralize(bytes[0], "bit")
} per sample.`,
),
}),
[PngNodeType.ihdrColourType]: ({ bytes }) => {
const colorType = bytes[0];
const colorTypeString = ({
0: "greyscale. That means each pixel is a greyscale.",
2: '"truecolor". That means each pixel has an RGB value.',
3: '"indexed-color". That means each pixel is looked up in the PLTE palette chunk.',
4: "greyscale with alpha. That means each pixel has a greyscale value followed by an alpha value.",
6: '"truecolor" with alpha. That means that each pixel has an RGBA value.',
})[colorType];
let description;
if (colorTypeString) {
description =
`The color type of the image. This image is of color type ${colorType}, also known as ${colorTypeString}`;
} else {
description =
`The color type of the image...but this one, ${colorType}, is invalid.`;
}
return { title: "Image color type", description: p(description) };
},
[PngNodeType.ihdrCompressionMethod]: () => ({
title: "Image compression method",
description: p(
"The compression method for the pixel data. All PNGs set this to 0, but this is here just in case the world wants to support a new compression method in the future.",
),
}),
[PngNodeType.ihdrFilterMethod]: () => ({
title: "Image filter method",
description: p(
"The filter method for the pixel data. All PNGs set this to 0, but this is here just in case the world wants to support a new compression method in the future.",
),
}),
[PngNodeType.ihdrInterlaceMethod]: ({ bytes }) => ({
title: "Image interlace method",
description: fragment(
p(
a("Interlacing", "https://www.w3.org/TR/png/#8Interlace"),
" can allow a rough image to be shown while it's loading. The PNG spec defines two interlace methods: none (where pixels are extracted sequentially from left to right) and Adam7, where pixels are transmitted in a more complex order.",
"This image uses the ",
bytes[0] === 0 ? "former" : "latter",
".",
),
),
}),
// PLTE
[PngNodeType.plte]: () => ({
title: "PLTE: Palette",
description: fragment(
p(
"This is a ",
a("palette chunk", "https://www.w3.org/TR/png/#11PLTE"),
". Is is typically used for indexed-color images where a palette is defined, and then each pixel is looked up in this palette. It can improve compression when you the image only has a small number of colors.",
),
p("In some cases (not detected by this tool), it can also be used for other purposes. See the spec for details."),
),
}),
[PngNodeType.plteChunkData]: () => ({
title: "Palette chunk data",
description: p(
"Each pixel is represented by a red, green, and blue value of 1 byte each.",
),
}),
[PngNodeType.plteColor]: ({ bytes }) => ({
title: "Palette color",
description: p(
"A palette 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])
}`,
),
),
}),
// IDAT
[PngNodeType.idat]: () => ({
title: "IDAT: Image data",
description: p(
"The ",
a("image data chunk", "https://www.w3.org/TR/png/#11IDAT"),
" contains the compressed image data. The compressed image data can be spread across multiple IDAT chunks or all in one.",
),
}),
[PngNodeType.idatChunkData]: ({ bytes }) => {
const inflatedBytes = maybeInflate(bytes);
return {
title: "Image data",
description: fragment(
p("The image data. This is compressed in the zlib format."),
...(inflatedBytes
? [
p("The uncompressed bytes look like this:"),
crel("pre", {}, formatBytes(inflatedBytes)),
]
: []),
),
};
},
// IEND
[PngNodeType.iend]: () => ({
title: "IEND: Image end",
description: p(
"The ",
a("IEND chunk", "https://www.w3.org/TR/png/#11IEND"),
" marks the end of a PNG and should always be the final chunk.",
),
}),
[PngNodeType.iendChunkLength]: () => ({
title: "Chunk length",
description: p("IEND chunks always have a length of 0."),
}),
// tRNS
[PngNodeType.trns]: () => ({
title: "tRNS: Transparency",
description: p(
"The ",
a("tRNS chunk", "https://www.w3.org/TR/png/#11tRNS"),
" encodes transparency information.",
),
}),
// cHRM
[PngNodeType.chrm]: () => ({
title: "cHRM: Primary chromaticities and white point",
description: p(
"The ",
a("cHRM chunk", "https://www.w3.org/TR/png/#11cHR"),
" encodes chromacities of colors and the white point.",
),
}),
// gAMA
[PngNodeType.gama]: () => ({
title: "gAMA: Image gamma",
description: p(
"The ",
a("gAMA chunk", "https://www.w3.org/TR/png/#11gAMA"),
" specifies the ",
a("gamma value", "https://www.w3.org/TR/png/#dfn-gamma-value"),
" of the image.",
),
}),
// iCCP
[PngNodeType.iccp]: () => ({
title: "iCCP: Color profile",
description: p(
"The ",
a("iCCP chunk", "https://www.w3.org/TR/png/#11iCCP"),
" encodes an embedded color profile from the International Color Consortium.",
),
}),
// sBIT
[PngNodeType.sbit]: () => ({
title: "sBIT: Significant bits",
description: p(
"The ",
a("sBIT chunk", "https://www.w3.org/TR/png/#11sBIT"),
" .",
),
}),
// sRGB
[PngNodeType.srgb]: () => ({
title: "sRGB: Standard RGB color space",
description: p(
"The ",
a("sRGB chunk", "https://www.w3.org/TR/png/#srgb-standard-colour-space"),
" specifies the rendering intent for the color space.",
),
}),
// cICP
[PngNodeType.cicp]: () => ({
title: "cICP",
description: p(
"The ",
a("cICP chunk", "https://www.w3.org/TR/png/#cICP-chunk"),
' specifies "coding-independent code points for video signal type identification".',
),
}),
// Text chunks
[PngNodeType.text]: () => ({
title: "tEXt: Text data",
description: p(
"You can encode text with the ",
a("tEXt chunk", "https://www.w3.org/TR/png/#11tEXt"),
". Each text chunk contains a keyword, a null separator, and 0 or more bytes of Latin-1 text.",
),
}),
[PngNodeType.textData]: () => ({
title: "Text data",
description: fragment(),
}),
[PngNodeType.textKeyword]: ({ bytes }) => ({
title: "Keyword",
description: p(
JSON.stringify(
new TextDecoder("latin1").decode(bytes),
),
", encoded with Latin-1.",
),
}),
[PngNodeType.textNullSeparator]: () => ({
title: "Null separator",
description: p("A null separator (a single zero)."),
}),
[PngNodeType.textString]: ({ bytes }) => ({
title: "Text",
description: p(
JSON.stringify(
new TextDecoder("latin1").decode(bytes),
),
", encoded with Latin-1.",
),
}),
[PngNodeType.ztxt]: () => ({
title: "zTXt: Compressed text data",
description: p(
"You can encode text with the ",
a("zTXt chunk", "https://www.w3.org/TR/png/#11zTXt"),
". Each of these chunks contains a keyword, a null separator, a compression method, and zlib-compressed Latin-1 text.",
),
}),
[PngNodeType.ztxtData]: () => ({
title: "Compressed text data",
description: fragment(),
}),
[PngNodeType.ztxtCompressionMethod]: () => ({
title: "Text compression method",
description: p(
"The compression method for the text. All PNGs set this to 0, but this is here in case we want to support a new compression method in the future.",
),
}),
[PngNodeType.ztxtString]: ({ bytes }) => {
const inflatedBytes = maybeInflate(bytes);
let description;
if (inflatedBytes) {
const deflatedBytes = new TextEncoder().encode("TODO");
description = p(
JSON.stringify(
new TextDecoder("latin1").decode(deflatedBytes),
),
", encoded with Latin-1 and then compressed.",
);
} else {
description = p(
"Text, encoded with Latin-1 and then compressed. I couldn't decompress it, though...",
);
}
return { title: "Text (compressed)", description };
},
[PngNodeType.itxt]: () => ({
title: "iTXt: International text data",
description: p(
"You can encode text with the ",
a("iTXt chunk", "https://www.w3.org/TR/png/#11iTXt"),
". Each of these chunks contains ",
),
}),
// bKGD
[PngNodeType.bkgd]: () => ({
title: "bKGD: Default background",
description: p(
"The ",
a("bKGD chunk", "https://www.w3.org/TR/png/#11bKGD"),
" specifies the default background color.",
),
}),
// hIST
[PngNodeType.hist]: () => ({
title: "hIST: Palette histogram",
description: p(
"The ",
a("hIST chunk", "https://www.w3.org/TR/png/#11hIST"),
" gives the approximate usage frequency of each color in the palette, and only makes sense if there's a PLTE chunk.",
),
}),
// pHYs
[PngNodeType.phys]: () => ({
title: "pHYs: Physical size",
description: p(
"The ",
a("pHYs chunk", "https://www.w3.org/TR/png/#11pHYs"),
" describes the intended physical size of the image.",
),
}),
// sPLT
[PngNodeType.splt]: () => ({
title: "sPLT: Suggested palette",
description: p(
"The ",
a("sPLT chunk", "https://www.w3.org/TR/png/#11sPLT"),
", not to be confused with the PLTE palette chunk, suggests a reduced palette to be used when the display can't display all the colors in the image.",
),
}),
// eXIf
[PngNodeType.exif]: () => ({
title: "eXIf: Exif data",
description: p(
"The ",
a("eXIf chunk", "https://www.w3.org/TR/png/#eXIf"),
" encodes exif data, which can include lots of metadata.",
),
}),
// tIME
[PngNodeType.time]: () => ({
title: "tIME: Last modified time",
description: p(
"The ",
a("tIME chunk", "https://www.w3.org/TR/png/#11tIME"),
" encodes the last modified time.",
),
}),
// acTL
[PngNodeType.actl]: () => ({
title: "acTL: Animation control",
description: p(
"The ",
a("acTL chunk", "https://www.w3.org/TR/png/#acTL-chunk"),
" provides some metadata about animated PNGs.",
),
}),
// fcTL
[PngNodeType.fctl]: () => ({
title: "fcTL: Animation frame control",
description: p(
"The ",
a("fcTL chunk", "https://www.w3.org/TR/png/#fcTL-chunk"),
" defines metadata (such as the duration) of an APNG's frame.",
),
}),
// fdAT
[PngNodeType.fdat]: () => ({
title: "fdAT: Animation frame data",
description: p(
"The ",
a("fdAT chunk", "https://www.w3.org/TR/png/#fdAT-chunk"),
" describes the frame of an animated PNG. It is like the IDAT chunk but for APNGs.",
),
}),
// oFFs
[PngNodeType.offs]: () => ({
title: "oFFs: Image offset",
description: p(
"The ",
a(
"oFFs chunk",
"https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html#C.oFFs",
),
" is a nonstandard chunk used in printing.",
),
}),
// pCAL
[PngNodeType.pcal]: () => ({
title: "pCAL: Pixel value calibration",
description: p(
"The ",
a(
"pCAL chunk",
"https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html#C.pCAL",
),
" is a nonstandard chunk, useful in unusual cases when the pixels store physical data other than color values, like temperatures.",
),
}),
// sCAL
[PngNodeType.scal]: () => ({
title: "sCAL: Physical scale",
description: p(
"The ",
a(
"sCAL chunk",
"https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html#C.sCAL",
),
" is a nonstandard chunk that describes the intended physical size of the image.",
),
}),
// gIFg
[PngNodeType.gifg]: () => ({
title: "gIFg: GIF graphic control extension",
description: p(
"The ",
a(
"gIFg chunk",
"https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html#C.gIFg",
),
" is a nonstandard chunk that extends PNGs to be animated.",
),
}),
// gIFx
[PngNodeType.gifx]: () => ({
title: "gIFx: GIF application extension",
description: p(
"The ",
a(
"gIFx chunk",
"https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html#C.gIFx",
),
" is a nonstandard chunk that extends PNGs to be animated.",
),
}),
// gIFt
[PngNodeType.gift]: () => ({
title: "gIFt: GIF plain text",
description: p(
"The ",
a(
"gIFt chunk",
"https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html#DC.gIFt",
),
" is a nonstandard, deprecated chunk for GIF text.",
),
}),
// sTER
[PngNodeType.ster]: () => ({
title: "sTER: Stereo images",
description: p(
"The ",
a(
"sTER chunk",
"https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html#C.sTER",
),
" is a nonstandard chunk for stereogram images.",
),
}),
// dSIG
[PngNodeType.dsig]: () => ({
title: "dSIG: Digital signature",
description: p(
"The ",
a(
"dSIG chunk",
"https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html#C.dSIG",
),
" is a nonstandard chunk for digital signatures.",
),
}),
// iDOT
[PngNodeType.idot]: () => ({
title: "iDOT: Apple's unusual chunk",
description: p(
"The iDOT chunk is a nonstandard chunk. It is most commonly generated by Apple platforms. Its undocumented workings have been ",
a(
"reverse engineered",
"https://www.hackerfactor.com/blog/index.php?/archives/895-Connecting-the-iDOTs.html",
),
".",
),
}),
};
/**
* @param {PngNode} 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);
};