UNPKG

ts-gif

Version:

TypeScript implementation of a performant GIF encoder & decoder.

314 lines (264 loc) 8.87 kB
import type { Buffer } from 'node:buffer'; import type { Frame } from './types'; export declare class Reader { private frames: Frame[] = [] private width: number private height: number private loop_count: number | null = null private buffer: Buffer constructor(buf: Buffer) { this.buffer = buf let p = 0 if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 || buf[p++] !== 0x38 || (buf[p++] + 1 & 0xFD) !== 0x38 || buf[p++] !== 0x61) { throw new Error('Invalid GIF 87a/89a header.') } this.width = buf[p++] | buf[p++] << 8 this.height = buf[p++] | buf[p++] << 8 const pf0 = buf[p++] const global_palette_flag = pf0 >> 7 const num_global_colors_pow2 = pf0 & 0x7 const num_global_colors = 1 << (num_global_colors_pow2 + 1) buf[p++] let global_palette_offset = null let global_palette_size = null if (global_palette_flag) { global_palette_offset = p global_palette_size = num_global_colors p += num_global_colors * 3 } let no_eof = true let delay = 0 let transparent_index = null let disposal = 0 while (no_eof && p < buf.length) { switch (buf[p++]) { case 0x21: switch (buf[p++]) { case 0xFF: if (buf[p] !== 0x0B || buf[p + 1] === 0x4E && buf[p + 2] === 0x45 && buf[p + 3] === 0x54 && buf[p + 4] === 0x53 && buf[p + 5] === 0x43 && buf[p + 6] === 0x41 && buf[p + 7] === 0x50 && buf[p + 8] === 0x45 && buf[p + 9] === 0x32 && buf[p + 10] === 0x2E && buf[p + 11] === 0x30 && buf[p + 12] === 0x03 && buf[p + 13] === 0x01 && buf[p + 16] === 0) { p += 14 this.loop_count = buf[p++] | buf[p++] << 8 p++ } else { p += 12 while (true) { const block_size = buf[p++] if (!(block_size >= 0)) throw new Error('Invalid block size') if (block_size === 0) break p += block_size } } break case 0xF9: { if (buf[p++] !== 0x4 || buf[p + 4] !== 0) throw new Error('Invalid graphics extension block.') const pf1 = buf[p++] delay = buf[p++] | buf[p++] << 8 transparent_index = buf[p++] if ((pf1 & 1) === 0) transparent_index = null disposal = pf1 >> 2 & 0x7 p++ break } case 0x01: case 0xFE: while (true) { const block_size = buf[p++] if (!(block_size >= 0)) throw new Error('Invalid block size') if (block_size === 0) break p += block_size } break default: throw new Error( `Unknown graphic control label: 0x${buf[p - 1].toString(16)}`, ) } break case 0x2C: { const x = buf[p++] | buf[p++] << 8 const y = buf[p++] | buf[p++] << 8 const w = buf[p++] | buf[p++] << 8 const h = buf[p++] | buf[p++] << 8 const pf2 = buf[p++] const local_palette_flag = pf2 >> 7 const interlace_flag = pf2 >> 6 & 1 const num_local_colors_pow2 = pf2 & 0x7 const num_local_colors = 1 << (num_local_colors_pow2 + 1) const data_offset = p let palette_offset = global_palette_offset let palette_size = global_palette_size let has_local_palette = false if (local_palette_flag) { has_local_palette = true palette_offset = p palette_size = num_local_colors p += num_local_colors * 3 } p++ while (true) { const block_size = buf[p++] if (!(block_size >= 0)) throw new Error('Invalid block size') if (block_size === 0) break p += block_size } this.frames.push({ x, y, width: w, height: h, has_local_palette, palette_offset, palette_size, data_offset, data_length: p - data_offset, transparent_index, interlaced: !!interlace_flag, delay, disposal, }) break } case 0x3B: no_eof = false break default: throw new Error(`Unknown gif block: 0x${buf[p - 1].toString(16)}`) } } } numFrames(): number { return this.frames.length } getLoopCount(): number | null { return this.loop_count } frameInfo(frame_num: number): Frame { if (frame_num < 0 || frame_num >= this.frames.length) throw new Error('Frame index out of range.') return this.frames[frame_num] } decodeAndBlitFrameBGRA(frame_num: number, pixels: Uint8Array): void { const frame = this.frameInfo(frame_num) const num_pixels = frame.width * frame.height const index_stream = new Uint8Array(num_pixels) readerLZWOutputIndexStream( this.buffer, frame.data_offset, index_stream, num_pixels, ) const palette_offset = frame.palette_offset let trans = frame.transparent_index if (trans === null) trans = 256 const framewidth = frame.width const framestride = this.width - framewidth let xleft = framewidth const opbeg = ((frame.y * this.width) + frame.x) * 4 const opend = ((frame.y + frame.height) * this.width + frame.x) * 4 let op = opbeg let scanstride = framestride * 4 if (frame.interlaced === true) { scanstride += this.width * 4 * 7 } let interlaceskip = 8 for (let i = 0, il = index_stream.length; i < il; ++i) { const index = index_stream[i] if (xleft === 0) { op += scanstride xleft = framewidth if (op >= opend) { scanstride = framestride * 4 + this.width * 4 * (interlaceskip - 1) op = opbeg + (framewidth + framestride) * (interlaceskip << 1) interlaceskip >>= 1 } } if (index === trans) { op += 4 } else { if (palette_offset === null) { throw new Error('No palette found for frame') } const r = this.buffer[palette_offset + index * 3] const g = this.buffer[palette_offset + index * 3 + 1] const b = this.buffer[palette_offset + index * 3 + 2] pixels[op++] = b pixels[op++] = g pixels[op++] = r pixels[op++] = 255 } --xleft } } decodeAndBlitFrameRGBA(frame_num: number, pixels: Uint8Array): void { const frame = this.frameInfo(frame_num) const num_pixels = frame.width * frame.height const index_stream = new Uint8Array(num_pixels) readerLZWOutputIndexStream( this.buffer, frame.data_offset, index_stream, num_pixels, ) const palette_offset = frame.palette_offset if (palette_offset === null) { throw new Error('No palette found for frame') } let trans = frame.transparent_index if (trans === null) trans = 256 const framewidth = frame.width const framestride = this.width - framewidth let xleft = framewidth const opbeg = ((frame.y * this.width) + frame.x) * 4 const opend = ((frame.y + frame.height) * this.width + frame.x) * 4 let op = opbeg let scanstride = framestride * 4 if (frame.interlaced === true) { scanstride += this.width * 4 * 7 } let interlaceskip = 8 for (let i = 0, il = index_stream.length; i < il; ++i) { const index = index_stream[i] if (xleft === 0) { op += scanstride xleft = framewidth if (op >= opend) { scanstride = framestride * 4 + this.width * 4 * (interlaceskip - 1) op = opbeg + (framewidth + framestride) * (interlaceskip << 1) interlaceskip >>= 1 } } if (index === trans) { op += 4 } else { const r = this.buffer[palette_offset + index * 3] const g = this.buffer[palette_offset + index * 3 + 1] const b = this.buffer[palette_offset + index * 3 + 2] pixels[op++] = r pixels[op++] = g pixels[op++] = b pixels[op++] = 255 } --xleft } } } export declare function readerLZWOutputIndexStream(code_stream: Buffer, p: number, output: Uint8Array, output_length: number): Uint8Array<ArrayBufferLike> | void;