2023-08-12 03:40:30 +00:00
|
|
|
// @ts-check
|
|
|
|
|
|
|
|
import { GifNodeType } from "./constants.js";
|
|
|
|
/** @typedef {import("../../types/gif.d.ts").GifNode} GifNode */
|
|
|
|
|
2023-09-16 19:06:58 +00:00
|
|
|
const BLOCK_TYPE_IMAGE = 0x2c;
|
|
|
|
const BLOCK_TYPE_EXTENSION = 0x21;
|
|
|
|
const BLOCK_TYPE_TRAILER = 0x3b;
|
|
|
|
|
2023-08-16 16:45:11 +00:00
|
|
|
/**
|
|
|
|
* @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),
|
|
|
|
});
|
|
|
|
|
2023-09-16 19:06:58 +00:00
|
|
|
/**
|
|
|
|
* @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,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-08-12 03:40:30 +00:00
|
|
|
/**
|
|
|
|
* @param {Uint8Array} bytes
|
2023-08-16 15:34:09 +00:00
|
|
|
* @returns {null | GifNode} The root node of the GIF tree, or null if the GIF is invalid.
|
2023-08-12 03:40:30 +00:00
|
|
|
*/
|
|
|
|
export default (bytes) => {
|
2023-08-16 16:45:11 +00:00
|
|
|
const widthBytes = bytes.subarray(6, 6 + 2);
|
|
|
|
const heightBytes = bytes.subarray(8, 8 + 2);
|
|
|
|
const packedFieldBytes = bytes.subarray(10, 10 + 1);
|
|
|
|
|
2023-08-12 03:40:30 +00:00
|
|
|
/** @type {GifNode[]} */
|
|
|
|
const children = [
|
|
|
|
{
|
|
|
|
type: GifNodeType.header,
|
|
|
|
bytes: bytes.subarray(0, 6),
|
|
|
|
children: [
|
|
|
|
{
|
|
|
|
type: GifNodeType.headerSignature,
|
|
|
|
bytes: bytes.subarray(0, 3),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: GifNodeType.headerVersion,
|
2023-08-16 15:46:31 +00:00
|
|
|
bytes: bytes.subarray(3, 3 + 3),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: GifNodeType.logicalScreenDescriptor,
|
|
|
|
bytes: bytes.subarray(6, 6 + 6),
|
|
|
|
children: [
|
|
|
|
{
|
|
|
|
type: GifNodeType.logicalScreenWidth,
|
2023-08-16 16:45:11 +00:00
|
|
|
bytes: widthBytes,
|
2023-08-16 15:46:31 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
type: GifNodeType.logicalScreenHeight,
|
2023-08-16 16:45:11 +00:00
|
|
|
bytes: heightBytes,
|
2023-08-16 15:46:31 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
type: GifNodeType.logicalScreenDescriptorPackedFields,
|
2023-08-16 16:45:11 +00:00
|
|
|
bytes: packedFieldBytes,
|
2023-08-16 15:46:31 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
type: GifNodeType.logicalScreenBackgroundColorIndex,
|
|
|
|
bytes: bytes.subarray(11, 11 + 1),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: GifNodeType.logicalScreenPixelAspectRatio,
|
|
|
|
bytes: bytes.subarray(12, 12 + 1),
|
2023-08-12 03:40:30 +00:00
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2023-08-16 16:59:33 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-09-16 19:06:58 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-08-12 03:40:30 +00:00
|
|
|
return { type: GifNodeType.root, bytes, children };
|
|
|
|
};
|