如何使用iTextPDF库替换PDF中的${placeholder}占位符文本?
用iText7替换PDF中的${placeholder}占位符解决方案
我懂你这种卡壳的感觉——照着官方示例操作却没效果确实让人头疼。其实你提到的那个官方示例是针对替换PDF底层对象(比如图片、字体这类资源)的,和你要的文本占位符替换完全不是一个场景,没效果太正常了。
PDF里的文本不像Word那样是规整的“占位符块”,它通常是由多个零散的文本片段组成的,所以要实现占位符替换,核心思路是遍历页面文本、定位占位符位置、覆盖原文本并写入新内容。下面给你一套可行的实现方案:
核心实现步骤
- 监听PDF页面的文本渲染事件,收集所有文本片段的内容和位置信息
- 筛选出包含目标占位符的文本片段
- 用白色矩形覆盖原占位符区域(避免残留)
- 在原位置写入替换后的文本,保持字体、字号、位置和原占位符一致
Java代码示例
基础版(占位符未被拆分)
如果你的占位符是作为一个完整的文本片段存在的,用这个版本就足够:
import com.itextpdf.kernel.geom.Rectangle; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfPage; import com.itextpdf.kernel.pdf.PdfReader; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.canvas.parser.EventType; import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor; import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData; import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo; import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class PlaceholderReplacer { public static void replacePlaceholder(String srcPath, String destPath, String placeholder, String replacement) throws IOException { // 打开PDF文档(读模式+写模式) try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(srcPath), new PdfWriter(destPath))) { // 遍历每一页 for (int pageNum = 1; pageNum <= pdfDoc.getNumberOfPages(); pageNum++) { PdfPage page = pdfDoc.getPage(pageNum); TextSearchListener listener = new TextSearchListener(placeholder); PdfCanvasProcessor processor = new PdfCanvasProcessor(listener); processor.processPageContent(page); // 处理找到的所有匹配占位符 for (TextRenderInfo textInfo : listener.getMatchingText()) { Rectangle placeholderRect = textInfo.getBaseline().getBoundingRectangle(); // 1. 用白色矩形覆盖原占位符(消除原文本) page.newContentStreamBefore().saveState() .setFillColorGray(1f) .rectangle(placeholderRect.getLeft(), placeholderRect.getBottom(), placeholderRect.getWidth(), placeholderRect.getHeight()) .fill() .restoreState(); // 2. 在原位置写入替换文本,保持原字体、字号和位置 page.newContentStreamBefore().beginText() .setFontAndSize(textInfo.getFont(), textInfo.getFontSize()) .setTextMatrix(textInfo.getTextMatrix().get(0, 0), textInfo.getTextMatrix().get(1, 0), textInfo.getTextMatrix().get(0, 1), textInfo.getTextMatrix().get(1, 1), textInfo.getTextMatrix().get(0, 2), textInfo.getTextMatrix().get(1, 2)) .showText(replacement) .endText(); } } } } // 自定义监听器:捕获包含目标占位符的文本片段 private static class TextSearchListener implements IEventListener { private final String targetPlaceholder; private final List<TextRenderInfo> matchingTextList = new ArrayList<>(); public TextSearchListener(String placeholder) { this.targetPlaceholder = placeholder; } @Override public void eventOccurred(IEventData data, EventType type) { if (type == EventType.RENDER_TEXT) { TextRenderInfo textInfo = (TextRenderInfo) data; String currentText = textInfo.getText(); if (currentText.contains(targetPlaceholder)) { matchingTextList.add(textInfo); } } } @Override public List<EventType> getSupportedEvents() { return List.of(EventType.RENDER_TEXT); } public List<TextRenderInfo> getMatchingText() { return matchingTextList; } } // 测试入口 public static void main(String[] args) throws IOException { replacePlaceholder("input.pdf", "output.pdf", "${username}", "Jane Smith"); } }
进阶版(处理被拆分的占位符)
有时候PDF生成工具会把一个完整的占位符拆成多个文本片段(比如${user和name}),这时候需要把相邻的文本片段合并后再匹配,修改监听器即可:
// 修改后的TextSearchListener,支持合并相邻文本片段 private static class TextSearchListener implements IEventListener { private final String targetPlaceholder; private final List<TextRenderInfo> matchingTextList = new ArrayList<>(); private List<TextRenderInfo> currentTextBlock = new ArrayList<>(); private float lastTextEndX = -1; public TextSearchListener(String placeholder) { this.targetPlaceholder = placeholder; } @Override public void eventOccurred(IEventData data, EventType type) { if (type == EventType.RENDER_TEXT) { TextRenderInfo textInfo = (TextRenderInfo) data; float currentTextStartX = textInfo.getBaseline().getStartPoint().get(0); // 判断当前文本是否和上一个片段相邻(阈值1f可根据实际调整) if (currentTextBlock.isEmpty() || Math.abs(currentTextStartX - lastTextEndX) < 1f) { currentTextBlock.add(textInfo); } else { // 检查当前文本块是否包含占位符 checkTextBlock(currentTextBlock); currentTextBlock.clear(); currentTextBlock.add(textInfo); } lastTextEndX = textInfo.getBaseline().getEndPoint().get(0); } } @Override public List<EventType> getSupportedEvents() { return List.of(EventType.RENDER_TEXT); } public List<TextRenderInfo> getMatchingText() { // 处理最后一个未检查的文本块 checkTextBlock(currentTextBlock); return matchingTextList; } // 检查文本块拼接后是否包含占位符 private void checkTextBlock(List<TextRenderInfo> textBlock) { StringBuilder blockText = new StringBuilder(); for (TextRenderInfo info : textBlock) { blockText.append(info.getText()); } if (blockText.toString().contains(targetPlaceholder)) { matchingTextList.addAll(textBlock); } } }
注意事项
- 如果你需要替换多个不同的占位符,只需多次调用
replacePlaceholder方法即可 - 如果替换后的文本长度和原占位符差异较大,可能会导致排版错乱,这时候需要更复杂的布局调整逻辑(比如重新计算文本位置)
- 确保你使用的iText7版本是最新稳定版,避免API兼容性问题
内容的提问来源于stack exchange,提问作者Tymur Berezhnoi
相关产品推荐
相关产品推荐

