从服务器端发送GIF动画的base64 / byte数组,并在客户端的画布上显示它

时间:2017-07-15 16:25:35

标签: javascript c# jquery ajax canvas

我构建了一个在asp.net中调用web API的接口(我使用c#和javascript / ajax来实现它)。

客户端调用控制器,控制器需要创建动画gif并通过一串base64或字节数组将其发送回客户端,当客户端获取base64时,他应该将其显示在画布中。

现在的问题是画布只显示动画gif的第一帧,就像静态图像一样。

我已经在互联网上阅读了很多内容并找到了: How Do I Convert A GIF Animation To Base64 String And Back To A GIF Animation?

但它对我没有帮助,因为我不想将图像保存在光盘上只是为了在客户端显示它。

*请注意,当我将图像从服务器端保存到光盘上时,将其保存为gif并将所有帧显示在一起,就像我希望的那样,当我将它传输到客户端时出错了。

*我使用ImageMagick创建动画gif。

这是我的客户端代码:

  <!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>

这是服务器代码:

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;             
          }
     }
}

有什么想法吗? 请帮忙。

编辑:几天后我发现了问题,我使用magicimage(magic.net)来创建动画的gif,base64还可以,但问题出在canvas元素中,画布没有显示动画像我想要所以我将元素画布更改为常规图像元素()并更改了图像动态的src。

此致 Jr.Rafa

1 个答案:

答案 0 :(得分:0)

在画布上加载播放gif的示例。

很抱歉,但不到30K的答案限制,代码和评论非常适合。如果需要,在评论中提问。有关基本用法,请参阅代码段底部。

&#13;
&#13;
/*

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);
    }
&#13;
canvas { border : 2px solid black; }
p { font-family : arial; font-size : 12px }
&#13;
<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>
&#13;
&#13;
&#13;