PdfBox品尝(II) TABLE组件的封装

为何使用pdfbox

pdfbox 是一个java操作pdf的工具。相信有经验的大家会说,操作pdf最六的不是Itext吗?各种对于pdf常用的方法应有尽有;确实,Itext和pdfbox我都使用过,Itext确实好用的多的多,也封装了非常多实用的方法。而pdfbox却显得弱的多,都是一些基础的方法,并且对于英语一般的国内小伙伴来说,在网上pdfbox的资料还非常的少。
但是无奈,在项目研发的技术选型中,咱们不能只优先考虑方便。
Itext的License是GPL协议的,实际项目使用它的话,发布需要公开自己的所有源码,或者支付一些money;所以咱们有时还是需要pdfbox这个不起眼的小伙伴的。

pdfbox基础操作与封装

pdfbox的一些基础操作可以看我上一篇文章: PdfBox品尝(一) 常用方法的简单封装 ;
下面的代码中也有一些引用了上一篇文章我自己封装的简单工具类

pdfbox table

如果做一些报表或者同级的话,我们时常需要在pdf中列一个表格。但是pdfbox并没有提供table方法,需要咱们自己封装;我这边封装了一个简单的table组件

table 的列实体
import lombok.AllArgsConstructor;
import lombok.Data;
 * @author Panda
