type ListenerFn = ( canvas : HTMLCanvasElement , delay : number ) => void ; type Listener = { finished : ListenerFn ; progress : ListenerFn ; } ; type TargetOffset = { dx : number ; dy : number ; width : number ; height : number ; sWidth : number ; sHeight : number ; } ; interface GifToCanvasOptions { canvasEl ? : HTMLCanvasElement | null ; hasCanvasDom ? : boolean ; clear ? : boolean ; targetOffset ? : TargetOffset ; autoPlay ? : boolean ; } interface PlayerOptions { frames : { data : ImageData ; delay : number | null ; } [ ] ; frameOffsets : { x : number ; y : number ; } [ ] ; tmpCanvas : HTMLCanvasElement ; canvas : HTMLCanvasElement ; targetOffset : TargetOffset ; hasCanvasDom ? : boolean ; loopDelay ? : number ; autoPlay ? : boolean ; onClear ? : ( ) => void ; onFrame ? : ListenerFn ; onFrameFinish ? : ListenerFn ; } interface DefaultHandler { type : 'img' | 'eof' | 'ext' ; sentinel : number ; } interface HeaderHandle { bgColor : number ; colorRes : number ; gct : number [ ] [ ] ; gctSize : number ; height : number ; pixelAspectRatio : number ; sig : string ; ver : string ; width : number ; sorted ? : boolean ; gctFlag ? : boolean ; } interface ImgHandler extends DefaultHandler { height : number ; lct : number [ ] [ ] ; lctSize : number ; leftPos : number ; topPos : number ; lzwMinCodeSize : number ; pixels : number [ ] ; reserved : boolean [ ] ; width : number ; sorted ? : boolean ; interlaced ? : boolean ; lctFlag ? : boolean ; } interface ExtHandler extends DefaultHandler { delayTime : number ; disposalMethod : number ; extType : string ; label : number ; reserved : boolean [ ] ; terminator : number ; comment : string ; transparencyIndex : number ; ptHeader : number [ ] ; ptData : string ; unknown : number ; iterations : number ; appData : string ; identifier : string ; authCode : string ; data : string ; transparencyGiven ? : boolean ; userInput ? : boolean ; } type HandlerBlock = DefaultHandler | ImgHandler | ExtHandler | HeaderHandle ; type HandleFn = ( block : HandlerBlock ) => void ; interface Handler { hdr : HandleFn ; gce : HandleFn ; img : HandleFn ; eof : HandleFn ; app : { NETSCAPE : HandleFn ; [ k ] : HandleFn ; } ; com ? : HandleFn ; pte ? : HandleFn ; unknown ? : HandleFn ; } const bitsToNum = function ( ba ) { return ba . reduce ( function ( s , n ) { return s * 2 + Number ( n ) ; } , 0 ) ; } ; const byteToBitArr = function ( bite ) { const a = [ ] ; for ( let i = 7 ; i >= 0 ; i -- ) { a . push ( ! ! ( bite & ( 1 << i ) ) ) ; } return a ; } ; class class Stream { pos = 0 ; data : Uint8Array | string ; constructor ( data ) { this . data = data ; } readByte ( ) { if ( this . pos >= this . data . length ) { throw new Error ( 'Attempted to read past end of stream.' ) ; } if ( this . data instanceof Uint8Array ) return this . data [ this . pos ++ ] ; else return this . data . charCodeAt ( this . pos ++ ) & 0xff ; } readBytes ( n ) { const bytes = [ ] ; for ( let i = 0 ; i < n ; i ++ ) { bytes . push ( this . readByte ( ) ) ; } return bytes ; } read ( n ) { let s = '' ; for ( let i = 0 ; i < n ; i ++ ) { s += String . fromCharCode ( this . readByte ( ) ) ; } return s ; } readUnsigned ( ) { const a = this . readBytes ( 2 ) ; return ( a [ 1 ] << 8 ) + a [ 0 ] ; } } class class ParseGif { stream : Stream ; handler : Handler ; constructor ( stream , handler ) { this . stream = stream ; this . handler = handler ; } init ( ) { return new Promise ( ( resolve , reject ) => { this . parseHeader ( ) ; const timer = setTimeout ( ( ) => { clearTimeout ( timer ) ; try { this . parseBlock ( ) ; resolve ( null ) ; } catch ( e ) { reject ( e ) ; } } , 0 ) ; } ) ; } parseBlock ( ) { const block = { } as DefaultHandler ; block . sentinel = this . stream . readByte ( ) ; switch ( String . fromCharCode ( block . sentinel ) ) { case '!' : block . type = 'ext' ; this . parseExt ( block as ExtHandler ) ; break ; case ',' : block . type = 'img' ; this . parseImg ( block as ImgHandler ) ; break ; case ';' : block . type = 'eof' ; this . handler . eof ( block ) ; break ; default : throw new Error ( 'Unknown block: 0x' + block . sentinel . toString ( 16 ) ) ; } if ( block . type !== 'eof' ) { this . parseBlock ( ) ; } } parseCT ( entries ) { const ct = [ ] ; for ( let i = 0 ; i < entries ; i ++ ) { ct . push ( this . stream . readBytes ( 3 ) ) ; } return ct ; } lzwDecode ( minCodeSize , data ) { let pos = 0 ; const readCode = function ( size ) { let code = 0 ; for ( let i = 0 ; i < size ; i ++ ) { if ( data . charCodeAt ( pos >> 3 ) & ( 1 << ( pos & 7 ) ) ) { code |= 1 << i ; } pos ++ ; } return code ; } ; const output = [ ] ; const clearCode = 1 << minCodeSize ; const eoiCode = clearCode + 1 ; let codeSize = minCodeSize + 1 ; let dict = [ ] ; const clear = function ( ) { dict = [ ] ; codeSize = minCodeSize + 1 ; for ( let i = 0 ; i < clearCode ; i ++ ) { dict [ i ] = [ i ] ; } dict [ clearCode ] = [ ] ; dict [ eoiCode ] = null as any ; } ; let code ; let last ; while ( true ) { last = code ; code = readCode ( codeSize ) ; if ( code === clearCode ) { clear ( ) ; continue ; } if ( code === eoiCode ) break ; if ( code < dict . length ) { if ( last !== clearCode ) { dict . push ( dict [ last as number ] . concat ( dict [ code ] [ 0 ] ) ) ; } } else { if ( code !== dict . length ) throw new Error ( 'Invalid LZW code.' ) ; dict . push ( dict [ last as number ] . concat ( dict [ last as number ] [ 0 ] ) ) ; } output . push . apply ( output , dict [ code ] ) ; if ( dict . length === 1 << codeSize && codeSize < 12 ) { codeSize ++ ; } } return output ; } parseImg ( img ) { const st = this . stream ; const deinterlace = ( pixels , width ) => { const newPixels = new Array ( pixels . length ) ; const rows = pixels . length / width ; const cpRow = ( toRow , fromRow ) => { const fromPixels = pixels . slice ( fromRow * width , ( fromRow + 1 ) * width ) ; newPixels . splice . apply ( newPixels , [ toRow * width , width , ... fromPixels ] ) ; } ; const offsets = [ 0 , 4 , 2 , 1 ] ; const steps = [ 8 , 8 , 4 , 2 ] ; let fromRow = 0 ; for ( let pass = 0 ; pass < 4 ; pass ++ ) { for ( let toRow = offsets [ pass ] ; toRow < rows ; toRow += steps [ pass ] ) { cpRow ( toRow , fromRow ) ; fromRow ++ ; } } return newPixels ; } ; img . leftPos = st . readUnsigned ( ) ; img . topPos = st . readUnsigned ( ) ; img . width = st . readUnsigned ( ) ; img . height = st . readUnsigned ( ) ; const bits = byteToBitArr ( st . readByte ( ) ) ; img . lctFlag = bits . shift ( ) ; img . interlaced = bits . shift ( ) ; img . sorted = bits . shift ( ) ; img . reserved = bits . splice ( 0 , 2 ) ; img . lctSize = bitsToNum ( bits . splice ( 0 , 3 ) ) ; if ( img . lctFlag ) { img . lct = this . parseCT ( 1 << ( img . lctSize + 1 ) ) ; } img . lzwMinCodeSize = st . readByte ( ) ; const lzwData = this . readSubBlocks ( ) ; img . pixels = this . lzwDecode ( img . lzwMinCodeSize , lzwData ) ; if ( img . interlaced ) { img . pixels = deinterlace ( img . pixels , img . width ) ; } this . handler . img ( img ) ; } readSubBlocks ( ) { let size , data ; data = '' ; do { size = this . stream . readByte ( ) ; data += this . stream . read ( size ) ; } while ( size !== 0 ) ; return data ; } parseExt ( block ) { const parseGCExt = ( block ) => { this . stream . readByte ( ) ; const bits = byteToBitArr ( this . stream . readByte ( ) ) ; block . reserved = bits . splice ( 0 , 3 ) ; block . disposalMethod = bitsToNum ( bits . splice ( 0 , 3 ) ) ; block . userInput = bits . shift ( ) ; block . transparencyGiven = bits . shift ( ) ; block . delayTime = this . stream . readUnsigned ( ) ; block . transparencyIndex = this . stream . readByte ( ) ; block . terminator = this . stream . readByte ( ) ; this . handler . gce ( block ) ; } ; const parseComExt = ( block ) => { block . comment = this . readSubBlocks ( ) ; this . handler . com ?. ( block ) ; } ; const parsePTExt = ( block ) => { this . stream . readByte ( ) ; block . ptHeader = this . stream . readBytes ( 12 ) ; block . ptData = this . readSubBlocks ( ) ; this . handler . pte ?. ( block ) ; } ; const parseAppExt = ( block ) => { const parseNetscapeExt = ( block ) => { this . stream . readByte ( ) ; block . unknown = this . stream . readByte ( ) ; block . iterations = this . stream . readUnsigned ( ) ; block . terminator = this . stream . readByte ( ) ; this . handler . app . NETSCAPE ( block ) ; } ; const parseUnknownAppExt = ( block ) => { block . appData = this . readSubBlocks ( ) ; this . handler . app [ block . identifier ] ?. ( block ) ; } ; this . stream . readByte ( ) ; block . identifier = this . stream . read ( 8 ) ; block . authCode = this . stream . read ( 3 ) ; switch ( block . identifier ) { case 'NETSCAPE' : parseNetscapeExt ( block ) ; break ; default : parseUnknownAppExt ( block ) ; break ; } } ; const parseUnknownExt = ( block ) => { block . data = this . readSubBlocks ( ) ; this . handler . unknown ?. ( block ) ; } ; block . label = this . stream . readByte ( ) ; switch ( block . label ) { case 0xf9 : block . extType = 'gce' ; parseGCExt ( block ) ; break ; case 0xfe : block . extType = 'com' ; parseComExt ( block ) ; break ; case 0x01 : block . extType = 'pte' ; parsePTExt ( block ) ; break ; case 0xff : block . extType = 'app' ; parseAppExt ( block ) ; break ; default : block . extType = 'unknown' ; parseUnknownExt ( block ) ; break ; } } parseHeader ( ) { const stream = this . stream ; const hdr = { } as HeaderHandle ; hdr . sig = stream . read ( 3 ) ; hdr . ver = stream . read ( 3 ) ; if ( hdr . sig !== 'GIF' ) throw new Error ( 'Not a GIF file.' ) ; hdr . width = stream . readUnsigned ( ) ; hdr . height = stream . readUnsigned ( ) ; const bits = byteToBitArr ( stream . readByte ( ) ) ; hdr . gctFlag = bits . shift ( ) ; hdr . colorRes = bitsToNum ( bits . splice ( 0 , 3 ) ) ; hdr . sorted = bits . shift ( ) ; hdr . gctSize = bitsToNum ( bits . splice ( 0 , 3 ) ) ; hdr . bgColor = stream . readByte ( ) ; hdr . pixelAspectRatio = stream . readByte ( ) ; if ( hdr . gctFlag ) { hdr . gct = this . parseCT ( 1 << ( hdr . gctSize + 1 ) ) ; } this . handler . hdr ( hdr ) ; } } class Player { playing = false ; forward = true ; i = - 1 ; options = { loopDelay : 0 , autoPlay : false } as PlayerOptions ; constructor ( options ) { this . options = { ... this . options , ... options } ; } putFrame ( ) { this . i = parseInt ( String ( this . i ) , 10 ) ; if ( this . i > this . options . frames . length - 1 ) { this . i = 0 ; } if ( this . i < 0 ) { this . i = 0 ; } const targetOffset = this . options . targetOffset ; const offset = this . options . frameOffsets [ this . i ] ; this . options . tmpCanvas . getContext ( '2d' ) ?. putImageData ( this . options . frames [ this . i ] . data , offset . x , offset . y ) ; const ctx = this . options . canvas . getContext ( '2d' ) as CanvasRenderingContext2D ; ctx . globalCompositeOperation = 'copy' ; ctx . drawImage ( this . options . tmpCanvas , targetOffset . dx , targetOffset . dy , targetOffset . sWidth , targetOffset . sHeight , 0 , 0 , targetOffset . sWidth , targetOffset . sHeight ) ; } getNextFrameNo ( ) { const delta = this . forward ? 1 : - 1 ; return ( this . i + delta + this . options . frames . length ) % this . options . frames . length ; } stepFrame ( amount ) { this . i = this . i + amount ; this . putFrame ( ) ; } step ( ) { let stepping = false ; let timer = null ; const completeLoop = ( ) => { if ( this . options . autoPlay ) { doStep ( ) ; } else { stepping = false ; this . playing = false ; this . options . onClear ?. ( ) ; } } ; const doStep = ( ) => { timer && clearTimeout ( timer ) ; stepping = this . playing ; if ( ! stepping ) return ; this . stepFrame ( 1 ) ; let delay = ( this . options . frames [ this . i ] . delay ?? 0 ) * 10 ; if ( ! delay ) delay = 100 ; const nextFrameNo = this . getNextFrameNo ( ) ; if ( nextFrameNo === 0 ) { this . options . onFrameFinish ?. ( this . options . canvas , delay ) ; delay += this . options . loopDelay ?? 0 ; timer = setTimeout ( completeLoop , delay ) ; } else { this . options . onFrame ?. ( this . options . canvas , delay ) ; timer = setTimeout ( doStep , delay ) ; } } ; doStep ( ) ; } play ( ) { this . playing = true ; this . step ( ) ; } init ( ) { this . play ( ) ; } } export class export class GifToCanvas { static canvas : HTMLCanvasElement ; url : string ; listener = { } as Listener ; options = { hasCanvasDom : false , autoPlay : false , targetOffset : { dx : 0 , dy : 0 } } as GifToCanvasOptions ; hdr = { } as HeaderHandle ; handler = { } as Handler ; canvas : HTMLCanvasElement ; tmpCanvas = document . createElement ( 'canvas' ) ; transparency : number | null = null ; disposalMethod : number | null = null ; lastDisposalMethod : number | null = null ; disposalRestoreFromIdx : number | null = null ; delay : number | null = null ; lastImg : ImgHandler | null = null ; frame : CanvasRenderingContext2D | null = null ; frames : { data : ImageData ; delay : number | null ; } [ ] = [ ] ; frameOffsets : { x : number ; y : number ; } [ ] = [ ] ; constructor ( url , options = { } ) { this . options = { ... this . options , ... options } ; this . url = url ; this . canvas = GifToCanvas . canvas = this . options . canvasEl || GifToCanvas . canvas || document . createElement ( 'canvas' ) ; } async init ( ) { if ( this . options . hasCanvasDom ) { this . initDom ( ) ; } await this . load ( ) ; } initDom ( ) { if ( ! this . options . canvasEl ) { document . body . appendChild ( this . canvas ) ; } } async load ( ) { const data = await this . createXhr ( this . url ) ; const stream = new Stream ( data ) ; this . handler = this . createHandle ( ) ; const parseGIF = new ParseGif ( stream , this . handler ) ; await parseGIF . init ( ) ; const player = new Player ( { frames : this . frames , frameOffsets : this . frameOffsets , tmpCanvas : this . tmpCanvas , canvas : this . canvas , hasCanvasDom : this . options . hasCanvasDom , autoPlay : this . options . autoPlay , targetOffset : this . options . targetOffset as TargetOffset , onClear : this . onClear . bind ( this ) , onFrame : this . listener . progress , onFrameFinish : this . listener . finished } ) ; player . init ( ) ; } on ( event , fn ) { this . listener [ event ] = fn ; } memberof setCanvas ( ) { const targetOffset = this . options . targetOffset ; if ( targetOffset ?. width ) { const l = this . hdr . width / targetOffset . width ; this . options . targetOffset = { ... targetOffset , dx : targetOffset . dx * l , dy : targetOffset . dy * l , sWidth : targetOffset . sWidth * l , sHeight : targetOffset . sHeight * l } ; this . canvas . width = this . options . targetOffset . sWidth ; this . canvas . height = this . options . targetOffset . sHeight ; } this . tmpCanvas . width = this . hdr . width ; this . tmpCanvas . height = this . hdr . height ; } memberof onClear ( ) { this . tmpCanvas . getContext ( '2d' ) ?. clearRect ( 0 , 0 , this . tmpCanvas . width , this . tmpCanvas . height ) ; this . canvas ?. getContext ( '2d' ) ?. clearRect ( 0 , 0 , this . canvas . width , this . canvas . height ) ; this . frames = [ ] ; this . clear ( ) ; if ( this . options . clear ) { this . canvas ?. parentNode ?. removeChild ( this . canvas ) ; } } param { string } url returns memberof createXhr ( url ) { return new Promise < any > ( ( resolve , reject ) => { const xhr = new XMLHttpRequest ( ) ; xhr . open ( 'GET' , url , true ) ; xhr . overrideMimeType ( 'text/plain; charset=x-user-defined' ) ; xhr . withCredentials = false ; xhr . onload = ( e ) => { if ( e . target . status !== 200 && e . target . status !== 304 ) { reject ( 'Status Error: ' + e . target . status ) ; return ; } let data = e . target . response ; if ( data . toString ( ) . indexOf ( 'ArrayBuffer' ) > 0 ) { data = new Uint8Array ( data ) ; } resolve ( data ) ; } ; xhr . onerror = ( e ) => { reject ( e ) ; } ; xhr . send ( ) ; } ) ; } clear ( ) { this . transparency = null ; this . delay = null ; this . lastDisposalMethod = this . disposalMethod ; this . disposalMethod = null ; this . frame = null ; } pushFrame ( ) { if ( ! this . frame ) return ; this . frames . push ( { data : this . frame . getImageData ( 0 , 0 , this . hdr . width , this . hdr . height ) , delay : this . delay } ) ; this . frameOffsets . push ( { x : 0 , y : 0 } ) ; } doHdr ( hdr ) { this . hdr = hdr ; this . setCanvas ( ) ; } doGCE ( gce ) { this . pushFrame ( ) ; this . clear ( ) ; this . transparency = gce . transparencyGiven ? gce . transparencyIndex : null ; this . delay = gce . delayTime ; this . disposalMethod = gce . disposalMethod ; } doNothing ( ) { return null ; } doImg ( img ) { if ( ! this . frame ) this . frame = this . tmpCanvas . getContext ( '2d' ) ; if ( ! this . frame ) return ; const currIdx = this . frames . length ; const ct = img . lctFlag ? img . lct : this . hdr . gct ; if ( currIdx > 0 ) { if ( this . lastDisposalMethod === 3 ) { if ( this . disposalRestoreFromIdx !== null ) { this . frame . putImageData ( this . frames [ this . disposalRestoreFromIdx ] . data , 0 , 0 ) ; } else { if ( this . lastImg ) { this . frame . clearRect ( this . lastImg . leftPos , this . lastImg . topPos , this . lastImg . width , this . lastImg . height ) ; } } } else { this . disposalRestoreFromIdx = currIdx - 1 ; } if ( this . lastDisposalMethod === 2 ) { if ( this . lastImg ) { this . frame . clearRect ( this . lastImg . leftPos , this . lastImg . topPos , this . lastImg . width , this . lastImg . height ) ; } } } const imgData = this . frame . getImageData ( img . leftPos , img . topPos , img . width , img . height ) ; img . pixels . forEach ( ( pixel , i ) => { if ( pixel !== this . transparency ) { imgData . data [ i * 4 + 0 ] = ct [ pixel ] [ 0 ] ; imgData . data [ i * 4 + 1 ] = ct [ pixel ] [ 1 ] ; imgData . data [ i * 4 + 2 ] = ct [ pixel ] [ 2 ] ; imgData . data [ i * 4 + 3 ] = 255 ; } } ) ; this . frame . putImageData ( imgData , img . leftPos , img . topPos ) ; this . lastImg = img ; } createHandle ( ) { const withProgress = ( fn ) => { return ( block ) => { fn . call ( this , block ) ; } ; } ; return { hdr : withProgress ( this . doHdr ) , gce : withProgress ( this . doGCE ) , img : withProgress ( this . doImg ) , com : withProgress ( this . doNothing ) , app : { NETSCAPE : withProgress ( this . doNothing ) } , eof : ( ) => { this . pushFrame ( ) ; } } ; } }