formats.exposed/public/gif/parseGif.js

528 lines
14 KiB
JavaScript

// @ts-check
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;
const EXTENSION_LABEL_FOR_APPLICATION = 0xff;
const EXTENSION_LABEL_FOR_COMMENT = 0xfe;
const EXTENSION_LABEL_FOR_GRAPHIC_CONTROL = 0xf9;
const EXTENSION_LABEL_FOR_PLAIN_TEXT = 0x01;
/**
* @typedef {object} LogicalScreenDescriptorPackedField
* @prop {boolean} globalColorTableFlag
* @prop {number} colorResolution
* @prop {boolean} sortFlag
* @prop {number} sizeOfGlobalColorTable
*/
/**
* @param {unknown} condition
* @param {string} [message]
*/
const assert = (condition, message = "Assertion failed") => {
if (!condition) throw new Error(message);
};
/**
* @param {number} byte
* @returns {LogicalScreenDescriptorPackedField}
*/
export const parseLogicalScreenDescriptorPackedField = (byte) => ({
globalColorTableFlag: (byte & 0b10000000) !== 0,
colorResolution: ((byte & 0b01110000) >> 4) + 1,
sortFlag: (byte & 0b00001000) !== 0,
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) => {
/** @type {GifNodeType} */ let type;
let offset = startingOffset;
/**
* @param {GifNodeType} outerType
* @param {GifNodeType} sizeType
* @param {GifNodeType} dataType
*/
const readSubBlocks = (outerType, sizeType, dataType) => {
let subblockSize = -1;
do {
subblockSize = bytes[offset++];
children.push({
type: outerType,
bytes: bytes.subarray(offset, offset + subblockSize),
children: [
{
type: sizeType,
bytes: new Uint8Array([subblockSize]),
},
...(subblockSize
? [
{
type: dataType,
bytes: bytes.subarray(offset + 1, offset + 1 + subblockSize),
},
]
: []),
],
});
offset += subblockSize;
} while (subblockSize !== 0);
};
/** @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) {
case EXTENSION_LABEL_FOR_APPLICATION: {
assert(extensionSize === 11);
type = GifNodeType.applicationExtensionBlock;
children.push({
type: GifNodeType.applicationIdentifier,
bytes: bytes.subarray(offset, offset + 8),
});
offset += 8;
children.push({
type: GifNodeType.applicationAuthenticationCode,
bytes: bytes.subarray(offset, offset + 3),
});
offset += 3;
readSubBlocks(
GifNodeType.applicationSubBlock,
GifNodeType.applicationSubBlockSize,
GifNodeType.applicationSubBlockData,
);
break;
}
case EXTENSION_LABEL_FOR_COMMENT: {
type = GifNodeType.commentExtensionBlock;
readSubBlocks(
GifNodeType.commentSubBlock,
GifNodeType.commentSubBlockSize,
GifNodeType.commentSubBlockData,
);
break;
}
case EXTENSION_LABEL_FOR_GRAPHIC_CONTROL: {
assert(extensionSize === 4);
type = GifNodeType.graphicControlExtensionBlock;
const packedField = bytes[offset++];
children.push({
type: GifNodeType.graphicControlExtensionPackedFields,
bytes: new Uint8Array([packedField]),
});
children.push({
type: GifNodeType.graphicControlExtensionDelayTime,
bytes: bytes.subarray(offset, offset + 2),
});
offset += 2;
const transparentColorIndex = bytes[offset++];
children.push({
type: GifNodeType.graphicControlExtensionTransparentColorIndex,
bytes: new Uint8Array([transparentColorIndex]),
});
const blockTerminator = bytes[offset++];
assert(blockTerminator === 0, "Expected block terminator");
children.push({
type: GifNodeType.extensionBlockTerminator,
bytes: new Uint8Array([blockTerminator]),
});
break;
}
case EXTENSION_LABEL_FOR_PLAIN_TEXT: {
assert(extensionSize === 12);
type = GifNodeType.plainTextExtensionBlock;
children.push({
type: GifNodeType.plainTextExtensionBlockTextGridLeftPosition,
bytes: bytes.subarray(offset, offset + 2),
});
offset += 2;
children.push({
type: GifNodeType.plainTextExtensionBlockTextGridTopPosition,
bytes: bytes.subarray(offset, offset + 2),
});
offset += 2;
children.push({
type: GifNodeType.plainTextExtensionBlockTextGridWidth,
bytes: bytes.subarray(offset, offset + 2),
});
offset += 2;
children.push({
type: GifNodeType.plainTextExtensionBlockTextGridHeight,
bytes: bytes.subarray(offset, offset + 2),
});
offset += 2;
const characterCellWidth = bytes[offset++];
children.push({
type: GifNodeType.plainTextExtensionBlockCharacterCellWidth,
bytes: new Uint8Array([characterCellWidth]),
});
const characterCellHeight = bytes[offset++];
children.push({
type: GifNodeType.plainTextExtensionBlockCharacterCellHeight,
bytes: new Uint8Array([characterCellHeight]),
});
const textForegroundColorIndex = bytes[offset++];
children.push({
type: GifNodeType.plainTextExtensionBlockTextForegroundColorIndex,
bytes: new Uint8Array([textForegroundColorIndex]),
});
const textBackgroundColorIndex = bytes[offset++];
children.push({
type: GifNodeType.plainTextExtensionBlockTextBackgroundColorIndex,
bytes: new Uint8Array([textBackgroundColorIndex]),
});
readSubBlocks(
GifNodeType.plainTextSubBlock,
GifNodeType.plainTextSubBlockSize,
GifNodeType.plainTextSubBlockData,
);
break;
}
default:
type = GifNodeType.unknownExtensionBlock;
children.push(
{
type: GifNodeType.unknownExtensionBlockLabel,
bytes: new Uint8Array([extensionLabel]),
},
extensionSizeNode,
{
type: GifNodeType.unknownExtensionBlockData,
bytes: bytes.subarray(offset, offset + extensionSize),
},
);
offset += extensionSize;
break;
}
return {
newOffset: offset,
node: {
type,
bytes: bytes.subarray(startingOffset, offset),
children,
},
};
};
/**
* @param {Uint8Array} bytes
* @param {number} offset
* @returns {{ node: GifNode, newOffset: number }}
*/
const readTrailerBlock = (bytes, offset) => ({
newOffset: offset + 1,
node: {
type: GifNodeType.gifTerminator,
bytes: bytes.subarray(offset, offset + 1),
},
});
/**
* @param {Uint8Array} bytes
* @returns {null | GifNode} The root node of the GIF tree, or null if the GIF is invalid.
*/
export default (bytes) => {
const widthBytes = bytes.subarray(6, 6 + 2);
const heightBytes = bytes.subarray(8, 8 + 2);
const packedFieldBytes = bytes.subarray(10, 10 + 1);
/** @type {GifNode[]} */
const children = [
{
type: GifNodeType.header,
bytes: bytes.subarray(0, 6),
children: [
{
type: GifNodeType.headerSignature,
bytes: bytes.subarray(0, 3),
},
{
type: GifNodeType.headerVersion,
bytes: bytes.subarray(3, 3 + 3),
},
],
},
{
type: GifNodeType.logicalScreenDescriptor,
bytes: bytes.subarray(6, 6 + 6),
children: [
{
type: GifNodeType.logicalScreenWidth,
bytes: widthBytes,
},
{
type: GifNodeType.logicalScreenHeight,
bytes: heightBytes,
},
{
type: GifNodeType.logicalScreenDescriptorPackedFields,
bytes: packedFieldBytes,
},
{
type: GifNodeType.logicalScreenBackgroundColorIndex,
bytes: bytes.subarray(11, 11 + 1),
},
{
type: GifNodeType.logicalScreenPixelAspectRatio,
bytes: bytes.subarray(12, 12 + 1),
},
],
},
];
let offset = 13;
const packedField = parseLogicalScreenDescriptorPackedField(
packedFieldBytes[0],
);
if (packedField.globalColorTableFlag) {
/** @type {GifNode[]} */ const colorNodes = [];
for (let i = 0; i < packedField.sizeOfGlobalColorTable; i++) {
colorNodes.push({
type: GifNodeType.globalColorTableColor,
bytes: bytes.subarray(13 + i * 3, 13 + i * 3 + 3),
});
}
const sizeOfGlobalColorTableInBytes = packedField.sizeOfGlobalColorTable *
3;
children.push({
type: GifNodeType.globalColorTable,
bytes: bytes.subarray(13, 13 + sizeOfGlobalColorTableInBytes),
children: colorNodes,
});
offset += sizeOfGlobalColorTableInBytes;
}
while (offset < bytes.byteLength) {
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:
readerFn = readTrailerBlock;
break;
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 };
};