GIF: basic parsing of image sections and skipping extensions
This commit is contained in:
parent
858c6c83a4
commit
c755f0bb6f
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
/** @enum {string} */
|
||||
export const GifNodeType = [
|
||||
const types = [
|
||||
"root",
|
||||
|
||||
"header",
|
||||
|
@ -17,7 +17,44 @@ export const GifNodeType = [
|
|||
|
||||
"globalColorTable",
|
||||
"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) => {
|
||||
result[id] = id;
|
||||
return result;
|
||||
}, 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";
|
||||
/** @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
|
||||
* @prop {boolean} globalColorTableFlag
|
||||
|
@ -22,6 +26,206 @@ export const parseLogicalScreenDescriptorPackedField = (byte) => ({
|
|||
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
|
||||
* @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;
|
||||
}
|
||||
|
||||
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 };
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue