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

)

大家好,我是[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);

        //  输出 PDF
        Document 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;  // 注意:如果是PdfUtil7

import 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水印/签名,敬请期待~

(完)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wáng bēn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值