如何使用PDFBox 2.0.26实现内容自动换行分页生成PDDocument
针对PDFBox 2.0.26版本,核心包本身没有内置高层流式排版能力,不存在调用单个方法就自动完成换行、分页、链接绑定的原生API,但相比早年需要手动逐点计算坐标的零散实现,目前已经有非常成熟的简便方案,不需要从零编写排版逻辑。
你可以直接基于PDPageContentStream封装一套轻量排版逻辑,核心只需要实现三个通用能力,代码可以直接复用到所有类似的长文本排版场景:
- 按当前字体、字号计算文本宽度,自动按可用页宽折行
- 写入每一行前判断剩余页高,不足时自动创建新页、重置写入坐标
- 写入文本时同步记录文本块坐标,直接绑定对应
PDAction生成链接注释,不需要事后反查位置
最小可运行实现参考:
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination; import java.awt.*; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class FlowPdfRenderer { // 基础排版配置 private static final float MARGIN = 50; private static final PDType1Font DEFAULT_FONT = PDType1Font.HELVETICA; private static final float DEFAULT_FONT_SIZE = 12; private static final float LINE_HEIGHT = 1.5f * DEFAULT_FONT_SIZE; public static PDDocument renderWithAutoPage(List<TextWithLink> content) throws IOException { PDDocument document = new PDDocument(); // 初始化第一页 PDPage currentPage = new PDPage(PDRectangle.A4); document.addPage(currentPage); PDPageContentStream contentStream = new PDPageContentStream(document, currentPage); contentStream.setFont(DEFAULT_FONT, DEFAULT_FONT_SIZE); float pageWidth = currentPage.getMediaBox().getWidth(); float pageHeight = currentPage.getMediaBox().getHeight(); float usableWidth = pageWidth - 2 * MARGIN; float currentY = pageHeight - MARGIN; // PDF坐标系从左下角原点开始计算 // 遍历所有内容块逐行写入 for (TextWithLink item : content) { List<String> lines = splitTextByWidth(item.getText(), usableWidth); for (String line : lines) { // 剩余高度不足时自动新建页面 if (currentY - LINE_HEIGHT < MARGIN) { contentStream.close(); currentPage = new PDPage(PDRectangle.A4); document.addPage(currentPage); contentStream = new PDPageContentStream(document, currentPage); contentStream.setFont(DEFAULT_FONT, DEFAULT_FONT_SIZE); currentY = pageHeight - MARGIN; } // 写入当前行文本 float lineWidth = DEFAULT_FONT.getStringWidth(line) / 1000 * DEFAULT_FONT_SIZE; contentStream.beginText(); contentStream.newLineAtOffset(MARGIN, currentY); contentStream.showText(line); contentStream.endText(); // 绑定链接动作(如果当前块配置了跳转动作) if (item.getAction() != null) { PDAnnotationLink link = new PDAnnotationLink(); link.setAction(item.getAction()); // 链接热区和文本位置完全对齐 link.setRectangle(new PDRectangle(MARGIN, currentY - 2, lineWidth, DEFAULT_FONT_SIZE + 4)); link.setColor(Color.BLUE); currentPage.getAnnotations().add(link); } currentY -= LINE_HEIGHT; } } contentStream.close(); return document; } // 按可用宽度自动折行的通用方法 private static List<String> splitTextByWidth(String text, float maxWidth) throws IOException { List<String> lines = new ArrayList<>(); String[] words = text.split("\\s+"); StringBuilder currentLine = new StringBuilder(); for (String word : words) { String testContent = currentLine.length() == 0 ? word : currentLine + " " + word; float testWidth = DEFAULT_FONT.getStringWidth(testContent) / 1000 * DEFAULT_FONT_SIZE; if (testWidth > maxWidth && currentLine.length() > 0) { lines.add(currentLine.toString()); currentLine = new StringBuilder(word); } else { currentLine = new StringBuilder(testContent); } } if (currentLine.length() > 0) lines.add(currentLine.toString()); return lines; } // 带链接动作的文本块结构 public static class TextWithLink { private String text; private PDActionGoTo action; public TextWithLink(String text, PDActionGoTo action) { this.text = text; this.action = action; } public String getText() {return text;} public PDActionGoTo getAction() {return action;} } }
你只需要把目录标题、每一条目录项封装成TextWithLink对象传入,就能直接得到排版完成、自动分页、链接可点击的PDDocument对象,目录页码右对齐、点引导线这类效果只需要在折行逻辑里简单扩展即可。
如果不想自己维护排版工具逻辑,可以直接使用适配PDFBox 2.0.x版本的轻量流式排版扩展库,这类库把折行、分页、样式、链接绑定、段落对齐等能力做了完整封装,你只需要按API要求添加文本、绑定对应动作,调用render方法即可直接生成最终的PDDocument,代码量比手写工具类减少70%以上。
这类扩展库是对PDFBox原生能力的薄封装,不会修改核心包逻辑,生成的PDF文件和原生API生成的文件完全兼容,适配2.0.26版本。
注意事项
PDFBox 2.0线的所有版本(包括2.0.26、最终维护版2.0.27)都没有计划在核心包加入高层排版API,所有自动排版能力本质都是对「宽度计算-折行-分页判断-坐标重置-注释绑定」这套基础逻辑的封装,不存在特殊黑魔法。上面两种方案都比早年公开的零散实现更稳定、代码更简洁,完全可以满足多页长文本带链接的排版需求。
内容的提问来源于stack exchange,提问作者Arshad Rehmani

