// @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 }; };