目录
-
- [1. 环境准备](#1. 环境准备)
- [2. 代码实现](#2. 代码实现)
- [2.1 左上角添加 logo](#2.1 左上角添加 logo)
- [2.2 添加可见标题](#2.2 添加可见标题)
- [2.3 基础单据信息](#2.3 基础单据信息)
- [2.4 动态表格渲染](#2.4 动态表格渲染)
- [2.5 二维码区域](#2.5 二维码区域)
- [2.6 添加水印](#2.6 添加水印)
- [2.7 辅助类和数据](#2.7 辅助类和数据)
- [3. 开发中的调整经验](#3. 开发中的调整经验)
- [4. 运行与测试](#4. 运行与测试)
实现效果

引言
在企业开发中,动态生成 PDF(如出库单、发票)是常见需求。传统方法依赖模板,灵活性不足。本文将展示如何使用 Java 17 结合 iTextPDF 和 ZXing 库,实现在无模版情况下动态生成 PDF,嵌入 logo、渲染动态表格并添加二维码和水印。文章提供完整 demo 代码和开发经验分享。
技术要点
- 无模版生成:无需预设模板,纯代码构建。
- 图片嵌入:添加 logo 等图像。
- 动态表格渲染:根据数据动态生成多列表格。
- Java 17 环境:使用现代 Java 版本。
- 依赖管理:通过 Maven 配置 iTextPDF 和 ZXing。
实现步骤
1. 环境准备
使用 Java 17,依赖通过 Maven 管理。以下是 pom.xml
文件:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>untitled</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.13.3</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
</project>
运行 mvn clean install
安装依赖。
2. 代码实现
2.1 左上角添加 logo
添加 logo 图片到 PDF 左上角:
java
private static void addLogo(Document document, PdfWriter writer) throws DocumentException, IOException {
// 替换为实际 logo 图片路径,例如 "src/main/resources/logo.jpeg"
Image logo = Image.getInstance("src/main/resources/logo.jpeg");
logo.scaleToFit(100, 100); // 调整 logo 大小
logo.setAlignment(Image.ALIGN_LEFT);
document.add(logo); // 使用文档流插入
}

好吧,其实并不是这样的logo,往下慢慢看吧。
2.2 添加可见标题
在文档顶部添加居中标题:
java
private static void addDocumentTitle(Document document) throws DocumentException, IOException {
BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
Font titleFont = new Font(bfChinese, 20, Font.BOLD);
Paragraph title = new Paragraph("出库单", titleFont);
title.setAlignment(Element.ALIGN_CENTER);
// title.setSpacingBefore(5f);
// title.setSpacingAfter(5f);
document.add(title);
}
2.3 基础单据信息
使用 4 列表格展示基础信息,动态从 API 数据填充:
java
// 添加基础单据信息 (4列布局,增加间距)
private static void addBasicInfo(Document document) throws DocumentException, IOException {
PdfPTable table = new PdfPTable(4);
table.setWidthPercentage(100);
table.setSpacingBefore(150f);
table.setSpacingAfter(50f);
BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
Font headerFont = new Font(bfChinese, 14, Font.BOLD);
Font contentFont = new Font(bfChinese, 14);
Map<String, String> apiData = getBaseInfoFromApi();
List<String> keys = new ArrayList<>(fieldNameToLabelMap.keySet());
for (int i = 0; i < keys.size(); i += 2) {
String key1 = keys.get(i);
String key2 = (i + 1 < keys.size()) ? keys.get(i + 1) : "";
String label1 = fieldNameToLabelMap.get(key1);
String value1 = apiData.getOrDefault(key1, "");
String label2 = key2.isEmpty() ? "" : fieldNameToLabelMap.get(key2);
String value2 = key2.isEmpty() ? "" : apiData.getOrDefault(key2, "");
addInfoRow(table, label1, value1, label2, value2, headerFont, contentFont);
}
document.add(table);
}
// 工具方法:添加单行 (4列)
private static void addInfoRow(PdfPTable table, String key1, String value1, String key2, String value2, Font keyFont, Font valueFont) {
PdfPCell keyCell1 = new PdfPCell(new Phrase(key1, keyFont));
keyCell1.setHorizontalAlignment(Element.ALIGN_LEFT);
keyCell1.setBorderWidth(0.5f);
keyCell1.setPadding(10f);
keyCell1.setMinimumHeight(25f);
PdfPCell valueCell1 = new PdfPCell(new Phrase(value1, valueFont));
valueCell1.setHorizontalAlignment(Element.ALIGN_LEFT);
valueCell1.setBorderWidth(0.5f);
valueCell1.setPadding(10f);
valueCell1.setMinimumHeight(25f);
PdfPCell keyCell2 = new PdfPCell(new Phrase(key2, keyFont));
keyCell2.setHorizontalAlignment(Element.ALIGN_LEFT);
keyCell2.setBorderWidth(0.5f);
keyCell2.setPadding(10f);
keyCell2.setMinimumHeight(25f);
PdfPCell valueCell2 = new PdfPCell(new Phrase(value2, valueFont));
valueCell2.setHorizontalAlignment(Element.ALIGN_LEFT);
valueCell2.setBorderWidth(0.5f);
valueCell2.setPadding(10f);
valueCell2.setMinimumHeight(25f);
table.addCell(keyCell1);
table.addCell(valueCell1);
table.addCell(keyCell2);
table.addCell(valueCell2);
}
2.4 动态表格渲染
动态生成出库单明细表格:
java
private static void addOutboundDetails(Document document) throws DocumentException, IOException {
// 模拟从接口获取的动态数据
List<OutboundDetail> details = generateSampleDetails();
// 创建表格 (7列: 序号, 订单号, 订单明细号, 产品名称, 单位, 数量, 单价)
PdfPTable table = new PdfPTable(7);
table.setWidthPercentage(100);
table.setSpacingBefore(50f); // 增加间距,向下移动以避免被上部内容覆盖
// 使用支持中文的字体
BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
Font headerFont = new Font(bfChinese, 12, Font.BOLD);
Font contentFont = new Font(bfChinese, 12);
// 表头
addTableHeader(table, headerFont);
// 动态添加数据
for (int i = 0; i < details.size(); i++) {
OutboundDetail detail = details.get(i);
table.addCell(createCell(String.valueOf(i + 1), contentFont));
table.addCell(createCell(detail.getOrderId(), contentFont));
table.addCell(createCell(detail.getDetailId(), contentFont));
table.addCell(createCell(detail.getProductName(), contentFont));
table.addCell(createCell(detail.getUnit(), contentFont));
table.addCell(createCell(String.valueOf(detail.getQuantity()), contentFont));
table.addCell(createCell(String.format("%.2f", detail.getUnitPrice()), contentFont));
}
document.add(table);
}
// 添加表格表头
private static void addTableHeader(PdfPTable table, Font font) {
String[] headers = {"序号", "订单号", "订单明细号", "产品名称", "单位", "数量", "单价"};
for (String header : headers) {
PdfPCell cell = new PdfPCell(new Phrase(header, font));
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
cell.setBackgroundColor(BaseColor.LIGHT_GRAY);
cell.setMinimumHeight(25f);
table.addCell(cell);
}
}
// 创建单元格
private static PdfPCell createCell(String content, Font font) {
PdfPCell cell = new PdfPCell(new Phrase(content, font));
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
cell.setBorderWidth(0.5f);
cell.setMinimumHeight(25f);
cell.setPadding(8f);
return cell;
}
2.5 二维码区域
在右上角添加二维码:
java
private static void addQRCodePlaceholder(Document document, PdfWriter writer) throws DocumentException {
try {
// 获取第一个订单信息作为二维码内容
List<OutboundDetail> details = generateSampleDetails();
if (!details.isEmpty()) {
OutboundDetail detail = details.get(0);
String qrContent = "OrderID: " + detail.getOrderId() + "\nUnitPrice: " + detail.getUnitPrice() +
"\nQuantity: " + detail.getQuantity() + "\nDate: 2025-07-10";
// 生成二维码图像
int size = 100;
BitMatrix bitMatrix = new MultiFormatWriter().encode(qrContent, BarcodeFormat.QR_CODE, size, size);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "png", baos);
Image qrImage = Image.getInstance(baos.toByteArray());
qrImage.scaleToFit(size, size);
// 设置二维码位置 ------ 保持原有位置
float x = PageSize.A4.getWidth() - size - 10f;
float y = PageSize.A4.getHeight() - qrImage.getScaledHeight() - 10f;
qrImage.setAbsolutePosition(x, y);
writer.getDirectContent().addImage(qrImage);
} else {
Paragraph qr = new Paragraph();
qr.add(new Chunk("二维码区域 (无数据)\n", FontFactory.getFont(FontFactory.HELVETICA, 14, Font.BOLD)));
qr.setAlignment(Element.ALIGN_RIGHT);
document.add(qr);
}
} catch (Exception e) {
e.printStackTrace();
Paragraph qr = new Paragraph();
qr.add(new Chunk("二维码区域 (生成失败)\n", FontFactory.getFont(FontFactory.HELVETICA, 14, Font.BOLD)));
qr.setAlignment(Element.ALIGN_RIGHT);
document.add(qr);
}
}
2.6 添加水印
在 PDF 背景添加倾斜水印:
java
private static void addTextWatermark(PdfWriter writer, String watermarkText) throws IOException, DocumentException {
PdfContentByte under = writer.getDirectContentUnder();
// 设置字体
BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
under.saveState();
// 设置透明度
PdfGState gs1 = new PdfGState();
gs1.setFillOpacity(0.1f); // 越小越淡
under.setGState(gs1);
// 设置字体大小和颜色
under.beginText();
under.setFontAndSize(baseFont, 60); // 字体大小
under.setColorFill(BaseColor.LIGHT_GRAY);
// 页面宽高
float pageWidth = PageSize.A4.getWidth();
float pageHeight = PageSize.A4.getHeight();
// 网格循环:每隔一定间距绘制一行水印(x 横向,y 纵向)
for (float x = -100; x < pageWidth + 100; x += 200) {
for (float y = -100; y < pageHeight + 100; y += 150) {
under.showTextAligned(Element.ALIGN_CENTER, watermarkText, x, y, 45); // 倾斜 45 度
}
}
under.endText();
under.restoreState();
}
}
2.7 辅助类和数据
数据模型和模拟 API 数据,包括 fieldNameToLabelMap
初始化:
java
static class OutboundDetail {
private String orderId;
private String detailId;
private String productName;
private String unit;
private double quantity;
private double unitPrice;
public OutboundDetail(String orderId, String detailId, String productName, String unit, double quantity, double unitPrice) {
this.orderId = orderId;
this.detailId = detailId;
this.productName = productName;
this.unit = unit;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public String getOrderId() { return orderId; }
public String getDetailId() { return detailId; }
public String getProductName() { return productName; }
public String getUnit() { return unit; }
public double getQuantity() { return quantity; }
public double getUnitPrice() { return unitPrice; }
}
private static final Map<String, String> fieldNameToLabelMap = new LinkedHashMap<>();
static {
fieldNameToLabelMap.put("orderCode", "单据编号");
fieldNameToLabelMap.put("outboundDate", "出库日期");
fieldNameToLabelMap.put("customerName", "客户名称");
fieldNameToLabelMap.put("warehouseCode", "仓库编号");
fieldNameToLabelMap.put("creator", "制单人");
fieldNameToLabelMap.put("auditor", "审核人");
fieldNameToLabelMap.put("createTime", "制单时间");
fieldNameToLabelMap.put("outboundOrderId", "出库单据ID");
}
private static Map<String, String> getBaseInfoFromApi() {
Map<String, String> data = new HashMap<>();
data.put("orderCode", "BF-03-05-1");
data.put("outboundDate", "2025-06-04");
data.put("customerName", "某某公司");
data.put("warehouseCode", "CK_7525080416537318");
data.put("creator", "张三");
data.put("auditor", "李四");
data.put("createTime", "2025-06-04 16:15:48");
data.put("outboundOrderId", "13500135000");
return data;
}
private static List<OutboundDetail> generateSampleDetails() {
List<OutboundDetail> details = new ArrayList<>();
details.add(new OutboundDetail("BF-03-05-1", "no1", "34CrNiMo6", "原材料", 6.60, 100.00));
details.add(new OutboundDetail("BF-03-05-2", "no2", "35CrMo", "半成品", 5.5, 120.00));
details.add(new OutboundDetail("BF-03-05-3", "no3", "Q235", "成品", 10.0, 80.00));
return details;
}
public static void main(String[] args) {
try {
Document document = new Document(PageSize.A4);
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream("outbound_document.pdf"));
document.open();
addLogo(document, writer);
addDocumentTitle(document);
addBasicInfo(document);
addOutboundDetails(document);
addQRCodePlaceholder(document, writer);
addTextWatermark(writer, "ikun");
document.close();
System.out.println("PDF 生成成功!文件位置: outbound_document.pdf");
} catch (Exception e) {
e.printStackTrace();
}
}
3. 开发中的调整经验
开发初期,我将二维码位置使用 setAbsolutePosition
固定在右上角(例如 x = PageSize.A4.getWidth() - 100f, y = PageSize.A4.getHeight() - 100f
),导致基础信息表格始终被覆盖。问题出在绝对定位与文档流冲突上。调整过程:
- 增加间距 :将
addBasicInfo
中的setSpacingBefore
从 50f 调整到 150f,确保表格向下移动。 - 动态布局 :改为使用
document.add()
添加元素,避免固定位置重叠。 - 调试优化 :通过生成 PDF 逐一检查布局,逐步调整
setSpacingBefore
值(例如增至 100f 或 150f),确保内容可见。
4. 运行与测试
- 将
logo.jpeg
放入src/main/resources/
目录。 - 运行
main
方法,生成outbound_document.pdf
。 - 检查 PDF 是否包含 logo、标题、基础信息表格、动态表格、二维码和水印。
注意事项
- 图片路径 :确保
logo.jpeg
路径正确。 - 字体支持 :依赖
itext-asian
,若字体加载失败,可替换为本地字体(如SimSun.ttf
)。 - 依赖冲突 :若版本冲突,调整
pom.xml
中的依赖版本。
总结
本文通过 Java 17 实现了无模版动态生成 PDF,结合 iTextPDF 嵌入图片和渲染动态表格,ZXing 生成二维码,并添加水印。完整 demo 代码提供参考,开发中通过调整间距解决了布局冲突问题。欢迎评论区交流优化建议!
作者 :@Brain
demo: 代码请见gitee