ts-gif
Version:
TypeScript implementation of a performant GIF encoder & decoder.
593 lines (590 loc) • 19.4 kB
JavaScript
// src/reader.ts
class Reader {
frames = [];
width;
height;
loop_count = null;
buffer;
constructor(buf) {
this.buffer = buf;
let p = 0;
if (buf[p++] !== 71 || buf[p++] !== 73 || buf[p++] !== 70 || buf[p++] !== 56 || (buf[p++] + 1 & 253) !== 56 || buf[p++] !== 97) {
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 & 7;
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 33:
switch (buf[p++]) {
case 255:
if (buf[p] !== 11 || buf[p + 1] === 78 && buf[p + 2] === 69 && buf[p + 3] === 84 && buf[p + 4] === 83 && buf[p + 5] === 67 && buf[p + 6] === 65 && buf[p + 7] === 80 && buf[p + 8] === 69 && buf[p + 9] === 50 && buf[p + 10] === 46 && buf[p + 11] === 48 && buf[p + 12] === 3 && buf[p + 13] === 1 && 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 249: {
if (buf[p++] !== 4 || 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 & 7;
p++;
break;
}
case 1:
case 254:
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 44: {
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 & 7;
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 59:
no_eof = false;
break;
default:
throw new Error(`Unknown gif block: 0x${buf[p - 1].toString(16)}`);
}
}
}
numFrames() {
return this.frames.length;
}
getLoopCount() {
return this.loop_count;
}
frameInfo(frame_num) {
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, pixels) {
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, pixels) {
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;
}
}
}
function readerLZWOutputIndexStream(code_stream, p, output, output_length) {
const min_code_size = code_stream[p++];
const clear_code = 1 << min_code_size;
const eoi_code = clear_code + 1;
let next_code = eoi_code + 1;
let cur_code_size = min_code_size + 1;
let code_mask = (1 << cur_code_size) - 1;
let cur_shift = 0;
let cur = 0;
let op = 0;
let subblock_size = code_stream[p++];
const code_table = new Int32Array(4096);
let prev_code = null;
while (true) {
while (cur_shift < 16) {
if (subblock_size === 0)
break;
cur |= code_stream[p++] << cur_shift;
cur_shift += 8;
if (subblock_size === 1) {
subblock_size = code_stream[p++];
} else {
--subblock_size;
}
}
if (cur_shift < cur_code_size)
break;
const code = cur & code_mask;
cur >>= cur_code_size;
cur_shift -= cur_code_size;
if (code === clear_code) {
next_code = eoi_code + 1;
cur_code_size = min_code_size + 1;
code_mask = (1 << cur_code_size) - 1;
prev_code = null;
continue;
} else if (code === eoi_code) {
break;
}
const chase_code = code < next_code ? code : prev_code;
let chase_length = 0;
let chase = chase_code;
while (chase > clear_code) {
chase = code_table[chase] >> 8;
++chase_length;
}
const k = chase;
const op_end = op + chase_length + (chase_code !== code ? 1 : 0);
if (op_end > output_length) {
console.log("Warning, gif stream longer than expected.");
return;
}
output[op++] = k;
op += chase_length;
let b = op;
if (chase_code !== code)
output[op++] = k;
chase = chase_code;
while (chase_length--) {
chase = code_table[chase];
output[--b] = chase & 255;
chase >>= 8;
}
if (prev_code !== null && next_code < 4096) {
code_table[next_code++] = prev_code << 8 | k;
if (next_code >= code_mask + 1 && cur_code_size < 12) {
++cur_code_size;
code_mask = code_mask << 1 | 1;
}
}
prev_code = code;
}
if (op !== output_length) {
console.log("Warning, gif stream shorter than expected.");
}
return output;
}
// src/writer.ts
class Writer {
buffer;
width;
height;
position = 0;
ended = false;
globalPalette;
constructor(buf, width, height, options = {}) {
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);
}
checkPaletteAndNumColors(palette) {
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;
}
writeHeader() {
this.buffer[this.position++] = 71;
this.buffer[this.position++] = 73;
this.buffer[this.position++] = 70;
this.buffer[this.position++] = 56;
this.buffer[this.position++] = 57;
this.buffer[this.position++] = 97;
}
writeLogicalScreenDescriptor(options) {
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 & 255;
this.buffer[this.position++] = this.width >> 8 & 255;
this.buffer[this.position++] = this.height & 255;
this.buffer[this.position++] = this.height >> 8 & 255;
this.buffer[this.position++] = (this.globalPalette !== null ? 128 : 0) | gp_num_colors_pow2;
this.buffer[this.position++] = background;
this.buffer[this.position++] = 0;
}
writeGlobalColorTable() {
if (this.globalPalette !== null) {
for (let i = 0;i < this.globalPalette.length; ++i) {
const rgb = this.globalPalette[i];
this.buffer[this.position++] = rgb >> 16 & 255;
this.buffer[this.position++] = rgb >> 8 & 255;
this.buffer[this.position++] = rgb & 255;
}
}
}
writeNetscapeLoopingExtension(loopCount) {
if (loopCount !== null && loopCount !== undefined) {
if (loopCount < 0 || loopCount > 65535) {
throw new Error("Loop count invalid.");
}
this.buffer[this.position++] = 33;
this.buffer[this.position++] = 255;
this.buffer[this.position++] = 11;
this.buffer[this.position++] = 78;
this.buffer[this.position++] = 69;
this.buffer[this.position++] = 84;
this.buffer[this.position++] = 83;
this.buffer[this.position++] = 67;
this.buffer[this.position++] = 65;
this.buffer[this.position++] = 80;
this.buffer[this.position++] = 69;
this.buffer[this.position++] = 50;
this.buffer[this.position++] = 46;
this.buffer[this.position++] = 48;
this.buffer[this.position++] = 3;
this.buffer[this.position++] = 1;
this.buffer[this.position++] = loopCount & 255;
this.buffer[this.position++] = loopCount >> 8 & 255;
this.buffer[this.position++] = 0;
}
}
addFrame(x, y, width, height, indexedPixels, options = {}) {
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++] = 33;
this.buffer[this.position++] = 249;
this.buffer[this.position++] = 4;
this.buffer[this.position++] = disposal << 2 | (useTransparency ? 1 : 0);
this.buffer[this.position++] = delay & 255;
this.buffer[this.position++] = delay >> 8 & 255;
this.buffer[this.position++] = transparentIndex;
this.buffer[this.position++] = 0;
}
this.buffer[this.position++] = 44;
this.buffer[this.position++] = x & 255;
this.buffer[this.position++] = x >> 8 & 255;
this.buffer[this.position++] = y & 255;
this.buffer[this.position++] = y >> 8 & 255;
this.buffer[this.position++] = width & 255;
this.buffer[this.position++] = width >> 8 & 255;
this.buffer[this.position++] = height & 255;
this.buffer[this.position++] = height >> 8 & 255;
this.buffer[this.position++] = usingLocalPalette ? 128 | minCodeSize - 1 : 0;
if (usingLocalPalette) {
for (let i = 0;i < palette.length; ++i) {
const rgb = palette[i];
this.buffer[this.position++] = rgb >> 16 & 255;
this.buffer[this.position++] = rgb >> 8 & 255;
this.buffer[this.position++] = rgb & 255;
}
}
this.position = writerOutputLZWCodeStream(this.buffer, this.position, minCodeSize < 2 ? 2 : minCodeSize, indexedPixels);
return this.position;
}
end() {
if (!this.ended) {
this.buffer[this.position++] = 59;
this.ended = true;
}
return this.position;
}
getOutputBuffer() {
return this.buffer;
}
setOutputBuffer(buffer) {
this.buffer = buffer;
}
getOutputBufferPosition() {
return this.position;
}
setOutputBufferPosition(position) {
this.position = position;
}
}
function writerOutputLZWCodeStream(buf, p, min_code_size, index_stream) {
buf[p++] = min_code_size;
let cur_subblock = p++;
const clear_code = 1 << min_code_size;
const code_mask = clear_code - 1;
const eoi_code = clear_code + 1;
let next_code = eoi_code + 1;
let cur_code_size = min_code_size + 1;
let cur_shift = 0;
let cur = 0;
function emit_bytes_to_buffer(bit_block_size) {
while (cur_shift >= bit_block_size) {
buf[p++] = cur & 255;
cur >>= 8;
cur_shift -= 8;
if (p === cur_subblock + 256) {
buf[cur_subblock] = 255;
cur_subblock = p++;
}
}
}
function emit_code(c) {
cur |= c << cur_shift;
cur_shift += cur_code_size;
emit_bytes_to_buffer(8);
}
let ib_code = index_stream[0] & code_mask;
let code_table = {};
emit_code(clear_code);
for (let i = 1, il = index_stream.length;i < il; ++i) {
const k = index_stream[i] & code_mask;
const cur_key = ib_code << 8 | k;
const cur_code = code_table[cur_key];
if (cur_code === undefined) {
cur |= ib_code << cur_shift;
cur_shift += cur_code_size;
while (cur_shift >= 8) {
buf[p++] = cur & 255;
cur >>= 8;
cur_shift -= 8;
if (p === cur_subblock + 256) {
buf[cur_subblock] = 255;
cur_subblock = p++;
}
}
if (next_code === 4096) {
emit_code(clear_code);
next_code = eoi_code + 1;
cur_code_size = min_code_size + 1;
code_table = {};
} else {
if (next_code >= 1 << cur_code_size)
++cur_code_size;
code_table[cur_key] = next_code++;
}
ib_code = k;
} else {
ib_code = cur_code;
}
}
emit_code(ib_code);
emit_code(eoi_code);
emit_bytes_to_buffer(1);
if (cur_subblock + 1 === p) {
buf[cur_subblock] = 0;
} else {
buf[cur_subblock] = p - cur_subblock - 1;
buf[p++] = 0;
}
return p;
}
// src/index.ts
var gif = {
Reader,
Writer
};
var src_default = gif;
export {
writerOutputLZWCodeStream,
readerLZWOutputIndexStream,
src_default as default,
Writer,
Reader
};