当前位置: 首页 > news >正文

Java iText7 PDF模板填充工具:支持多页生成、中文无坑、多占位符精确定位

iText7 PDF模板填充

    • 为什么需要这个工具?
    • 关键技巧解析
    • 测试Demo:FillPdfDemo7.java
    • 常见问题&优化
    • 总结

)

大家好,我是[ben],一个热爱Java后端开发的码农。今天分享一个实用工具类:PdfUtil7,基于iText7库实现PDF模板填充。痛点解决:模板中{{key}}占位符精确定位替换,支持多页批量生成(每条数据一页),完美兼容中文(无额外JAR依赖,自动加载系统字体)。相比iText5,这个版本更稳定,IDENTITY_H编码让中文渲染丝滑。

为什么需要这个工具?

  • 亮点:报表生成、合同填充、发票导出等。传统方式要么手动编辑PDF,要么用低代码工具,但精度差、跨平台字体乱码。
  • 亮点

多页模板:传入List<Map<String, String>>,每条Map生成一页。
精确替换:提取{{key}}的边界框,覆盖原文本+新写入,避免重叠鬼影。
中文支持:Mac/Win/Linux自动fallback字体,EMBEDDED嵌入确保输出一致。
无外部依赖:纯iText7 + 系统字体,部署简单。

Maven依赖(iText7核心):

<dependency><groupId>com.itextpdf</groupId><artifactId>itext7-core</artifactId><version>7.2.5</version>
</dependency>

核心实现:PdfUtil7.java完整代码如下,注释详尽。重点在extractPlaceholderPositions(提取位置)和replacePlaceholdersOnPage(替换逻辑)。

