html页面导出pdf,本来是一件很简单的事情,在浏览器直接打印(Mac快捷键为⌘+p;Windows快捷键为ctrl+p),就可以把页面另存为pdf文件,但对于要经常把页面导出为pdf的用户来说并不友好,一个合格程序员的标准就是:做出来的软件猪都要会用,否则你就是猪。

调研了几种html导出pdf的实现方式,这里把要点记录下来分享下。

调研对象

优点

缺点

分页

图片

表格

链接

中文

特殊字符、样式

导出样例

备注

jsPDF

1、整个过程在客户端执行(不需要服务器参与),调用简单

1、生成的pdf为图片形式,且内容失真

支持

支持

支持

不支持

支持

支持

iText

1、功能基本可以实现,比较灵活2、生成pdf质量较高

1、对html标签严;格,少一个结束标签就会报错;2、后端实现复杂,服务器需要安装字体;3、图片渲染比较复杂(暂时还没解决)

支持

支持

支持

支持

支持

支持

wkhtmltopdf

1、调用方式简单(只需执行一行脚本);2、生成pdf质量较高

1、服务器需要安装wkhtmltopdf环境;2、根据网址生成pdf,对于有权限控制的页面需要在拦截器进行处理

支持

支持

支持

支持

支持

支持

上面三种是着重调研的三种方式,下面进行简单介绍。

一、html2canvas+jsPDF


这种方式的原理是利用html2canvas遍历页面中的dom节点,渲染成canvas image,再用jsPDF把canvas image转化为pdf,最后转化的pdf的内容都是图片形式,类似于把整个网页截图、切割,再一页一页拼接成一个完整的pdf。

代码样例
html:

<span style="background-color:#f6f8fa"><span style="color:#000000"><code class="language-html"><span style="color:#006666"><<span style="color:#4f4f4f">button</span> <span style="color:#4f4f4f">id</span>=<span style="color:#009900">"exportToPdf"</span>></span>导出为PDF<span style="color:#006666"></<span style="color:#4f4f4f">button</span>></span>
<span style="color:#006666"><<span style="color:#4f4f4f">div</span> <span style="color:#4f4f4f">id</span>=<span style="color:#009900">"export_content"</span>></span>
    这里是要导出为pdf中的内容
<span style="color:#006666"></<span style="color:#4f4f4f">div</span>></span></code></span></span>

javascript(需要依赖jspdf和html2canvas相关js):

<span style="background-color:#f6f8fa"><span style="color:#000000"><code class="language-javascript"><script src=<span style="color:#009900">"js/jspdf.debug.js"</span>><span style="color:#006666"></<span style="color:#4f4f4f">script</span>></span>
<span style="color:#006666"><<span style="color:#4f4f4f">script</span> <span style="color:#4f4f4f">src</span>=<span style="color:#009900">"js/html2canvas.js"</span>></span><span style="color:#006666"></<span style="color:#4f4f4f">script</span>></span>
<span style="color:#006666"><<span style="color:#4f4f4f">script</span> <span style="color:#4f4f4f">type</span>=<span style="color:#009900">"text/javascript"</span>></span>
    <span style="color:#000088">var</span> downPdf = document.getElementById(<span style="color:#009900">"exportToPdf"</span>);
    downPdf.onclick = <span style="color:#000088">function</span> <span style="color:#4f4f4f">()</span> {
        html2canvas(
                document.getElementById(<span style="color:#009900">"export_content"</span>),
                {
                    dpi: <span style="color:#006666">172</span>,<span style="color:#880000">//导出pdf清晰度</span>
                    onrendered: <span style="color:#000088">function</span> <span style="color:#4f4f4f">(canvas)</span> {
                        <span style="color:#000088">var</span> contentWidth = canvas.width;
                        <span style="color:#000088">var</span> contentHeight = canvas.height;

                        <span style="color:#880000">//一页pdf显示html页面生成的canvas高度;</span>
                        <span style="color:#000088">var</span> pageHeight = contentWidth / <span style="color:#006666">592.28</span> * <span style="color:#006666">841.89</span>;
                        <span style="color:#880000">//未生成pdf的html页面高度</span>
                        <span style="color:#000088">var</span> leftHeight = contentHeight;
                        <span style="color:#880000">//pdf页面偏移</span>
                        <span style="color:#000088">var</span> position = <span style="color:#006666">0</span>;
                        <span style="color:#880000">//html页面生成的canvas在pdf中图片的宽高(a4纸的尺寸[595.28,841.89])</span>
                        <span style="color:#000088">var</span> imgWidth = <span style="color:#006666">595.28</span>;
                        <span style="color:#000088">var</span> imgHeight = <span style="color:#006666">592.28</span> / contentWidth * contentHeight;

                        <span style="color:#000088">var</span> pageData = canvas.toDataURL(<span style="color:#009900">'image/jpeg'</span>, <span style="color:#006666">1.0</span>);
                        <span style="color:#000088">var</span> pdf = <span style="color:#000088">new</span> jsPDF(<span style="color:#009900">''</span>, <span style="color:#009900">'pt'</span>, <span style="color:#009900">'a4'</span>);

                        <span style="color:#880000">//有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)</span>
                        <span style="color:#880000">//当内容未超过pdf一页显示的范围,无需分页</span>
                        <span style="color:#000088">if</span> (leftHeight < pageHeight) {
                            pdf.addImage(pageData, <span style="color:#009900">'JPEG'</span>, <span style="color:#006666">0</span>, <span style="color:#006666">0</span>, imgWidth, imgHeight);
                        } <span style="color:#000088">else</span> {
                            <span style="color:#000088">while</span> (leftHeight > <span style="color:#006666">0</span>) {
                                pdf.addImage(pageData, <span style="color:#009900">'JPEG'</span>, <span style="color:#006666">0</span>, position, imgWidth, imgHeight)
                                leftHeight -= pageHeight;
                                position -= <span style="color:#006666">841.89</span>;
                                <span style="color:#880000">//避免添加空白页</span>
                                <span style="color:#000088">if</span> (leftHeight > <span style="color:#006666">0</span>) {
                                    pdf.addPage();
                                }
                            }
                        }
                        pdf.save(<span style="color:#009900">'content.pdf'</span>);
                    },
                    <span style="color:#880000">//背景设为白色(默认为黑色)</span>
                    background: <span style="color:#009900">"#fff"</span>  
                })
    }
