[教学] 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


<<:  [Day 29] 毁灭人类的人工智慧

>>:  [Day29] 建立购物车系统 - 12

Angular Stock登入(三)(Day23)

今天我们要来串接我们之前写好的 使用者登入 的API连结。 昨天我们已经可以在按钮绑定的doLogi...

Day12 - 正则表示式

在昨天我们建立了模型,并可使用管理网站手动增加书籍与作者的资料。 但回顾 Day10所列的需求,实际...

【左京淳的JAVA学习笔记】第五章 class定义与物件生成

如果把程序当成是魔法,前面几章都是基本的咒文。 到这章开始需要用到想像力了。 class(类) cl...

关於交换器SFP用途

请问各位先进,最近公司刚买了台交换器,一直不懂上面的sfp垖的主要用途及好处在那?是要用在特别的环境...

Day20 - 使用Django进行自动化测试 (2)

今天的实作内容主要根据教学网站进行。 接续昨天的内容,今天将实作model和form的测试程序。 内...