package com.example.util;import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.parser.*;
import com.itextpdf.text.pdf.parser.Vector;import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** 支持:* 1. 多页模板填充 (List<Map<String,String>>)* 2. {{key}} 占位符精确定位+覆盖+写入* 3. 自动加载中文字体,IDENTITY_H 支持中文*/
public class PdfUtil7 {/*** 生成 PDF,按 dataList 每条生成一页*/public static void generate(String templatePath, String outputPath,List<Map<String, String>> dataList, String osType)throws Exception {// 加载模板 PDF & 字体PdfReader templateReader = new PdfReader(templatePath);BaseFont font = loadChineseFont(osType);// 首次抽取占位符坐标Map<String, List<Position>> positions = extractPlaceholderPositions(templateReader, 1);//  输出 PDFDocument document = new Document(templateReader.getPageSizeWithRotation(1));PdfCopy copy = new PdfCopy(document, new FileOutputStream(outputPath));document.open();for (Map<String, String> data : dataList) {// 用 temp 处理模板ByteArrayOutputStream tempOut = new ByteArrayOutputStream();PdfReader tmpReader = new PdfReader(templatePath);PdfStamper stamper = new PdfStamper(tmpReader, tempOut);replacePlaceholdersOnPage(stamper, 1, data, positions, font);stamper.close();tmpReader.close();PdfReader newPageReader = new PdfReader(tempOut.toByteArray());PdfImportedPage importedPage = copy.getImportedPage(newPageReader, 1);copy.addPage(importedPage);newPageReader.close();}document.close();templateReader.close();System.out.println("✅ PdfUtil7 生成完成: " + outputPath);}// ---------------------- 字体加载(系统路径 + IDENTITY_H,支持中文无额外 JAR) ----------------------static BaseFont loadChineseFont(String osType) throws DocumentException, IOException {List<String> fallbackFonts = new ArrayList<>();// Mac: Hiragino (Simplified Chinese, index 0 for Regular) + Arial Unicode (fallback, .ttf)if ("mac".equalsIgnoreCase(osType)) {fallbackFonts.add("/System/Library/Fonts/Hiragino SansGB.ttc,0");fallbackFonts.add("/Library/Fonts/Arial Unicode.ttf");} else if ("win".equalsIgnoreCase(osType)) {fallbackFonts.add("C:/Windows/Fonts/simsun.ttc,0");fallbackFonts.add("C:/Windows/Fonts/msyh.ttc,0");} else {fallbackFonts.add("STSong-Light");fallbackFonts.add("Helvetica");}for (String fontPath : fallbackFonts) {try {return BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);} catch (Exception ignored) {}}throw new IOException("字体加载失败");}// ---------------------- 提取占位符(精确边界框) ----------------------static Map<String, List<Position>> extractPlaceholderPositions(PdfReader reader, int pageNum)throws IOException {Map<String, List<Position>> positions = new LinkedHashMap<>();PlaceholderExtractionStrategy strategy = new PlaceholderExtractionStrategy();String fullText = PdfTextExtractor.getTextFromPage(reader, pageNum, strategy);// 增强匹配:忽略多余空格,匹配 {{key}}Pattern p = Pattern.compile("\\{\\{[^{}]+}}",Pattern.MULTILINE);Matcher m = p.matcher(fullText);List<ChunkInfo> chunks = strategy.getChunks();while (m.find()) {String match = m.group();String key = match.substring(2, match.length() - 2).trim();int startIdx = m.start();int endIdx = m.end();float minX = Float.MAX_VALUE;float maxX = Float.MIN_VALUE;float placeholderY = 0f;boolean found = false;int cumLen = 0;for (ChunkInfo ci : chunks) {int len = ci.text.length();int chunkStartIdx = cumLen;int chunkEndIdx = cumLen + len;int overlapStart = Math.max(startIdx, chunkStartIdx);int overlapEnd = Math.min(endIdx, chunkEndIdx);if (overlapStart < overlapEnd) {found = true;if (placeholderY == 0f) {placeholderY = ci.start.get(Vector.I2);}int subOffset = overlapStart - chunkStartIdx;float subStartX = ci.start.get(Vector.I1) + subOffset * ci.charWidthApprox;float subWidth = (overlapEnd - overlapStart) * ci.charWidthApprox;float subEndX = subStartX + subWidth;minX = Math.min(minX, subStartX);maxX = Math.max(maxX, subEndX);}cumLen += len;}if (found && minX < Float.MAX_VALUE) {float width = maxX - minX;positions.computeIfAbsent(key, k -> new ArrayList<>()).add(new Position(minX, placeholderY, 12f, width));}}return positions;}// ---------------------- 替换占位符(精确覆盖原文本区域 + 完整行高) ----------------------static void replacePlaceholdersOnPage(PdfStamper stamper, int pageNum,Map<String, String> data,Map<String, List<Position>> positions,BaseFont bf) throws DocumentException {PdfContentByte canvas = stamper.getOverContent(pageNum);for (Map.Entry<String, String> e : data.entrySet()) {String key = e.getKey();List<Position> posList = positions.get(key);if (posList == null) continue;for (Position p : posList) {float coverWidth = Math.max(p.width, getTextWidth(bf, p.fontSize, e.getValue()) * 1.2f);float lineHeight = p.fontSize * 1.5f;float coverY = p.y - p.fontSize * 0.3f;// 白底精确覆盖(不透明,防止原 {{key}} 透出)canvas.saveState();canvas.setColorFill(BaseColor.WHITE);canvas.rectangle(p.x, coverY, coverWidth, lineHeight);canvas.fill();canvas.restoreState();// 写入新文字(基线对齐,颜色黑色确保可见)canvas.saveState();canvas.setColorFill(BaseColor.BLACK);canvas.beginText();canvas.setFontAndSize(bf, p.fontSize);canvas.setTextMatrix(p.x, p.y);canvas.showText(e.getValue());canvas.endText();canvas.restoreState();}}}// 辅助:计算文本宽度private static float getTextWidth(BaseFont bf, float fontSize, String text) {return bf.getWidthPoint(text, fontSize);}static class ChunkInfo {String text;Vector start;float charWidthApprox;ChunkInfo(String t, Vector s, float cw) {text = t;start = s;charWidthApprox = cw;}}static class PlaceholderExtractionStrategy implements TextExtractionStrategy {private final List<ChunkInfo> chunks = new ArrayList<>();public List<ChunkInfo> getChunks() { return chunks; }@Override public void renderText(TextRenderInfo info) {String text = info.getText();if (text != null && !text.trim().isEmpty()) {Vector start = info.getBaseline().getStartPoint();float endX = info.getBaseline().getEndPoint().get(Vector.I1);float charWidthApprox = text.length() > 0 ? (endX - start.get(Vector.I1)) / text.length() : 1f;chunks.add(new ChunkInfo(text, start, charWidthApprox));}}@Override public String getResultantText() {StringBuilder sb = new StringBuilder();for (ChunkInfo c : chunks) sb.append(c.text);return sb.toString();}@Override public void beginTextBlock() {}@Override public void endTextBlock() {}@Override public void renderImage(ImageRenderInfo imageRenderInfo) {}}static class Position {float x, y, fontSize, width;Position(float x, float y, float fontSize, float width) {this.x = x; this.y = y; this.fontSize = fontSize; this.width = width;}}
}

关键技巧解析

