写在前面
由于之前使用Itext5工具填充PDF模板后,会导致填充后的PDF文件体积变得很庞大。
怀疑了嵌入字体、PDF模板编辑转换和编辑等等的原因,但最后都无功而返,查阅了官方文档,也没得出解决方案。
因此,退而求其次,换上了Itext7,官方说过,性能相较Itext5更出色。
依赖
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>8.0.1</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>bouncy-castle-adapter</artifactId>
<version>8.0.1</version>
</dependency>
注:引入bouncy-castle-adapter是因为Itext7解析PDF时用到了加密算法,因此可能还需引入
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
工具
import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.fields.PdfFormCreator;
import com.itextpdf.forms.fields.PdfFormField;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.image.ImageData;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.kernel.events.Event;
import com.itextpdf.kernel.events.IEventHandler;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.*;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfWidgetAnnotation;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
/**
* PDF 模板填充工具类(基于itext7)
*/
@Slf4j
public final class Pdf7TemplateFillUtil {
/**
* 待填充的PDF模板文档
*/
private final transient PdfReader template;
/**
* 未填充的表单域可编辑
*/
private boolean unfilledFormEditable = false;
/**
* 显示页码
*/
private boolean pageNo = false;
/**
* 首页显示页码
*/
private boolean firstPageNo = false;
/**
* 填充及返回填充后的数据
*
* @param fillData - 待填充数据
* @return - 填充后bytes数据
*/
public byte[] fill(Map<String, Object> fillData) throws IOException {
ByteArrayOutputStream dest = new ByteArrayOutputStream();
PdfDocument doc = null;
try {
PdfFont font = PdfFontFactory.createFont("fons/AlibabaPuHuiTi-3-35-Thin.ttf",// font class path
PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
PdfWriter pdfWriter = new PdfWriter(dest);
pdfWriter.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
doc = new PdfDocument(template, pdfWriter);
if (pageNo) {
doc.addEventHandler(PdfDocumentEvent.END_PAGE, new PageNumberEvent(font, firstPageNo));
}
PdfAcroForm form = PdfAcroForm.getAcroForm(doc, true);
for (Map.Entry<String, Object> entry : fillData.entrySet()) {
String keyword = entry.getKey();
PdfFormField templateFormField = form.getField(keyword);
if (templateFormField == null) {
continue;
}
Object value = entry.getValue();
if (value instanceof byte[]) {
PdfArray pos = templateFormField.getWidgets().get(0).getRectangle();
float x = pos.getAsNumber(0).floatValue();
float y = pos.getAsNumber(1).floatValue();
float width = pos.getAsNumber(2).floatValue() - x;
float height = pos.getAsNumber(3).floatValue() - y;
Rectangle rectangle = new Rectangle(x, y, width, height);
PdfFormField formField = PdfFormCreator.createFormField(new PdfWidgetAnnotation(rectangle), doc);
PdfPage annotationPage = findAnnotationPage(doc, keyword);
if (annotationPage != null) {
doFillFieldImage(annotationPage, formField, (byte[]) value);
}
} else {
if (value instanceof Boolean) {
templateFormField.setValue(String.valueOf(value), true);
} else {
templateFormField.setValue(String.valueOf(value));
}
templateFormField.setFontAndSize(font, templateFormField.getFontSize());
templateFormField.setReadOnly(true);
}
if (unfilledFormEditable) {
form.partialFormFlattening(keyword);
}
}
form.flattenFields();
} finally {
if (doc != null && !doc.isClosed()) {
doc.close();
}
}
return dest.toByteArray();
}
/**
* 启用未填充的表单域可编辑
*/
public Pdf7TemplateFillUtil unfilledFormEditable() {
this.unfilledFormEditable = true;
return this;
}
/**
* 启用页码
*/
public Pdf7TemplateFillUtil pageNumber() {
this.pageNo = true;
return this;
}
/**
* 启用页码
*
* @param firstPageNo - 首页显示页码
*/
public Pdf7TemplateFillUtil pageNumber(boolean firstPageNo) {
this.pageNo = true;
this.firstPageNo = firstPageNo;
return this;
}
/**
* 图片填充
*
* @param newPage - 当前页
* @param formField - 表单文本域
* @param imgBytes - 图片文件字节数组
*/
private void doFillFieldImage(PdfPage newPage, PdfFormField formField, byte[] imgBytes) {
Rectangle rtl = formField.getWidgets().get(0).getRectangle().toRectangle(); // 获取表单域的xy坐标
PdfCanvas canvas = new PdfCanvas(newPage);
ImageData img = ImageDataFactory.create(imgBytes);
if (Float.compare(img.getWidth(), rtl.getWidth()) <= 0 && Float.compare(img.getHeight(), rtl.getHeight()) <= 0) {// 不处理
canvas.addImageAt(img, rtl.getX(), rtl.getY(), true);
} else {
// 压缩图片。计算得到图片放缩的最大比例
float scale = Math.max(img.getWidth() / rtl.getWidth(), img.getHeight() / rtl.getHeight());
int imgWidth = Math.round(img.getWidth() / scale);
int imgHeight = Math.round(img.getHeight() / scale);
// 压缩图片
byte[] compressImgBytes;
try {
compressImgBytes = ImageCompressUtils.resizeByThumbnails(imgBytes, imgWidth, imgHeight);
} catch (IOException e) {
throw new RuntimeException(e);
}
img = ImageDataFactory.create(compressImgBytes);
canvas.addImageAt(img, rtl.getX(), rtl.getY(), true);
}
}
/**
* 根据表单域关键字查找当前关键字所在页对象(PdfPage)
*
* @param keyword - 关键字
* @return - page object
*/
private PdfPage findAnnotationPage(PdfDocument doc, String keyword) {
int pages = doc.getNumberOfPages();
for (int index = 1; index <= pages; index++) {
PdfPage page = doc.getPage(index);
for (PdfAnnotation annotation : page.getAnnotations()) {
PdfString title = annotation.getPdfObject().getAsString(PdfName.T);
if (title != null && keyword.equals(String.valueOf(title))) {
return page;
}
}
}
return null;
}
/**
* 获取模板文件表单域关键字位置信息
*/
private Map<Integer, Map<String, float[]>> getFormKeywordsPos(PdfDocument doc) {
int pages = doc.getNumberOfPages();
Map<Integer, Map<String, float[]>> maps = new HashMap<>(pages);
for (int index = 1; index <= pages; index++) {
maps.putIfAbsent(index, new HashMap<>());
PdfPage page = doc.getPage(index);
// 获取当前页的表单域
int finalIndex = index;
page.getAnnotations().forEach(anno -> {
PdfString title = anno.getTitle();
PdfArray rectangle = anno.getRectangle();
float x = rectangle.getAsNumber(0).floatValue();
float y = rectangle.getAsNumber(1).floatValue();
float width = rectangle.getAsNumber(2).floatValue() - x;
float height = rectangle.getAsNumber(3).floatValue() - y;
maps.get(finalIndex).put(title.getValue(), new float[]{x, y, width, height});
});
}
return maps;
}
private Pdf7TemplateFillUtil(PdfReader template) {
this.template = template;
}
public static Pdf7TemplateFillUtil load(String template) throws IOException {
return new Pdf7TemplateFillUtil(new PdfReader(template));
}
public static Pdf7TemplateFillUtil load(File template) throws IOException {
return new Pdf7TemplateFillUtil(new PdfReader(template));
}
public static Pdf7TemplateFillUtil load(byte[] template) throws IOException {
return new Pdf7TemplateFillUtil(new PdfReader(new ByteArrayInputStream(template)));
}
public static Pdf7TemplateFillUtil load(InputStream template) throws IOException {
return new Pdf7TemplateFillUtil(new PdfReader(template));
}
static class PageNumberEvent implements IEventHandler {
private transient final PdfFont font;
private transient final boolean firstPageNo;
PageNumberEvent(PdfFont font, boolean firstPageNo) {
this.font = font;
this.firstPageNo = firstPageNo;
}
@Override
public void handleEvent(Event event) {
PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
PdfDocument pdf = docEvent.getDocument();
PdfPage page = docEvent.getPage();
int pageNumber = pdf.getPageNumber(page);
pageNumber = firstPageNo ? pageNumber : pageNumber - 1;
if (pageNumber > 0) {
Rectangle pageSize = page.getPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(page.getLastContentStream(), page.getResources(), pdf);
Canvas canvas = new Canvas(pdfCanvas, pageSize);
float x = (pageSize.getLeft() + pageSize.getRight()) / 2;
float y = pageSize.getBottom() + 10;
Paragraph p = new Paragraph(String.valueOf(pageNumber))
.setFontSize(10)
.setFont(font);
canvas.showTextAligned(p, x, y, TextAlignment.CENTER);
canvas.close();
}
}
}
}
特别注意
字体加载有一个巨坑,一定要放在具体的方法中去加载,保证用完即销毁。
否则填充时会出错:Pdf indirect object belongs to other PDF document....