formats.exposed/public/gif/parseGif.js

330 lines
8.8 KiB
JavaScript
Raw Normal View History

// @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;
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),
});
/**
* @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) => {
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);
/** @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-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;
}
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 };
};