// @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; /** * @typedef {object} LogicalScreenDescriptorPackedField * @prop {boolean} globalColorTableFlag * @prop {number} colorResolution * @prop {boolean} sortFlag * @prop {number} sizeOfGlobalColorTable */ /** * @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) => { 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. */ 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; } 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 }; };