export type Frame = { x: number; y: number; width: number; height: number; hasLocalPalette: boolean; paletteOffset: number | null; paletteSize: number | null; dataOffset: number; dataLength: number; transparentIndex: number | null; interlaced: boolean; delay: number; disposal: number; }; /** * @classdesc This class is a TS refactoring of 'omggif's GifReader constructor, I simply copy-pasta'd it to be able to include using a deno bundler since they currently do not work properly with npm packages. Due to this, if anything doesn't work, do NOT contact the original author for issues with this class * @author original - Dean McNamee * @author refactor - Emma Short */ export class GifReader { private buf: Uint8Array; private p: number; public width: number; public height: number; private globalPaletteOffset: number | null; private globalPaletteSize: number | null; private frames: Frame[]; private loopCountValue: number | null; constructor(buf: Uint8Array) { this.buf = buf; this.p = 0; this.width = 0; this.height = 0; this.globalPaletteOffset = null; this.globalPaletteSize = null; this.frames = []; this.loopCountValue = null; this.parseHeader(); this.parseFrames(); } public numFrames(): number { return this.frames.length; } public loopCount(): number | null { return this.loopCountValue; } public frameInfo(frameNum: number): Frame { if (frameNum < 0 || frameNum >= this.frames.length) { throw new Error("Frame index out of range."); } return this.frames[frameNum]; } public decodeAndBlitFrameBGRA( frameNum: number, pixels: Uint8ClampedArray, ): void { const frame = this.frameInfo(frameNum); const numPixels = frame.width * frame.height; const indexStream = new Uint8Array(numPixels); // At most 8-bit indices. GifReaderLZWOutputIndexStream( this.buf, frame.dataOffset, indexStream, numPixels, ); const paletteOffset = frame.paletteOffset; let trans = frame.transparentIndex; if (trans === null) trans = 256; // We are possibly just blitting to a portion of the entire frame. // That is a subRect within the frameRect, so the additional pixels // must be skipped over after we finished a scanline. const frameWidth = frame.width; const frameStride = this.width - frameWidth; let xLeft = frameWidth; // Number of subRect pixels left in scanline. // Output index of the top left corner of the subRect. const opBeg = ((frame.y * this.width) + frame.x) * 4; // Output index of what would be the left edge of the subRect, one row // below it, i.e. the index at which an interlace pass should wrap. const opEnd = ((frame.y + frame.height) * this.width + frame.x) * 4; let op = opBeg; let scanStride = frameStride * 4; // Use scanStride to skip past the rows when interlacing. This is skipping // 7 rows for the first two passes, then 3 then 1. if (frame.interlaced === true) { scanStride += this.width * 4 * 7; // Pass 1. } let interlaceSkip = 8; // Tracking the row interval in the current pass. for (let i = 0, il = indexStream.length; i < il; ++i) { const index = indexStream[i]; if (xLeft === 0) { // Beginning of new scan line op += scanStride; xLeft = frameWidth; if (op >= opEnd) { // Catch the wrap to switch passes when interlacing. scanStride = frameStride * 4 + this.width * 4 * (interlaceSkip - 1); // interlaceSkip / 2 * 4 is interlaceSkip << 1. op = opBeg + (frameWidth + frameStride) * (interlaceSkip << 1); interlaceSkip >>= 1; } } if (index === trans) { op += 4; } else { const r = this.buf[(paletteOffset || 0) + index * 3]; const g = this.buf[(paletteOffset || 0) + index * 3 + 1]; const b = this.buf[(paletteOffset || 0) + index * 3 + 2]; pixels[op++] = b; pixels[op++] = g; pixels[op++] = r; pixels[op++] = 255; } --xLeft; } } public decodeAndBlitFrameRGBA( frameNum: number, pixels: Uint8ClampedArray, ): void { const frame = this.frameInfo(frameNum); const numPixels = frame.width * frame.height; const indexStream = new Uint8Array(numPixels); // At most 8-bit indices. GifReaderLZWOutputIndexStream( this.buf, frame.dataOffset, indexStream, numPixels, ); // debugger; const paletteOffset = frame.paletteOffset; let trans = frame.transparentIndex; if (trans === null) trans = 256; // We are possibly just blitting to a portion of the entire frame. // That is a subRect within the frameRect, so the additional pixels // must be skipped over after we finished a scanline. const frameWidth = frame.width; const frameStride = this.width - frameWidth; let xLeft = frameWidth; // Number of subRect pixels left in scanline. // Output index of the top left corner of the subRect. const opBeg = ((frame.y * this.width) + frame.x) * 4; // Output index of what would be the left edge of the subRect, one row // below it, i.e. the index at which an interlace pass should wrap. const opEnd = ((frame.y + frame.height) * this.width + frame.x) * 4; let op = opBeg; let scanStride = frameStride * 4; // Use scanStride to skip past the rows when interlacing. This is skipping // 7 rows for the first two passes, then 3 then 1. if (frame.interlaced === true) { scanStride += this.width * 4 * 7; // Pass 1. } let interlaceSkip = 8; // Tracking the row interval in the current pass. for (let i = 0, il = indexStream.length; i < il; ++i) { const index = indexStream[i]; if (xLeft === 0) { // Beginning of new scan line op += scanStride; xLeft = frameWidth; if (op >= opEnd) { // Catch the wrap to switch passes when interlacing. scanStride = frameStride * 4 + this.width * 4 * (interlaceSkip - 1); // interlaceSkip / 2 * 4 is interlaceSkip << 1. op = opBeg + (frameWidth + frameStride) * (interlaceSkip << 1); interlaceSkip >>= 1; } } if (index === trans) { op += 4; } else { const rI = (paletteOffset || 0) + index * 3; const r = this.buf[rI]; const g = this.buf[rI + 1]; const b = this.buf[rI + 2]; pixels[op++] = r; pixels[op++] = g; pixels[op++] = b; pixels[op++] = 255; } --xLeft; } } // Additional private or public methods should be implemented below private parseHeader(): void { // Parse the GIF file header if ( this.buf[this.p++] !== 0x47 || this.buf[this.p++] !== 0x49 || this.buf[this.p++] !== 0x46 || this.buf[this.p++] !== 0x38 || (this.buf[this.p++] + 1 & 0xfd) !== 0x38 || this.buf[this.p++] !== 0x61 ) { throw new Error("Invalid GIF 87a/89a header."); } } private parseLogicalScreenDescriptor(): void { // Parse the Logical Screen Descriptor block } private parseGlobalColorTable(): void { // Parse the Global Color Table block if it exists } private parseFrames(): void { const width = this.buf[this.p++] | this.buf[this.p++] << 8; const height = this.buf[this.p++] | this.buf[this.p++] << 8; const pf0 = this.buf[this.p++]; // . const global_palette_flag = pf0 >> 7; const num_global_colors_pow2 = pf0 & 0x7; const num_global_colors = 1 << (num_global_colors_pow2 + 1); const background = this.buf[this.p++]; this.buf[this.p++]; // Pixel aspect ratio (unused?). let global_palette_offset = null; let global_palette_size = null; if (global_palette_flag) { global_palette_offset = this.p; global_palette_size = num_global_colors; this.p += num_global_colors * 3; // Seek past palette. } let no_eof = true; const frames = []; let delay = 0; let transparentIndex = null; let disposal = 0; // 0 - No disposal specified. let loopCount = null; this.width = width; this.height = height; while (no_eof && this.p < this.buf.length) { switch (this.buf[this.p++]) { case 0x21: // Graphics Control Extension Block switch (this.buf[this.p++]) { case 0xff: // Application specific block // Try if it's a Netscape block (with animation loop counter). if ( this.buf[this.p] !== 0x0b || // 21 FF already read, check block size. // NETSCAPE2.0 this.buf[this.p + 1] == 0x4e && this.buf[this.p + 2] == 0x45 && this.buf[this.p + 3] == 0x54 && this.buf[this.p + 4] == 0x53 && this.buf[this.p + 5] == 0x43 && this.buf[this.p + 6] == 0x41 && this.buf[this.p + 7] == 0x50 && this.buf[this.p + 8] == 0x45 && this.buf[this.p + 9] == 0x32 && this.buf[this.p + 10] == 0x2e && this.buf[this.p + 11] == 0x30 && // Sub-block this.buf[this.p + 12] == 0x03 && this.buf[this.p + 13] == 0x01 && this.buf[this.p + 16] == 0 ) { this.p += 14; loopCount = this.buf[this.p++] | this.buf[this.p++] << 8; this.p++; // Skip terminator. } else { // We don't know what it is, just try to get past it. this.p += 12; while (true) { // Seek through subblocks. const block_size = this.buf[this.p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator this.p += block_size; } } break; case 0xf9: { // Graphics Control Extension if (this.buf[this.p++] !== 0x4 || this.buf[this.p + 4] !== 0) { throw new Error("Invalid graphics extension block."); } const pf1 = this.buf[this.p++]; delay = this.buf[this.p++] | this.buf[this.p++] << 8; transparentIndex = this.buf[this.p++]; if ((pf1 & 1) === 0) transparentIndex = null; disposal = pf1 >> 2 & 0x7; this.p++; // Skip terminator. break; } // Plain Text Extension could be present and we just want to be able // to parse past it. It follows the block structure of the comment // extension enough to reuse the path to skip through the blocks. case 0x01: // Plain Text Extension (fallthrough to Comment Extension) case 0xfe: // Comment Extension. while (true) { // Seek through subblocks. const block_size = this.buf[this.p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator this.p += block_size; } break; default: throw new Error( "Unknown graphic control label: 0x" + this.buf[this.p - 1].toString(16), ); } break; case 0x2c: { // Image Descriptor. const x = this.buf[this.p++] | this.buf[this.p++] << 8; const y = this.buf[this.p++] | this.buf[this.p++] << 8; const w = this.buf[this.p++] | this.buf[this.p++] << 8; const h = this.buf[this.p++] | this.buf[this.p++] << 8; const pf2 = this.buf[this.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); 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 = this.p; // Override with local palette. palette_size = num_local_colors; this.p += num_local_colors * 3; // Seek past palette. } const data_offset = this.p; this.p++; // codeSize while (true) { const block_size = this.buf[this.p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator this.p += block_size; } this.frames.push({ x, y, width: w, height: h, hasLocalPalette: has_local_palette, paletteOffset: palette_offset, paletteSize: palette_size, dataOffset: data_offset, dataLength: this.p - data_offset, transparentIndex: transparentIndex, interlaced: !!interlace_flag, delay: delay, disposal: disposal, }); break; } case 0x3b: // Trailer Marker (end of file). no_eof = false; break; default: throw new Error( "Unknown gif block: 0x" + this.buf[this.p - 1].toString(16), ); } } } // private readSubBlocks(): string { // // Read a series of sub-blocks // return ""; // } // private readBlockTerminator(): void { // // Read a block terminator if necessary // } } function GifReaderLZWOutputIndexStream( codeStream: Uint8Array, p: number, output: Uint8Array, outputLength: number, ) { const minCodeSize = codeStream[p++]; const clear_code = 1 << minCodeSize; const eoi_code = clear_code + 1; let nextCode = eoi_code + 1; let curCodeSize = minCodeSize + 1; // Number of bits per code. // NOTE: This shares the same name as the encoder, but has a different // meaning here. Here this masks each code coming from the code stream. let codeMask = (1 << curCodeSize) - 1; let curShift = 0; let cur = 0; let op = 0; // Output pointer. let subBlockSize = codeStream[p++]; const codeTable = new Int32Array(4096); // Can be signed, we only use 20 bits. let prevCode = null; // Track code-1. while (true) { // Read up to two bytes, making sure we always 12-bits for max sized code. while (curShift < 16) { if (subBlockSize === 0) break; // No more data to be read. cur |= codeStream[p++] << curShift; curShift += 8; if (subBlockSize === 1) { // Never let it get to 0 to hold logic above. subBlockSize = codeStream[p++]; // Next subBlock. } else { --subBlockSize; } } if (curShift < curCodeSize) { break; } const code = cur & codeMask; cur >>= curCodeSize; curShift -= curCodeSize; if (code === clear_code) { // We don't actually have to clear the table. This could be a good idea // for greater error checking, but we don't really do any anyway. We // will just track it with next_code and overwrite old entries. nextCode = eoi_code + 1; curCodeSize = minCodeSize + 1; codeMask = (1 << curCodeSize) - 1; // Don't update prev_code ? prevCode = null; continue; } else if (code === eoi_code) { break; } // We have a similar situation as the decoder, where we want to store // variable length entries (code table entries), but we want to do in a // faster manner than an array of arrays. The code below stores sort of a // linked list within the code table, and then "chases" through it to // construct the dictionary entries. When a new entry is created, just the // last byte is stored, and the rest (prefix) of the entry is only // referenced by its table entry. Then the code chases through the // prefixes until it reaches a single byte code. We have to chase twice, // first to compute the length, and then to actually copy the data to the // output (backwards, since we know the length). The alternative would be // storing something in an intermediate stack, but that doesn't make any // more sense. I implemented an approach where it also stored the length // in the code table, although it's a bit tricky because you run out of // bits (12 + 12 + 8), but I didn't measure much improvements (the table // entries are generally not the long). Even when I created benchmarks for // very long table entries the complexity did not seem worth it. // The code table stores the prefix entry in 12 bits and then the suffix // byte in 8 bits, so each entry is 20 bits. const chaseCode: number = code < nextCode ? code : prevCode as number; // Chase what we will output, either {CODE} or {CODE-1}. let chaseLength = 0; let chase = chaseCode as number; while (chase > clear_code) { chase = codeTable[chase] >> 8; ++chaseLength; } const k = chase; const op_end = op + chaseLength + (chaseCode !== code ? 1 : 0); if (op_end > outputLength) { console.log("Warning, gif stream longer than expected."); return; } // Already have the first byte from the chase, might as well write it fast. output[op++] = k; op += chaseLength; let b = op; // Track pointer, writing backwards. if (chaseCode !== code) { // The case of emitting {CODE-1} + k. output[op++] = k; } chase = chaseCode; while (chaseLength--) { chase = codeTable[chase]; output[--b] = chase & 0xff; // Write backwards. chase >>= 8; // Pull down to the prefix code. } if (prevCode !== null && nextCode < 4096) { codeTable[nextCode++] = prevCode << 8 | k; if (nextCode >= codeMask + 1 && curCodeSize < 12) { ++curCodeSize; codeMask = codeMask << 1 | 1; } } prevCode = code; } if (op !== outputLength) { console.log("Warning, gif stream shorter than expected."); } return output; } export function handleGIF( data: Uint8Array, ) { const framesBase64: ({ canvas: HTMLCanvasElement } & Frame)[] = []; const reader = new GifReader(data); for (let i = 0; i < reader.numFrames(); i++) { const frameData = reader.frameInfo(i); // const buf = new Uint8Array(frameData.width * frameData.height * 4); const canvas = document.createElement("canvas"); canvas.width = reader.width; canvas.height = reader.height; const ctx = canvas.getContext("2d")!; const imageData = ctx.createImageData(reader.width, reader.height); reader.decodeAndBlitFrameRGBA(i, imageData.data); ctx.putImageData( imageData, 0, 0, frameData.x, frameData.y, frameData.width, frameData.height, ); framesBase64.push({ ...frameData, canvas }); } return { w: reader.width, h: reader.height, frames: framesBase64, }; }