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

安卓Java环境下使用PDFbox实现PDF动态分页的技术咨询

PDFBox 动态分页功能实现方案

问题背景

我开发了一款将文本存储在Room数据库的应用,用户可选择导出从数据库查询到的文本生成PDF文件。使用PDFBox创建PDF文件时,PDFBox本身不支持自动分页,需要手动实现动态分页逻辑,现有基础代码如下:

现有文件写入核心代码

public void writeDataIntoFile() {
    if (adapter.getItemCount()> 0) {
        String filePath = createFile();
        Log.d("saving pdf", filePath);
        PDFBoxResourceLoader.init(getApplicationContext());
        PDDocument document = new PDDocument();
        PDPage page = new PDPage(PDRectangle.A4);
        PDFont font = PDType1Font.TIMES_ROMAN;
        document.addPage(page);
        PDPageContentStream contentStream;
        try {
            // 定义用于添加PDF内容的内容流
            contentStream = new PDPageContentStream(document, page);
            contentStream.beginText();
            contentStream.setStrokingColor(44, 61, 79);
            contentStream.setFont(font, 18);
            contentStream.setLeading(14.5f);
            ExecutorService executor = Executors.newSingleThreadExecutor();
            Handler handler = new Handler(Looper.getMainLooper());
            showProgressBar();
            executor.execute(() -> {
                try { // 后台任务处理逻辑
                    //TODO 需要添加按分类返回单词的函数
                    contentStream.newLineAtOffset(25, 770);
                    contentStream.showText("Category: " + categoryName);
                    contentStream.newLine();
                    contentStream.newLine();
                    contentStream.setNonStrokingColor(15, 38, 192);
                    contentStream.setFont(font, 12);
                    for (Word word : adapter.getCurrentList()) {
                        showMultiLineText(word.getWord() + ": "
                                + word.getTranslation(),
                                550,
                                contentStream,font,
                                12);
                        contentStream.newLine();
                        count++;
                    }
                    contentStream.endText();
                    contentStream.close();
                    document.save(filePath);
                    saved = true;
                    document.close();
                } catch (IOException e) {
                    Log.d("error", String.valueOf(e));
                }
                handler.post(() -> {
                    // UI线程处理逻辑,更新进度条
                    hideProgressBar();
                    openingPDF(filePath);
                    Toast.makeText(this, "the file has been saved in /Download/MemorizeWords", Toast.LENGTH_SHORT).show();
                });
            });
            executor.shutdown();
        } catch (IOException e) {
            Log.d("error", String.valueOf(e));
        }
    } else {
        Toast.makeText(this, "This category has no words!", Toast.LENGTH_SHORT).show();
    }
}

现有文本自动换行实现

已实现文本宽度检测函数,可在文本超出页面宽度时自动拆分换行:

private void showMultiLineText(String text, int allowedWidth, PDPageContentStream contentStream,
                                          PDFont font,
                                          int fontSize) throws IOException {
        List<String> lines = new ArrayList<String>();
        String line = "";
        // 按空格拆分文本
        String[] words = text.split(" ");
        for(String word : words) {
            if(!line.isEmpty()) {
                line += " ";
            }
            // 检查添加当前单词后是否超出页面宽度
            int size = (int) (fontSize * font.getStringWidth(line + word) / 1000);
            if(size > allowedWidth) {
                // 超出宽度则添加当前行(不含当前单词)到行列表
                lines.add(line);
                // 用当前单词开启新行
                line = word;
            } else {
                // 未超出宽度则将当前单词加入行
                line += word;
            }
        }
        lines.add(line);
        for(String ln : lines) {
            count = lines.size();
            contentStream.showText(ln);
            contentStream.newLine();
        }
    }

动态分页实现方案

核心逻辑

通过跟踪当前页面的剩余垂直空间实现分页:A4纸高度为842pt,预留上下边距各25pt的情况下,页面有效内容高度范围为Y坐标25~770(PDF坐标原点在页面左下角),每次输出内容前计算需要占用的高度,剩余高度不足时新建页面即可。

改造步骤

  1. 新增分页处理方法,用于创建新页面、重置内容流和文本样式
  2. 修改showMultiLineText方法,返回当前文本占用的行数,用于计算高度消耗
  3. 在遍历写入单词前新增剩余高度校验,不足时触发分页

完整修改后代码

首先修改showMultiLineText方法,返回占用行数:

