You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于PDFBox的Java多页PDF续表及边框实现技术求助

实现PDFBox表格自动分页与跨页延续功能

需求概述

  • 生成四边带边框的PDF文档,页面顶部展示文本内容,下方为表格
  • 表格超出页面边框范围时,自动延续至下一页
  • 技术栈:Java语言 + PDFBox库

现有问题

已完成边框、文本内容及表格的基础绘制,但无法实现自动分页及表格跨页延续功能

具体实现方案

核心思路

  1. 计算单页可容纳的表格行数:基于页面尺寸、边框边距、顶部文本占用高度,算出表格区域的可用高度,再除以行高得到单页最大行数
  2. 拆分表格数据:将完整表格数据按单页行数拆分为多个子集
  3. 逐页绘制:每一页都重复绘制边框、顶部文本,然后绘制对应子集的表格内容,数据未写完则自动新建页面

修改后的完整代码

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();
            }
        }
    }
}

关键修改说明

  1. 提取通用绘制方法:将边框、顶部文本的绘制逻辑拆分为独立方法,避免重复代码,方便多页复用
  2. 计算单页容量:通过页面尺寸、文本高度计算出表格的可用空间,进而得到单页能容纳的最大表格行数
  3. 循环分页绘制:通过while循环拆分表格数据,每处理完一页数据就新建页面,直到所有数据绘制完成
  4. 修复文本换行逻辑:原fitTextToWidth方法的行宽计算存在误差,修复后更准确地计算文本换行

内容的提问来源于stack exchange,提问作者Varun Teja

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 22:39:50