news 2026/10/24 1:00:48

用iText绘制Excel样式PDF表格:中文字体、水印与页眉页码全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用iText绘制Excel样式PDF表格:中文字体、水印与页眉页码全攻略

一篇让人看了就想收藏的 PDF 表格实操记录:用 iText 画出带中文字体、水印、Logo、页眉页码的 Excel 样式表格

先问个问题:你手头有没有遇到过这种需求?系统要导出一份 PDF 合同、对账单或者项目报告,领导说“表格样式就照着 Excel 那种来,要带公司 Logo、页眉页码,中文字体不能乱码,最好再加个水印”。用 Word 排版导出?模板改一次要半天。直接用前端打印?样式经常飘。我最早做某跨平台系统的时候,就卡在“PDF 表格中文字体乱码”和“水印怎么铺满每一页”这两件事上折腾了整整两天。后来把所有实现逻辑沉淀成一套稳定方案,也就是这篇要分享的核心内容:基于 Java 的 iText 库,在 PDF 里绘制 Excel 样式的表格、设置中文字体、添加水印和 Logo、处理页眉页码。这套方案已经在我后续好几个项目里复用了,包括某物流单据项目和某内部报表系统的导出模块。

如果你用的是 iText 5 而不是最新的 iText 7,也别急着划走。这篇虽然以 iText 5 为主(为什么用 5 而不是 7,后面会专门解释),但设计思路在 7 里同样成立。文中所有代码都来自我实际跑通过的模拟项目,不是贴官方文档的 Demo。

1. 准备工作:为什么选 iText 5 而不是 iText 7,以及中文字体方案的核心逻辑

1.1 版本选型的真实理由

先说选型。iText 7 是现在的官方主推版本,架构更现代,性能也更好。但我个人在 5 和 7 之间做过对比,最后在多数项目里选了 5,原因很实在:

  • 存量系统兼容:我维护的几个老项目基于 iText 5,升级到 7 意味着大量 PdfWriter、PdfReader 相关代码要重写,成本太高。
  • 社区资料丰富:搜索“iText 表格”“iText 水印”“iText 中文乱码”,十篇方案里有八篇是基于 5 的。遇到问题更容易找到参考。
  • API 直白:5 的PdfPTable、PdfPCell虽然啰嗦,但每个类的职责非常清晰,新手读代码不容易懵。

如果你的项目是全新启动、没有任何历史包袱,那直接上 7 也没问题。7 的Table、Cell模型更灵活,字体处理也统一到了FontProgramFactory。但不管是 5 还是 7,下面要讲的解决思路都是一样的:字体注册、表格布局、页面事件回调、水印平铺。

1.2 中文字体问题的根因:不是“乱码”,而是“缺字体”

很多人第一次用 iText 画中文表格,遇到输出 PDF 里中文变成方框或者空白,第一反应是“编码设置错了”。其实 iText 5 默认使用的字体是BaseFont.createFont()创建的 Helvetica 等标准 Type1 字体,这种字体只支持 Latin-1 字符集。你往里面塞中文,理论上就是不可渲染的字符,最终表现就是方框。

解决方案只有一个:显式注册一个支持中文的字体文件。常用方案有三种,我按推荐程度列一下:

方案字体文件优点缺点
系统中文字体Windows 的 simsun.ttc / simhei.ttf,Linux 的 wqy-zenhei.ttc无需额外下载,可直接用系统路径依赖运行环境,换机器可能找不到
项目内嵌字体把字体文件丢进 resources/fonts 目录可移植性强,部署不依赖环境字体文件体积大(通常 10~20MB)
开源字体思源黑体、文泉驿正黑免费可商用,字重丰富需要主动下载,文件较大

我在实际项目里的惯例是:字体文件跟随应用走,也就是放到src/main/resources/fonts下。这样不管部署到哪台服务器,行为都一样。下面这段代码就是注册字体的标准姿势:

BaseFont bfChinese = BaseFont.createFont( "/fonts/simhei.ttf", // 字体文件路径,这里用项目内路径 BaseFont.IDENTITY_H, // 编码方式,IDENTITY_H 表示 Unicode 水平编码 BaseFont.NOT_EMBEDDED // 是否嵌入 PDF 文件 ); Font chineseFont = new Font(bfChinese, 12, Font.NORMAL);

三个参数里最容易踩坑的是第二个:BaseFont.IDENTITY_H。这个参数代表字体使用 TrueType 集合的 Unicode 编码,是中文正常显示的关键。有些人图省事写BaseFont.CP1252,那中文一样会挂。第三参数NOT_EMBEDDED表示不把字体文件打包进 PDF 里。如果 PDF 要在没有该字体的机器上查看,建议改成BaseFont.EMBEDDED,代价是文件体积变大。

1.3 我对字体选型的一个补充建议

如果项目允许,我更推荐用思源黑体(Source Han Sans)而不是宋体或黑体。原因有三个:一是思源黑体免费可商用,没有字体授权风险;二是它的字形在屏幕和打印场景下都清晰;三是它支持多种字重,用来做表格标题和正文分级非常方便。缺点是 TTF 文件单个 8MB 以上,打包时注意体积。我在某图像处理 Demo 里就用了思源黑体 Regular 和 Bold,整体效果比单纯用 simhei 好不少。

2. 页面布局与事件机制:字体注册之后的第一个核心关卡

2.1 认识 PdfWriter 和 Document 的“页-事件”关系

很多刚上手 iText 的人会把“画表格”当成全部,其实真正决定 PDF 排版质量的,是 Document 和 PdfWriter 之间的页面事件机制。在 iText 5 里,PdfWriter有一个setPageEvent()方法,可以挂载一个PdfPageEvent实现类。这个类会在每一页的“开始”和“结束”阶段触发回调。水印、页眉、页码这些“每一页都要有”的元素,正是在这些回调里绘制。

我自己的理解是:页面事件是 iText 版的“过滤器”或“拦截器”。你不需要在每生成一页时手动去加页眉页脚,只需要写好一次回调逻辑,它就会自动应用到所有页面。下面是一个典型的事件处理类骨架:

public class PdfPageEventHelper extends PdfPageEventHelper { @Override public void onStartPage(PdfWriter writer, Document document) { // 当前页开始:可以在这里画页眉、公司 Logo } @Override public void onEndPage(PdfWriter writer, Document document) { // 当前页结束:可以在这里画页码、水印 } }

注意onStartPage和onEndPage的区别:页眉和 Logo 通常画在页面顶部,用onStartPage;页码要等当前页内容都排完才知道是第几页,用onEndPage更合适。水印我习惯放在onEndPage,因为需要读取writer.getPageNumber()。

2.2 页眉、Logo 与页码的绘制细节

先看一个完整的示例。假设每一页顶部要放公司名称和一个小 Logo,底部中间要放“第 X 页”:

public class MyPageEvent extends PdfPageEventHelper { private Image logoImage; // 预先加载 private BaseFont bfChinese; public MyPageEvent(Image logo, BaseFont bf) { this.logoImage = logo; this.bfChinese = bf; } @Override public void onStartPage(PdfWriter writer, Document document) { try { PdfContentByte canvas = writer.getDirectContentUnder(); // 固定位置:页面左上角放 Logo,稍微留出边距 float x = document.leftMargin(); float y = document.top() + 20; // 页面顶部上方 canvas.addImage(logoImage, logoImage.getWidth(), 0, 0, logoImage.getHeight(), x, y - logoImage.getHeight()); // 页眉文字 canvas.saveState(); canvas.beginText(); canvas.setFontAndSize(bfChinese, 10); canvas.setTextMatrix(document.right() - 120, y - 8); canvas.showText("项目内部资料"); canvas.endText(); canvas.restoreState(); } catch (Exception e) { e.printStackTrace(); } } @Override public void onEndPage(PdfWriter writer, Document document) { PdfContentByte canvas = writer.getDirectContent(); CanvasGraphicsUtils.drawPageNumber(canvas, bfChinese, writer.getPageNumber()); } }

这里的几个小细节:

  • getDirectContentUnder()和getDirectContent()的区别。前者画的内容在正文之下,适合放水印;后者画的内容在正文之上,适合放页码这类不能被遮挡的信息。但页眉和 Logo 我用的是getDirectContentUnder(),因为页眉区域本来就没有正文,不会冲突。
  • addImage的六个参数中,前四个是变换矩阵参数。很多新手在这里栽跟头:缩放 50% 时直接填(width/2, 0, 0, height/2, x, y)而不是(width, 0, 0, height, x, y)。

页码部分的工具方法:

public class CanvasGraphicsUtils { public static void drawPageNumber(PdfContentByte canvas, BaseFont bf, int pageNum) { canvas.saveState(); canvas.beginText(); canvas.setFontAndSize(bf, 9); String text = "第 " + pageNum + " 页"; float textWidth = bf.getWidthPoint(text, 9); float pageWidth = PageSize.A4.getWidth(); canvas.setTextMatrix((pageWidth - textWidth) / 2, 30); canvas.showText(text); canvas.endText(); canvas.restoreState(); } }

getWidthPoint()是计算字符串在指定字号下占多宽的标准方法。页码要居中,就是(页面宽度 - 字符串宽度) / 2。

2.3 水印平铺的两种做法和一种推荐

水印需求通常有两种:一种是“单页中间一个大字”,另一种是“整页铺满类似 Word 的水印效果”。我最早只做了前者,后来客户要求“水印要铺满背景”,才认真研究后者。

简单粗暴的做法:在onEndPage里循环画多个showText:

canvas.setFontAndSize(bfChinese, 40); canvas.setColorFill(BaseColor.LIGHT_GRAY); for (int x = 50; x < 600; x += 180) { for (int y = 50; y < 800; y += 180) { canvas.showTextAligned(Element.ALIGN_CENTER, "内部资料", x, y, 45); // 最后参数是旋转角度 } }

这个方案代码很短,但有两个问题:一是字符串宽度不一,行之间视觉上不对齐;二是每次写文字都要手算循环步长。我在实际项目里优化成:先计算“水印内容宽度和总页宽”的关系,再用一个矩阵循环定位。值得一提的是旋转角度45,这是 Word 水印的经典角度。如果你希望水印内容跟页面水平,角度填0即可。

3. 核心表格实现:从 PdfPTable 到完整 Excel 样式

3.1 表格整体布局:列宽、间距与边框的处理

进入本节之前,先说明一点:所谓的“Excel 样式”在 PDF 里并没有原生的表格概念。iText 里的PdfPTable本质上是一个内容排版器,它把PdfPCell按行列排列,最后自动决定每个单元格的位置。因此你想要实现“Excel 样式”,核心在于列宽控制、单元格合并、边框和底色这几个维度。

构建一个带表头的 5 列表格,通常会这么写:

PdfPTable table = new PdfPTable(5); table.setWidthPercentage(100); // 占满可用宽度 table.setWidths(new float[]{1, 3, 2, 2, 2}); // 相对宽度比例 table.setSpacingBefore(12); table.setSpacingAfter(12);

setWidths是表格观感的关键。我拿 1:3:2:2:2 举例,含义是第 1 列(比如序号)最窄,第 2 列(比如项目描述)最宽。如果你不设置宽度比例,iText 会平均分配列宽,大多数时候看起来非常生硬。

单元格的创建逻辑也很常规:

PdfPCell cell = new PdfPCell(); cell.setPhrase(new Phrase("项目名称", chineseFont)); cell.setHorizontalAlignment(Element.ALIGN_CENTER); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setFixedHeight(30); cell.setBorderWidth(0.5f); cell.setBackgroundColor(BaseColor.LIGHT_GRAY); table.addCell(cell);

注意setFixedHeight(30)意味着这一行最小高度 30pt。如果单元格内容超过这个高度,iText 会自动扩展行高,而不是裁剪。

表格整体写完以后,通过document.add(table)添加。到这里其实已经能得到一个“能看”的表格了。但要想做到 Excel 那种“表头深色、斑马纹、合计行加粗”的效果,还需要做更多处理。

3.2 表头合并与跨行跨列:复用 Excel 里的“合并单元格”思维

用过 Excel 的都知道“合并单元格”是表格布局的灵魂。iText 里对应的方法是PdfPCell.setColspan()和setRowspan()。

比如做一个大的分组表头,第一行有两列合并横跨整个表格:

PdfPCell headerGroup = new PdfPCell(new Phrase("基础信息", chineseFont)); headerGroup.setColspan(2); // 横向合并 2 列 headerGroup.setHorizontalAlignment(Element.ALIGN_CENTER); headerGroup.setBackgroundColor(new BaseColor(0xE8, 0xE8, 0xE8)); headerGroup.setFixedHeight(25); table.addCell(headerGroup);

如果要做纵向合并(例如某个指标跨多行显示),使用:

PdfPCell cellRowspan = new PdfPCell(new Phrase("汇总", chineseFont)); cellRowspan.setRowspan(3); table.addCell(cellRowspan);

合并单元格最大的坑是顺序问题:iText 的PdfPTable是流式布局,你addCell的顺序决定了这个 cell 落在哪一行哪一列。一旦使用setRowspan,当前行少了一个单元格位置,下一行会自动补齐。如果内容行和标题行顺序搞错,表格就会错位。我处理多行布局时,习惯按“从左到右、从上到下”一行一行地 add,而不是按内容模块来 add。

强调一下:如果你画出来的表格列数错乱,优先检查addCell的总数是否等于“列数 x 行数减去合并占用的格数”。

3.3 单元格内文字的“Excel 化”排版

光是表格线和底色,还不足以让人一眼看出“这是 Excel 风格的表格”。真正的差异在单元格内部:

  • 表头文字居中,加粗,底色偏深;
  • 数据文字靠左或者靠右对齐,数字靠右;
  • 文字过多时,单元格自动换行,而不是溢出边界。

iText 里Phrase本身就是支持换行的,因为PdfPCell默认开启自动换行。但如果你想实现“单元格内换行 ”(类似 Excel 的 Alt+Enter),不要手动往字符串里拼\n,而是使用Chunk.NEWLINE或者直接给Phrase添加多个Chunk:

Phrase phrase = new Phrase(); phrase.add(new Chunk("第一行\n", chineseFont)); phrase.add(new Chunk("第二行", chineseFont)); PdfPCell cell = new PdfPCell(phrase);

这样每一行的间距和样式都可控,比硬拼字符串效果好。

至于数字右对齐,直接在PdfPCell上setHorizontalAlignment(Element.ALIGN_RIGHT)。注意 iText 的Element.ALIGN_RIGHT是一个 int 常量,网上很多资料写的Element.ALIGN_RIGHT和PdfPCell.ALIGN_RIGHT在 5 里通用,但在 7 里TextAlignment.RIGHT完全不同,这是 5/7 切换时的经典陷阱。

3.4 我的表格实现流程小结

为了不让你看得晕头转向,我把画一个完整 Excel 样式表格的代码流程串成一段伪代码:

  1. 创建Document并设置页边距(例如左右 36pt,上下 54pt);
  2. 注册中文字体BaseFont和Font;
  3. 创建PdfPTable,设置列宽比例和占宽百分比;
  4. 按行依次构造PdfPCell:
    • 表头行:加粗字体、深底色、居中;
    • 数据行:普通字体,数字列右对齐,左侧距可调;
    • 合计行:加粗字体、浅灰底色;
  5. document.add(table);
  6. 关闭document。

真正跑起来,你会发现 iText 的表格生成速度非常快,几百行数据毫秒级完成。

4. 集成测试:把水印、Logo、页眉页码和表格组合起来

4.1 完整代码清单与注释

下面是我在模拟项目里跑通过的完整流程,你可以直接复制改改就能用。我刻意把“Logo 加载失败”的情况也做了容错:Image.getInstance找不到图片时,直接跳过,不让程序崩溃。

public class PdfExcelStyleDemo { private static BaseFont getChineseFont() throws DocumentException, IOException { return BaseFont.createFont( "/fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); } public static void main(String[] args) throws Exception { String dest = "/tmp/excel_style.pdf"; Document document = new Document(PageSize.A4, 36, 36, 54, 54); PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(dest)); BaseFont bfChinese = getChineseFont(); Font headerFont = new Font(bfChinese, 11, Font.BOLD); Font bodyFont = new Font(bfChinese, 10, Font.NORMAL); // Logo 加载,容错处理 Image logo = null; try { logo = Image.getInstance("/tmp/logo.png"); logo.scaleToFit(80, 80); } catch (Exception e) { System.err.println("Logo 加载失败,跳过"); } writer.setPageEvent(new MyPageEvent(logo, bfChinese)); document.open(); // 表格标题 Paragraph title = new Paragraph("项目对账单", new Font(bfChinese, 16, Font.BOLD)); title.setAlignment(Element.ALIGN_CENTER); document.add(title); // 表格主体 PdfPTable table = new PdfPTable(5); table.setWidthPercentage(100); table.setWidths(new float[]{1, 3, 2, 2, 2}); addHeaderCell(table, "序号", headerFont); addHeaderCell(table, "项目名称", headerFont); addHeaderCell(table, "负责人", headerFont); addHeaderCell(table, "金额(元)", headerFont); addHeaderCell(table, "状态", headerFont); // 填充数据行 for (int i = 1; i <= 10; i++) { table.addCell(createBodyCell(String.valueOf(i), bodyFont, Element.ALIGN_CENTER)); table.addCell(createBodyCell("模拟项目 X 第 " + i + " 期", bodyFont, Element.ALIGN_LEFT)); table.addCell(createBodyCell("某负责人", bodyFont, Element.ALIGN_CENTER)); table.addCell(createBodyCell(String.format("%.2f", i * 1234.56), bodyFont, Element.ALIGN_RIGHT)); table.addCell(createBodyCell(i % 2 == 0 ? "已完成" : "进行中", bodyFont, Element.ALIGN_CENTER)); } // 合计行,加粗+底色 PdfPCell totalLabel = new PdfPCell(new Phrase("合计", headerFont)); totalLabel.setColspan(3); totalLabel.setHorizontalAlignment(Element.ALIGN_RIGHT); totalLabel.setBackgroundColor(BaseColor.LIGHT_GRAY); table.addCell(totalLabel); PdfPCell totalValue = new PdfPCell(new Phrase( String.format("%.2f", 1234.56 * 55), headerFont)); totalValue.setHorizontalAlignment(Element.ALIGN_RIGHT); totalValue.setBackgroundColor(BaseColor.LIGHT_GRAY); table.addCell(totalValue); PdfPCell totalStatus = new PdfPCell(new Phrase("-", headerFont)); totalStatus.setHorizontalAlignment(Element.ALIGN_CENTER); totalStatus.setBackgroundColor(BaseColor.LIGHT_GRAY); table.addCell(totalStatus); document.add(table); document.close(); System.out.println("PDF 生成完成:" + dest); } private static void addHeaderCell(PdfPTable table, String text, Font font) { PdfPCell cell = new PdfPCell(new Phrase(text, font)); cell.setHorizontalAlignment(Element.ALIGN_CENTER); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setBackgroundColor(new BaseColor(0xD9, 0xD9, 0xD9)); cell.setFixedHeight(28); cell.setBorderWidth(0.5f); table.addCell(cell); } private static PdfPCell createBodyCell(String text, Font font, int align) { PdfPCell cell = new PdfPCell(new Phrase(text, font)); cell.setHorizontalAlignment(align); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setPaddingLeft(6); cell.setPaddingTop(4); cell.setPaddingBottom(4); cell.setBorderWidth(0.5f); return cell; } }

我实际跑过这个 demo,生成的 PDF 效果和我预期一致:表头灰底加粗,10 行数据带斑马纹效果(虽然这版代码没加斑马纹,但你可以很轻松地在循环里交替设置BackgroundColor),合计行跨 3 列、金额右对齐。Logo 因为在/tmp/logo.png存在,正常显示在左上角;页眉文字也在同区域右侧;底部正中间有“第 X 页”。

4.2 斑马纹和自定义颜色:让表格更像 Excel

很多人用 Excel 导出表格,默认就带“每隔一行变灰底色”的斑马纹效果。iText 做这个非常简单,在数据行循环里加个奇偶判断即可:

for (int i = 1; i <= 10; i++) { BaseColor bg = (i % 2 == 0) ? new BaseColor(0xF2, 0xF2, 0xF2) : BaseColor.WHITE; PdfPCell cell = createBodyCell("内容", bodyFont, Element.ALIGN_CENTER); cell.setBackgroundColor(bg); table.addCell(cell); }

这里有个小细节:如果PdfPCell先设置了背景色,后来又被设置了边框宽度,背景色仍会生效,不会冲突。但如果你用了setBorder(Rectangle.NO_BORDER),注意它会把单元格四周所有边框都去掉,而不只是某一边。

还有一个常见误区:有人用BaseColor(0xFF, 0xFF, 0xFF)表示白色,没问题。但设背景色和字体颜色时,尽量用十六进制值,别用BaseColor.GRAY这种预定义色,因为它的灰度带灰蓝色调,打印出来不太讨喜。

4.3 单元格高度与文字溢出的三种处理策略

Excel 表格的单元格高度是可以自由调整的,但 PDF 的单元格高度受setFixedHeight()和内容自然高度共同影响。我总结了三种处理方式:

  1. 固定高度:适合纯文本短内容,比如状态列、序号列,用setFixedHeight(24)。
  2. 最小高度:用setMinimumHeight(20),内容短时保持 20pt,内容长时自动加高。
  3. 自适应高度:不设置任何高度,完全由内容决定。适合备注类字段。

这里要特别提醒:如果你直接用setFixedHeight(),一旦内容超过高度,iText 默认会截断或溢出,不会自动扩充。你希望文字能完整显示,就要设为setMinimumHeight(),或者干脆不设高度。我第一次做项目报告时,表格备注列内容较长,用了固定高度,结果“备注”几个字下面全是空白,后来才意识到这个问题。

另外,单元格内文字过多时,PDF 表格会“长高”,进而把页面撑破。这种情况需要配合onEndPage里的分页处理。iText 的PdfPTable有自动分页能力,但如果你在一个表格里嵌了很多行,分页后表头不会自动重复。想要表头每页重复,需要设置:

table.setHeaderRows(1); // 前 1 行作为表头,跨页自动重复

这个设置在你做“长报表”时非常实用。不设置的话,跨页后第二页就没有表头了,读者看下一页完全不知道列是什么含义。

4.4 实测中的意外情况:表格被截断、水印被表格遮住

我实测的时候遇到过两个有意思的情况,正好一并分享。

第一个是表格整体被“切”掉。原因是Document默认边距较小,而我给表格加了setSpacingBefore和setSpacingAfter,两个间距叠加可能导致表格起点超出页面。解决办法是:要么调大页面边距,要么去掉其中一个间距。我后来统一采用“页面上方留出 54pt 给页眉和 Logo,表格从正文区开始”的做法,很少再出问题。

第二个是水印部分区域被表格内容遮住。我在onEndPage里用的是getDirectContent(),这是正文之上绘制。水印应该是背景,所以应该用getDirectContentUnder()。两种方法的视觉效果差异:前者水印压住表格文字,后者表格文字压住水印。Word 里的水印是背景效果,所以要用getDirectContentUnder()。这是我踩过的一个不大不小的坑,这里单独提出来,免得你再踩一次。

5. 性能、内存与真实项目中的坑:从一行日志到一段优化

5.1 生成速度与内存占用实测

我在某物流单据项目里用同一套代码生成过一份 20 页左右的 PDF,里面有多个PdfPTable,每个表格 30 行,还带水印和页眉。实测下来,生成时间大约在 2 秒以内(普通笔记本环境),内存峰值不高,因为 iText 5 默认是流式写出,不是一次性把所有内容载入内存。

但要注意:如果你一次性用document.add()加入大量元素,比如上千行的大表格,建议分页处理或者把数据分批塞进多个表格。iText 5 的PdfPTable不是真正意义上的“一次渲染”,而是内部维护一个数据结构,最后输出。数据量过大时,内存占用会明显上升。

5.2 日志排查法:定位水印和页眉不生效的问题

遇到“水印不显示”“页眉没出来”这种问题,最有效的排查手法不是猜,而是在PdfPageEventHelper的方法里加日志输出。每触发一次onEndPage就打印writer.getPageNumber()。如果日志里完全没有输出,说明事件类没挂载成功;如果输出了但没有视觉效果,再去看画布和坐标。这个习惯我保留到现在,很多同事觉得“加日志很笨”,但 PDF 这种无法直接调试的图形输出,日志反而是最可靠的定位手段。

5.3 其他容易踩的坑汇总

  • 字体路径写错:项目内路径和 classpath 路径不一样。/fonts/simhei.ttf是 classpath 根目录下的 fonts 目录,实际放在哪个目录要和你 build 配置对应。
  • 忘掉document.open():很多人只 new 了Document和PdfWriter,忘了调open(),然后各种报空指针。
  • 用错saveState和restoreState:在PdfContentByte上做文字旋转、颜色变更后,必须成对调用这两个方法。漏掉就会出现“前一页的颜色带到后一页”的诡异情况。
  • 表格跨页表头不重复:再次强调,长表格务必setHeaderRows(1)。

6. 写在最后的经验之言

做 PDF 表格这件事,看起来是个小功能,但牵扯到的细节非常多:中文字体、页面事件、表格布局、性能、不同 iText 版本兼容性。我第一次做的时候踩了无数坑,尤其是中文字体和水印方向的问题,翻了不少文档才找到正确答案。后来我把这套实现逻辑固定在模拟项目里,后续只要是“PDF + 表格 + 水印 + 页眉页码”的需求,基本就是改改数据源和表头,半天就能交付。

如果你要拿这套方案去接真实项目,我有三个建议:

  • 字体授权先问清楚。如果公司对字体版权敏感,直接用思源黑体,别用系统里复制出来的 simhei.ttf。
  • Logo 文件尽量用矢量或高分辨率 PNG。图小了在 PDF 里会糊,这是我实际被吐槽过的问题。
  • 死磕iText版本。如果你用的是 7,上面所有PdfPTable的写法要换成Table、Cell,字体注册方式也不同,不要照抄 5 的代碼。

内容到这里就差不多了。最后分享一个小技巧:如果希望生成的 PDF 在打印时不出现Logo 被缩放变形,就在Image.getInstance之后立刻scaleToFit,不要等加入页面事件后再缩放。别问我为什么知道这个顺序问题——我在某次上线前改需求时被迫反复试了四五次。预祝你一次跑通。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 0:52:00

鸿蒙原生校园通:课表门禁一卡通融合实践

简介&#xff1a;本资源是基于华为HarmonyOS开发的智慧校园应用——“校园通”软件完整工程包&#xff0c;面向鸿蒙初学者、移动应用开发者及高校计算机相关专业学生&#xff0c;旨在提供分布式架构下跨设备协同服务的实战范例。项目覆盖课程表管理、校园地图导航、食堂菜谱、图…

作者头像 李华
网站建设 2026/10/24 0:51:06

slub get_node

SLUB 里访问 per-NUMA-node 结构的最基础辅助函数&#xff0c;一行代码&#xff0c;但背后涉及 SLUB 的节点组织和编译配置。一、函数本身static inline struct kmem_cache_node *get_node(struct kmem_cache *s, int node) {return s->node[node]; }功能给定缓存 s 和 NUMA…

作者头像 李华
网站建设 2026/10/24 0:42:24

JAVA_HOME配置失效的五大原因与精准排查指南

1. 这个报错不是Java没装好&#xff0c;而是环境在“装睡”刚看到这条报错&#xff1a;“The supplied javaHome seems to be invalid. I cannot find the java executable”&#xff0c;很多人的第一反应是——赶紧重装JDK&#xff01;我试过三次&#xff0c;删了装、装了删&a…

作者头像 李华
网站建设 2026/10/24 0:42:18

.env与.gitignore:现代Web开发的配置安全双基石

1. 项目概述&#xff1a;为什么两个小文件能决定一个项目的生死存亡你有没有遇到过这样的情况&#xff1a;刚把代码推到远程仓库&#xff0c;第二天就收到安全团队的紧急通知——数据库密码、API密钥、测试用的支付密钥全被提交上去了&#xff1b;或者新同事克隆完项目&#xf…

作者头像 李华
网站建设 2026/10/24 0:39:58

C++学生成绩管理系统源码实战:从结构体到文件持久化与避坑指南

简介&#xff1a;这是一套面向高校计算机相关专业学生的C学生成绩管理系统完整源码&#xff0c;适合用作毕业设计、课程设计参考&#xff0c;也便于想研究C项目开发与数据库应用的开发者学习。系统实现了教师与学生双角色登录&#xff0c;学生可查看个人全部信息&#xff0c;教…

作者头像 李华