Android PDF 生成技术调研

公司业务需要安卓客户端动态生成 pdf 文档并且打印出来,鉴于安卓本身对 pdf 支持并不是太强大,对比 ios 那样直接定制,批量生成,Android 本身 pdf 功能只能塞入 bitmap 就比较鸡肋。于是多方调研找了总结了相关实现。

一、WebView 内容 PDF 输出

Android SDK 21 api 的 webview 中提供了 createPrintDocumentAdapter 方法,可以用这个方法对 webview 内容生成 pdf

Creates a PrintDocumentAdapter that provides the content of this WebView for printing. The adapter works by converting the WebView contents to a PDF stream. The WebView cannot be drawn during the conversion process – any such draws are undefined. It is recommended to use a dedicated off screen WebView for the printing. If necessary, an application may temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance wrapped around the object returned and observing the onStart and onFinish methods. 

此方法需要网页加载完成才能生成 pdf,如果数据过多,页面比较复杂,比较有可能会 OOM,目前一页内容生成的 pdf 只有不到 3M pdf 生成,如果选择非彩色模式,尝试了一下生成 108页,也最多3M的内容,比起后面方式要好很多,不需要考虑图片拼接,也需要像 itext 考虑字体。缺点就是用户操作过程会欠缺,另外因为由于内容是网页内容,会出现拆分到两页的内容的情况。相当于是一种廉价且比较稳定虽有缺点的解决方案了。
代表库有:
https://github.com/tejpratap46/PDFCreatorAndroid

二、使用 Apache PDFBox 库实现 PDF 输出

PDFBox 核心思路是插入 PDF 内容然后指定排版,然后输出到 PDF 文件中

// Define a content stream for adding to the PDF
contentStream = new PDPageContentStream(document, page);

// Write Hello World in blue text
contentStream.beginText();
contentStream.setNonStrokingColor(15, 38, 192);
contentStream.setFont(font, 12);
contentStream.newLineAtOffset(100, 700);
contentStream.showText("Hello World");
contentStream.endText();

// Load in the images
InputStream in = assetManager.open("falcon.jpg");
InputStream alpha = assetManager.open("trans.png");

// Draw a green rectangle
contentStream.addRect(5, 500, 100, 100);
contentStream.setNonStrokingColor(0, 255, 125);
contentStream.fill();

// Draw the falcon base image
PDImageXObject ximage = JPEGFactory.createFromStream(document, in);
contentStream.drawImage(ximage, 20, 20);

// Draw the red overlay image
Bitmap alphaImage = BitmapFactory.decodeStream(alpha);
PDImageXObject alphaXimage = LosslessFactory.createFromImage(document, alphaImage);
contentStream.drawImage(alphaXimage, 20, 20 );

// Make sure that the content stream is closed:
contentStream.close();    

可以从这个代码看出,通过代码指定 pdf 文字内容宽高及位置,目前这个代码是 pdfbox-android-lite 专门适配安卓,但是这个库对于字库非常不友好,并且不能够获取到系统的合适对应的显示字库,尤其是中文相关的,直接显示中文会崩溃。另外假如去强行开启读取系统全部的字体库,会导致 app OOM 异常,读取字库直接失败,因此 PDFBox 系列的只能针对字体支持友好的语言。
代表库有:
https://github.com/tripplemac/pdfbox-android-lite
https://github.com/TomRoush/PdfBox-Android
https://github.com/danfickle/openhtmltopdf

三、安卓系统提供标准生成的 pdf

 // create a new document
 PdfDocument document = new PdfDocument();

 // create a page description
 PageInfo pageInfo = new PageInfo.Builder(100, 100, 1).create();

 // start a page
 Page page = document.startPage(pageInfo);

 // draw something on the page
 View content = getContentView();
 content.draw(page.getCanvas());

 // finish the page
 document.finishPage(page);
 . . .
 // add more pages
 . . .
 // write the document content
 document.writeTo(getOutputStream());

 // close the document
 document.close();

标准系统生成的方式,主要方式是操纵 view 的 canvas,这个不需要关注是否是塞入文本,也不需要考虑塞入的图片,全部由 view 来确定。

此方法需动态生成画布才能生成 pdf,如果数据过多,页面比较复杂,比较有可能会 OOM
直接使用 TextView 生成一个例子,生成出来内容很小的 pdf 文件就有 4M 大小,而使用 webview 打印选择生成 pdf 复杂内容不到 3M,因此如果生成很简单的 pdf 内容就可以使用这种方式。毕竟简洁快速且有系统保证。
代表库:
https://github.com/IanDarwin/Android-Cookbook-Examples/tree/master/PdfShare

四、itext PDF

itext 有两个版本,最新版本是其官方发布的,但是如果使用要么使用他们的商业库,要么遵循他们的 AGPL 协议,前者需要支付使用费,后者需要开源项目本身,并且有对应限制条件。这两个库对中文支持也没有那么友好,与PDFbox 类似,需要确定字体,如果没有对应文字的字体就会显示乱码,建议如果只是练手复杂项目,或者纯个人使用可以用这种方式,但还是有比较大缺陷。如果使用这种方式还不如服务器搭建 js 生成 pdf 服务,相关字体由服务器生成,这样不用担心包体过大,或者对于特殊文字不能马上支持的情况了
代表库:
https://itextpdf.com/
https://github.com/LibrePDF/OpenPDF/tree/master/openpdf-fonts-extra

五、搭建 JS 生成 pdf

依托于目前强大的浏览器,或者强大的开源库,既可以搭建纯客户端的 js 服务,也可以搭建服务器后台服务生成 pdf,所需要的字体资源还有其他资源都通过本地或者服务器来提供,相对而言灵活一点。内容全部放在客户端,会导致包体过大,放到服务器处理完以后,生成好 pdf 之后,客户端进行下载或者展示也是一种不错的选择方式,当然这种方式会吃掉服务器的一些资源。这些库 github 上面很多 JavaScript 相关的项目
代表库:
https://github.com/search?q=pdf

总结:相对于 ios 系统开发的封闭,但是提供了稳定的系统环境,不用担心字体,发音资源的问题,Android 本身国内国外厂商会各种定制,Google Android 本身提供了很多功能,但是因为各种原因,各种机器上面资源会根据地区还有厂商做一定的优化,导致某些必须的资源不存在,开发起功能束手束脚的。仁者见仁智者见智,选择适合自己方式实现功能才是目的。