近做了一個奇葩的需求,研究了一下Java純后端生成PDF報表的方案,順便將研究的方案做個總結(jié)復(fù)盤,分享一下。
需求分析:Java后端定時任務(wù)統(tǒng)計匯總成報表數(shù)據(jù),并生成PDF格式的報表文件,并通過郵件、企業(yè)微信等發(fā)送給指定接收人。報表界面包含動態(tài)文字說明、折線圖、餅圖、條形圖等圖表,界面效果和前端生成的界面相同。
功能難點:前端要生成樣式好看的圖表比較簡單,像Echarts這些前端工具都有現(xiàn)成的功能來完成。但是現(xiàn)在的需求是后端定時任務(wù)生成報表文件,報表界面的渲染都必須有后端來完成,由于缺少前端的用戶操作動作,也無法在前端生成圖表的圖片后傳到后端來。
方案一:
使用FreeMarker+iText生成PDF文件。
原理和流程:
FreeMarker是一款模板引擎: 即一種基于模板和要改變的數(shù)據(jù), 并用來生成輸出文本(HTML網(wǎng)頁、電子郵件、配置文件、源代碼等)的通用工具。
iText是一種生成PDF報表的Java組件。通過在服務(wù)器端使用Jsp或JavaBean生成PDF報表,客戶端采用超鏈接顯示或下載得到生成的報表,這樣就很好的解決了B/S系統(tǒng)的報表處理問題。
具體的流程如下:
缺點:這種方案只能生成很簡單的Table模板,由于iText對html的要求非常嚴格,太復(fù)雜的界面會報錯,所以無法生成Echarts的圖表。
方案二:
SwingUI+JFreeChart+JFreePDF生成PDF文件
這里JFreeChart和JFreePDF都是maven依賴包
JFreeChart是Java客戶端應(yīng)用的一個界面組件,在SwingUI上畫出圖表控件。
JFreePDF是能將JPanel面板截屏生成PDF的插件。
流程和原理:
缺點:
由于是將JPanel截屏生成的PDF。所以界面樣式上比較難看,比不上前端界面生成的報表頁面。
而且JFreePDF這個maven依賴的插件是基于JDK11開發(fā)的,如果要兼容JDK8,就要到github上將源碼下載下來,自己編譯生成一個兼容JDK8的依賴包。
方案三:(最終采用方案)
使用wkhtmltopdf+靜態(tài)html界面生成pdf界面
wkhtmltopdf是一個將靜態(tài)html網(wǎng)頁截屏生成pdf文件的工具,Linux、Mac、Windows各個操作系統(tǒng)的版本都有。只需要輸入目標網(wǎng)頁的URL就能將網(wǎng)頁完成的導(dǎo)出PDF文件。
流程和原理:
1.在操作系統(tǒng)安裝wkhtmltopdf工具
2.前端編碼html+jquery+echarts的純靜態(tài)頁面,由于wkhtmltopdf工具使用內(nèi)置的WebKit內(nèi)核版本較低,所以不兼容太新的js語言,像VueJS這些最新的框架就無法使用這個工具。目前測試的能夠兼容的echarts版本是4.2.1.
3.調(diào)用wkhtmltopdf命令輸入靜態(tài)網(wǎng)頁地址生成pdf文件。
之前為了調(diào)試網(wǎng)頁寫了一個Java桌面應(yīng)用來調(diào)用wkhtmltopdf工具生成pdf。
github地址:https://github.com/WrathLi/html2pdf
缺點:
1.需要在服務(wù)器系統(tǒng)中先安裝wkhtmltopdf工具;
2.只能單獨開發(fā)一個純靜態(tài)的html頁面來生成報表
優(yōu)點:
界面美觀,因為是直接截取html網(wǎng)頁,所以和前端生成的圖表樣式一樣。
開發(fā)量最小。
最終效果:
tml2canvas
簡介
我們可以直接在瀏覽器端使用html2canvas,對整個或局部頁面進行‘截圖’。但這并不是真的截圖,而是通過遍歷頁面DOM結(jié)構(gòu),收集所有元素信息及相應(yīng)樣式,渲染出canvas image。
由于html2canvas只能將它能處理的生成canvas image,因此渲染出來的結(jié)果并不是100%與原來一致。但它不需要服務(wù)器參與,整個圖片都由客戶端瀏覽器生成,使用很方便。
使用
使用的API也很簡潔,下面代碼可以將某個元素渲染成canvas:
html2canvas(element, { onrendered: function(canvas) { // canvas is the final rendered <canvas> element } });
通過onrendered方法,可以將生成的canvas進行回調(diào),比如插入到頁面中:
html2canvas(element, { onrendered: function(canvas) { document.body.appendChild(canvas); } });
做個小例子代碼如下,在線展示鏈接demo1
<html> <head> <title>html2canvas example</title> <style type="text/css">...</style> </head> <body> <header> <nav> <ul> <li>one</li> ... </ul> </nav> </header> <section> <aside> <h3>it is a title</h3> <a href="">Stone Giant</a> ... </aside> <article> <img src="./Stone.png"> <h2>Stone Giant</h2> <p>Coming ... </p> <p>以一團石頭...</p> </article> </section> <footer>write by linwalker @2017</footer> <script type="text/javascript" src="./html2canvas.js"></script> <script type="text/javascript"> html2canvas(document.body, { onrendered:function(canvas) { document.body.appendChild(canvas) } }) </script> </body> </html>
這個例子將頁面body中的元素渲染成canvas,并插入到body中
jsPDF
jsPDF庫可以用于瀏覽器端生成PDF。
文字生成PDF
使用方法如下:
// 默認a4大小,豎直方向,mm單位的PDF var doc = new jsPDF(); // 添加文本‘Download PDF’ doc.text('Download PDF!', 10, 10); doc.save('a4.pdf');
在線演示demo2
圖片生成PDF
使用方法如下:
// 三個參數(shù),第一個方向,第二個單位,第三個尺寸格式 var doc = new jsPDF('landscape','pt',[205, 115]) // 將圖片轉(zhuǎn)化為dataUrl var imageData = ‘data:image/png;base64,iVBORw0KGgo...’; doc.addImage(imageData, 'PNG', 0, 0, 205, 115); doc.save('a4.pdf');
在線演示demo3
文字與圖片生成PDF
// 三個參數(shù),第一個方向,第二個尺寸,第三個尺寸格式 var doc = new jsPDF('landscape','pt',[205, 155]) // 將圖片轉(zhuǎn)化為dataUrl var imageData = ‘data:image/png;base64,iVBORw0KGgo...’; //設(shè)置字體大小 doc.setFontSize(20); //10,20這兩參數(shù)控制文字距離左邊,與上邊的距離 doc.text('Stone', 10, 20); // 0, 40, 控制文字距離左邊,與上邊的距離 doc.addImage(imageData, 'PNG', 0, 40, 205, 115); doc.save('a4.pdf')
在線演示demo4
生成pdf需要把轉(zhuǎn)化的元素添加到j(luò)sPDF實例中,也有添加html的功能,但某些元素?zé)o法生成在pdf中,因此可以使用html2canvas + jsPDF的方式將頁面轉(zhuǎn)成pdf。通過html2canvas將遍歷頁面元素,并渲染生成canvas,然后將canvas圖片格式添加到j(luò)sPDF實例,生成pdf。
html2canvas + jsPDF
單頁
將demo1的例子修改下:
<script type="text/javascript" src="./js/jsPdf.debug.js"></script> <script type="text/javascript"> var downPdf = document.getElementById("renderPdf"); downPdf.onclick = function() { html2canvas(document.body, { onrendered:function(canvas) { //返回圖片dataURL,參數(shù):圖片格式和清晰度(0-1) var pageData = canvas.toDataURL('image/jpeg', 1.0); //方向默認豎直,尺寸ponits,格式a4[595.28,841.89] var pdf = new jsPDF('', 'pt', 'a4'); //addImage后兩個參數(shù)控制添加圖片的尺寸,此處將頁面高度按照a4紙寬高比列進行壓縮 pdf.addImage(pageData, 'JPEG', 0, 0, 595.28, 592.28/canvas.width * canvas.height ); pdf.save('stone.pdf'); } }) } </script>
在線演示demo5
如果頁面內(nèi)容根據(jù)a4比例轉(zhuǎn)化后高度超過a4紙高度呢,生成的pdf會怎么樣?會分頁嗎?
你可以試試,驗證一下自己的想法: demo6
jsPDF提供了一個很有用的API,addPage(),我們可以通過pdf.addPage(),來添加一頁pdf,然后通過pdf.addImage(...),將圖片賦予這頁pdf來顯示。
那么我們?nèi)绾未_定哪里分頁?
這個問題好回答,我們可以設(shè)置一個pageHeight,超過這個高度的內(nèi)容放入下一頁pdf。
來捋一下思路,將html頁面內(nèi)容生成canvas圖片,通過addImage將第一頁圖片添加到pdf中,超過一頁內(nèi)容,通過addPage()添加pdf頁數(shù),然后再通過addImage將下一頁圖片添加到pdf中。
嗯~,很好!巴特,難道沒有發(fā)現(xiàn)問題嗎?
這個方法實現(xiàn)的前提是 — — 我們能根據(jù)pageHeight先將整頁內(nèi)容生成的canvas圖片分割成對應(yīng)的小圖片,然后一個蘿卜一個坑,一頁一頁addImage進去。
What? 想一想我們的canvas是腫么來的,不用拉上去,直接看下面:
html2canvas(document.body, { onrendered:function(canvas) { //it is here we handle the canvas } })
這里的body就是要生成canvas的元素對象,一個元素生成一個canvas;那么我們需要一頁一頁的canvas,也就是說。。。
你覺得可能嗎? 我覺得不太現(xiàn)實,按這思路要獲取頁面上不同位置的DOM元素,然后通過htnl2canvas(element,option)來處理,先不說能不能剛好在每個pageHeight的位置剛好找到一個DOM元素,就算找到了,這樣做累不累。
累的話
:)可以看看下面這種方法
多頁
我提供的思路是我們只生成一個canvas,對就一個,轉(zhuǎn)化元素就是你要轉(zhuǎn)成pdf內(nèi)容的母元素,在這篇demo里就是body了;其他不變,也是超過一頁內(nèi)容就addPage,然后addImage,只不過這里添加的是同一個canvas。
當然這樣做只會出現(xiàn)多頁重復(fù)的pdf,那到底怎么實現(xiàn)正確分頁顯示。其實主要利用了jsPDF的兩點:
- 超過jsPDF實例格式尺寸的內(nèi)容不顯示 (var pdf = new jsPDF('', 'pt', 'a4'); demo中就是a4紙的尺寸) - addImage有兩個參數(shù)可以控制圖片在pdf中的位置
雖然每一頁pdf上顯示的圖片是相同的,但我們通過調(diào)整圖片的位置,產(chǎn)生了分頁的錯覺。以第二頁為例,將豎直方向上的偏移設(shè)置為-841.89即一張a4紙的高度,又因為超過a4紙高度范圍的圖片不顯示,所以第二頁顯示了圖片豎直方向上[841.89,1682.78]范圍內(nèi)的內(nèi)容,這就得到了分頁的效果,以此類推。
還是看代碼吧:
html2canvas(document.body, { onrendered:function(canvas) { var contentWidth = canvas.width; var contentHeight = canvas.height; //一頁pdf顯示html頁面生成的canvas高度; var pageHeight = contentWidth / 592.28 * 841.89; //未生成pdf的html頁面高度 var leftHeight = contentHeight; //頁面偏移 var position = 0; //a4紙的尺寸[595.28,841.89],html頁面生成的canvas在pdf中圖片的寬高 var imgWidth = 595.28; var imgHeight = 592.28/contentWidth * contentHeight; var pageData = canvas.toDataURL('image/jpeg', 1.0); var pdf = new jsPDF('', 'pt', 'a4'); //有兩個高度需要區(qū)分,一個是html頁面的實際高度,和生成pdf的頁面高度(841.89) //當內(nèi)容未超過pdf一頁顯示的范圍,無需分頁 if (leftHeight < pageHeight) { pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight ); } else { while(leftHeight > 0) { pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight) leftHeight -= pageHeight; position -= 841.89; //避免添加空白頁 if(leftHeight > 0) { pdf.addPage(); } } } pdf.save('content.pdf'); } })
在線演示demo7
兩邊留邊距
修改imgWidth,并且在addImage時x方向參數(shù)設(shè)置你要的邊距,具體代碼如下
var imgWidth = 555.28; var imgHeight = 555.28/contentWidth * contentHeight; ... pdf.addImage(pageData, 'JPEG', 20, 0, imgWidth, imgHeight ); ... pdf.addImage(pageData, 'JPEG', 20, position, imgWidth, imgHeight);
在線演示demo8
tml2pdf
Selenium 通過使用 WebDriver 支持市場上所有主流瀏覽器的自動化。 Webdriver 是一個 API 和協(xié)議,它定義了一個語言中立的接口,用于控制 web 瀏覽器的行為。 每個瀏覽器都有一個特定的 WebDriver 實現(xiàn),稱為驅(qū)動程序。 驅(qū)動程序是負責(zé)委派給瀏覽器的組件,并處理與 Selenium 和瀏覽器之間的通信。
這種分離是有意識地努力讓瀏覽器供應(yīng)商為其瀏覽器的實現(xiàn)負責(zé)的一部分。 Selenium 在可能的情況下使用這些第三方驅(qū)動程序, 但是在這些驅(qū)動程序不存在的情況下,它也提供了由項目自己維護的驅(qū)動程序。
Selenium 框架通過一個面向用戶的界面將所有這些部分連接在一起, 該界面允許透明地使用不同的瀏覽器后端, 從而實現(xiàn)跨瀏覽器和跨平臺自動化。
# selenium 驅(qū)動
https://selenium-python.readthedocs.io/installation.html#drivers
https://selenium-python.readthedocs.io/api.html
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.16.1</version>
</dependency>
// 獲取 java 版本
String version = System.getProperty("java.specification.version");
// 獲取系統(tǒng)類型
String platform = System.getProperty("os.name", "");
platform = platform.toLowerCase().contains("window") ? "win" : "linux";
// 當前程序目錄
String current = System.getProperty("user.dir");
System.out.println("current:" + current);
// firefox 運行參數(shù)配置
FirefoxOptions options = new FirefoxOptions();
// 無頭模式
options.addArguments("--headless");
// 最大化
options.addArguments("--start-maximized");
FirefoxDriver browser = new FirefoxDriver(options);
Path url = Paths.get(current, "..", "index.html");
System.out.println("url:" + url.toString());
// NOTE 要使用 file 協(xié)議
browser.get(String.format("file://%s", url.toString()));
// 打印設(shè)置
PrintOptions print = new PrintOptions();
Pdf pdf = browser.print(print);
// pdf base64 內(nèi)容
String content = pdf.getContent();
// 解碼內(nèi)容
Base64.Decoder decoder = Base64.getDecoder();
byte[] buffer = decoder.decode(content);
try {
// 將 byte 寫入文件
Path file = Paths.get(String.format("java%s_%s.pdf", version, platform));
Files.write(file, buffer);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
browser.quit();
}
selenium/java11_linux.pdf · yjihrp/linux-html2pdf-demo - Gitee.com
selenium/java11_win.pdf · yjihrp/linux-html2pdf-demo - Gitee.com
測試結(jié)果
下一篇 6-LINUX HTML 轉(zhuǎn) PDF-selenium-python
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。