Highcharts結合PhantomJS在服務端生成高質量的圖表圖片


項目背景

最近忙着給部門開發一套交互式的報表系統,來替換原有的靜態報表系統。

老系統是基於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的源碼:點此下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM