前不久公司需要为用户生成一份报告,报告的模板是统一的,内容不同,并且用户可以打印。于是调研了有关 PDF 生成的方式,这里简单记录一下。方案是使用 itextpdf 这个类库对 PDF 模板进行内容填充,从而达到效果。demo 代码中有用到 zxing 进行二维码的生成,多张 PDF 合并成一张 PDF 等。

官方 tutorial

Examples iText 7 jump-start tutorial

注意点

准备 PDF From 域需要 AdobeAcrobat DC 中的高级功能,普通版无法添加 From 域。

部分代码

下面的代码实现了 PDF 填充,多张 PDF 合并成一个 PDF,PDF 中添加图片,根据 URL 生成二维码图片。
PDF 模板下载地址:模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package pdf;

import com.alibaba.druid.util.StringUtils;
import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.fields.PdfFormField;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.utils.PdfMerger;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;
import com.itextpdf.layout.element.Paragraph;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* 填充 pdf 并插入 二维码,如果有多个 pdf 进行合并操作
*
* @author Chengloong
*/
public class PdfTempleTest {

private static final int NORMAL_FONT_SIZE = 12;
private static final int TWO_ROWS_FONT_SIZE = 7;

public static void main(String[] args) throws Exception {

//生成二维码
QRCodeUtil.generateQRCode("https://blog.csdn.net/jinqilin721/article/details/78841028", 600, 600, "jpg", "D:\\qr2.png");

//填充 pdf 模板
fillTemplate();

}

/**
* 利用模板生成pdf
*/
public static void fillTemplate() {
// 模板路径
String templatePath = "D:/pdf-template-form.pdf";

List<ByteArrayOutputStream> baosList = new ArrayList<>();
try {

//根据路径生成 pdf
// PdfWriter writer = new PdfWriter(newPDFPath);

//内存中创建 PDF
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfReader reader = new PdfReader(templatePath);
PdfDocument pdf = new PdfDocument(reader, writer);
PdfAcroForm form = PdfAcroForm.getAcroForm(pdf, true);
Map<String, PdfFormField> fields = form.getFormFields();

//处理中文问题
PdfFont font = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H", false);

for (String name : fields.keySet()) {
//获取文本域名称
if (StringUtils.equals(name, "name")) {
fields.get(name).setValue("李四").setFont(font).setFontSize(NORMAL_FONT_SIZE);
}

if (StringUtils.equals(name, "sex")) {
fields.get(name).setValue("男").setFont(font).setFontSize(NORMAL_FONT_SIZE);
}

if (StringUtils.equals(name, "id_number")) {
fields.get(name).setValue("320123199309223211").setFont(font).setFontSize(NORMAL_FONT_SIZE);
}

if (StringUtils.equals(name, "address")) {
fields.get(name).setValue("江苏省无锡市滨湖区第一人民医院").setFont(font).setFontSize(NORMAL_FONT_SIZE);
}

if (StringUtils.equals(name, "input_1")) {
//超过一行的文字使用 7 号字体
fields.get(name).setValue("盐城继续教育公需课我是一段很长很长很长超过一行的文字").setFont(font).setFontSize(TWO_ROWS_FONT_SIZE);
}

if (StringUtils.equals(name, "input_2")) {
fields.get(name).setValue("专业类别").setFont(font).setFontSize(NORMAL_FONT_SIZE);
}

if (StringUtils.equals(name, "input_3")) {
fields.get(name).setValue("学习形式").setFont(font).setFontSize(NORMAL_FONT_SIZE);
}

if (StringUtils.equals(name, "input_4")) {
fields.get(name).setValue("2").setFont(font).setFontSize(NORMAL_FONT_SIZE);
}

if (StringUtils.equals(name, "input_5")) {
fields.get(name).setValue("20180909").setFont(font).setFontSize(NORMAL_FONT_SIZE);
}

if (StringUtils.equals(name, "input_6")) {
fields.get(name).setValue("优秀").setFont(font).setFontSize(NORMAL_FONT_SIZE);
}
}

//生成的 pdf 不可以编辑
form.flattenFields();

//插入图片
Document document = new Document(pdf);
Image image = new Image(ImageDataFactory.create("D:\\qr2.png"));
image.setWidth(80);
image.setHeight(80);

Paragraph paragraph = new Paragraph().add(image);
paragraph.setPaddings(140, 0, 0, 550);

document.add(paragraph);
document.close();
pdf.close();

baosList.add(baos);


mergerPDF(baosList);

} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 合并 PDF
*/
public static void mergerPDF(List<ByteArrayOutputStream> baosList) {
try {
PdfWriter writer = new PdfWriter("D:\\merger.pdf");
PdfDocument pdf = new PdfDocument(writer);

PdfMerger merger = new PdfMerger(pdf);

// 这里可以直接读取已存在的文件
// PdfMerger merger = new PdfMerger(pdf);
// PdfDocument firstSourcePdf = new PdfDocument(new PdfReader("D:\\photo1.pdf"));
// merger.merge(firstSourcePdf, 1, firstSourcePdf.getNumberOfPages());

for (ByteArrayOutputStream byteArrayOutputStream : baosList) {
PdfDocument firstSourcePdf = new PdfDocument(new PdfReader(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())));
merger.merge(firstSourcePdf, 1, firstSourcePdf.getNumberOfPages());
}

pdf.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
package pdf;

import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.Binarizer;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.EncodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.Result;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

/**
* 二维码生成工具类
*/
public final class QRCodeUtil extends LuminanceSource {

private static final Logger logger = LoggerFactory.getLogger(QRCodeUtil.class);

// 二维码颜色
private static final int BLACK = 0xFF000000;
// 二维码颜色
private static final int WHITE = 0xFFFFFFFF;

private final BufferedImage image;
private final int left;
private final int top;

public QRCodeUtil(BufferedImage image) {
this(image, 0, 0, image.getWidth(), image.getHeight());
}

public QRCodeUtil(BufferedImage image, int left, int top, int width, int height) {
super(width, height);
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
if (left + width > sourceWidth || top + height > sourceHeight) {
throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
}
for (int y = top; y < top + height; y++) {
for (int x = left; x < left + width; x++) {
if ((image.getRGB(x, y) & 0xFF000000) == 0) {
image.setRGB(x, y, 0xFFFFFFFF); // = white
}
}
}
this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY);
this.image.getGraphics().drawImage(image, 0, 0, null);
this.left = left;
this.top = top;
}

@Override
public byte[] getRow(int y, byte[] row) {
if (y < 0 || y >= getHeight()) {
throw new IllegalArgumentException("Requested row is outside the image: " + y);
}
int width = getWidth();
if (row == null || row.length < width) {
row = new byte[width];
}
image.getRaster().getDataElements(left, top + y, width, 1, row);
return row;
}

@Override
public byte[] getMatrix() {
int width = getWidth();
int height = getHeight();
int area = width * height;
byte[] matrix = new byte[area];
image.getRaster().getDataElements(left, top, width, height, matrix);
return matrix;
}

@Override
public boolean isCropSupported() {
return true;
}

@Override
public LuminanceSource crop(int left, int top, int width, int height) {
return new QRCodeUtil(image, this.left + left, this.top + top, width, height);
}

@Override
public boolean isRotateSupported() {
return true;
}

@Override
public LuminanceSource rotateCounterClockwise() {
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth);
BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = rotatedImage.createGraphics();
g.drawImage(image, transform, null);
g.dispose();
int width = getWidth();
return new QRCodeUtil(rotatedImage, top, sourceWidth - (left + width), getHeight(), width);
}

/**
* @param matrix
* @return
*/
private static BufferedImage toBufferedImage(BitMatrix matrix) {
int width = matrix.getWidth();
int height = matrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE);
}
}
return image;
}

