GIF: basic parsing of image sections and skipping extensions
This commit is contained in:
parent
858c6c83a4
commit
c755f0bb6f
|
@ -1,7 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
export const GifNodeType = [
|
const types = [
|
||||||
"root",
|
"root",
|
||||||
|
|
||||||
"header",
|
"header",
|
||||||
|
@ -17,7 +17,44 @@ export const GifNodeType = [
|
||||||
|
|
||||||
"globalColorTable",
|
"globalColorTable",
|
||||||
"globalColorTableColor",
|
"globalColorTableColor",
|
||||||
|
|
||||||
|
"imageSection",
|
||||||
|
"imageDescriptor",
|
||||||
|
"imageSeparator",
|
||||||
|
"imageDescriptorLeftPosition",
|
||||||
|
"imageDescriptorTopPosition",
|
||||||
|
"imageDescriptorWidth",
|
||||||
|
"imageDescriptorHeight",
|
||||||
|
"imageDescriptorPackedFields",
|
||||||
|
|
||||||
|
"localColorTable",
|
||||||
|
"localColorTableColor",
|
||||||
|
|
||||||
|
"imageData",
|
||||||
|
"imageDataLzwMinimumCodeSize",
|
||||||
|
"imageDataSubBlock",
|
||||||
|
"imageDataSubBlockSize",
|
||||||
|
"imageDataSubBlockData",
|
||||||
|
"imageDataTerminator",
|
||||||
|
|
||||||
|
"extensionBlockIntroducer",
|
||||||
|
"extensionBlockSize",
|
||||||
|
|
||||||
|
"unknownExtensionBlock",
|
||||||
|
"unknownExtensionBlockLabel",
|
||||||
|
"unknownExtensionBlockData",
|
||||||
].reduce((result, id) => {
|
].reduce((result, id) => {
|
||||||
result[id] = id;
|
result[id] = id;
|
||||||
return result;
|
return result;
|
||||||
}, Object.create(null));
|
}, Object.create(null));
|
||||||
|
|
||||||
|
// TODO: Revert this and just export the thing
|
||||||
|
export const GifNodeType = new Proxy(types, {
|
||||||
|
get(target, prop) {
|
||||||
|
const result = target[prop];
|
||||||
|
if (!result) {
|
||||||
|
throw new Error(`cannot get ${String(prop)}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -181,6 +181,123 @@ const NODE_UI_FNS = {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Image descriptor
|
||||||
|
|
||||||
|
[GifNodeType.imageSection]: () => ({
|
||||||
|
title: "Image",
|
||||||
|
description: p("A single image in the 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 }) => ({
|
||||||
|
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"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 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"),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
import { GifNodeType } from "./constants.js";
|
import { GifNodeType } from "./constants.js";
|
||||||
/** @typedef {import("../../types/gif.d.ts").GifNode} GifNode */
|
/** @typedef {import("../../types/gif.d.ts").GifNode} GifNode */
|
||||||
|
|
||||||
|
const BLOCK_TYPE_IMAGE = 0x2c;
|
||||||
|
const BLOCK_TYPE_EXTENSION = 0x21;
|
||||||
|
const BLOCK_TYPE_TRAILER = 0x3b;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} LogicalScreenDescriptorPackedField
|
* @typedef {object} LogicalScreenDescriptorPackedField
|
||||||
* @prop {boolean} globalColorTableFlag
|
* @prop {boolean} globalColorTableFlag
|
||||||
|
@ -22,6 +26,206 @@ export const parseLogicalScreenDescriptorPackedField = (byte) => ({
|
||||||
sizeOfGlobalColorTable: 2 ** ((byte & 0b00000111) + 1),
|
sizeOfGlobalColorTable: 2 ** ((byte & 0b00000111) + 1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ImageDescriptorPackedField
|
||||||
|
* @prop {boolean} localColorTableFlag
|
||||||
|
* @prop {boolean} interlaceFlag
|
||||||
|
* @prop {boolean} sortFlag
|
||||||
|
* @prop {number} reservedBits
|
||||||
|
* @prop {number} sizeOfLocalColorTable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} byte
|
||||||
|
* @returns {ImageDescriptorPackedField}
|
||||||
|
*/
|
||||||
|
export const parseImageDescriptorPackedField = (byte) => ({
|
||||||
|
localColorTableFlag: (byte & 0b10000000) !== 0,
|
||||||
|
interlaceFlag: (byte & 0b1000000) !== 0,
|
||||||
|
sortFlag: (byte & 0b100000) !== 0,
|
||||||
|
reservedBits: (byte & 0b11000) >> 2,
|
||||||
|
sizeOfLocalColorTable: 2 ** ((byte & 0b111) + 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {number} startingOffset
|
||||||
|
* @returns {{ node: GifNode, newOffset: number }}
|
||||||
|
*/
|
||||||
|
const readImageBlock = (bytes, startingOffset) => {
|
||||||
|
let offset = startingOffset;
|
||||||
|
|
||||||
|
/** @type {GifNode[]} */ const children = [{
|
||||||
|
type: GifNodeType.imageSeparator,
|
||||||
|
bytes: bytes.subarray(offset, offset + 1),
|
||||||
|
}];
|
||||||
|
offset++;
|
||||||
|
|
||||||
|
// Image descriptor
|
||||||
|
// TODO: remove these commented-out lines?
|
||||||
|
// const imageLeftPosition = dataView.getUint16(offset, true);
|
||||||
|
// const imageTopPosition = dataView.getUint16(offset + 2, true);
|
||||||
|
// const imageWidth = dataView.getUint16(offset + 4, true);
|
||||||
|
// const imageHeight = dataView.getUint16(offset + 6, true);
|
||||||
|
const packedField = parseImageDescriptorPackedField(bytes[offset + 8]);
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.imageDescriptor,
|
||||||
|
bytes: bytes.subarray(offset, offset + 9),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: GifNodeType.imageDescriptorLeftPosition,
|
||||||
|
bytes: bytes.subarray(offset, offset + 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: GifNodeType.imageDescriptorTopPosition,
|
||||||
|
bytes: bytes.subarray(offset + 2, offset + 4),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: GifNodeType.imageDescriptorWidth,
|
||||||
|
bytes: bytes.subarray(offset + 4, offset + 6),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: GifNodeType.imageDescriptorHeight,
|
||||||
|
bytes: bytes.subarray(offset + 6, offset + 8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: GifNodeType.imageDescriptorPackedFields,
|
||||||
|
bytes: bytes.subarray(offset + 8, offset + 9),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
offset += 9;
|
||||||
|
|
||||||
|
// Local color table
|
||||||
|
if (packedField.localColorTableFlag) {
|
||||||
|
/** @type {GifNode[]} */ const colorNodes = [];
|
||||||
|
for (let i = 0; i < packedField.sizeOfLocalColorTable; i++) {
|
||||||
|
colorNodes.push({
|
||||||
|
type: GifNodeType.localColorTableColor,
|
||||||
|
bytes: bytes.subarray(offset + (i * 3), offset + (i * 3) + 3),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const sizeOfLocalColorTableInBytes = packedField.sizeOfLocalColorTable *
|
||||||
|
3;
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.localColorTable,
|
||||||
|
bytes: bytes.subarray(
|
||||||
|
offset,
|
||||||
|
offset + sizeOfLocalColorTableInBytes,
|
||||||
|
),
|
||||||
|
children: colorNodes,
|
||||||
|
});
|
||||||
|
offset += sizeOfLocalColorTableInBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageDataStartOffset = offset;
|
||||||
|
|
||||||
|
// TODO: remove this?
|
||||||
|
// const lzwMinimumCodeSize = bytes[offset++];
|
||||||
|
offset++;
|
||||||
|
|
||||||
|
/** @type {GifNode[]} */ const imageDataSubBlocks = [];
|
||||||
|
while (true) {
|
||||||
|
const subBlockSize = bytes[offset];
|
||||||
|
const subBlockSizeBytes = bytes.subarray(offset, offset + 1);
|
||||||
|
|
||||||
|
if (subBlockSize === 0) {
|
||||||
|
imageDataSubBlocks.push({
|
||||||
|
type: GifNodeType.imageDataTerminator,
|
||||||
|
bytes: subBlockSizeBytes,
|
||||||
|
});
|
||||||
|
offset++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageDataSubBlocks.push({
|
||||||
|
type: GifNodeType.imageDataSubBlock,
|
||||||
|
bytes: bytes.subarray(offset, offset + 1 + subBlockSize),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: GifNodeType.imageDataSubBlockSize,
|
||||||
|
bytes: subBlockSizeBytes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: GifNodeType.imageDataSubBlockData,
|
||||||
|
bytes: bytes.subarray(offset + 1, offset + 1 + subBlockSize),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
offset += 1 + subBlockSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.imageData,
|
||||||
|
bytes: bytes.subarray(imageDataStartOffset, offset),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: GifNodeType.imageDataLzwMinimumCodeSize,
|
||||||
|
bytes: bytes.subarray(imageDataStartOffset, imageDataStartOffset + 1),
|
||||||
|
},
|
||||||
|
...imageDataSubBlocks,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
newOffset: offset,
|
||||||
|
node: {
|
||||||
|
type: GifNodeType.imageSection,
|
||||||
|
bytes: bytes.subarray(startingOffset, offset),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {number} startingOffset
|
||||||
|
* @returns {{ node: GifNode, newOffset: number }}
|
||||||
|
*/
|
||||||
|
const readExtensionBlock = (bytes, startingOffset) => {
|
||||||
|
let offset = startingOffset;
|
||||||
|
|
||||||
|
/** @type {GifNode[]} */ const children = [{
|
||||||
|
type: GifNodeType.extensionBlockIntroducer,
|
||||||
|
bytes: bytes.subarray(offset, offset + 1),
|
||||||
|
}];
|
||||||
|
offset++;
|
||||||
|
|
||||||
|
const extensionLabel = bytes[offset++];
|
||||||
|
|
||||||
|
const extensionSize = bytes[offset++];
|
||||||
|
const extensionSizeNode = {
|
||||||
|
type: GifNodeType.extensionBlockSize,
|
||||||
|
bytes: new Uint8Array([extensionSize]),
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (extensionLabel) {
|
||||||
|
// TODO: handle various documented extensions
|
||||||
|
|
||||||
|
default:
|
||||||
|
children.push(
|
||||||
|
{
|
||||||
|
type: GifNodeType.unknownExtensionBlockLabel,
|
||||||
|
bytes: new Uint8Array([extensionLabel]),
|
||||||
|
},
|
||||||
|
extensionSizeNode,
|
||||||
|
{
|
||||||
|
type: GifNodeType.unknownExtensionBlockData,
|
||||||
|
bytes: bytes.subarray(offset, offset + extensionSize),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
offset += extensionSize;
|
||||||
|
return {
|
||||||
|
newOffset: offset,
|
||||||
|
node: {
|
||||||
|
type: GifNodeType.unknownExtensionBlock,
|
||||||
|
bytes: bytes.subarray(startingOffset, offset),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} bytes
|
* @param {Uint8Array} bytes
|
||||||
* @returns {null | GifNode} The root node of the GIF tree, or null if the GIF is invalid.
|
* @returns {null | GifNode} The root node of the GIF tree, or null if the GIF is invalid.
|
||||||
|
@ -98,5 +302,28 @@ export default (bytes) => {
|
||||||
offset += sizeOfGlobalColorTableInBytes;
|
offset += sizeOfGlobalColorTableInBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readingBlocks:
|
||||||
|
while (true) {
|
||||||
|
const blockType = bytes[offset];
|
||||||
|
|
||||||
|
let readerFn;
|
||||||
|
switch (blockType) {
|
||||||
|
case BLOCK_TYPE_IMAGE:
|
||||||
|
readerFn = readImageBlock;
|
||||||
|
break;
|
||||||
|
case BLOCK_TYPE_EXTENSION:
|
||||||
|
readerFn = readExtensionBlock;
|
||||||
|
break;
|
||||||
|
case BLOCK_TYPE_TRAILER:
|
||||||
|
break readingBlocks;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown GIF block type: ${blockType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { node, newOffset } = readerFn(bytes, offset);
|
||||||
|
children.push(node);
|
||||||
|
offset = newOffset;
|
||||||
|
}
|
||||||
|
|
||||||
return { type: GifNodeType.root, bytes, children };
|
return { type: GifNodeType.root, bytes, children };
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue