commit 95078384eb36799ffc2e6ab1eaa303407f088c05 Author: Arturo Filastò hellais@gmail.com Date: Wed Mar 14 13:43:01 2012 -0700
Implement graph saving functionality --- js/libs/flot/canvas2image.js | 235 ++++++++++++++++++++++++++++++++++++++++++ js/main.js | 1 + js/views/details/main.js | 3 + templates/details/main.html | 6 + 4 files changed, 245 insertions(+), 0 deletions(-)
diff --git a/js/libs/flot/canvas2image.js b/js/libs/flot/canvas2image.js new file mode 100644 index 0000000..bb6aecd --- /dev/null +++ b/js/libs/flot/canvas2image.js @@ -0,0 +1,235 @@ +/* + * Canvas2Image v0.1 + * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk + * MIT License [http://www.opensource.org/licenses/mit-license.php] + */ + +var Canvas2Image = (function() { + + // check if we have canvas support + var bHasCanvas = false; + var oCanvas = document.createElement("canvas"); + if (oCanvas.getContext("2d")) { + bHasCanvas = true; + } + + // no canvas, bail out. + if (!bHasCanvas) { + return { + saveAsBMP : function(){}, + saveAsPNG : function(){}, + saveAsJPEG : function(){} + } + } + + var bHasImageData = !!(oCanvas.getContext("2d").getImageData); + var bHasDataURL = !!(oCanvas.toDataURL); + var bHasBase64 = !!(window.btoa); + + var strDownloadMime = "image/octet-stream"; + + // ok, we're good + var readCanvasData = function(oCanvas) { + var iWidth = parseInt(oCanvas.width); + var iHeight = parseInt(oCanvas.height); + return oCanvas.getContext("2d").getImageData(0,0,iWidth,iHeight); + } + + // base64 encodes either a string or an array of charcodes + var encodeData = function(data) { + var strData = ""; + if (typeof data == "string") { + strData = data; + } else { + var aData = data; + for (var i=0;i<aData.length;i++) { + strData += String.fromCharCode(aData[i]); + } + } + return btoa(strData); + } + + // creates a base64 encoded string containing BMP data + // takes an imagedata object as argument + var createBMP = function(oData) { + var aHeader = []; + + var iWidth = oData.width; + var iHeight = oData.height; + + aHeader.push(0x42); // magic 1 + aHeader.push(0x4D); + + var iFileSize = iWidth*iHeight*3 + 54; // total header size = 54 bytes + aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256); + aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256); + aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256); + aHeader.push(iFileSize % 256); + + aHeader.push(0); // reserved + aHeader.push(0); + aHeader.push(0); // reserved + aHeader.push(0); + + aHeader.push(54); // dataoffset + aHeader.push(0); + aHeader.push(0); + aHeader.push(0); + + var aInfoHeader = []; + aInfoHeader.push(40); // info header size + aInfoHeader.push(0); + aInfoHeader.push(0); + aInfoHeader.push(0); + + var iImageWidth = iWidth; + aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256); + aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256); + aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256); + aInfoHeader.push(iImageWidth % 256); + + var iImageHeight = iHeight; + aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256); + aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256); + aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256); + aInfoHeader.push(iImageHeight % 256); + + aInfoHeader.push(1); // num of planes + aInfoHeader.push(0); + + aInfoHeader.push(24); // num of bits per pixel + aInfoHeader.push(0); + + aInfoHeader.push(0); // compression = none + aInfoHeader.push(0); + aInfoHeader.push(0); + aInfoHeader.push(0); + + var iDataSize = iWidth*iHeight*3; + aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256); + aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256); + aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256); + aInfoHeader.push(iDataSize % 256); + + for (var i=0;i<16;i++) { + aInfoHeader.push(0); // these bytes not used + } + + var iPadding = (4 - ((iWidth * 3) % 4)) % 4; + + var aImgData = oData.data; + + var strPixelData = ""; + var y = iHeight; + do { + var iOffsetY = iWidth*(y-1)*4; + var strPixelRow = ""; + for (var x=0;x<iWidth;x++) { + var iOffsetX = 4*x; + + strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX+2]); + strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX+1]); + strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX]); + } + for (var c=0;c<iPadding;c++) { + strPixelRow += String.fromCharCode(0); + } + strPixelData += strPixelRow; + } while (--y); + + var strEncoded = encodeData(aHeader.concat(aInfoHeader)) + encodeData(strPixelData); + + return strEncoded; + } + + + // sends the generated file to the client + var saveFile = function(strData) { + document.location.href = strData; + } + + var makeDataURI = function(strData, strMime) { + return "data:" + strMime + ";base64," + strData; + } + + // generates a <img> object containing the imagedata + var makeImageObject = function(strSource) { + var oImgElement = document.createElement("img"); + oImgElement.src = strSource; + return oImgElement; + } + + var scaleCanvas = function(oCanvas, iWidth, iHeight) { + if (iWidth && iHeight) { + var oSaveCanvas = document.createElement("canvas"); + oSaveCanvas.width = iWidth; + oSaveCanvas.height = iHeight; + oSaveCanvas.style.width = iWidth+"px"; + oSaveCanvas.style.height = iHeight+"px"; + + var oSaveCtx = oSaveCanvas.getContext("2d"); + + oSaveCtx.drawImage(oCanvas, 0, 0, oCanvas.width, oCanvas.height, 0, 0, iWidth, iHeight); + return oSaveCanvas; + } + return oCanvas; + } + + return { + + saveAsPNG : function(oCanvas, bReturnImg, iWidth, iHeight) { + if (!bHasDataURL) { + return false; + } + var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight); + var strData = oScaledCanvas.toDataURL("image/png"); + if (bReturnImg) { + return makeImageObject(strData); + } else { + return strData; + } + return true; + }, + + saveAsJPEG : function(oCanvas, bReturnImg, iWidth, iHeight) { + if (!bHasDataURL) { + return false; + } + + var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight); + var strMime = "image/jpeg"; + var strData = oScaledCanvas.toDataURL(strMime); + + // check if browser actually supports jpeg by looking for the mime type in the data uri. + // if not, return false + if (strData.indexOf(strMime) != 5) { + return false; + } + + if (bReturnImg) { + return makeImageObject(strData); + } else { + saveFile(strData.replace(strMime, strDownloadMime)); + } + return true; + }, + + saveAsBMP : function(oCanvas, bReturnImg, iWidth, iHeight) { + if (!(bHasImageData && bHasBase64)) { + return false; + } + + var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight); + + var oData = readCanvasData(oScaledCanvas); + var strImgData = createBMP(oData); + if (bReturnImg) { + return makeImageObject(makeDataURI(strImgData, "image/bmp")); + } else { + saveFile(makeDataURI(strImgData, strDownloadMime)); + } + return true; + } + }; + +})(); diff --git a/js/main.js b/js/main.js index 2de75cb..0a37d0d 100644 --- a/js/main.js +++ b/js/main.js @@ -15,6 +15,7 @@ require.config({ popover: 'libs/bootstrap/bootstrap-popover', collapse: 'libs/bootstrap/bootstrap-collapse', flot: 'libs/flot/jquery.flot', + canvas2img: 'libs/flot/canvas2image', templates: '../templates' }
diff --git a/js/views/details/main.js b/js/views/details/main.js index 0fb6501..36b81d4 100644 --- a/js/views/details/main.js +++ b/js/views/details/main.js @@ -10,6 +10,7 @@ define([ 'tooltip', 'popover', 'flot', + 'canvas2img', 'collapse', 'helpers' ], function($, _, Backbone, relayModel, graphModel, mainDetailsTemplate){ @@ -57,6 +58,8 @@ define([ xaxis: {mode: 'time', tickLength: 5}, });
+ $("#save_"+g).attr('href', Canvas2Image.saveAsPNG($("#"+g+" > canvas.base")[0], false)); + var previousItem = null; $("#"+g).bind("plothover", function (event, pos, item){ if (item) { diff --git a/templates/details/main.html b/templates/details/main.html index ec8fbc3..52cd1cb 100644 --- a/templates/details/main.html +++ b/templates/details/main.html @@ -97,6 +97,7 @@ </div> <div class="caption"> <h5>3 Days graph</h5> + <a id="save_days" href="">Save Graph</a> </div> </div> </li> @@ -108,6 +109,7 @@ </div> <div class="caption"> <h5>1 Week graph</h5> + <a id="save_week" href="">Save Graph</a> </div> </div> </li> @@ -123,6 +125,7 @@ </div> <div class="caption"> <h5>1 Month graph</h5> + <a id="save_month" href="">Save Graph</a> </div> </div> </li> @@ -134,6 +137,7 @@ </div> <div class="caption"> <h5>3 Months graph</h5> + <a id="save_months" href="">Save Graph</a> </div> </div> </li> @@ -149,6 +153,7 @@ </div> <div class="caption"> <h5>1 Year graph</h5> + <a id="save_year" href="">Save Graph</a> </div> </div> </li> @@ -160,6 +165,7 @@
<div class="caption"> <h5>5 Years graph</h5> + <a id="save_years" href="">Save Graph</a> </div> </div> </li>
tor-commits@lists.torproject.org