iT邦幫忙

0

[教學] ASP NET Core將HighChart圖片插入到Word中並提供下載

大家好,我是一名菜鳥工程師,這篇文章用來記錄我工作遇到的需求及解決方式,如果有更好的解決方式,也歡迎大家提出,話不多說,趕快開始今天的教學吧。

需求

這次客戶那邊的需求是希望把某個用於呈現統計圖表的頁面加上一個匯出Word檔的功能
,內容包含目前頁面所呈現的表格及圖片

使用的後端框架及套件

這次雖然用的是 ASP .NET Core 2.2 如果是用別的版本的應該沒差,原理上差不多,稍微修改一下應該能用
圖表部分當然就是HighChart.js
產生Word檔這次用的是Novacode Docx裝在ASP端,用NuGet裝搜尋DocXCore
Imgur

解決心路歷程

首先,這個頁面呈現圖表的地方已經是做好的功能,簡單描述一下作法就是:先透過linq group by資料包成好一個Model回傳Json,然後透過Ajax的方式把Model的內容塞到HighChart提供的圖的js中,有興趣知道怎麼做的在下方留言,我在做一篇專門來教學,這裡就不多加贅述了。

這次的需求看似簡單,但實際上有一段難度(對我來說 哈哈),主要的難度在於HighChart是透過js的方式Render出圖片,因此在Html Code的裡面不是一個img標籤,如果直接把這段Script丟到ASP中不可能產生出圖片的。
因此我一開始的思路是這樣的,既然資料Select是在ASP端,那就試試看能不能夠在ASP端就產生圖片,或是用NovaCode這個套件直接產生圖片Insert到檔案中。

但事與願違,原因如下
1.HighChart的圖太美了太強大了XD,客戶那邊無法接受畫面跟產生出來的圖片看起來不一樣
2.Novacode Docx這個套件只提供一些簡單的圓餅圖、長條圖、折線圖,但沒辦法像HighChart一樣有複合圖形(EX:同時包含折線圖,長條圖)
3.我不太知道怎麼用C#產生圖片,也沒辦法產生像HighChart一樣精美的圖

OK既然這條路不通,那就是走下一條,那就是怎麼真的把畫面上的圖片內容傳到ASP端後能夠產生一樣的圖片。
有用過HighChart套件的應該都知道HighChart有提供可以直接下載圖片的功能在右上角的地方,如下圖

Imgur

或是在js中HighChart有提供export可以匯出圖片,這邊我就不介紹了,google大神有超多範例

大多數的範例都是放一個button然後在click事件的調用highchart.export()這個方法,然後實現圖片下載的功能

不過這次是希望能夠在C#端產生,所以我稍微得查了一下,發現highchart.export()主要透過https://export.highcharts.com/ 去產生圖片

所以要在C#端產生圖片,作法就是把highchart的script透過json的方式然後透過C#的WebRequest去進行Request跟Response然後存成圖片或是Image 物件

這邊我先去highchart下載一個範例,然後HTML只放這個圖就好
HTML 部分

<div>
    <div id="chartTest"></div>
</div>
<!--------------------------引入套件--------------------------->
<script src="https://code.jquery.com/jquery-1.12.4.js"
           integrity="sha256-Qw82+bXyGq6MydymqBxNPYTaUXXq7c8v3CwiYwLLNXU="
           crossorigin="anonymous"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>

<!---------------------------主要程式--------------------------->
<script src="~/js/chartTest.js"></script>

由於Highchart有用到jQuery所以必須引入,highchart部分我是直接用cdn的方式,主要js,我放在chartTest.js中
chartTest.js內容

(function ($) {

    $(document).ready(function () {
    //highchart官方給的範例
        var option = {
            title: {
                text: 'Solar Employment Growth by Sector, 2010-2016'
            },

            subtitle: {
                text: 'Source: thesolarfoundation.com'
            },

            yAxis: {
                title: {
                    text: 'Number of Employees'
                }
            },

            xAxis: {
                accessibility: {
                    rangeDescription: 'Range: 2010 to 2017'
                }
            },

            legend: {
                layout: 'vertical',
                align: 'right',
                verticalAlign: 'middle'
            },

            plotOptions: {
                series: {
                    label: {
                        connectorAllowed: false
                    },
                    pointStart: 2010
                }
            },

            series: [{
                name: 'Installation',
                data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175]
            }, {
                name: 'Manufacturing',
                data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434]
            }, {
                name: 'Sales & Distribution',
                data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387]
            }, {
                name: 'Project Development',
                data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227]
            }, {
                name: 'Other',
                data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111]
            }],

            responsive: {
                rules: [{
                    condition: {
                        maxWidth: 500
                    },
                    chartOptions: {
                        legend: {
                            layout: 'horizontal',
                            align: 'center',
                            verticalAlign: 'bottom'
                        }
                    }
                }]
            }

        };
        
        //將option給chartTest產生圖片
        Highcharts.chart('chartTest', option);

    });

}(jQuery));

結果
Imgur

OK 接下來就是重點了,要怎麼產生圖片呢? 答案是透過HttpWebRequest跟HttpWebResponse
原理就是透過HttpWebRequest把chartTest中的option用json丟到https://export.highcharts.com/
然後再把Response回來的東西轉換成Image,這樣就能得到圖片,再來就是把圖片插入到Word中就完成了
完整程式碼如下

javascript

(function ($) {

    $(document).ready(function () {
        var option = {

            title: {
                text: 'Solar Employment Growth by Sector, 2010-2016'
            },

            subtitle: {
                text: 'Source: thesolarfoundation.com'
            },

            yAxis: {
                title: {
                    text: 'Number of Employees'
                }
            },

            xAxis: {
                accessibility: {
                    rangeDescription: 'Range: 2010 to 2017'
                }
            },

            legend: {
                layout: 'vertical',
                align: 'right',
                verticalAlign: 'middle'
            },

            plotOptions: {
                series: {
                    label: {
                        connectorAllowed: false
                    },
                    pointStart: 2010
                }
            },

            series: [{
                name: 'Installation',
                data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175]
            }, {
                name: 'Manufacturing',
                data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434]
            }, {
                name: 'Sales & Distribution',
                data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387]
            }, {
                name: 'Project Development',
                data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227]
            }, {
                name: 'Other',
                data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111]
            }],

            responsive: {
                rules: [{
                    condition: {
                        maxWidth: 500
                    },
                    chartOptions: {
                        legend: {
                            layout: 'horizontal',
                            align: 'center',
                            verticalAlign: 'bottom'
                        }
                    }
                }]
            }

        };
        
        Highcharts.chart('chartTest', option);
        
        
        var imageData = {
            data: option,
            width: false,
            scale: false,
            constr: "Test",
            type: "image/png",
            async: true
        };
        var imageDataJson = JSON.stringify(imageData) + "";


        $('#Test').on('click', function () {
            var imageData = [];
            imageData.push(imageDataJson);
            $.ajax({
                type: 'POST',
                //timeout: 120000,
                beforeSend: function () { $.blockUI({ message: '<h1>檔案產生中...</h1>' }) },
                data: { imgDatas: imageData},
                url: '/Home/ExportDocx/',
                success: function (data) {

                },
                complete: function () { }
            }).done(function (data) {
                $.unblockUI();
                
                //下載的url
                location.href = '/Home/HighChartDocxDownload?fileName=' + data.fileName;
            });

        });
       

    });

}(jQuery));

這邊的重點
1.不能把option直接丟給https://export.highcharts.com/ 而是要用imageData這個物件才行
2.頁面部分我就新增一個button做為觸發輸出文件用,在ajax的部分我分成兩部分第一次會透過/Home/ExportDocx/ 產生檔案並暫存在資料夾內,再透過/Home/HighChartDocxDownload 把檔案載下來
會這麼做的主要原因是因為Asp端如果在Action 最後 return File () 在ajax會失效
3.Post Data部分 我是用把imageDataJson丟到一個陣列(imgDatas)中,這麼做主要是想提供大家需要插入多張圖片時的作法

ASP端

  [HttpPost]
  public IActionResult ExportDocx(List<string> imgDatas)
  {
   try
     {
        //產生doc相關內容
        var result = ExportDocxFile(imgDatas);
        
        //產生一個"測試.docx"在wwwrooot資料夾中
        var _fullPath = Path.Combine($"{_hostingEnvironment.WebRootPath}", "測試.docx");
        FileStream file = new FileStream(_fullPath, FileMode.Create, FileAccess.Write);
        result.Position = 0;
        result.WriteTo(file);//儲存檔案
               
        file.Close();

         var resultObj = new
          {
           FileName = $@"測試",
           Successful = true
          };
           return Json(resultObj);//回傳檔名
               
       }
       catch (Exception ex)
       {
           return Content(ex.Message);
       }

           // return View();
        }

稍微解釋一下
這邊的主要是藉由ExportDocxFile這個function產生相關docx內容,然後先暫存一份到wwwroot中,回傳檔名,讓等一下下載用

public MemoryStream ExportDocxFile(List<string> imgDatas)
        {          
          using (var document = DocX.Load($@"{_hostingEnvironment.WebRootPath}/ChartTest.docx"))
            {
                var imageTableIdx = 1;//定義第一張圖的表格位置               
                var failImage = new List<int>();
                using (var ms = new MemoryStream())
                {
                    var mmsg = "";
                    var imageTable = document.Tables[0];//
                    foreach (var imgData in imgDatas)
                    {
                        using (var Img = GenerateHighChartImage(imgData, ref mmsg))
                        {
                            
                            if (Img != null)
                            {
                               
                           Img.Save(ms, ImageFormat.Png);//Save your picture in a memory stream.
                                ms.Seek(0, SeekOrigin.Begin);
                                Novacode.Image img = document.AddImage(ms);

                                //Paragraph p = document.InsertParagraph("Hello", false);

                                Picture pic1 = img.CreatePicture();     
                                var row = imageTable.Rows[0];
                                row.MergeCells(0, 1);
                                var MaxWidth = row.Cells[0].Width;
                                var ratio = MaxWidth / Img.Width;
                                var width = Math.Round((double)Img.Width * 0.35);
                                var height = Math.Round((double)Img.Height * 0.35);
                                row.Cells[0].Paragraphs[0].Alignment = Alignment.center;
                                row.Cells[0].Paragraphs[0].InsertPicture(pic1, 0);

                                row.Cells[0].VerticalAlignment = VerticalAlignment.Center;
                                // imageTable.Alignment = Alignment.center;
                                //imageTableIdx += 2;
                            }

                        }
                    }
                    
                }
                var memory = new MemoryStream();

                document.SaveAs(memory);

                return memory;
                //return FileTool.ConvertDocToMemoryStream(document);
            }
        }

這邊就是產生docx內容的程式碼了,我這邊先讀取ChartTest這個docx,透過GenerateHighChartImage這個Function產生圖片,那在這個docx套件塞圖片的方式不是簡單的用 "img = document.AddImage(ms);"就好了
必須用CreatePicture的方式才能把圖片塞進去。
至於圖片放的位置我這邊用的方式是抓table,因為在Novacode這個套件可以抓到文件中有幾個table
所以可以用table來指定圖片要放的位置,例如我今天想要把圖片放在某段文字之後,這個時候可以在文字後面放一個空的table,放圖片的時候就可以抓這個table把圖放進去,下面是我的ChartTest內容
Imgur

這邊可以注意到table部分我不是用單行單列的table,原因主要是如果使用單行單列的table好像會出錯,所以我才這麼做

那些下來是最後了,產生圖片的code

 public System.Drawing.Image GenerateHighChartImage(string option, ref string mmsg)
        {
            option = option.Replace("\r\n", "");
            option = option.Replace(" ", "");
            var request = (HttpWebRequest)WebRequest.Create("https://export.highcharts.com/");
            System.Drawing.Image ResultImg;
            var bytes = Encoding.UTF8.GetBytes(option);
            //因應關閉TLS1.0與TLS1.2
            ServicePointManager.SecurityProtocol = (SecurityProtocolType)192 |   
                        (SecurityProtocolType)768 | (SecurityProtocolType)3072;

            request.Method = "POST";
            request.ContentType = "application/json;charset=utf-8";
            request.ContentLength = bytes.Length;

            //var mmsg = "";
            var ErrorMessage = "";
            try
            {
                request.GetRequestStream().Write(bytes, 0, bytes.Length);
                //var ResultImg = new System.Drawing.Image();
                using (var response = (HttpWebResponse)request.GetResponse())
                {
              mmsg = new StreamReader(response.GetResponseStream(), Encoding.UTF8).ReadToEnd();
              if (!string.IsNullOrEmpty(mmsg))
             {                        
              var requestPic = WebRequest.Create("https://export.highcharts.com/" + mmsg);
              using (WebResponse responsePic = requestPic.GetResponse())
              {
                 using (var webImage = System
                                .Drawing
                                .Image
                                .FromStream(responsePic.GetResponseStream()))
                            {
                                ResultImg = (System.Drawing.Image)webImage.Clone();
                                return ResultImg;
                            }
                            {
                                ResultImg = (System.Drawing.Image)webImage.Clone();
                                return ResultImg;
                            }
                        }
                    }
                }

            }
            catch (Exception exception1)
            {
                //ProjectData.SetProjectError(exception1);
                //Exception exception = exception1;
                ErrorMessage = exception1.Message;
                // ProjectData.ClearProjectError();
            }

            ResultImg = null;
            mmsg = ErrorMessage;
            return ResultImg;
        }

這邊的code原理很簡單就是透過HttpWebRequest跟WebResponse拿到圖片,主要都是丟到https://export.highcharts.com 這裡去進行轉換的動作

那最後就是下載的部分

public IActionResult HighChartDocxDownload(string fileName)
{
   using (var document = DocX.Load($@"{_hostingEnvironment.WebRootPath}/{fileName}.docx"))
    {
     var memory = new MemoryStream();

     document.SaveAs(memory);
     memory.Position = 0;
     return File(memory.ToArray(),
     "application/vnd.openxmlformats-officedocument.wordprocessingml.document", $"測試2.docx");
    }
     
}

產生的結果
Imgur

心得

這次的需求花了我一段時間才完成,由於我剛進入這個行業不久,很多東西都在摸索,如果有什麼地方需要改進,也請大家指導,這部分的程式碼我會放在我的github上,如果有興趣的人可以下載來試看看,謝謝大家
github網址: https://github.com/paul09253336/GenerateHighChartImageAndInertToDocx_project


尚未有邦友留言

立即登入留言