/**
* 生成二维码图片
*
* @param matrix
* @param format
* @param file
* @throws IOException
*/
public static void writeToFile(BitMatrix matrix, String format, File file) throws IOException {
BufferedImage image = toBufferedImage(matrix);
if (!ImageIO.write(image, format, file)) {
throw new IOException("Could not write an image of format " + format + " to " + file);
}
}

/**
* 生成二维码图片流
*
* @param matrix
* @param format
* @param stream
* @throws IOException
*/
public static void writeToStream(BitMatrix matrix, String format, OutputStream stream) throws IOException {
BufferedImage image = toBufferedImage(matrix);
if (!ImageIO.write(image, format, stream)) {
throw new IOException("Could not write an image of format " + format);
}
}

/**
* 根据内容,生成指定宽高、指定格式的二维码图片
*
* @param text 内容
* @param width 宽
* @param height 高
* @param format 图片格式
* @return 生成的二维码图片路径
* @throws Exception
*/
public static String generateQRCode(String text, int width, int height, String format, String pathName)
throws Exception {
Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");// 指定编码格式
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);// 指定纠错等级
hints.put(EncodeHintType.MARGIN, 0); // 白边大小,取值范围0~4
BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
File outputFile = new File(pathName);
writeToFile(bitMatrix, format, outputFile);
return pathName;
}

/**
* 输出二维码图片流
*
* @param text 二维码内容
* @param width 二维码宽
* @param height 二维码高
* @param format 图片格式eg: png, jpg, gif
* @param response HttpServletResponse
* @throws Exception
*/
public static void generateQRCode(String text, int width, int height, String format, HttpServletResponse response)
throws Exception {
Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");// 指定编码格式
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);// 指定纠错等级
hints.put(EncodeHintType.MARGIN, 1); // 白边大小,取值范围0~4
BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
writeToStream(bitMatrix, format, response.getOutputStream());
}

/**
* 解析指定路径下的二维码图片
*
* @param filePath 二维码图片路径
* @return
*/
public static String parseQRCode(String filePath) {
String content = "";
try {
File file = new File(filePath);
BufferedImage image = ImageIO.read(file);
LuminanceSource source = new QRCodeUtil(image);
Binarizer binarizer = new HybridBinarizer(source);
BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer);
Map<DecodeHintType, Object> hints = new HashMap<>();
hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
MultiFormatReader formatReader = new MultiFormatReader();
Result result = formatReader.decode(binaryBitmap, hints);

logger.info("result 为:" + result.toString());
logger.info("resultFormat 为:" + result.getBarcodeFormat());
logger.info("resultText 为:" + result.getText());
// 设置返回值
content = result.getText();
} catch (Exception e) {
logger.error(e.getMessage());
}
return content;
}

public static void main(String[] args) {
String text = "hello world!"; // 随机生成验证码
System.out.println("随机码: " + text);
int width = 100; // 二维码图片的宽
int height = 100; // 二维码图片的高
String format = "png"; // 二维码图片的格式

try {
// 生成二维码图片,并返回图片路径
String pathName = generateQRCode(text, width, height, format, "D:/new.png");
System.out.println("生成二维码的图片路径: " + pathName);

String content = parseQRCode(pathName);
System.out.println("解析出二维码的图片的内容为: " + content);
} catch (Exception e) {
e.printStackTrace();
}
}
}