寫給讀者的話^_^:
眾所周知,基於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