<span style="color:#006666"></<span style="color:#4f4f4f">script</span>></span>
</code></span></span>

这种方法的优点是所有的过程都由js在客户端完成,不需要依赖服务器。
目前发现的两个比较明显的缺点:
1、生成的pdf质量不高,失真比较严重(不过在github上这个方法可以适当提高下生成pdf的清晰度Add dpi/scale options for custom resolution by eKoopmans · Pull Request #1087 · niklasvh/html2canvas · GitHub);
2、在分页处如果有图片的话,不会自动识别隔页处理(甚至一行文字也能给你上下一分为二),而是无情地把图片一分为二,满满的违和感~如下图:

html转pdf java 图片 html生成pdf java_WKHTMLTOPDF

github上有一篇文章说明比较详细,还有具体的demo:https://github.com/linwalker/render-html-to-pdf

二、iText


iText是一个第三方报表java插件,可以在后端利用java随意生成、转化pdf文件,提供了很多api,比较灵活。

代码样例
pom依赖:

<span style="background-color:#f6f8fa"><span style="color:#000000"><code class="language-xml"><span style="color:#006666"><<span style="color:#4f4f4f">dependency</span>></span>
    <span style="color:#006666"><<span style="color:#4f4f4f">groupId</span>></span>org.eclipse.birt.runtime.3_7_1<span style="color:#006666"></<span style="color:#4f4f4f">groupId</span>></span>
    <span style="color:#006666"><<span style="color:#4f4f4f">artifactId</span>></span>com.lowagie.text<span style="color:#006666"></<span style="color:#4f4f4f">artifactId</span>></span>
    <span style="color:#006666"><<span style="color:#4f4f4f">version</span>></span>2.1.7<span style="color:#006666"></<span style="color:#4f4f4f">version</span>></span>
<span style="color:#006666"></<span style="color:#4f4f4f">dependency</span>></span>
<span style="color:#006666"><<span style="color:#4f4f4f">dependency</span>></span>
    <span style="color:#006666"><<span style="color:#4f4f4f">groupId</span>></span>org.xhtmlrenderer<span style="color:#006666"></<span style="color:#4f4f4f">groupId</span>></span>
    <span style="color:#006666"><<span style="color:#4f4f4f">artifactId</span>></span>flying-saucer-pdf<span style="color:#006666"></<span style="color:#4f4f4f">artifactId</span>></span>
    <span style="color:#006666"><<span style="color:#4f4f4f">version</span>></span>9.0.8<span style="color:#006666"></<span style="color:#4f4f4f">version</span>></span>
<span style="color:#006666"></<span style="color:#4f4f4f">dependency</span>></span>
<span style="color:#006666"><<span style="color:#4f4f4f">dependency</span>></span>
    <span style="color:#006666"><<span style="color:#4f4f4f">groupId</span>></span>com.itextpdf<span style="color:#006666"></<span style="color:#4f4f4f">groupId</span>></span>
    <span style="color:#006666"><<span style="color:#4f4f4f">artifactId</span>></span>itextpdf<span style="color:#006666"></<span style="color:#4f4f4f">artifactId</span>></span>
    <span style="color:#006666"><<span style="color:#4f4f4f">version</span>></span>5.4.2<span style="color:#006666"></<span style="color:#4f4f4f">version</span>></span>
<span style="color:#006666"></<span style="color:#4f4f4f">dependency</span>></span></code></span></span>

java实现:

<span style="background-color:#f6f8fa"><span style="color:#000000"><code class="language-java">ITextRenderer renderer = <span style="color:#000088">new</span> ITextRenderer();
ITextFontResolver fontResolver = renderer.getFontResolver();
fontResolver.addFont(<span style="color:#009900">"/Users/hehe/share/Fonts/simsun.ttc"</span>, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
OutputStream os = <span style="color:#000088">new</span> FileOutputStream(<span style="color:#009900">"/Users/hehe/Desktop/iTextPDF.pdf"</span>);
String htmlstr = HttpHandler.sendGet(<span style="color:#009900">"http://localhost:10086/test/iTextPDF.html"</span>);<span style="color:#880000">//HttpHandler.sendGet只是单纯获得指定网页的html字符串内容</span>
renderer.setDocumentFromString(htmlstr);
renderer.layout();
renderer.createPDF(os);</code></span></span>

以上只是简单利用html字符串来生成pdf,需要注意的是:
1、如果页面中有中文,服务器端需要下载字体库simsun.ttc,在后台进行引用,同时在页面的样式中加入对应字体的定义,如:body{font-family: SimSun;},否则中文无法渲染(中文处渲染出来的效果是空白);
2、页面中如果有图片,如果图片引用是绝对路径或者base64则不用考虑,如果是相对路径,需要在后台用renderer.getSharedContext().setBaseURL("图片绝对路径目录");来指定图片路径,否则图片无法渲染。
3、要转化的页面必须是标准的XHTML页面,有一处不符合规范就会报错,小编再试的时候就经常报诸如org.xml.sax.SAXParseException;lineNumber: 24; columnNumber: 6;元素类型 "span" 必须由匹配的结束标记 "</span> 终止"之类的错误,所以如果要用iText来大量爬取网络中的页面的话,还是放弃吧,毕竟网上很多页面都是不标准的~

三、wkhtmltopdf


wkhtmltopdf是一个可以把html转为pdf的插件,有windows、linux等平台的版本,最大的特点就是使用简单,语言无关性。
1、下载:官网下载 wkhtmltopdf 2、执行:该插件是“绿色版”,无需编译安装,下载解压后,在bin目录下有wkhtmltoimage和wkhtmltopdf两个文件,生成pdf可以直接运行wkhtmltopdf(也可以把bin目录配置到环境变量),执行wkhtmltopdf -V查看是否可以执行。
执行的时候可能会报错wkhtmltopdf: error while loading shared libraries: libXrender.so.1 或者 ./wkhtmltopdf: error while loading shared libraries: libfontconfig.so.1: cannot open shared object file: No such file or directory 具体解决方法可参考wkhtmltopdf: error while loading shared libraries: libXrender.so.1 - SvennD

如果执行完打印出wkhtmltopdf的版本号,则说明OK了,下面来一个打印html页面的例子试试看,就把本页面转化成pdf吧:

<span style="background-color:#f6f8fa"><span style="color:#000000"><code>wkhtmltopdf --disable-smart-shrinking https://blog<span style="color:#009900">.csdn</span><span style="color:#009900">.net</span>/huyuyang6688/article/details/<span style="color:#006666">79710704</span> myBlog<span style="color:#009900">.pdf</span></code></span></span>

执行完之后,就会在当前目录生成一个pdf(当然生成pdf的目录可以指定),--disable-smart-shrinking 这个参数是关闭缩放,如果不加的话,生成的pdf内容会特别“瘦”,不造为啥这个命令在mac环境下不是很有效,不敢在linux环境生成的PDF是正常的。具体更详细的用法可以参考如下文章:
1、HTML 转 PDF 之 wkhtmltopdf 工具简介 2、HTML 转 PDF 之 wkhtmltopdf 工具精讲 3、wkhtmltopdf参数详解 4、解决wkhtmltopdf支持中文和缩放问题:wkhtmltopdf折腾记

与之类似的还有一个叫Phantomjs的插件,效果差不多,还没深入研究。