动态生成 PDF 时如何优雅实现“第 x 页 共 y 页”的页码效果?
在使用 iTextSharp 导出 PDF 文件时产品需求定义页脚显示类似 “第 x 页 共 y 页” 的格式。然而当 PDF 内容包含大量动态元素如可变行数的表格、不确定长度的文本等时在生成过程中无法提前获知总页数这就给准确展示“共 y 页”带来了挑战。能不能通过动态部分的内容精确计算出总页数答案其实是肯定的——总页数是可以准确计算出来的只是实现起来需要付出更多代码量并对各种布局边界情况做充分预判。我在实际项目中就经历了这样一段“布满荆棘”的探索之路。在即将完成两种典型 PDF 模板的开发后逐渐意识到最初的技术路径虽然可行但维护成本高、扩展性差于是转向了一种更成熟、更通用的解决方案。下面是已经做完的两种 PDF 模板场景的回顾请不要参考。场景一基础内容 动态表格第一种场景相对简单PDF 主体为固定内容仅包含一个动态行数的表格。此时我们可以通过以下方式估算分页预先测量一页最多容纳多少行根据实际数据行数计算所需页数控制单元格内容自动换行避免因某列过长导致换行。在这种情况下页数预测基本是准确的误差极小。场景二表格 不确定长度的富文本第二种场景复杂得多除了动态表格还包含长度不可控的段落文本如商品描述、备注等。此时仅靠行数估算已不够可靠——文本高度受字体、字号、换行、段间距等多重因素影响。为了提升准确性我尝试对文本高度进行模拟渲染例如使用 iTextSharp 的 ColumnText 预测高度并结合多种边界条件做兜底判断。然而随着业务复杂度上升预判逻辑变得越来越臃肿甚至出现了“为了适配页码而限制内容展示”的反模式。正是在这个阶段我意识到或许我的方向走偏了——我们不应该在生成前“预测”总页数而应该在生成后“回填”总页数。转折点使用 PdfTemplate 实现页码回填重点最终我采用了 iTextSharp 中经典的 “占位 回写” 方案借助PdfPageEventHelper和PdfTemplate实现精准总页数展示在每页生成时页脚写入 “第 x 页 共 页”其中“总页数”位置插入一个空白的 PdfTemplate 占位符整个文档生成完成后即调用 document.close() 之前此时总页数已知遍历所有页面将占位模板更新为真实的总页数值。总结当前方案将预判计算总页数代码替换后代码逻辑复杂度都降低了不少。代码行数肉眼可见的减少到这里功能完成了那如何体现出“优雅”呢其实最开始的预先计算和当前的空白模板都在优雅范围内体现的点主要在于 PDF 生成和页码是一体成型只消耗一次内存当前这种方案因为代码量减少也就更优雅了。完整代码示意如下using iTextSharp.text; using iTextSharp.text.pdf; namespace XZHR.Shop.Api.Services; /// summary /// Pdf 页码生成事件 /// /summary public class PageEventHelper : PdfPageEventHelper { private readonly BaseFont _baseFont; private readonly Font _font; private readonly PdfTemplate _totalPageCountTemplate; // 总页数占位模板 private int _totalPages 0; private float _footerYoffset 20; public PageEventHelper(BaseFont baseFont, PdfTemplate pdfTemplate, float footerYoffset 20) { _baseFont baseFont; _font new Font(baseFont, 12, Font.NORMAL, BaseColor.Black); _totalPageCountTemplate pdfTemplate; // 创建文档时赋值占位模板 _footerYoffset footerYoffset; } public override void OnCloseDocument(PdfWriter writer, Document document) { // 文档关闭时总页数已确定更新占位模板内容 _totalPageCountTemplate.BeginText(); _totalPageCountTemplate.SetFontAndSize(_baseFont, _font.Size); _totalPageCountTemplate.SetColorFill(_font.Color); _totalPageCountTemplate.SetTextMatrix(0, 0); _totalPageCountTemplate.ShowText(_totalPages.ToString()); // 更新最终的总页数 _totalPageCountTemplate.EndText(); base.OnCloseDocument(writer, document); } public override void OnEndPage(PdfWriter writer, Document document) { _totalPages writer.PageNumber; // 写入“第X页”部分 var part1 $第 {_totalPages} 页 共 ; var part2 页; // 计算第一部分文本的宽度 var part1Width _baseFont.GetWidthPoint(part1, _font.Size); var part2Width _baseFont.GetWidthPoint(part2, _font.Size); // 计算起始X坐标以使整体居中 var totalWidth part1Width part2Width; var pageWidth document.PageSize.Width; var startX (pageWidth - totalWidth) / 2; var footerY document.BottomMargin - _footerYoffset; var cb writer.DirectContent; // 写入“第 X 页 共 ” cb.BeginText(); cb.SetFontAndSize(_baseFont, _font.Size); cb.SetColorFill(_font.Color); cb.SetTextMatrix(startX, footerY); cb.ShowText(part1); cb.EndText(); // 添加总页数占位符模板 cb.AddTemplate(_totalPageCountTemplate, startX part1Width, footerY); // 写入“页” cb.BeginText(); cb.SetTextMatrix(startX part1Width 10, footerY); // 10是模板的预估宽度偏移值 cb.ShowText(part2); cb.EndText(); } }调用代码var baseFont BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); writer.PageEvent new PageEventHelper(baseFont, writer.DirectContent.CreateTemplate(30, 16));