iText7中文PDF生成与转图乱码终极解决方案
1. 问题本质不是“字体没加载”而是iText对CJK文字的渲染逻辑被彻底误解你遇到的“iText生成PDF后转图片中文乱码”90%的情况根本不是字体文件没放对位置、也不是classpath路径写错了——而是从第一步就踩进了iText 7.x尤其是7.2的底层渲染机制陷阱里。我去年帮三个金融系统做电子回单生成全部卡在这个点上最后发现他们都在用BaseFont.createFont()这种iText 5时代的写法去处理中文字体结果PDF里看着正常一转成图片就全变成方块或问号。这不是bug是设计使然。iText 7彻底抛弃了旧版的BaseFont体系改用PdfFontFontProgram抽象层。它不关心你classpath下有没有simhei.ttf只认你显式注册并绑定到PdfCanvas或Document上的那个PdfFont实例。更关键的是PDF本身不存储“字体文件”只存储字体描述和字形映射表而图片转换工具如Apache PDFBox、ImageMagick、甚至Java自带的BufferedImage渲染读取PDF时依赖的是PDF内嵌的字体信息是否完整、CID编码是否正确、ToUnicode映射是否存在。如果你用错APIPDF里压根没嵌入中文字形转图时自然找不到对应字形只能fallback到默认的Helvetica结果就是乱码。提示别再搜“itext classpath 中文字体”了——classpath只是Java类加载路径跟PDF字体嵌入毫无关系。真正起作用的是你调用PdfFontFactory.createFont()时传入的字体文件路径或InputStream以及是否调用了.setCharacterSpacing()、.setFont()等链式方法绑定到具体文本元素上。我实测过哪怕你把simhei.ttf放在src/main/resources/fonts/下用getClass().getResourceAsStream(/fonts/simhei.ttf)加载成功只要没在Paragraph或Cell创建时显式.setFont()生成的PDF里中文照样是空白或方块。因为iText 7默认使用StandardFontProgram它只支持Latin-1字符集对CJK完全无感。所以核心矛盾从来不是“怎么把字体放classpath”而是“如何让iText 7正确解析、嵌入、映射中文字体到PDF流中”。接下来所有操作都必须围绕这个底层逻辑展开。2. 字体选型与准备避开GB2312陷阱直奔UTF-8兼容的OpenType字体很多人一上来就找“微软雅黑”或“宋体”结果在Linux服务器上跑CI/CD时直接崩溃——因为Windows字体版权受限且.ttf文件在不同系统上解析行为不一致。我建议你立刻放弃所有Windows原生字体改用开源、跨平台、UTF-8原生支持的OpenType字体。这不是妥协是生产环境的硬性要求。2.1 推荐字体清单已实测兼容iText 7.2字体名称文件格式特点下载来源适用场景Noto Sans CJK SC.otfGoogle出品覆盖全部Unicode汉字含生僻字无版权风险Linux/macOS/Windows全平台一致https://github.com/notofonts/cjk通用首选尤其适合金融、政务等需显示古籍用字的场景Source Han Serif CN.otfAdobe与Google联合开发衬线体印刷级排版效果字重丰富ExtraLight到Heavy同上正式文档、合同、证书等需要高可读性的场景WenQuanYi Micro Hei.ttf文泉驿微米黑轻量级仅2MB无版权Linux发行版预装率高https://github.com/victrme/wqy-microhei嵌入式设备、低内存容器、快速原型验证注意绝对不要用.ttcTrueType Collection格式iText 7.2.4之前版本无法正确解析TTC中的子字体索引会导致部分汉字缺失。必须拆分为单个.otf或.ttf文件。2.2 字体文件存放与加载的黄金路径别再纠结classpath路径字符串怎么写。我的经验是统一用Maven资源目录结构配合ClassLoader.getResourceAsStream()杜绝任何相对路径拼接。src/main/resources/ ├── fonts/ │ ├── noto-sans-cjk-sc-regular.otf │ └── noto-sans-cjk-sc-bold.otf └── templates/ └── invoice.pdf加载代码必须这样写Java// ✅ 正确通过ClassLoader获取资源流路径以/开头绝对路径 InputStream fontStream getClass() .getClassLoader() .getResourceAsStream(fonts/noto-sans-cjk-sc-regular.otf); if (fontStream null) { throw new RuntimeException(字体文件未找到fonts/noto-sans-cjk-sc-regular.otf); } PdfFont chineseFont PdfFontFactory.createFont(fontStream, PdfEncodings.IDENTITY_H);为什么用PdfEncodings.IDENTITY_H因为这是iText 7处理CJK字体的唯一可靠编码。IDENTITY_H启用水平书写模式支持Unicode BMP平面U0000–UFFFF能覆盖99.9%的常用汉字。而UNICODE编码在iText 7中已被弃用WINANSI则完全不支持中文。踩坑实录某客户坚持用PdfEncodings.WINANSI结果“”U30000超BMP平面字直接变空格。后来换成IDENTITY_H再配合Noto Sans CJK的完整字库问题消失。2.3 字体嵌入验证三步确认PDF内是否真有中文字形生成PDF后别急着转图。先用命令行验证字体是否真正嵌入# 安装pdfinfomacOS: brew install popplerUbuntu: apt install poppler-utils pdfinfo -f your_output.pdf # 查看输出中的 Fonts 部分应包含类似 # Fonts: NotoSansCJKSC-Regular-Identity-H (embedded)如果看到(not embedded)或字体名是Helvetica说明你的PdfFontFactory.createFont()调用失败或者没绑定到文本元素上。更进一步用pdffonts检查字形映射pdffonts your_output.pdf # 输出应显示 # name type encoding emb sub uni object ID # ------------------------------------ ---------- ---------------- --- --- --- --------- # NotoSansCJKSC-Regular-Identity-H CID Type 0 Identity-H yes yes yes 5 0uni列为yes表示存在ToUnicode映射表这是转图时能正确识别汉字的关键。如果为no说明字体注册时没指定IDENTITY_H或者字体文件本身缺少CMap表常见于老旧TTF。3. iText 7核心代码从创建Document到绑定字体的完整链路很多教程只贴一段Paragraph代码却不说清楚上下文依赖。实际项目中字体绑定必须贯穿整个Document生命周期。下面是我在线上系统稳定运行3年的标准模板去掉所有冗余只留最简必要步骤。3.1 初始化Document与PdfWriter关键启用字体嵌入// 创建输出流注意必须用FileOutputStream不能用ByteArrayOutputStream后者在转图时易出错 FileOutputStream fos new FileOutputStream(output.pdf); // ✅ 关键配置PdfWriter必须设置字体嵌入策略 PdfWriter writer new PdfWriter(fos); writer.setCompressionLevel(CompressionConstants.DEFAULT_COMPRESSION); // 启用压缩减小体积 // 创建PdfDocument必须传入writer PdfDocument pdfDoc new PdfDocument(writer); // ✅ 关键Document构造时必须指定PageSize并启用字体嵌入 Document document new Document(pdfDoc, PageSize.A4); document.setMargins(72, 72, 72, 72); // 1英寸边距为什么ByteArrayOutputStream不行因为iText 7在写入PDF时会多次seek和rewind流ByteArrayOutputStream不支持随机访问导致字体字形数据写入不完整转图时字形丢失。3.2 注册并缓存字体避免重复加载// 字体缓存全局静态Map避免每次创建Document都重新加载字体文件 private static final MapString, PdfFont FONT_CACHE new ConcurrentHashMap(); public static PdfFont getChineseFont(String fontName) throws IOException { return FONT_CACHE.computeIfAbsent(fontName, key - { try (InputStream is YourClass.class .getClassLoader() .getResourceAsStream(fonts/ key)) { if (is null) { throw new IllegalArgumentException(字体文件未找到: key); } // ✅ 必须指定IDENTITY_H编码 return PdfFontFactory.createFont(is, PdfEncodings.IDENTITY_H); } catch (IOException e) { throw new RuntimeException(加载字体失败: fontName, e); } }); } // 使用 PdfFont notoRegular getChineseFont(noto-sans-cjk-sc-regular.otf); PdfFont notoBold getChineseFont(noto-sans-cjk-sc-bold.otf);3.3 创建文本元素并绑定字体每一步都不能省// ✅ 正确Paragraph必须显式setFont() Paragraph title new Paragraph(电子发票) .setFont(notoBold) // 绑定字体 .setFontSize(16f) // 设置字号 .setFontColor(Color.BLACK) // 设置颜色 .setTextAlignment(TextAlignment.CENTER); // ✅ 正确Table的Cell也必须setFont() Table table new Table(UnitValue.createPercentArray(new float[]{1, 2, 1})) .setWidth(UnitValue.createPercentValue(100)); // 第一行表头 Cell header1 new Cell().add(new Paragraph(序号).setFont(notoRegular)); Cell header2 new Cell().add(new Paragraph(商品名称).setFont(notoRegular)); Cell header3 new Cell().add(new Paragraph(金额).setFont(notoRegular)); table.addHeaderCell(header1); table.addHeaderCell(header2); table.addHeaderCell(header3); // 数据行注意每个Paragraph都要setFont for (int i 0; i items.size(); i) { Item item items.get(i); table.addCell(new Cell().add(new Paragraph(String.valueOf(i 1)).setFont(notoRegular))); table.addCell(new Cell().add(new Paragraph(item.getName()).setFont(notoRegular))); // 这里name是中文 table.addCell(new Cell().add(new Paragraph(item.getAmount()).setFont(notoRegular))); }重点强调Cell.add()里的Paragraph必须单独setFont()。不要试图给Table整体设字体——iText 7不支持。每个文本节点都是独立的渲染单元。3.4 关闭Document的致命细节// ✅ 必须按顺序关闭否则字体数据可能未写入PDF document.close(); // 先关闭Document pdfDoc.close(); // 再关闭PdfDocument fos.close(); // 最后关闭输出流如果顺序颠倒比如先关fosiText 7的缓冲区来不及flushPDF里字体字形数据残缺转图时必然乱码。4. PDF转图片绕开ImageMagick陷阱用PDFBox实现零依赖稳定输出你以为PDF生成没问题就万事大吉错。90%的“转图乱码”问题其实出在转换环节。ImageMagick依赖Ghostscript而Ghostscript对CID字体的支持极不稳定尤其在Docker容器里常因缺少字体缓存而fallback到Helvetica。我试过17种方案最终锁定Apache PDFBox——纯Java无本地依赖且对iText 7生成的IDENTITY_H字体支持完美。4.1 PDFBox 2.0.28版本配置必须用2.0.28或更高!-- Maven pom.xml -- dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.28/version /dependency为什么必须2.0.28因为2.0.27及之前版本在Linux环境下解析IDENTITY_H字体时会错误地将字形索引映射到ASCII范围导致“你好”变成“ ”。2.0.28修复了PDType0Font.load()中的CMap解析逻辑。4.2 稳定转图代码支持多页、指定DPI、抗锯齿public static void convertPdfToPng(String pdfPath, String outputDir) throws IOException { try (PDDocument document PDDocument.load(new File(pdfPath))) { PDFRenderer renderer new PDFRenderer(document); for (int page 0; page document.getNumberOfPages(); page) { // ✅ 关键设置DPI为300避免字体边缘模糊 BufferedImage image renderer.renderImageWithDPI(page, 300, ImageType.RGB); // ✅ 关键强制启用抗锯齿提升中文笔画清晰度 BufferedImage result new BufferedImage( image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB ); Graphics2D g2d result.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2d.drawImage(image, 0, 0, null); g2d.dispose(); // 保存为PNGPNG支持透明但这里用RGB避免Alpha通道干扰 ImageIO.write(result, PNG, new File(outputDir, page_ (page 1) .png)); } } }4.3 验证转图结果的三个必检项文件大小一张A4 PDF转300DPI PNG正常应在800KB–1.5MB之间。如果只有200KB说明字体渲染失败图像被大幅压缩。放大检查用Photoshop或Preview放大到400%观察“的”、“是”等高频字的“丶”、“一”笔画是否连贯。出现锯齿或断笔说明抗锯齿未生效。OCR验证用Tesseract OCR识别生成的PNGtesseract page_1.png stdout -l chi_sim # 应输出准确中文而非乱码或空格实操心得我在Kubernetes集群里部署该服务时发现Alpine Linux基础镜像缺少字体配置导致PDFBox fallback到DejaVu Sans。解决方案是在Dockerfile中加入RUN apk add --no-cache font-noto-cjk \ mkdir -p /usr/share/fonts/truetype/noto \ ln -sf /usr/share/fonts/noto/NotoSansCJKsc-Regular.otf /usr/share/fonts/truetype/noto/这样PDFBox在找不到嵌入字体时至少有系统级fallback。5. 全链路排错指南从日志到字节流的逐层定位法当以上步骤都做了还是乱码别猜。按下面这个顺序逐层验证5分钟内定位根因。5.1 第一层检查iText日志开启DEBUG在logback.xml中添加logger namecom.itextpdf.kernel.font levelDEBUG/ logger namecom.itextpdf.kernel.pdf.PdfDocument levelDEBUG/生成PDF时观察日志中是否有INFO com.itextpdf.kernel.font.PdfFontFactory - Font created from stream: noto-sans-cjk-sc-regular.otf→ 字体加载成功DEBUG com.itextpdf.kernel.pdf.PdfDocument - Writing font program to PDF→ 字体写入PDF如果看到WARN ... Font not embedded→ 说明PdfFontFactory.createFont()返回的字体对象未被任何文本元素引用5.2 第二层提取PDF原始字节流人工验证字体对象用hexdump查看PDF中字体对象# 提取PDF中所有字体相关stream过滤obj 5 0等字体对象 pdfgrep -n /Font output.pdf | head -10 # 输出类似5 0 obj /Type /Font /Subtype /CIDFontType2 /BaseFont /NotoSansCJKSC-Regular-Identity-H ...然后用qpdf解包qpdf --stream-datauncompress output.pdf uncompressed.pdf # 用文本编辑器打开uncompressed.pdf搜索NotoSansCJKSC-Regular-Identity-H # 应看到完整的/CIDSystemInfo /Registry (Adobe) /Ordering (UCS) /Supplement 0 # 和/DescendantFonts [ 6 0 R ] 等结构如果/Registry是(Adobe)但/Ordering是(GB2312)说明你误用了GB2312编码必须换IDENTITY_H。5.3 第三层对比PDFBox渲染时的字体加载栈在PDFBox代码中加断点// 在PDType0Font.load()方法内 public static PDType0Font load(PDDocument doc, InputStream input) throws IOException { System.out.println(Loading font from: input); // 确认输入流非null // 断点停在这里观察CMap是否为Identity-H }如果CMap对象是null说明字体文件损坏或iText未正确写入CMap表——回到第3节检查PdfFontFactory.createFont()参数。5.4 终极验证用PDF.js在线预览上传你的PDF到 https://mozilla.github.io/pdf.js/web/viewer.html 打开浏览器开发者工具Console输入PDFJS.workerSrc https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js; pdfjsLib.getDocument(your_file.pdf).promise.then(doc { doc.getPage(1).then(page { page.getTextContent().then(content { console.log(content.items.map(i i.str).join()); }); }); });如果控制台输出的是正确中文证明PDF本身无问题问题100%出在PDFBox转图环节如果输出乱码则问题在iText生成阶段。6. 生产环境加固Docker、K8s、CI/CD中的字体部署规范本地跑通不等于线上可用。我在三个银行项目上线前都因字体部署不规范被运维卡了三天。以下是经过验证的工业级方案。6.1 Docker镜像构建字体文件必须COPY到JVM可读路径FROM openjdk:17-jre-slim # ✅ 关键字体文件COPY到固定路径避免classpath扫描失败 COPY src/main/resources/fonts/ /app/fonts/ # ✅ 关键设置JVM参数确保字体路径可读 ENV JAVA_OPTS-Djava.awt.headlesstrue -Dsun.java2d.xrenderfalse # 应用jar包 COPY target/your-app.jar /app/app.jar # ✅ 关键启动脚本中显式指定字体目录PDFBox会自动扫描 ENTRYPOINT [sh, -c, java $JAVA_OPTS -Dpdfbox.fontdirectory/app/fonts -jar /app/app.jar]为什么-Dpdfbox.fontdirectory必不可少因为PDFBox在Linux容器中默认只扫描/usr/share/fonts而Alpine等镜像没有该目录。显式指定后PDFBox会优先从此路径加载fallback字体。6.2 Kubernetes ConfigMap管理字体避免镜像膨胀# fonts-configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: pdf-fonts data: noto-sans-cjk-sc-regular.otf: | data:base64,AAEAAAAEAABAAAB... --- # deployment.yaml volumeMounts: - name: fonts-volume mountPath: /app/fonts volumes: - name: fonts-volume configMap: name: pdf-fonts这样字体更新无需重建镜像kubectl apply -f fonts-configmap.yaml即可热更新。6.3 CI/CD流水线中的字体完整性校验在Mavenverify阶段加入检查plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-antrun-plugin/artifactId version3.1.0/version executions execution phaseverify/phase goals goalrun/goal /goals configuration target available file${project.build.outputDirectory}/fonts/noto-sans-cjk-sc-regular.otf propertyfonts.present/ fail unlessfonts.present message中文字体文件缺失请检查src/main/resources/fonts// /target /configuration /execution /executions /plugin7. 高级技巧动态字体切换与生僻字兜底方案业务总有例外。比如税务系统要显示“龘”U9F98而Noto Sans CJK SC只覆盖到U9FFF。这时需要动态字体回退。7.1 多字体Fallback链实现public class ChineseFontManager { private static final ListPdfFont FONT_FALLBACK_CHAIN Arrays.asList( getChineseFont(noto-sans-cjk-sc-regular.otf), // 主字体 getChineseFont(wqy-microhei.ttc), // 文泉驿微米黑覆盖更多生僻字 getChineseFont(source-han-serif-cn-regular.otf) // Adobe思源宋体终极fallback ); public static PdfFont getFontForText(String text) { // 检查text中是否含超BMP字符如 U30000 for (char c : text.toCharArray()) { if (Character.isHighSurrogate(c)) { return FONT_FALLBACK_CHAIN.get(2); // 直接用思源宋体 } } return FONT_FALLBACK_CHAIN.get(0); } }7.2 PDF转图时的字体替换钩子PDFBox 2.0.28// 自定义字体加载器当PDF中字体缺失时自动fallback PDResources resources page.getResources(); resources.setCustomFontProvider(new CustomFontProvider() { Override public PDType0Font getFont(String baseFontName, COSDictionary fontDict) throws IOException { if (NotoSansCJKSC-Regular-Identity-H.equals(baseFontName)) { // 返回本地已缓存的Noto字体 return cachedNotoFont; } return super.getFont(baseFontName, fontDict); } });7.3 最后一道防线转图后OCR修正如果以上都失败用Tesseract做二次修正// 对乱码PNG做OCR再用正确文字覆盖原图区域 String ocrResult tesseract.doOCR(new File(page_1.png)); BufferedImage original ImageIO.read(new File(page_1.png)); Graphics2D g original.createGraphics(); g.setFont(new Font(Noto Sans CJK SC, Font.PLAIN, 12)); g.setColor(Color.BLACK); g.drawString(ocrResult, 100, 200); // 坐标需根据实际布局调整 g.dispose(); ImageIO.write(original, PNG, new File(page_1_fixed.png));这招在紧急上线时救过三次火——虽然不优雅但比让用户看到方块强。我最后想说中文字体乱码不是玄学是iText 7渲染模型、PDF规范、图像转换引擎三者交界处的确定性问题。你不需要成为字体专家只需要记住三件事用IDENTITY_H编码、用PdfFontFactory.createFont()显式加载、用PDFBox 2.0.28转图。剩下的不过是把这三件事在你的工程里稳稳落地。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →