ts-gif
Version:
TypeScript implementation of a performant GIF encoder & decoder.
314 lines (264 loc) • 8.87 kB
TypeScript
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;