@Data
@AllArgsConstructor
public class Column {
    private String name;
    private float width;
table 实体
import lombok.Data;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import java.util.ArrayList;
import java.util.List;
 * @author Panda
@Data
public class Table {
     * Table 位置
    private float margin;
    private float height;
    private PDRectangle pageSize;
    private float rowHeight;
     * table 字体
    private PDFont textFont;
    private float fontSize;
     * table 内容
    private Integer numberOfRows;
    private List<Column> header;
    private List<List<String>> records;
    private float cellMargin;
    public float getWidth() {
        float tableWidth = 0f;
        for (Column column : header) {
            tableWidth += column.getWidth();
        return tableWidth;
    public List<String> getColumnsNamesAsArray() {
        List<String> columnNames = new ArrayList<>(getNumberOfColumns());
        header.forEach(e -> columnNames.add(e.getName()));
        return columnNames;
    public Integer getNumberOfColumns() {
        return this.getHeader().size();
    public Integer getNumberOfRows() {
        return this.records.size();
     * 获取page显示多少行数据
     * @return
    public Integer getRowsPerPage() {
        return new Double(Math.floor(this.getHeight() / this.getRowHeight())).intValue() - 1;
table 构造器
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import java.util.List;
 * @author Panda
public class TableBuilder {
    private Table table = new Table();
    public TableBuilder setHeight(float height) {
        table.setHeight(height);
        return this;
    public TableBuilder setNumberOfRows(Integer numberOfRows) {
        table.setNumberOfRows(numberOfRows);
        return this;
    public TableBuilder setRowHeight(float rowHeight) {
        table.setRowHeight(rowHeight);
        return this;
    public TableBuilder setContent(List<List<String>> content) {
        table.setRecords(content);
        return this;
    public TableBuilder setColumns(List<Column> columns) {
        table.setHeader(columns);
        return this;
    public TableBuilder setCellMargin(float cellMargin) {
        table.setCellMargin(cellMargin);
        return this;
    public TableBuilder setMargin(float margin) {
        table.setMargin(margin);
        return this;
    public TableBuilder setPageSize(PDRectangle pageSize) {
        table.setPageSize(pageSize);
        return this;
    public TableBuilder setTextFont(PDFont textFont) {
        table.setTextFont(textFont);
        return this;
    public TableBuilder setFontSize(float fontSize) {
        table.setFontSize(fontSize);
        return this;
    public Table build() {
        return table;
pdf 中放置table的第一页实体

因为封装的这个table实现了超过了pdf页高后会自动分页,而且默认是一页一个table开始。如果你要将自己的table放在某个有其它内容的页面中;需要将这个页的table实体,也就是第一页的table实体

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
 * @author Andy
@Data
public class FirstTablePage {
    private PDPage firstPdPage;
    @ApiModelProperty("第一页显示的数据条数")
    private Integer dataNum;
    private Float margin;
    private PDPageContentStream contentStream;
table生成器
import com.zmg.panda.utils.pdfbox.PdfBoxUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
 * @author Andy
public class PdfTableGenerator {
     * 生成table,无main业务首页
     * @param document
     * @param table
     * @throws IOException
    public void generatePDF(PDDocument document, Table table) throws IOException{
        // 每页的行数
        Integer rowsPerPage = table.getRowsPerPage();
        // 计算需要多少页
        int numberOfPages = new Double(Math.ceil(table.getNumberOfRows().floatValue() / rowsPerPage)).intValue();
        // 生成每一页
        generateEachPage(document, table, rowsPerPage, numberOfPages);
     * 生成table,含main业务首页
     * @param doc
     * @param firstTablePage main业务首页
     * @param table
     * @throws IOException
    public void drawTableCustom(PDDocument doc, FirstTablePage firstTablePage, Table table) throws IOException {
        // 处理第一页是和业务相关,非独立的
        if (firstTablePage != null) {
            handleMainPage(firstTablePage, table);
        // 每页的行数
        int rowsPerPage = table.getRowsPerPage();
        // 计算需要多少页
        int numberOfPages = new Double(Math.ceil(table.getNumberOfRows().floatValue() / rowsPerPage)).intValue();
        // 剩下的的页
        generateEachPage(doc, table, rowsPerPage, numberOfPages);
     * 处理pdf拥有table的第一页
     * @param firstTablePage
     * @param table
     * @throws IOException
    private void handleMainPage(FirstTablePage firstTablePage, Table table) throws IOException {
        Integer dataNum = firstTablePage.getDataNum();
        PDPageContentStream contentStream = firstTablePage.getContentStream();
        contentStream.setFont(table.getTextFont(), table.getFontSize());
        List<List<String>> content = table.getRecords();
        dataNum = dataNum > content.size() ? content.size() : dataNum;
        List<List<String>> firstPageContent = new ArrayList<>(dataNum);
        Iterator<List<String>> iterator = content.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            List<String> next = iterator.next();
            firstPageContent.add(next);
            iterator.remove();
            index ++;
            if (index >= dataNum) {
                break;
        table.setRecords(content);
        drawFirstCurrentPage(table, firstPageContent, contentStream, firstTablePage.getMargin());
     * 遍历自动生成page
     * @param doc
     * @param table
     * @param rowsPerPage
     * @param numberOfPages
     * @throws IOException
    private void generateEachPage(PDDocument doc, Table table, Integer rowsPerPage, int numberOfPages) throws IOException {
        for (int pageCount = 0; pageCount < numberOfPages; pageCount++) {
            PDPage page = generatePage(doc, table);
            PDPageContentStream contentStream = generateContentStream(doc, page, table);
            List<List<String>> currentPageContent = getContentForCurrentPage(table, rowsPerPage, pageCount);
            drawCurrentPage(table, currentPageContent, contentStream);
     * 写页面
     * @param table
     * @param currentPageContent
     * @param contentStream
     * @throws IOException
    private void drawCurrentPage(Table table, List<List<String>> currentPageContent, PDPageContentStream contentStream)
            throws IOException {
        float tableTopY = table.getPageSize().getHeight() - table.getMargin();
        drawPage(table, currentPageContent, contentStream, tableTopY);
     * 在页面中写入table
     * @param table
     * @param currentPageContent
     * @param contentStream
     * @param tableTopY
     * @throws IOException
    private void drawPage(Table table, List<List<String>> currentPageContent, PDPageContentStream contentStream, float tableTopY) throws IOException {
        // 给table画网格
        drawTableGrid(table, currentPageContent, contentStream, tableTopY);
        // 游标开始点
        float nextTextX = table.getMargin() + table.getCellMargin();
        // 考虑字体高度计算单元格中文本的中心对齐方式
        float nextTextY = tableTopY - (table.getRowHeight() / 2)
                - ((table.getTextFont().getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * table.getFontSize()) / 4);
        // 写入table的表头
        writeContentLine(table.getColumnsNamesAsArray(), contentStream, nextTextX, nextTextY, table);
        nextTextY -= table.getRowHeight();
        nextTextX = table.getMargin() + table.getCellMargin();
        // 写入表数据
        for (int i = 0; i < currentPageContent.size(); i++) {
            writeContentLine(currentPageContent.get(i), contentStream, nextTextX, nextTextY, table);
            nextTextY -= table.getRowHeight();
            nextTextX = table.getMargin() + table.getCellMargin();
        contentStream.close();
     * 写入含有业务的第一页数据
     * @param table
     * @param currentPageContent
     * @param contentStream
     * @throws IOException
    private void drawFirstCurrentPage(Table table, List<List<String>> currentPageContent, PDPageContentStream contentStream, Float margin)
            throws IOException {
        float tableTopY = table.getPageSize().getHeight() - table.getMargin() - margin;
        // 在页面中写入table
        drawPage(table, currentPageContent, contentStream, tableTopY);
     * 为table每一行写入数据
     * @param lineContent
     * @param contentStream
     * @param nextTextX
     * @param nextTextY
     * @param table
     * @throws IOException
    private void writeContentLine(List<String> lineContent, PDPageContentStream contentStream, float nextTextX, float nextTextY,
                                  Table table) throws IOException {
        for (int i = 0; i < table.getNumberOfColumns(); i++) {
            String text = lineContent.get(i);
            contentStream.beginText();
            contentStream.newLineAtOffset(nextTextX, nextTextY);
            contentStream.showText(text != null ? text : "");
            contentStream.endText();
            nextTextX += table.getHeader().get(i).getWidth();
     * 画页面中的table网格
     * @param table
     * @param currentPageContent
     * @param contentStream
     * @param tableTopY
     * @throws IOException
    private void drawTableGrid(Table table, List<List<String>> currentPageContent, PDPageContentStream contentStream, float tableTopY)
            throws IOException {
        // 画行线
        float nextY = tableTopY;
        for (int i = 0; i <= currentPageContent.size() + 1; i++) {
            PdfBoxUtils.drawLine(contentStream, table.getMargin(), nextY, table.getMargin() + table.getWidth(), nextY);
            nextY -= table.getRowHeight();
        // 画列线
        final float tableYLength = table.getRowHeight() + (table.getRowHeight() * currentPageContent.size());
        final float tableBottomY = tableTopY - tableYLength;
        float nextX = table.getMargin();
        for (int i = 0; i < table.getNumberOfColumns(); i++) {
            PdfBoxUtils.drawLine(contentStream, nextX, tableTopY, nextX, tableBottomY);
            nextX += table.getHeader().get(i).getWidth();
        PdfBoxUtils.drawLine(contentStream, nextX, tableTopY, nextX, tableBottomY);
     * 获取page中需要展示的数据行
     * @param table
     * @param rowsPerPage
     * @param pageCount
     * @return
    private List<List<String>> getContentForCurrentPage(Table table, Integer rowsPerPage, int pageCount) {
        int startRange = pageCount * rowsPerPage;
        int endRange = (pageCount * rowsPerPage) + rowsPerPage;
        if (endRange > table.getNumberOfRows()) {
            endRange = table.getNumberOfRows();
        List<List<String>> content = table.getRecords();
        List<List<String>> result = new ArrayList<>(endRange - startRange);
        for (int i = startRange; i < endRange; i ++){
            result.add(content.get(i));
        return result;
     * 生成page
     * @param doc
     * @param table
     * @return
    private PDPage generatePage(PDDocument doc, Table table) {
        PDPage page = new PDPage(table.getPageSize());
        doc.addPage(page);
        return page;
     * 生成页面画笔输出流
     * @param doc
     * @param page
     * @param table
     * @return
     * @throws IOException
    private PDPageContentStream generateContentStream(PDDocument doc, PDPage page, Table table) throws IOException {
        PDPageContentStream contentStream = new PDPageContentStream(doc, page);
        contentStream.setFont(table.getTextFont(), table.getFontSize());
        return contentStream;
    private PDRectangle pageSize = PDRectangle.A4;
    private Integer marginX = 50;
    private Integer marginY = 50;
    @Test
    public void test1() throws IOException {
        PDDocument document = new PDDocument();
        PDType0Font font = PDType0Font.load(document, new FileInputStream(new File("d:\\tmp\\simsun.ttf")));
        drawFirstPage(document, font);
        drawSecondPage(document, font);
        document.save(new FileOutputStream(new File("d:\\tmp\\test2.pdf")));
        document.close();
    private void drawSecondPage(PDDocument document, PDType0Font font) throws IOException {
        PDPage mainTablePage = new PDPage(pageSize);
        document.addPage(mainTablePage);
        PDPageContentStream contentStream = new PDPageContentStream(document, mainTablePage);
        PdfBoxUtils.beginTextSteam(contentStream, 20f, marginX.floatValue(), pageSize.getHeight() - 2*marginY);
        // 书写信息
        PdfBoxUtils.drawParagraph(contentStream, "买卖人商品提交明细", font, 18);
        PdfBoxUtils.endTextSteam(contentStream);
        // 开始绘制table
        List<Column> header = initTableHeader();
        List<List<String>> records = new ArrayList<>();
        for (int i = 0; i < 90; i++) {
            records.add(Arrays.asList( "李太白" + i, "广州市分机构","20202020", "10000000"));
        float tableHight = pageSize.getHeight() - (2 * marginY);
        Table table = new TableBuilder()
                .setCellMargin(4)
                .setRowHeight(20)
                .setColumns(header)
                .setContent(records)
                .setHeight(tableHight)
                .setMargin(marginX)
                .setPageSize(pageSize)
                .setTextFont(font)
                .setFontSize(13)
                .build();
        // 每页最多显示的条数
        Integer rowsPerPage = table.getRowsPerPage();
        // 首页
        Integer dataNum = 30;
        FirstTablePage firstTablePage = new FirstTablePage();
        firstTablePage.setDataNum(dataNum);
        firstTablePage.setMargin(100f);
        firstTablePage.setContentStream(contentStream);
        int firstBatch = rowsPerPage + dataNum;
        List<List<String>> firstRecords = new ArrayList<>(firstBatch);
        Iterator<List<String>> iterator = records.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            List<String> record = iterator.next();
            firstRecords.add(record);
            iterator.remove();
            index ++;
            if (index >= firstBatch) {
                break;
        table.setRecords(firstRecords);
        new PdfTableGenerator().drawTableCustom(document, firstTablePage, table);
        // 剩下的
        int batchNum = rowsPerPage * 2;
        List<List<String>> batchRecords = new ArrayList<>(batchNum);
        iterator = records.iterator();
        index = 0;
        while (iterator.hasNext()) {
            List<String> record = iterator.next();
            batchRecords.add(record);
            iterator.remove();
            index ++;
            if (index % batchNum == 0) {
                table.setRecords(batchRecords);
                new PdfTableGenerator().drawTableCustom(document, null, table);
                batchRecords = new ArrayList<>(batchNum);
        table.setRecords(batchRecords);
        new PdfTableGenerator().drawTableCustom(document, null, table);
    private List<Column> initTableHeader() {
        List<Column> header = new ArrayList<Column>();
        header.add(new Column("买卖人人名称", 150));
        header.add(new Column("店铺名称", 150));
        header.add(new Column("商品号", 100));
        header.add(new Column("商品价格(元)", 100));
        return header;