UNPKG

ts-gif

Version:

TypeScript implementation of a performant GIF encoder & decoder.

593 lines (590 loc) 19.4 kB
// 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 };