UNPKG

ts-gif

Version:

TypeScript implementation of a performant GIF encoder & decoder.

238 lines (199 loc) 7.42 kB
import type { Buffer } from 'node:buffer'; import type { FrameOptions, WriterOptions } from './types'; export declare class Writer { private buffer: Buffer private width: number private height: number private position: number = 0 private ended: boolean = false private globalPalette: number[] | null constructor(buf: Buffer, width: number, height: number, options: WriterOptions = {}) { this.buffer = buf this.width = width this.height = height this.globalPalette = options.palette ?? null if (width <= 0 || height <= 0 || width > 65535 || height > 65535) { throw new Error('Width/Height invalid.') } this.writeHeader() this.writeLogicalScreenDescriptor(options) this.writeGlobalColorTable() this.writeNetscapeLoopingExtension(options.loop) } private checkPaletteAndNumColors(palette: number[]): number { const num_colors = palette.length if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors - 1)) { throw new Error('Invalid code/color length, must be power of 2 and 2 .. 256.') } return num_colors } private writeHeader(): void { this.buffer[this.position++] = 0x47 this.buffer[this.position++] = 0x49 this.buffer[this.position++] = 0x46 this.buffer[this.position++] = 0x38 this.buffer[this.position++] = 0x39 this.buffer[this.position++] = 0x61 } private writeLogicalScreenDescriptor(options: WriterOptions): void { let gp_num_colors_pow2 = 0 let background = 0 if (this.globalPalette !== null) { let gp_num_colors = this.checkPaletteAndNumColors(this.globalPalette) while (gp_num_colors >>= 1) ++gp_num_colors_pow2 gp_num_colors = 1 << gp_num_colors_pow2 --gp_num_colors_pow2 if (options.background !== undefined) { background = options.background if (background >= gp_num_colors) { throw new Error('Background index out of range.') } if (background === 0) { throw new Error('Background index explicitly passed as 0.') } } } this.buffer[this.position++] = this.width & 0xFF this.buffer[this.position++] = this.width >> 8 & 0xFF this.buffer[this.position++] = this.height & 0xFF this.buffer[this.position++] = this.height >> 8 & 0xFF this.buffer[this.position++] = (this.globalPalette !== null ? 0x80 : 0) | gp_num_colors_pow2 this.buffer[this.position++] = background this.buffer[this.position++] = 0 } private writeGlobalColorTable(): void { if (this.globalPalette !== null) { for (let i = 0; i < this.globalPalette.length; ++i) { const rgb = this.globalPalette[i] this.buffer[this.position++] = rgb >> 16 & 0xFF this.buffer[this.position++] = rgb >> 8 & 0xFF this.buffer[this.position++] = rgb & 0xFF } } } private writeNetscapeLoopingExtension(loopCount: number | null | undefined): void { if (loopCount !== null && loopCount !== undefined) { if (loopCount < 0 || loopCount > 65535) { throw new Error('Loop count invalid.') } this.buffer[this.position++] = 0x21 this.buffer[this.position++] = 0xFF this.buffer[this.position++] = 0x0B this.buffer[this.position++] = 0x4E this.buffer[this.position++] = 0x45 this.buffer[this.position++] = 0x54 this.buffer[this.position++] = 0x53 this.buffer[this.position++] = 0x43 this.buffer[this.position++] = 0x41 this.buffer[this.position++] = 0x50 this.buffer[this.position++] = 0x45 this.buffer[this.position++] = 0x32 this.buffer[this.position++] = 0x2E this.buffer[this.position++] = 0x30 this.buffer[this.position++] = 0x03 this.buffer[this.position++] = 0x01 this.buffer[this.position++] = loopCount & 0xFF this.buffer[this.position++] = loopCount >> 8 & 0xFF this.buffer[this.position++] = 0x00 } } public addFrame( x: number, y: number, width: number, height: number, indexedPixels: Uint8Array, options: FrameOptions = {}, ): number { if (this.ended) { --this.position this.ended = false } if (x < 0 || y < 0 || x > 65535 || y > 65535) { throw new Error('x/y invalid.') } if (width <= 0 || height <= 0 || width > 65535 || height > 65535) { throw new Error('Width/Height invalid.') } if (indexedPixels.length < width * height) { throw new Error('Not enough pixels for the frame size.') } const usingLocalPalette = options.palette !== undefined && options.palette !== null const palette = usingLocalPalette ? options.palette! : this.globalPalette if (!palette) { throw new Error('Must supply either a local or global palette.') } let numColors = this.checkPaletteAndNumColors(palette) let minCodeSize = 0 while (numColors >>= 1) ++minCodeSize numColors = 1 << minCodeSize const delay = options.delay ?? 0 const disposal = options.disposal ?? 0 if (disposal < 0 || disposal > 3) { throw new Error('Disposal out of range.') } let useTransparency = false let transparentIndex = 0 if (options.transparent !== undefined && options.transparent !== null) { useTransparency = true transparentIndex = options.transparent if (transparentIndex < 0 || transparentIndex >= numColors) { throw new Error('Transparent color index.') } } if (disposal !== 0 || useTransparency || delay !== 0) { this.buffer[this.position++] = 0x21 this.buffer[this.position++] = 0xF9 this.buffer[this.position++] = 4 this.buffer[this.position++] = disposal << 2 | (useTransparency ? 1 : 0) this.buffer[this.position++] = delay & 0xFF this.buffer[this.position++] = delay >> 8 & 0xFF this.buffer[this.position++] = transparentIndex this.buffer[this.position++] = 0 } this.buffer[this.position++] = 0x2C this.buffer[this.position++] = x & 0xFF this.buffer[this.position++] = x >> 8 & 0xFF this.buffer[this.position++] = y & 0xFF this.buffer[this.position++] = y >> 8 & 0xFF this.buffer[this.position++] = width & 0xFF this.buffer[this.position++] = width >> 8 & 0xFF this.buffer[this.position++] = height & 0xFF this.buffer[this.position++] = height >> 8 & 0xFF this.buffer[this.position++] = usingLocalPalette ? (0x80 | (minCodeSize - 1)) : 0 if (usingLocalPalette) { for (let i = 0; i < palette.length; ++i) { const rgb = palette[i] this.buffer[this.position++] = rgb >> 16 & 0xFF this.buffer[this.position++] = rgb >> 8 & 0xFF this.buffer[this.position++] = rgb & 0xFF } } this.position = writerOutputLZWCodeStream( this.buffer, this.position, minCodeSize < 2 ? 2 : minCodeSize, indexedPixels, ) return this.position } public end(): number { if (!this.ended) { this.buffer[this.position++] = 0x3B this.ended = true } return this.position } public getOutputBuffer(): Buffer { return this.buffer } public setOutputBuffer(buffer: Buffer): void { this.buffer = buffer } public getOutputBufferPosition(): number { return this.position } public setOutputBufferPosition(position: number): void { this.position = position } }