为何使用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;