671 lines
18 KiB
JavaScript
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);
|
|
};
|