前言
笔者翻遍了网上关于Java生成pdf文章,发现大多都是使用itext来生成pdf,但是由于协议问题,这个方案对于商用不太友好。另外还有一种方式是采用Apache基金会开源的pdfbox来生成pdf,这也是笔者这次使用的方案,但是目前网上的博客或多或少都存在一些问题,于是我决定将自己的方案分享出来供大家参考。
方案主要分为下面3步:
- 准备pdf表单
- 生成pdf
- 将pdf转为图片并写入pdf文件
Maven依赖:
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.26</version>
</dependency>
1.准备pdf表单
本文使用Acrobat软件准备pdf表单
1.1 打开Acrobat
1.2 打开已有的pdf模板
1.3 选择添加文本域
一个文本域其实就是一个表单项。
1.4 编辑文本域
双击文本域即可进入编辑窗口,可以设置文本的样式等信息。这里的文本域的名称就是表单项名称,后续在代码中我们可以根据这个名称向指定的表单项插入值。
1.5 保存模板
准备好表单后保存模板即可
2.生成pdf
2.1 导入资源文件
将刚刚生成的pdf已经提前准备好的字体文件导入。
String filePath = attachmentArchiveFolder + "/" + RandomUtils.randomInt(1,100) + ".pdf";
InputStream inputStream = new ClassPathResource("templates/pdfTemplate.pdf").getInputStream();
// 按需要添加特殊的字体,但是这会导致生成的PDF变大
InputStream microsoftYaHeiBold = new ClassPathResource("fonts/微软雅黑粗体.ttf").getInputStream();
InputStream sourceHanSerifCN = new ClassPathResource("fonts/SourceHanSerifCN-Regular.ttf").getInputStream();
PDDocument pdfDocument = PDDocument.load(inputStream);
FileOutputStream fileOut = new FileOutputStream(filePath)
2.2 准备好表单以及字体
这里的titleApperance以及contentAppearance很重要,他们决定了pdf中文字的样式。
注意:
在导入字体时笔者使用
PDFont microsoftYaHeiBoldFont = PDType0Font.load(pdfDocument,microsoftYaHeiBold,false);
参数中的false意味不使用字体子集。这使得生成的pdf中会包含字体文件,导致pdf文件过大。
在笔者导入两个字体文件不作特殊处理的情况下文件大小将达到40MB左右,这取决于你的字体文件大小。
解决方案有三种:
1.如果你的pdf文件中不需要插入中文,那么可以不导入字体,导入字体的原因是pdf自带的字体不支持中文。
2.将false参数去掉或者改为true。这种情况下代码会读取字体文件的最小子集。但是这种方式在实际使用中很容易出现文件中缺失某个文字导致生成pdf失败的情况。
3.第三种方式是笔者采用的将生成的pdf转为图片再重新写入。
PDDocumentCatalog documentCatalog = pdfDocument.getDocumentCatalog();
PDAcroForm acroForm = documentCatalog.getAcroForm();
PDResources res = acroForm.getDefaultResources();
PDFont microsoftYaHeiBoldFont = PDType0Font.load(pdfDocument, microsoftYaHeiBold,false);
PDFont sourceHanSerifCNFont = PDType0Font.load(pdfDocument, sourceHanSerifCN,false);
// 添加字体
String microsoftYaHeiBoldFontName = res.add(microsoftYaHeiBoldFont).getName();
String sourceHanSerifCNFontName = res.add(sourceHanSerifCNFont).getName();
String titleAppearance = "/" + microsoftYaHeiBoldFontName + " 18 Tf 0 g";
String contentAppearance = "/" + sourceHanSerifCNFontName + " 11 Tf 0 g";
2.3填充表单并写入输出流
这里的field即为制作表单时填写的表单项名称。至此pdf实际上已经制作完成,但是目前还有一个问题没有解决:pdf文件过大。
注意:
other.setDefaultAppearance(contentAppearance);
这行代码很重要,否则生成的表单将会十分难看!
PDVariableText title = (PDVariableText) acroForm.getField("title");
title.setQ(PDVariableText.QUADDING_LEFT);
title.setDefaultAppearance(titleAppearance);
title.setValue(generatePDFDTO.getTitle());
pdfDocument.save(fileOut);
3.将pdf转为图片并写入pdf文件
这里的originalDocument就是上面的步骤中的PDDocument,我们将pdf文件的每一页都转换为图片再将图片写为一个新的pdf文件。经过测试,一个40MB的文件转换后仅300KB左右,提升了约99%的空间利用率。如果对于图片质量要求不高还可以进一步压缩。
PDDocument newDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(originalDocument);
for (int page = 0; page < originalDocument.getNumberOfPages(); ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
PDImageXObject pdImage = PDImageXObject.createFromByteArray(newDocument, convertBufferedImageToByteArray(bim), "image-" + page);
PDPage newPage = new PDPage(new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
newDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
contentStream.drawImage(pdImage, 0, 0);
contentStream.close();
}
newDocument.save("final-output.pdf");
newDocument.close();
public byte[] convertBufferedImageToByteArray(BufferedImage image) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", baos); // 可以根据需要选择格式
return baos.toByteArray();
}
结尾
除了笔者本次所述生成pdf的方案外,还有很多方式可以生成pdf文档。例如可以让前端使用HTML生成pdf,或者在使用pdfbox时直接写入所有的pdf内容而不使用模板。
笔者这套方案的主要优势是方便生成有较为复杂的样式的pdf文件,同时可以较为精确的控制生成pdf文件。当然这里的精确是相对的,实际上pdfbox可以向某个坐标插入文字,但是这种方式个人认为开发时心智负担较大,不好调试,实际效果可能还不如使用模板的方式。