private int showMultiLineText(String text, int allowedWidth, PDPageContentStream contentStream,
                              PDFont font, int fontSize) throws IOException {
    List<String> lines = new ArrayList<>();
    String line = "";
    String[] words = text.split(" ");
    for(String word : words) {
        if(!line.isEmpty()) {
            line += " ";
        }
        int size = (int) (fontSize * font.getStringWidth(line + word) / 1000);
        if(size > allowedWidth) {
            lines.add(line);
            line = word;
        } else {
            line += word;
        }
    }
    lines.add(line);
    for(String ln : lines) {
        contentStream.showText(ln);
        contentStream.newLine();
    }
    // 返回本次输出的行数
    return lines.size();
}

然后修改writeDataIntoFile方法,新增分页逻辑:

// 定义全局常量
private static final float PAGE_BOTTOM_MARGIN = 25f;
private static final float PAGE_TOP_MARGIN = 770f;
private static final float LEADING = 14.5f;
private float currentY; // 跟踪当前Y坐标
private PDDocument document;
private PDFont font;
private PDPageContentStream contentStream;

public void writeDataIntoFile() {
    if (adapter.getItemCount() <= 0) {
        Toast.makeText(this, "This category has no words!", Toast.LENGTH_SHORT).show();
        return;
    }
    String filePath = createFile();
    Log.d("saving pdf", filePath);
    PDFBoxResourceLoader.init(getApplicationContext());
    document = new PDDocument();
    font = PDType1Font.TIMES_ROMAN;
    // 初始化第一页
    createNewPage();
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Handler handler = new Handler(Looper.getMainLooper());
    showProgressBar();
    executor.execute(() -> {
        try {
            contentStream.beginText();
            contentStream.newLineAtOffset(25, currentY);
            // 写入分类标题
            contentStream.setStrokingColor(44, 61, 79);
            contentStream.setFont(font, 18);
            contentStream.showText("Category: " + categoryName);
            contentStream.newLine();
            contentStream.newLine();
            currentY -= LEADING * 2; // 标题占2行
            // 写入单词列表样式设置
            contentStream.setNonStrokingColor(15, 38, 192);
            contentStream.setFont(font, 12);
            for (Word word : adapter.getCurrentList()) {
                String content = word.getWord() + ": " + word.getTranslation();
                // 先计算当前内容需要占用的行数
                int lineCount = calculateLineCount(content, 550, font, 12);
                // 检查剩余高度是否足够
                if (currentY - lineCount * LEADING < PAGE_BOTTOM_MARGIN) {
                    // 不足就新建页面
                    contentStream.endText();
                    contentStream.close();
                    createNewPage();
                    contentStream.beginText();
                    contentStream.newLineAtOffset(25, currentY);
                    // 新页面重新设置样式
                    contentStream.setNonStrokingColor(15, 38, 192);
                    contentStream.setFont(font, 12);
                }
                // 写入内容
                showMultiLineText(content, 550, contentStream, font, 12);
                currentY -= lineCount * LEADING;
                contentStream.newLine();
                currentY -= LEADING;
            }
            contentStream.endText();
            contentStream.close();
            document.save(filePath);
            saved = true;
            document.close();
        } catch (IOException e) {
            Log.d("error", String.valueOf(e));
        }
        handler.post(() -> {
            hideProgressBar();
            openingPDF(filePath);
            Toast.makeText(this, "the file has been saved in /Download/MemorizeWords", Toast.LENGTH_SHORT).show();
        });
    });
    executor.shutdown();
}
// 新建页面方法
private void createNewPage() throws IOException {
    PDPage page = new PDPage(PDRectangle.A4);
    document.addPage(page);
    contentStream = new PDPageContentStream(document, page);
    contentStream.setLeading(LEADING);
    currentY = PAGE_TOP_MARGIN;
}
// 计算文本占用行数的方法,避免提前写入才知道行数
private int calculateLineCount(String text, int allowedWidth, PDFont font, int fontSize) throws IOException {
    List<String> lines = new ArrayList<>();
    String line = "";
    String[] words = text.split(" ");
    for(String word : words) {
        if(!line.isEmpty()) {
            line += " ";
        }
        int size = (int) (fontSize * font.getStringWidth(line + word) / 1000);
        if(size > allowedWidth) {
            lines.add(line);
            line = word;
        } else {
            line += word;
        }
    }
    lines.add(line);
    return lines.size();
}

内容的提问来源于stack exchange,提问作者Yasser Rostom

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 05:30:00