基于PDFBox的Java多页PDF续表及边框实现技术求助
实现PDFBox表格自动分页与跨页延续功能
需求概述
- 生成四边带边框的PDF文档,页面顶部展示文本内容,下方为表格
- 表格超出页面边框范围时,自动延续至下一页
- 技术栈:Java语言 + PDFBox库
现有问题
已完成边框、文本内容及表格的基础绘制,但无法实现自动分页及表格跨页延续功能
具体实现方案
核心思路
- 计算单页可容纳的表格行数:基于页面尺寸、边框边距、顶部文本占用高度,算出表格区域的可用高度,再除以行高得到单页最大行数
- 拆分表格数据:将完整表格数据按单页行数拆分为多个子集
- 逐页绘制:每一页都重复绘制边框、顶部文本,然后绘制对应子集的表格内容,数据未写完则自动新建页面
修改后的完整代码
package PDF; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; public class PDF_Example { public static final String DEST = "21.pdf"; // 页面通用参数 private static final float START_X = 50; private static final float START_Y = 50; private static final float BORDER_WIDTH = 2f; private static final float TEXT_FONT_SIZE = 12f; private static final float TEXT_LINE_HEIGHT = 14f; private static final float TABLE_ROW_HEIGHT = 20f; private static final float TABLE_X = 80; private static final float TABLE_WIDTH = 300f; private static final float CELL_MARGIN = 5f; public static void main(String[] args) throws IOException { new PDF_Example().createPdf(DEST); } public void createPdf(String dest) throws IOException { try (PDDocument document = new PDDocument()) { List<List<String>> tableData = generateTableData(); String topText = "This is a sample paragraph. It contains multiple lines of text to demonstrate how to create paragraphs in a PDF using Apache PDFBox in Java. Adjust the width and line height as needed to fit your requirements."; // 计算单页可容纳的表格行数 PDPage firstPage = new PDPage(); float pageHeight = firstPage.getMediaBox().getHeight(); float pageWidth = firstPage.getMediaBox().getWidth(); float contentWidth = pageWidth - 2 * START_X; // 计算顶部文本占用的高度 String[] textLines = fitTextToWidth(topText, new PDType1Font(Standard14Fonts.FontName.HELVETICA), TEXT_FONT_SIZE, contentWidth); float textTotalHeight = textLines.length * TEXT_LINE_HEIGHT; // 表格起始Y坐标 = 页面顶部 - 顶部边距(100) - 文本高度 float tableStartY = pageHeight - 100 - textTotalHeight; // 表格可用高度 = 表格起始Y - 底部边距(START_Y) float tableAvailableHeight = tableStartY - START_Y; // 单页最大表格行数 int maxRowsPerPage = (int) (tableAvailableHeight / TABLE_ROW_HEIGHT); // 拆分表格数据,逐页绘制 int totalRows = tableData.size(); int currentRow = 0; while (currentRow < totalRows) { PDPage page = new PDPage(); document.addPage(page); try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { // 绘制页面边框 drawPageBorder(contentStream, page); // 绘制顶部文本 drawTopText(contentStream, page, topText); // 计算当前页要绘制的表格行范围 int endRow = Math.min(currentRow + maxRowsPerPage, totalRows); List<List<String>> pageTableData = tableData.subList(currentRow, endRow); // 绘制当前页表格,注意起始Y坐标 drawTable(contentStream, TABLE_X, tableStartY, pageTableData); currentRow = endRow; } } document.save(dest); } } // 绘制页面边框 private void drawPageBorder(PDPageContentStream contentStream, PDPage page) throws IOException { float width = page.getMediaBox().getWidth() - 2 * START_X; float height = page.getMediaBox().getHeight() - 2 * START_Y; contentStream.setLineWidth(BORDER_WIDTH); // 底部边框 contentStream.moveTo(START_X, START_Y); contentStream.lineTo(START_X + width, START_Y); // 右侧边框 contentStream.lineTo(START_X + width, START_Y + height); // 顶部边框 contentStream.lineTo(START_X, START_Y + height); // 左侧边框 contentStream.lineTo(START_X, START_Y); contentStream.stroke(); } // 绘制顶部文本 private void drawTopText(PDPageContentStream contentStream, PDPage page, String text) throws IOException { PDType1Font font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); float textX = START_X + 30; // 和原代码的80对应(50+30) float textY = page.getMediaBox().getHeight() - 100; float contentWidth = page.getMediaBox().getWidth() - 2 * START_X; String[] lines = fitTextToWidth(text, font, TEXT_FONT_SIZE, contentWidth); contentStream.setFont(font, TEXT_FONT_SIZE); for (String line : lines) { contentStream.beginText(); contentStream.newLineAtOffset(textX, textY); contentStream.showText(line); contentStream.endText(); textY -= TEXT_LINE_HEIGHT; } } private List<List<String>> generateTableData() { List<List<String>> tableData = new ArrayList<>(); tableData.add(List.of("Name", "Age")); // 重复添加数据,模拟大量行 for (int i = 0; i < 50; i++) { tableData.add(List.of("John Doe", "30")); tableData.add(List.of("Jane Smith", "25")); } return tableData; } private String[] fitTextToWidth(String text, PDType1Font font, float fontSize, float width) throws IOException { List<String> lines = new ArrayList<>(); String[] words = text.split("\\s+"); StringBuilder line = new StringBuilder(); float currentLineWidth = 0; for (String word : words) { float wordWidth = font.getStringWidth(word) / 1000 * fontSize; // 加上空格的宽度 float spaceWidth = font.getStringWidth(" ") / 1000 * fontSize; if (currentLineWidth + wordWidth + spaceWidth > width && line.length() > 0) { lines.add(line.toString().trim()); line = new StringBuilder(); currentLineWidth = 0; } line.append(word).append(" "); currentLineWidth += wordWidth + spaceWidth; } if (line.length() > 0) { lines.add(line.toString().trim()); } return lines.toArray(new String[0]); } private void drawTable(PDPageContentStream contentStream, float x, float y, List<List<String>> tableData) throws IOException { if (tableData.isEmpty()) return; final int rows = tableData.size(); final int cols = tableData.get(0).size(); final float colWidth = TABLE_WIDTH / (float) cols; PDType1Font font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); // 绘制表格横线 float nextY = y; for (int i = 0; i <= rows; i++) { contentStream.moveTo(x, nextY); contentStream.lineTo(x + TABLE_WIDTH, nextY); contentStream.stroke(); nextY -= TABLE_ROW_HEIGHT; } // 绘制表格竖线 float nextX = x; for (int i = 0; i <= cols; i++) { contentStream.moveTo(nextX, y); contentStream.lineTo(nextX, y - rows * TABLE_ROW_HEIGHT); contentStream.stroke(); nextX += colWidth; } // 填充表格内容 contentStream.setFont(font, 14); for (int i = 0; i < rows; i++) { nextY = y - TABLE_ROW_HEIGHT * i; for (int j = 0; j < cols; j++) { nextX = x + colWidth * j; float textX = nextX + CELL_MARGIN; float textY = nextY - 15; contentStream.beginText(); contentStream.newLineAtOffset(textX, textY); contentStream.showText(tableData.get(i).get(j)); contentStream.endText(); } } } }
关键修改说明
- 提取通用绘制方法:将边框、顶部文本的绘制逻辑拆分为独立方法,避免重复代码,方便多页复用
- 计算单页容量:通过页面尺寸、文本高度计算出表格的可用空间,进而得到单页能容纳的最大表格行数
- 循环分页绘制:通过while循环拆分表格数据,每处理完一页数据就新建页面,直到所有数据绘制完成
- 修复文本换行逻辑:原
fitTextToWidth方法的行宽计算存在误差,修复后更准确地计算文本换行
内容的提问来源于stack exchange,提问作者Varun Teja
相关产品推荐
相关产品推荐

