項目背景
最近忙着給部門開發一套交互式的報表系統,來替換原有的靜態報表系統。
老系統是基於dotnetCHARTING開發的,dotnetCHARTING的優勢是圖表類型豐富,接口調用簡單,使用時只需綁定數據源即可(指定連接字符和sql語句,簡單的配置一下就能出圖),支持生成靜態圖表圖片;缺點就是生成好的圖是圖片,傳到了前台就失去了交互性(當然它還提供了一個jsCharting,不過感覺交互性做的還是不夠好),再有就是這東東是收費的呀,用的話需要折騰破解版本。
我最終選擇了Highcharts(Interactive JavaScript charts for your webpage)來展現前台圖表,通過Highcharts良好的交互性實現與服務端的數據交互,將數據可視化。
dotnetCHARTING在數據加載的設計上做的還是很不錯的,我在開發過程中借鑒了其處理思想,自己實現了一套數據加載方案,能夠很方便的把數據傳給Highcharts。這套數據加載方案,簡單的說就是指定好數據庫連接信息和sql查詢信息,服務端采用ADO.NET執行查詢生成DataSet,然后分析DataSet將數據轉換為Highcharts能夠直接使用的json格式。
報表的處理細節還是蠻多的,這里就不在一一討論了,如題,接下來重點跟大家分享一下,服務端生成圖表圖片那部分的處理細節。
Highcharts服務端生成圖表圖片流程簡介
生成圖片的數據流向倒是比較簡單,如下圖所示:
ASP.NET在服務端生成圖表圖片的方式
根據上述生成圖片的步驟,核心其實就是對第二步的處理,也就是如何將SVG數據在服務端做處理,生成圖表圖片。
這樣的話,我們的處理思路就很清晰了,直接在服務端把SVG處理為圖片不就可以了,這么想,也就這么做,剛好網上也有人這么弄過,於是也就直接借鑒了其代碼,代碼不上了,介紹下用到的dll:
在nuget中搜索svg,可以找到一個SVG Rendering Library的包,可以用這個包將SVG格式的數據保存為圖片,用法也比較簡單,大家可以到其官網查閱使用方法。
這個大家不必自己去實現,因為highcharts官網已經給出了第三方的ASP.NET導出圖表的模塊(他就是基於這個SVG Rendering Library實現的):
https://github.com/imclem/Highcharts-export-module-asp.net
SVG Rendering Library的問題
在使用SVG Rendering Library服務端生成圖表圖片的過程中,發現一些問題:
- 生成的圖片中文字體模糊發虛,整體圖片質量差,跟實際在網頁中顯示的效果差別還真不小
- 圖表上數據點的dataLabel無法顯示(一開始以為是highcharts配置的問題,后來鑒定是SVG Rendering Library的問題,這個必須修改svg.dll才能解決)
先看一下圖片質量的問題,首先是Chrome中實際展現的圖表的截圖:
在來一張使用svg.dll在后台生成的圖:
對比着兩張圖,可以和明顯的看出生成的圖片中漢字發虛(尤其是下面的月份)。正是這個原因,促使我去尋找一個更好的方案來替代SVG Rendering Library,以確保服務端生成圖表圖片的質量。
心想highcharts在瀏覽器中的顯示效果已經不錯了,要不做截圖,但是截圖的話跟服務端也沒關系了呀,突然想到了在服務端渲染截圖這么個思路。但是具體怎么做呢?先找找資料吧。
神器PhantomJS華麗登場
第一次接觸Phantomjs是半年前左右,當時正在開發web漏洞檢測工具,需要執行頁面上的js,進行分析,沒有經驗的我,各處找資料,看到PhantomJS后,心想,這貨不是已經有人做過了么,干嘛還重復造車輪子,后來隨着業務變更,也沒有深入研究它。
這次搜索“服務端,截圖”這個關鍵字的時候,再次看到了PhantomJS,對它的印象不深了,先去官網看看介紹吧,PhantomJS: Headless WebKit with JavaScript API,哦,原來是個可以執行js並集成了webkit的動動,只是沒有可視化的部分而已。
PhantomJS能干啥呢?
- HEADLESS WEBSITE TESTING(非可視化的Web測試)
- SCREEN CAPTURE,Programmatically capture web contents, including SVG and Canvas.(截屏啊,支持SVG啊,吼吼,這不正是我想要的么)
- PAGE AUTOMATION(頁面自動化,可以使用jQuery操作DOM)
- NETWORK MONITORING(監視頁面加載,還可以結合Jenkins做自動化分析,流弊啊!)
對Phantomjs做過一番了解后,就確定用它來處理服務端生成圖表圖片的問題了。我設計的處理流程如下:
畫的很挫,能看明白處理過程就好,接下來分享一下具體處理過程中需要解決的問題。
新方案的處理細節
Highcharts中導出圖表的配置
圖表的其他配置不需要修改,只需修改導出圖片的配置即可,導出的配置如下:
var chart = new Highcharts.Chart({ //... exporting: { url: '/Chart/Export', // 導出圖表的服務端處理地址 filename: 'chart_from_phantomjs' // 返回下載的文件名 }, //... });
我們使用Chrome調試一下,看看下載圖片的時候,Highcharts都向服務端提交了哪些信息,截圖如下:
Highcharts向/Chart/Export發送了一個Post請求,提交的信息如上圖所示,在服務端,我們需要根據type來生成不同的圖片格式,可以通過svg獲取Highcharts提交的圖表數據。
ASP.NET中SVG的處理
首先直接將Highcharts傳遞的SVG數據保存為本地文件,PhantomJS需要通過http://xxx/xxx.svg的形式請求SVG圖像,直接請求ASP.NET會以將svg數據以文件的形式返回,因此需要對svg的請求做單獨處理。代碼如下:
/// <summary> /// 處理Svg文件請求,避免直接返回文件 /// </summary> public class SvgHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { var file = context.Server.MapPath(context.Request.Url.AbsolutePath); if (File.Exists(file)) { context.Response.ContentType = "image/svg+xml"; context.Response.WriteFile(file); } else { context.Response.Write("請求的文件不存在"); } } public bool IsReusable { get { return true; } } }
最后在Web.config中配置一下:
<httpHandlers> <add verb="*" path="*.svg" type="Highcharts.Exporting.Helper.SvgHandler, Highcharts.Exporting, Version=1.0.0.0, Culture=neutral"/> </httpHandlers>
ASP.NET與PhantomJS的交互處理
由於PhantomJS是個獨立的進程,這樣ASP.NET在與之交互的時候需要讓PhantomJS一直運行,不然每次啟動一個新的進程開銷也比較大。
PhantomJS支持js腳本調用,我們可以通過編寫腳本實現PhantomJS以服務的方式長期運行,代碼篇幅較長,下面會給出源碼。
PhantomJS中通過接收post請求,從請求信息中獲取url信息,url就是要渲染的SVG地址,將對應SVG渲染截圖,並返回BASE64編碼的數據處理,代碼如下:
page.open(req.post.url,function(status){ if(status !== "success"){ res.send(status); } else { setTimeout(function() { // 發送渲染后的圖片 var pic = page.renderBase64('png'); res.send(pic); }, req.post.timeout || 1000); } });
PhantomJS截圖服務腳本:點此下載。啟動方法:PhantomJS server.js [port]如不指定端口號,則默認使用8000端口:
ASP.NET對PhantomJS返回的圖像數據做處理
ASP.NET需要將PhantomJS返回的BASE64數據反編碼,得到PNG圖像數據,然后結合需要返回的圖片類型做格式轉換,並以文件的形式返回給客戶端瀏覽器,核心代碼如下:
// 提交SvgUrl到PhantomJS,讓其生成圖片 WebClient webClient = new WebClient(); NameValueCollection postValues = new NameValueCollection(); postValues.Add("url", siteUrl + svgFile); byte[] data = webClient.UploadValues(phantomJSUrl, postValues); // 從返回的Base64編碼中獲取圖片數據 string imageInfo = Encoding.UTF8.GetString(data); if (!String.IsNullOrEmpty(imageInfo)) { data = Convert.FromBase64String(imageInfo); MemoryStream ms = new MemoryStream(); ms.Write(data, 0, data.Length); image = Image.FromStream(ms); ms.Close(); }
返回Highcharts請求的圖片信息:
MemoryStream tStream = new MemoryStream(); var image = ImageHelper.SvgImageFromPhantomJs(tSvg); string tExt = "png"; string tTypeString = "-m image/png"; switch (tType) { case "image/png": tTypeString = "-m image/png"; tExt = "png"; break; case "image/jpeg": tTypeString = "-m image/jpeg"; tExt = "jpg"; break; case "application/pdf": tTypeString = "-m application/pdf"; tExt = "pdf"; break; case "image/svg+xml": tTypeString = "-m image/svg+xml"; tExt = "svg"; break; } if (tTypeString != "") {switch (tExt) { case "jpg": image.Save(tStream, ImageFormat.Jpeg); break; case "png": image.Save(tStream, ImageFormat.Png); break; case "pdf": PdfWriter tWriter = null; Document tDocumentPdf = null; try { image.Save(tStream, ImageFormat.Png); tDocumentPdf = new Document(new Rectangle(image.Width, image.Height)); tDocumentPdf.SetMargins(0.0f, 0.0f, 0.0f, 0.0f); iTextSharp.text.Image tGraph = iTextSharp.text.Image.GetInstance(tStream.ToArray()); tGraph.ScaleToFit(image.Width, image.Height); tStream = new MemoryStream(); tWriter = PdfWriter.GetInstance(tDocumentPdf, tStream); tDocumentPdf.Open(); tDocumentPdf.NewPage(); tDocumentPdf.Add(tGraph); tDocumentPdf.CloseDocument(); } catch (Exception ex) { throw ex; } finally { tDocumentPdf.Close(); tDocumentPdf.Dispose(); tWriter.Close(); tWriter.Dispose(); } break; case "svg": MemoryStream tData = new MemoryStream(Encoding.UTF8.GetBytes(tSvg)); tStream = tData; break; } } return tStream;
最后將tStream的圖像數據以文件的形式返回給前台:
[HttpPost] [ValidateInput(false)] public ActionResult Export() { string siteUrl = String.Format("{0}://{1}:{2}/", Request.Url.Scheme, Request.Url.Host, Request.Url.Port); MemoryStream tStream = new MemoryStream(); string tType = Request.Form["type"]; string tSvg = Request.Form["svg"]; string tFileName = Request.Form["filename"]; if (String.IsNullOrEmpty(tFileName)) { tFileName = "chart"; } ChartHelper chartHelper = new ChartHelper(); tStream = chartHelper.GetSvgImageFromPhantomJs(siteUrl, tType, tSvg); return File(tStream.ToArray(), tType, tFileName); }
借助PhantomJS生成的圖表圖片
來一張效果圖,跟原來的對比一下:
可見漢字部分清晰了不少吧。
總結
在服務端使用PhantomJS生成圖表圖片好處就是能將圖像渲染到最佳效果(直接使用WebKit內核渲染),缺點就是速度慢了些。
服務端生成Pdf圖表可以使用iTextSharp生成。
附ASP.NET導出Highcharts的源碼:點此下載