0

I built an interface that calls a web API in asp.net (i use c# and javascript/ajax to implement that).

The client side call to the controller, the controller needs to create animation gif and send it back to the client side by a string of base64 or byte array, when the client side gets the base64 he should display it into a canvas.

Now the problem is that the canvas display only the first frame of the animation gif like a static image.

I already read a lot on the internet and find this: How Do I Convert A GIF Animation To Base64 String And Back To A GIF Animation?

But it's not helped me because I don't want to save the image on the disc just to display it on the client side.

*Note that when I save the image from server side on my disc its save it as gif and display all frames together like I wish, something wrong when I transfer it to client side.

*I use ImageMagick to create the animated gif.

Here is my client side code:

  <!DOCTYPE html>
  <html> 
      <head>
          <title></title>
          <meta charset="utf-8" />
          <link href="Content/bootstrap.min.css" rel="stylesheet" /> 
      </head> 
      <body style="padding-top: 20px;">
          <div class="col-md-10 col-md-offset-1">
              <div class="well">
                  <!---->
                  <canvas id="canvasImage" width="564" height="120">          
                      <p>We apologize, your browser does not support canvas at this time!</p>     
                  </canvas>             
                  <!---->
              </div>
          </div>
      <script src="Scripts/jquery-1.10.2.min.js"></script>
      <script src="Scripts/bootstrap.min.js"></script>
      <script type="text/javascript">
          $(document).ready(function () {
              $.ajax({
                  url: '/api/EngineProccess',
                  method: 'GET',
                  success: function (data) {
                      var imageObj = new Image();
                      var canvas = document.getElementById("canvasImage");                     
                      var context = canvas.getContext('2d');                     
                      var image = new Image();                     
                      image.onload = function () {                         
                          context.drawImage(image, 0, 0);                     
                      };
                      console.log(data);
                      image.src = "data:image/gif;base64," + data;                 
                  },
                  error: function (jqXHR) {                     
                      $('#divErrorText').text(jqXHR.responseText);                     
                      $('#divError').show('fade');
                  }
              });
          });
      </script>
  </body>
</html>

and here is the server code:

public class EngineProccessController : ApiController     
{         
     // GET api/EngineProccess
     public String Get()         
     {             
          using (MagickImageCollection collection = new MagickImageCollection())             
          {                 
               // Add first image and set the animation delay to 100ms                 
               collection.Add("Snakeware1.gif");              
               collection[0].AnimationDelay = 100;                  

               // Add second image, set the animation delay to 100ms and flip the image                 
               collection.Add("Snakeware2.gif");             
               collection[1].AnimationDelay = 100;                 
               collection[1].Flip();                  

               // Optionally reduce colors                 
               QuantizeSettings settings = new QuantizeSettings();                 
               settings.Colors = 256;                 
               collection.Quantize(settings);

               // Optionally optimize the images (images should have the same size).                 
               collection.Optimize();

               // Save gif                 
               //collection.Write("D://Test01//Test01//Animated.gif");                
               string data = collection.ToBase64();                 
               return data;             
          }
     }
}

Any ideas? Please help.

Edit: after some days i found the problem, i use magicimage (magic.net) to create the gif animaited, the base64 was ok but the problem was in the canvas element, the canvas didnt display the animation likei want so i changed the element canvas to be an regular image element () and the changed the src of the image dynamic.

Regards, Jr.Rafa

Rafael
  • 157
  • 3
  • 13
  • You will need to write or use a gif decoder, decoding the gif will give you each frame and the timing etc.. It is relatively simple to decode with the most complex bit being the lwz decoder for the compressed pixel data. Data is stored with headers. All you need is in this document https://www.w3.org/Graphics/GIF/spec-gif87.txt – Blindman67 Jul 15 '17 at 16:50
  • Possible duplicate of [Animated GIF on Fabric.js Canvas](https://stackoverflow.com/questions/28056636/animated-gif-on-fabric-js-canvas) *[even if the question was about fabricjs library, the answer is generic]* – Kaiido Jul 16 '17 at 01:32

1 Answers1

0

Example loading playing gif on canvas.

Sorry but just under 30K answer limit, code and comment very cut down to fit. Ask in comments if needed. See bottom of snippet on basic usage.

/*

The code was created as to the specifications set out in https://www.w3.org/Graphics/GIF/spec-gif89a.txt

The document states usage conditions

    "The Graphics Interchange Format(c) is the Copyright property of
    CompuServe Incorporated. GIF(sm) is a Service Mark property of
    CompuServe Incorporated."

    https://en.wikipedia.org/wiki/GIF#Unisys_and_LZW_patent_enforcement last paragraph

    Additional sources
      https://en.wikipedia.org/wiki/GIF
      https://www.w3.org/Graphics/GIF/spec-gif87.txt

 */

var GIF = function () {
    var timerID;                         
    var st;                               
    var interlaceOffsets  = [0, 4, 2, 1]; // used in de-interlacing.
    var interlaceSteps    = [8, 8, 4, 2];
    var interlacedBufSize = undefined;    
    var deinterlaceBuf    = undefined;
    var pixelBufSize      = undefined;    
    var pixelBuf          = undefined;
    const GIF_FILE = {
        GCExt   : 0xF9,
        COMMENT : 0xFE,
        APPExt  : 0xFF,
        UNKNOWN : 0x01,                   
        IMAGE   : 0x2C,
        EOF     : 59,                     
        EXT     : 0x21,
    };      
    var Stream = function (data) { // simple buffered stream
        this.data = new Uint8ClampedArray(data);
        this.pos  = 0;
        var len   = this.data.length;
        this.getString = function (count) { 
            var s = "";
            while (count--) {
                s += String.fromCharCode(this.data[this.pos++]);
            }
            return s;
        };
        this.readSubBlocks = function () { 
            var size, count, data;
            data = "";
            do {
                count = size = this.data[this.pos++];
                while (count--) {
                    data += String.fromCharCode(this.data[this.pos++]);
                }
            } while (size !== 0 && this.pos < len);
            return data;
        }
        this.readSubBlocksB = function () { // reads a set of blocks as binary
            var size, count, data;
            data = [];
            do {
                count = size = this.data[this.pos++];
                while (count--) {
                    data.push(this.data[this.pos++]);
                }
            } while (size !== 0 && this.pos < len);
            return data;
        }
    };
    // LZW decoder uncompressed each frame's pixels
    var lzwDecode = function (minSize, data) {
        var i, pixelPos, pos, clear, eod, size, done, dic, code, last, d, len;
        pos      = 0;
        pixelPos = 0;
        dic      = [];
        clear    = 1 << minSize;
        eod      = clear + 1;
        size     = minSize + 1;
        done     = false;
        while (!done) { 
            last = code;
            code = 0;
            for (i = 0; i < size; i++) {
                if (data[pos >> 3] & (1 << (pos & 7))) {
                    code |= 1 << i;
                }
                pos++;
            }
            if (code === clear) { // clear and reset the dictionary
                dic = [];
                size = minSize + 1;
                for (i = 0; i < clear; i++) {
                    dic[i] = [i];
                }
                dic[clear] = [];
                dic[eod] = null;
                continue;
            }
            if (code === eod) { // end of data
                done = true;
                return;
            }
            if (code >= dic.length) {
                dic.push(dic[last].concat(dic[last][0]));
            } else
                if (last !== clear) {
                    dic.push(dic[last].concat(dic[code][0]));
                }
                d = dic[code];
                len = d.length;
            for (i = 0; i < len; i++) {
                pixelBuf[pixelPos++] = d[i];
            }
            if (dic.length === (1 << size) && size < 12) {
                size++;
            }
        }
    };
    var parseColourTable = function (count) { // get a colour table of length count
                                              // Each entry is 3 bytes, for RGB.
        var colours = [];
        for (var i = 0; i < count; i++) {
            colours.push([st.data[st.pos++], st.data[st.pos++], st.data[st.pos++]]);
        }
        return colours;
    };
    var parse = function () {        // read the header. This is the starting point of the decode and async calls parseBlock
        var bitField;
        st.pos                += 6;  // skip the first stuff see GifEncoder for details
        gif.width             = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
        gif.height            = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
        bitField              = st.data[st.pos++];
        gif.colorRes          = (bitField & 0b1110000) >> 4;
        gif.globalColourCount = 1 << ((bitField & 0b111) + 1);
        gif.bgColourIndex     = st.data[st.pos++];
        st.pos++;                    // ignoring pixel aspect ratio. if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
        if (bitField & 0b10000000) { // global colour flag
            gif.globalColourTable = parseColourTable(gif.globalColourCount);
        }
        setTimeout(parseBlock, 0);
    };
    var parseAppExt = function () { // get application specific data. Netscape added iterations and terminator. Ignoring that
        st.pos += 1;
        if ('NETSCAPE' === st.getString(8)) {
            st.pos += 8;            // ignoring this data. iterations (word) and terminator (byte)
        } else {
            st.pos += 3;            // 3 bytes of string usually "2.0" when identifier is NETSCAPE
            st.readSubBlocks();     // unknown app extension
        }
    };
    var parseGCExt = function () { // get GC data
        var bitField;
        st.pos++;
        bitField              = st.data[st.pos++];
        gif.disposalMethod    = (bitField & 0b11100) >> 2;
        gif.transparencyGiven = bitField & 0b1 ? true : false; // ignoring bit two that is marked as  userInput???
        gif.delayTime         = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
        gif.transparencyIndex = st.data[st.pos++];
        st.pos++;
    };
    var parseImg = function () {                           // decodes image data to create the indexed pixel image
        var deinterlace, frame, bitField;
        deinterlace = function (width) {                   // de interlace pixel data if needed
            var lines, fromLine, pass, toline;
            lines = pixelBufSize / width;
            fromLine = 0;
            if (interlacedBufSize !== pixelBufSize) {      
                deinterlaceBuf = new Uint8Array(pixelBufSize);
                interlacedBufSize = pixelBufSize;
            }
            for (pass = 0; pass < 4; pass++) {
                for (toLine = interlaceOffsets[pass]; toLine < lines; toLine += interlaceSteps[pass]) {
                    deinterlaceBuf.set(pixelBuf.subArray(fromLine, fromLine + width), toLine * width);
                    fromLine += width;
                }
            }
        };
        frame                = {}
        gif.frames.push(frame);
        frame.disposalMethod = gif.disposalMethod;
        frame.time           = gif.length;
        frame.delay          = gif.delayTime * 10;
        gif.length          += frame.delay;
        if (gif.transparencyGiven) {
            frame.transparencyIndex = gif.transparencyIndex;
        } else {
            frame.transparencyIndex = undefined;
        }
        frame.leftPos = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
        frame.topPos  = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
        frame.width   = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
        frame.height  = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
        bitField      = st.data[st.pos++];
        frame.localColourTableFlag = bitField & 0b10000000 ? true : false; 
        if (frame.localColourTableFlag) {
            frame.localColourTable = parseColourTable(1 << ((bitField & 0b111) + 1));
        }
        if (pixelBufSize !== frame.width * frame.height) { // create a pixel buffer if not yet created or if current frame size is different from previous
            pixelBuf     = new Uint8Array(frame.width * frame.height);
            pixelBufSize = frame.width * frame.height;
        }
        lzwDecode(st.data[st.pos++], st.readSubBlocksB()); // decode the pixels
        if (bitField & 0b1000000) {                        // de interlace if needed
            frame.interlaced = true;
            deinterlace(frame.width);
        } else {
            frame.interlaced = false;
        }
        processFrame(frame);                               // convert to canvas image
    };
    var processFrame = function (frame) { 
        var ct, cData, dat, pixCount, ind, useT, i, pixel, pDat, col, frame, ti;
        frame.image        = document.createElement('canvas');
        frame.image.width  = gif.width;
        frame.image.height = gif.height;
        frame.image.ctx    = frame.image.getContext("2d");
        ct = frame.localColourTableFlag ? frame.localColourTable : gif.globalColourTable;
        if (gif.lastFrame === null) {
            gif.lastFrame = frame;
        }
        useT = (gif.lastFrame.disposalMethod === 2 || gif.lastFrame.disposalMethod === 3) ? true : false;
        if (!useT) {
            frame.image.ctx.drawImage(gif.lastFrame.image, 0, 0, gif.width, gif.height);
        }
        cData = frame.image.ctx.getImageData(frame.leftPos, frame.topPos, frame.width, frame.height);
        ti  = frame.transparencyIndex;
        dat = cData.data;
        if (frame.interlaced) {
            pDat = deinterlaceBuf;
        } else {
            pDat = pixelBuf;
        }
        pixCount = pDat.length;
        ind = 0;
        for (i = 0; i < pixCount; i++) {
            pixel = pDat[i];
            col   = ct[pixel];
            if (ti !== pixel) {
                dat[ind++] = col[0];
                dat[ind++] = col[1];
                dat[ind++] = col[2];
                dat[ind++] = 255;      // Opaque.
            } else
                if (useT) {
                    dat[ind + 3] = 0; // Transparent.
                    ind += 4;
                } else {
                    ind += 4;
                }
        }
        frame.image.ctx.putImageData(cData, frame.leftPos, frame.topPos);
        gif.lastFrame = frame;
        if (!gif.waitTillDone && typeof gif.onload === "function") { // if !waitTillDone the call onload now after first frame is loaded
            doOnloadEvent();
        }
    };
    var finnished = function () { // called when the load has completed
        gif.loading           = false;
        gif.frameCount        = gif.frames.length;
        gif.lastFrame         = null;
        st                    = undefined;
        gif.complete          = true;
        gif.disposalMethod    = undefined;
        gif.transparencyGiven = undefined;
        gif.delayTime         = undefined;
        gif.transparencyIndex = undefined;
        gif.waitTillDone      = undefined;
        pixelBuf              = undefined; // dereference pixel buffer
        deinterlaceBuf        = undefined; // dereference interlace buff (may or may not be used);
        pixelBufSize          = undefined;
        deinterlaceBuf        = undefined;
        gif.currentFrame      = 0;
        if (gif.frames.length > 0) {
            gif.image = gif.frames[0].image;
        }
        doOnloadEvent();
        if (typeof gif.onloadall === "function") {
            (gif.onloadall.bind(gif))({
                type : 'loadall',
                path : [gif]
            });
        }
        if (gif.playOnLoad) {
            gif.play();
        }
    }
    var canceled = function () { // called if the load has been cancelled
        finnished();
        if (typeof gif.cancelCallback === "function") {
            (gif.cancelCallback.bind(gif))({
                type : 'canceled',
                path : [gif]
            });
        }
    }
    var parseExt = function () {              // parse extended blocks
        switch (st.data[st.pos++]) {
        case GIF_FILE.GCExt:
            parseGCExt();
            break;
        case GIF_FILE.COMMENT:
            gif.comment += st.readSubBlocks(); // found a comment field
            break;
        case GIF_FILE.APPExt:
            parseAppExt();
            break;
        case GIF_FILE.UNKNOWN:                // not keeping this data
            st.pos += 13;                     // deliberate fall through to default
        default:                              // not keeping this if it happens
            st.readSubBlocks();
            break;
        }
    }
    var parseBlock = function () { // parsing the blocks
        if (gif.cancel !== undefined && gif.cancel === true) {
            canceled();
            return;
        }
        switch (st.data[st.pos++]) {
        case GIF_FILE.IMAGE: // image block
            parseImg();
            if (gif.firstFrameOnly) {
                finnished();
                return;
            }
            break;
        case GIF_FILE.EOF: // EOF found so cleanup and exit.
            finnished();
            return;
        case GIF_FILE.EXT: // extend block
        default:
            parseExt();
            break;
        }
        if (typeof gif.onprogress === "function") {
            gif.onprogress({
                bytesRead  : st.pos,
                totalBytes : st.data.length,
                frame      : gif.frames.length
            });
        }
        setTimeout(parseBlock, 0);
    };
    var cancelLoad = function (callback) { 
        if (gif.complete) {
            return false;
        }
        gif.cancelCallback = callback;
        gif.cancel         = true;
        return true;
    }
    var error = function (type) {
        if (typeof gif.onerror === "function") {
            (gif.onerror.bind(this))({
                type : type,
                path : [this]
            });
        }
        gif.onerror = undefined;
        gif.onload  = undefined;
        gif.loading = false;
    }
    var doOnloadEvent = function () { // fire onload event if set
        gif.currentFrame = 0;
        gif.lastFrameAt  = new Date().valueOf(); 
        gif.nextFrameAt  = new Date().valueOf(); 
        if (typeof gif.onload === "function") {
            (gif.onload.bind(gif))({
                type : 'load',
                path : [gif]
            });
        }
        gif.onload  = undefined;
        gif.onerror = undefined;
    }
    var dataLoaded = function (data) { 
        st = new Stream(data);
        parse();
    }
    var loadGif = function (filename) { // starts the load
        var ajax = new XMLHttpRequest();
        ajax.responseType = "arraybuffer";
        ajax.onload = function (e) {
            if (e.target.status === 400) {
                error("Bad Request response code");
            } else if (e.target.status === 404) {
                error("File not found");
            } else {
                dataLoaded(ajax.response);
            }
        };
        ajax.open('GET', filename, true);
        ajax.send();
        ajax.onerror = function (e) {
            error("File error");
        };
        this.src = filename;
        this.loading = true;
    }
    function play() { // starts play if paused
        if (!gif.playing) {
            gif.paused  = false;
            gif.playing = true;
            playing();
        }
    }
    function pause() { // stops play
        gif.paused  = true;
        gif.playing = false;
        clearTimeout(timerID);
    }
    function togglePlay(){
        if(gif.paused || !gif.playing){
            gif.play();
        }else{
            gif.pause();
        }
    }
    function seekFrame(frame) { // seeks to frame number.
        clearTimeout(timerID);
        frame = frame < 0 ? (frame % gif.frames.length) + gif.frames.length : frame;
        gif.currentFrame = frame % gif.frames.length;
        if (gif.playing) {
            playing();
        } else {
            gif.image = gif.frames[gif.currentFrame].image;
        }
    }
    function seek(time) { // time in Seconds 
        clearTimeout(timerID);
        if (time < 0) {
            time = 0;
        }
        time *= 1000; // in ms
        time %= gif.length;
        var frame = 0;
        while (time > gif.frames[frame].time + gif.frames[frame].delay && frame < gif.frames.length) {
            frame += 1;
        }
        gif.currentFrame = frame;
        if (gif.playing) {
            playing();
        } else {
            gif.image = gif.frames[gif.currentFrame].image;
        }
    }
    function playing() {
        var delay;
        var frame;
        if (gif.playSpeed === 0) {
            gif.pause();
            return;
        }

        if (gif.playSpeed < 0) {
            gif.currentFrame -= 1;
            if (gif.currentFrame < 0) {
                gif.currentFrame = gif.frames.length - 1;
            }
            frame = gif.currentFrame;
            frame -= 1;
            if (frame < 0) {
                frame = gif.frames.length - 1;
            }
            delay = -gif.frames[frame].delay * 1 / gif.playSpeed;
        } else {
            gif.currentFrame += 1;
            gif.currentFrame %= gif.frames.length;
            delay = gif.frames[gif.currentFrame].delay * 1 / gif.playSpeed;
        }
        gif.image = gif.frames[gif.currentFrame].image;
        timerID = setTimeout(playing, delay);
    }
    var gif = {                      // the gif image object
        onload         : null,       // fire on load. Use waitTillDone = true to have load fire at end or false to fire on first frame
        onerror        : null,       // fires on error
        onprogress     : null,       // fires a load progress event
        onloadall      : null,       // event fires when all frames have loaded and gif is ready
        paused         : false,      // true if paused
        playing        : false,      // true if playing
        waitTillDone   : true,       // If true onload will fire when all frames loaded, if false, onload will fire when first frame has loaded
        loading        : false,      // true if still loading
        firstFrameOnly : false,      // if true only load the first frame
        width          : null,       // width in pixels
        height         : null,       // height in pixels
        frames         : [],         // array of frames
        comment        : "",         // comments if found in file. Note I remember that some gifs have comments per frame if so this will be all comment concatenated
        length         : 0,          // gif length in ms (1/1000 second)
        currentFrame   : 0,          // current frame. 
        frameCount     : 0,          // number of frames
        playSpeed      : 1,          // play speed 1 normal, 2 twice 0.5 half, -1 reverse etc...
        lastFrame      : null,       // temp hold last frame loaded so you can display the gif as it loads
        image          : null,       // the current image at the currentFrame
        playOnLoad     : true,       // if true starts playback when loaded
        // functions
        load           : loadGif,    // call this to load a file
        cancel         : cancelLoad, // call to stop loading
        play           : play,       // call to start play
        pause          : pause,      // call to pause
        seek           : seek,       // call to seek to time
        seekFrame      : seekFrame,  // call to seek to frame
        togglePlay     : togglePlay, // call to toggle play and pause state
    };
    return gif;
}





/*=================================================================
  USEAGE Example below
  
  Image used from Wiki  see HTML for requiered image atribution
===================================================================*/  




    const ctx = canvas.getContext("2d");
    ctx.font = "16px arial";
    var changeFrame = false;
    var changeSpeed = false;

    frameNum.addEventListener("mousedown",()=>{changeFrame = true ; changeSpeed = false});
    speedInput.addEventListener("mousedown",()=>{changeSpeed = true; changeFrame = false});
    const gifSrc =  "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Odessa_TX_Oil_Well_with_Lufkin_320D_pumping_unit.gif/220px-Odessa_TX_Oil_Well_with_Lufkin_320D_pumping_unit.gif"
    var myGif = GIF();                  // Creates a new gif  
    myGif.load(gifSrc);            // set URL and load
    myGif.onload = function(event){     // fires when loading is complete
        frameNum.max = myGif.frameCount-1;
        animate();
    }
    myGif.onprogress = function(event){ // Note this function is not bound to myGif
        if(canvas.width !== myGif.width || canvas.height !== myGif.height){
            canvas.width = myGif.width;
            canvas.height = myGif.height;
            ctx.font = "16px arial";
        }
        if(myGif.lastFrame !== null){
            ctx.drawImage(myGif.lastFrame.image,0,0);
        }
        ctx.fillStyle = "black";
        ctx.fillText("Loaded frame "+event.frame,8,20);
        frameNum.max = event.frame-1;
        frameNum.value = event.frame;
        frameText.textContent = frameNum.value + "/" + (frameNum.max-1);
    }
    myGif.onerror = function(event){ 
        ctx.fillStyle = "black";
        ctx.fillText("Could not load the Gif ",8,20);
        ctx.fillText("Error : " + event.type,8,40);
        
    }




    function animate(){
        if(changeFrame){
            if(myGif.playing){
                myGif.pause();
            }
            myGif.seekFrame(Number(frameNum.value));
        
        }else if(changeSpeed){
            myGif.playSpeed = speedInput.value;
            if(myGif.paused){
                myGif.play();
            }
        }
        frameNum.value = myGif.currentFrame;
        frameText.textContent = frameNum.value + "/" + frameNum.max;
        if(myGif.paused){
            speedInput.value = 0;
        }else{
            speedInput.value = myGif.playSpeed;
        }
        speedText.textContent = speedInput.value;
    
        ctx.drawImage(myGif.image,0,0); 
        requestAnimationFrame(animate);
    }
canvas { border : 2px solid black; }
p { font-family : arial; font-size : 12px }
<canvas id="canvas"></canvas>
<div id="inputs">
<input id="frameNum" type = "range" min="0" max="1" step="1" value="0"></input>
Frame : <span id="frameText"></span><br>
<input id="speedInput" type = "range" min="-3" max="3" step="0.1" value="1"></input>
Speed : <span id="speedText"></span><br>
</div>
<p>Image source <a href="https://commons.wikimedia.org/wiki/File:Odessa_TX_Oil_Well_with_Lufkin_320D_pumping_unit.gif#/media/File:Odessa_TX_Oil_Well_with_Lufkin_320D_pumping_unit.gif"></a><br>By <a href="//commons.wikimedia.org/w/index.php?title=User:DASL51984&amp;action=edit&amp;redlink=1" class="new" title="User:DASL51984 (page does not exist)">DASL51984</a> - Original YouTube video by user "derekdz", looped by <a href="//commons.wikimedia.org/w/index.php?title=User:DASL51984&amp;action=edit&amp;redlink=1" class="new" title="User:DASL51984 (page does not exist)">DASL51984</a>, <a href="http://creativecommons.org/licenses/by-sa/4.0" title="Creative Commons Attribution-Share Alike 4.0">CC BY-SA 4.0</a>, <a href="https://commons.wikimedia.org/w/index.php?curid=48467951">Link</a></p>
Community
  • 1
  • 1
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Please Note that this only works for same domain or CORS enabled GIFs. Sorry I could not add more to answer but at the answer size limit. – Blindman67 Jul 16 '17 at 13:58