我构建了一个在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
答案 0 :(得分:0)
很抱歉,但不到30K的答案限制,代码和评论非常适合。如果需要,在评论中提问。有关基本用法,请参阅代码段底部。
/*
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&action=edit&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&action=edit&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;