ts-gif
Version:
TypeScript implementation of a performant GIF encoder & decoder.
238 lines (199 loc) • 7.42 kB
TypeScript
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
}
}