Highcharts图表导出为pdf的JavaWeb实践
写给读者的话^_^:
众所周知,基于Highcharts插件生成的svg图片组(注意这里鄙人指的组是若干图有序组合,并非一张图片,具有业务意义)导出为PDF文档是有难度滴。鄙人也曾“异想天开”用前端技术拍个快照然后转换为pdf文件导出,后来因为能力有限未能完美实现。因此,参照互联网已有的经验和做法,创造出一套较为有操作性的方案,详情见下文。
---------------------------------------------------说正事儿分割线----------------------------------------------------
假设需求如下:
- 如图所示的复杂图表报告
- 对其进行PDF导出(demo中所有数据为伪造,并无任何价值)
-
此图仅作为demo展示,不涉及商业数据,所有数据均为构造假数据
那么问题来了,肿么导出哩,先看下导出后的效果,卖个关子,如下图:
-
当然,不可否认的是图像质量会打折。但是效果终究实现了。接下来我们去看看前端怎么写,然后提交到后台又如何处理返回一个文档输出流。
- 前端html自定义元素属性,如下:
<div class="timeFenBuPic" id="timeFenBuPic"> <div class="timeFenBuOne" id="timeFenBuOne" softOrHard="hard" position="center" getSvg="true" h4="VR眼镜使用饱和度"> </div> </div>
例如:其中position咱们可以定义给它三个值属性:left,center,right代表了在文档中,每一组svg图的相对位置,其余几个属性自己结合后台程序使用即可。
- 前端html自定义元素属性,如下:
- 前端js脚本获取并且组织svg图像元素并提交给服务端(这里我们用的服务端时Java写的struts2作为控制器层的服务端接口),js写法如下:
function PDFExecute(){ //循环拿到各个绘图区域id $("#svgPDF").empty(); $.each($("[getSvg='true']"),function(index,ele){ //根据每个绘图区域的id获取svg,position,softOrHard等属性 var svg = $(this).highcharts(); if(typeof(svg)=='undefined'||svg==null){ svg = 'noData'; }else{ svg = svg.getSVG(); } $("#svgPDF").append("<input id='SVG"+$(this).attr("id")+"' name='svg' type='hidden' value='' />"); $("#SVG"+$(this).attr("id")).val( $(this).attr("id")+ "___"+$(this).attr("position")+ "___"+encodeURI($(this).attr("h4")+getSvgUnit($(this).parents('li').children('ul').children('li .curr').text()))+ "___"+$(this).attr("softOrHard")+ "___"+svg); }); $("#svgPDF").append("<input name='logoT' type='hidden' value='"+encodeURI($(".logoT").text())+"' />"); //处理文本锚点异常错误 // $('[text-anchor="undefined"]').attr('text-anchor',''); $("#svgPDF").submit(); }
- 服务端处理 服务端处理采用itext作为pdf生成第三方工具包,然后返回一个输出流到前端
-
pdf导出接口
/** * PDF导出入口方法 * 参数要求: * 1.一个页面的title(encode后的) * 2.所有highcharts的svg * 3.页面所有查询参数(用于表格类型的数据查询,因为表格类型前端无法传给后台) * 4.svg详述: * svg为一个数组 * svg的每个数组元素为字符串,且包含多个信息,以三个连续英文半角的下划线___做split操作,得到数组,具体内容如下: * 页面每个hicharts图的绘制id___此图在水平方向的相对位置(left还是right)___encode后的每两个图组成的title标题 * (例如xx投放趋势)___此图为软广还是硬广(soft还是hard)___svg字符串用来转换图片输出流 * 因此 svg.split("___")结果为: * ["charts图id","left/right","xx趋势图","soft/hard","<svg.../>"] * 5.使用时修改ByteArrayOutputStream方法下参数及布局规则 */ public String svgPDF(){ try { request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); Map<String,Object> map = new HashMap<String,Object>(); String logoT = request.getParameter("logoT"); if(StringUtils.isNotEmpty(logoT)){ logoT = URLDecoder.decode(logoT,"utf-8"); } downloadFileName= URLEncoder.encode(logoT,"utf-8")+".pdf"; String[] svg = request.getParameterValues("svg"); map.put("svg", svg); map.put("logoT", logoT); //实例化文档绘制工具类 ComprehensivePdfUtil cpu = new ComprehensivePdfUtil(); ByteArrayOutputStream buff = cpu.getPDFStream(request,response,map); inputStream = new ByteArrayInputStream(buff.toByteArray()); buff.close(); return "success"; } catch (IOException e) { e.printStackTrace(); return null; } }
此接口响应来自客户端的http请求并返回输出流
- PDF文档绘制工具类
package com.demo.utils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.image.PNGTranscoder; import com.itextpdf.text.BadElementException; import com.itextpdf.text.BaseColor; import com.itextpdf.text.Document; import com.itextpdf.text.DocumentException; import com.itextpdf.text.Element; import com.itextpdf.text.Font; import com.itextpdf.text.Image; import com.itextpdf.text.Paragraph; import com.itextpdf.text.Phrase; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.BaseFont; import com.itextpdf.text.pdf.PdfPCell; import com.itextpdf.text.pdf.PdfPRow; import com.itextpdf.text.pdf.PdfPTable; import com.itextpdf.text.pdf.PdfWriter; /** * @Description XXX分析页面PDF导出工具方法 */ public class ComprehensivePdfUtil { /** * 获得PDF字节输出流及pdf布局业务逻辑 * @param request * @param response * @param resultMap 包含参数:svg(绘图svg参数及hicharts图布局参数) logoT(页面总标题) * @param list 页面包含植入栏目排行表格图,该list存储绘制表格所用的数据 * @param tableTh 页面包含植入栏目排行表格图,该字符串作为表格表头 * @param tableTd 页面包含植入栏目排行表格图,该字符串作为表格内容填充时,实体类反射值所用的方法名(必须与实体方法严格一致) * @return */ public ByteArrayOutputStream getPDFStream(HttpServletRequest request, HttpServletResponse response, Map<String,Object> resultMap){ try { //图片变量定义 String noData = "/style/images/noData.png";//无数据左右图 String noDataCenter = "/style/images/noDataCenter.png";//无数据中间图 String waterMark = "/style/images/PDFSHUIYIN.png";//PDF导出文件水印图片 String [] svgName = (String[]) resultMap.get("svg");//导出PDF页面所有svg图像 Document document = new Document(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); PdfWriter pdfWriter = PdfWriter.getInstance(document, buffer); //设置页面大小 int pageHeight = 2000; Rectangle rect = new Rectangle(0,0,1200,pageHeight); rect.setBackgroundColor(new BaseColor(248,248,248));//页面背景色 document.setPageSize(rect);//页面参数 //页边空白 document.setMargins(20, 20, 30, 20); document.open(); //设置页头信息 if(null!=resultMap.get("logoT")){ BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); Font FontChinese = new Font(bfChinese,20, Font.BOLD); Paragraph paragraph = new Paragraph((String)resultMap.get("logoT"),FontChinese); paragraph.setAlignment(Element.ALIGN_CENTER); document.add(paragraph); } PdfPTable table = null; String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; //开始循环写入svg图像到pdf文档对象 for(String str:svgName){ ////////////////////////////////////////////////////////////////////////////////////////////////////////// //positionAndSvg数组中元素说明: //positionAndSvg[0]表示svg图像所在页面的div的id //positionAndSvg[1]表示svg图像在水平方向的相对位置: // 1.left(水平方向两张图,居左且占比50%) // 2.right(水平方向两张图,居右且占比50%) // 3.center(水平方向一张图,居中且占比100%) //positionAndSvg[2]表示svg图像模块的标题如:xxx走势图 //positionAndSvg[3]表示soft/hard即软广图或者硬广图,当无数据时为无数据提示效果图提供判断依据 //positionAndSvg[4]表示svg图像元素,形如<svg...../> ////////////////////////////////////////////////////////////////////////////////////////////////////////// String[] positionAndSvg = str.split("___"); Image image1 = null; boolean havaData = true; if("noData".equals(positionAndSvg[4])){//无数据时 image1 = Image.getInstance(basePath+noData); havaData = false; }else{//有数据 image1 = Image.getInstance(highcharts(request,response,positionAndSvg[4]).toByteArray()); havaData = true; } if("left".equals(positionAndSvg[1])){ String title1 = URLDecoder.decode(positionAndSvg[2],"utf-8"); setTitleByCharts(document,30,title1,"",0,87,55,Element.ALIGN_LEFT,headfont); if(!"cooperateProporOne".equals(positionAndSvg[0])){ setTitleByCharts(document,0,"左图","右图",248,248,248,Element.ALIGN_CENTER,blackTextFont); }else{ setTitleByCharts(document,0,"","",248,248,248,Element.ALIGN_CENTER,blackTextFont); } table = new PdfPTable(2); float[] wid ={0.50f,0.50f}; //列宽度的比例 table.setWidths(wid); table = PdfPTableImage(table,image1,80f); }else if("right".equals(positionAndSvg[1])){ table = PdfPTableImage(table,image1,80f); table.setSpacingBefore(10); table=setTableHeightWeight(table,360f,1000); document.add(table); table = null; }else if("center".equals(positionAndSvg[1])){//总览全局 String title1 = URLDecoder.decode(positionAndSvg[2],"utf-8"); setTitleByCharts(document,30,title1,"",0,87,55,Element.ALIGN_LEFT,headfont); setTitleByCharts(document,0,"","",248,248,248,Element.ALIGN_CENTER,blackTextFont); table = new PdfPTable(1); float[] wid ={1.00f}; //列宽度的比例 table.setWidths(wid); if(havaData){ table = PdfPTableImageTable(table,image1,1000f,600f); }else{ table = PdfPTableImageTable(table,Image.getInstance(basePath+noDataCenter),1000f,600f); } table=setTableHeightWeight(table,400f,1000); document.add(table); table=null; } } //添加水印Start--------------------------------------------------------------------------------------------- PdfFileExportUtil pdfFileExportUtil = new PdfFileExportUtil(); pdfWriter.setPageEvent(pdfFileExportUtil.new PictureWaterMarkPdfPageEvent(basePath+waterMark)); // pdfWriter.setPageEvent(pdfFileExportUtil.new TextWaterMarkPdfPageEvent("xxx科技")); //添加水印End----------------------------------------------------------------------------------------------- document.close(); return buffer; } catch (BadElementException e) { e.printStackTrace(); return null; } catch (MalformedURLException e) { e.printStackTrace(); return null; } catch (DocumentException e) { e.printStackTrace(); return null; } catch (IOException e) { e.printStackTrace(); return null; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 设置图片类型Cell属性 * @param table * @param image1 * @param imgPercent * @return * @throws Exception */ private PdfPTable PdfPTableImage(PdfPTable table,Image image1,float imgPercent){ table = useTable(table,Element.ALIGN_CENTER); PdfPCell cellzr = createCellImage(image1,imgPercent); cellzr.setBorder(0); cellzr.setBackgroundColor(new BaseColor(248,248,248)); table.addCell(cellzr); return table; } /** * 设置图片类型Table的Cell属性 * @param table * @param image1 * @param imgPercentWidth * @param imgPercentHeight * @return * @throws Exception */ private PdfPTable PdfPTableImageTable(PdfPTable table,Image image1,float imgPercentWidth,float imgPercentHeight){ table = useTable(table,Element.ALIGN_CENTER); PdfPCell cellzr = createCellImageTable(image1,imgPercentWidth,imgPercentHeight); cellzr.setBorder(0); cellzr.setBackgroundColor(new BaseColor(248,248,248)); table.addCell(cellzr); return table; } /** * 设置表头 * @param document * @param SpacingBefore * @param title1 * @param title2 * @param r1 * @param r2 * @param r3 * @param ele * @param font * @throws Exception */ private void setTitleByCharts(Document document,int SpacingBefore,String title1,String title2,int r1,int r2,int r3,int ele,Font font){ try { float[] titlewidthsLeft = {0.50f,0.50f}; PdfPTable zrfbtitleTable = createTable(titlewidthsLeft); PdfPCell cellzr = createCellLeft(title1,font,ele); cellzr.setBorder(0); cellzr.setBackgroundColor(new BaseColor(r1,r2,r3)); zrfbtitleTable.addCell(cellzr); PdfPCell cellzr1 = createCellLeft(title2,font,ele); cellzr1.setBorder(0); cellzr1.setBackgroundColor(new BaseColor(r1,r2,r3)); zrfbtitleTable.addCell(cellzr1); zrfbtitleTable.setSpacingBefore(SpacingBefore); zrfbtitleTable=setTableHeightWeight(zrfbtitleTable,30f,1000); document.add(zrfbtitleTable); } catch (DocumentException e) { e.printStackTrace(); } } /** * 导出Pdf所用字体静态变量 */ private static Font headfont ;// title字体 private static Font blackTextFont ;// 黑色字体 private static Font colorfont; int maxWidth = 500; static{ BaseFont bfChinese; try { bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); headfont = new Font(bfChinese, 15, Font.BOLD);// 设置字体大小 headfont.setColor(BaseColor.WHITE); blackTextFont = new Font(bfChinese, 11, Font.BOLD);// 设置字体大小 blackTextFont.setColor(BaseColor.BLACK); colorfont = new Font(bfChinese, 11, Font.NORMAL);// 设置字体大小 colorfont.setColor(BaseColor.RED); } catch (Exception e) { e.printStackTrace(); } } /** * 创建指定内容背景色的Table元素Cell * @param value * @param font * @param c1 * @param c2 * @param c3 * @return */ public PdfPCell createCell(String value,Font font,int c1,int c2, int c3){ PdfPCell cell = new PdfPCell(); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setHorizontalAlignment(Element.ALIGN_CENTER); cell.setPhrase(new Phrase(value,font)); cell.setBackgroundColor(new BaseColor(c1,c2,c3)); cell.setFixedHeight(33.33f); cell.setBorder(0); return cell; } /** * 创建指定位置的Table元素Cell * @param value * @param font * @param ele * @return */ public PdfPCell createCellLeft(String value,Font font,int ele){ PdfPCell cell = new PdfPCell(); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setHorizontalAlignment(ele); cell.setPaddingLeft(10); cell.setPhrase(new Phrase(value,font)); return cell; } /** * 创建内容为Image的Table元素Cell * @param image * @param imgPercent * @return */ public PdfPCell createCellImage(Image image,float imgPercent){ image.scalePercent(imgPercent); PdfPCell cell = new PdfPCell(image,false); cell.setUseAscender(true); cell.setUseDescender(true); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setHorizontalAlignment(Element.ALIGN_CENTER); cell.setPaddingLeft(10); return cell; } /** * 创建table元素cell * @param image * @param imgPercentWidth * @param imgPercentHeight * @return */ public PdfPCell createCellImageTable(Image image,float imgPercentWidth,float imgPercentHeight){ image.scaleAbsoluteWidth(imgPercentWidth); if(imgPercentHeight==410f){ image.scaleAbsoluteHeight(imgPercentHeight); } PdfPCell cell = new PdfPCell(image,false); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setHorizontalAlignment(Element.ALIGN_CENTER); return cell; } /** * 创建Table * @param widths 列宽比例 * @return */ public PdfPTable createTable(float[] widths){ for(int i=0;i<widths.length;i++){ widths[i] = widths[i]*maxWidth; } PdfPTable table = new PdfPTable(widths); try{ table.setTotalWidth(maxWidth); table.setLockedWidth(true); table.setHorizontalAlignment(Element.ALIGN_CENTER); table.getDefaultCell().setBorder(1); }catch(Exception e){ e.printStackTrace(); } return table; } /** * 设置table参数 * @param table * @param position * @return */ public PdfPTable useTable(PdfPTable table,int position){ try{ table.setTotalWidth(maxWidth); table.setLockedWidth(true); table.setHorizontalAlignment(position); table.getDefaultCell().setBorder(0); }catch(Exception e){ e.printStackTrace(); } return table; } /** * 设置PdfTable行高 * @param table * @param maxHeight * @param maxWidth * @return */ public PdfPTable setTableHeightWeight(PdfPTable table,float maxHeight,float maxWidth){ table.setTotalWidth(maxWidth); List<PdfPRow> list=new ArrayList<PdfPRow>(); list=table.getRows(); for(PdfPRow pr:list){ pr.setMaxHeights(maxHeight); } return table; } /** * 根据SVG字符串得到一个输出流 * @param request * @param response * @param svg * @return * @throws Exception */ public ByteArrayOutputStream highcharts(HttpServletRequest request,HttpServletResponse response,String svg){ try { request.setCharacterEncoding("utf-8");// 注意编码 //转码防止乱码 byte[] arrayStr = svg.getBytes("utf-8"); svg = new String(arrayStr, "UTF-8"); ByteArrayOutputStream stream = new ByteArrayOutputStream(); try { stream=this.transcode(stream, svg); } catch (Exception e) { e.printStackTrace(); } return stream; } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } } /** * 对svg进行转码 * @param stream * @param svg * @return * @throws Exception */ public synchronized ByteArrayOutputStream transcode(ByteArrayOutputStream stream, String svg){ try { TranscoderInput input = new TranscoderInput(new StringReader(svg)); TranscoderOutput transOutput = new TranscoderOutput(stream); PNGTranscoder transcoder = new PNGTranscoder(); transcoder.transcode(input, transOutput); return stream; } catch (TranscoderException e) { e.printStackTrace(); return null; } } }
此工具类可以根据前端传来的svg信息,前文中提到的自定义position等属性,布局完成所要输出的PDF文档,因时间有限,不再一一赘述,有想研究的可以下载demo,我已做了一个demo供各位交流学习,下载地址:http://yun.baidu.com/share/link?shareid=2976350494&uk=657798452