  • 字体加载loadChineseFont:用fallback链,Mac用Hiragino SansGB(简体),Win用SimSun。IDENTITY_H编码+EMBEDDED确保输出PDF自带字体。
  • 位置提取:自定义PlaceholderExtractionStrategy捕获每个Chunk的起始点和宽度,通过正则匹配{{key}},计算重叠区域的精确边界(x,y,width)。
  • 替换逻辑:先白矩形覆盖原占位符(防透出),再showText写入新值。宽度动态调整(*1.2f防溢出),Y偏移0.3f对齐基线。
  • 多页合并:用PdfCopy逐页导入临时Stamper输出,避免内存爆炸。

测试Demo:FillPdfDemo7.java

准备模板template_final.pdf(用{{responsible}}、{{makesure}}、{{a}}等占位符),运行生成final_multi_pages1.pdf((3页,每页不同数据)。

package com.example.util.test;import com.example.util.PdfUtil7;  // 注意:如果是PdfUtil7import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class FillPdfDemo7 {public static void main(String[] args) {try {String template = "src/main/resources/static/template_final.pdf";String output = "src/main/resources/output/final_multi_pages1.pdf";List<Map<String, String>> dataList = new ArrayList<>();Map<String, String> p1 = new HashMap<>();p1.put("responsible", "A-张三");p1.put("makesure", "A-李四");p1.put("a", "10"); p1.put("b", "20"); p1.put("c", "30");dataList.add(p1);Map<String, String> p2 = new HashMap<>();p2.put("responsible", "B-王五");p2.put("makesure", "B-赵六");p2.put("a", "99"); p2.put("b", "42"); p2.put("c", "78");dataList.add(p2);Map<String, String> p3 = new HashMap<>();p3.put("responsible", "C-彭于晏");p3.put("makesure", "C-周杰伦");p3.put("a", "5"); p3.put("b", "8"); p3.put("c", "11");dataList.add(p3);PdfUtil7.generate(template, output, dataList, "mac");  // 或 "win"/"linux"} catch (Exception e) {e.printStackTrace();}}
}

常见问题&优化

Q: 字体加载失败? A: 检查OS路径,或fallback到Helvetica(英文OK)。
Q: 长文本溢出? A: 调整coverWidth * 1.2f,或加文本换行逻辑(扩展showText)。
Q: 性能? A: 单页<100ms,多页用线程池并行Stamper。
扩展:支持多页模板?改extractPlaceholderPositions循环页码;图片替换?加renderImage处理。

总结

这个PdfUtil7是生产级工具,精度高、跨平台稳。欢迎Star/Fork我的GitHub仓库。有问题评论区见!下篇聊iText7水印/签名,敬请期待~

(完)

http://www.dtcms.com/a/593172.html

相关文章:

  • 2025年12月英语四级大纲词汇表PDF电子版(含正序版、乱序版和默写版)
  • 蝶山网站建设樟木头仿做网站
  • 【Linux网络编程】套接字编程
  • 网站怎么做弹出表单网站竞价 英文
  • 电子电气架构 --- 当前技术水平
  • OS 特性之PendSV 异常
  • 跆拳道东莞网站建设触屏版网站开发
  • 在电脑端企业微信打开内置浏览器并调试
  • Seata原理与简单示例
  • LeetCode 420 - 强密码检验器
  • 优化学校网站建设方案下载官方网站app
  • Visual Basic 创建状态栏
  • 网站建设的人才怎么称呼wordpress多语言模板
  • LeetCode 分类刷题:876. 链表的中间结点
  • LeetCode 分类刷题:143. 重排链表
  • 分布式专题——51 ES 深度分页问题及其解决方案详解
  • 2025.11.10 力扣每日一题
  • 麻城网站开发同一网站相同form id
  • dede网站制作wordpress长微博工具
  • 专业定制铸铁T型槽地轨,提供稳定的制造、实验基准线
  • PyCharm如何像其他idea软件跨行选择文本
  • 【场景题】线上接口响应慢,应该如何排查问题?
  • Product Hunt 每日热榜 | 2025-11-10
  • 【智能体(Agent)技术深度解析】从架构到实现细节,核心是实现“感知环境→处理信息→决策行动→影响环境”的闭环
  • 网站的登陆注册页面怎么做做水果网站弄个什么名字
  • Advisor与@Aspect
  • Java基础——集合进阶6
  • 网站建设湛江分类信息网站如何做排名
  • 二十七、通信接口
  • 个人网站备案条件个人怎么做网页