JAVA操作pdf——创建表格
一、前言
在实习的时候遇到了需要将查询到的数据构建成为PDF的情况,于是在网上查找到了相关的Java操作pdf的教程,看到大部分的文章都是使用ITextPdf操作的,于是边借此机会写个笔记记录一下。
准备工作:
此次使用的是maven工具构建环境依赖,依赖如下:
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>7.0.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>io</artifactId>
<version>7.0.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>7.0.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>font-asian</artifactId>
<version>7.0.3</version>
</dependency>
由于我在使用的时候这个依赖的版本已经是7.0.3了,在网上我找到的教程大都是5.0+版本的,所以结合网上的教程和自己的摸索,来探究这个包对pdf的使用方式是怎么样的
二、ItextPdf基本使用
创建一个pdf文档
// 准备写入一个pdf文件
// file对象中的路径是待写入文件的路径
File file = new File("C:/Users/achao/Desktop/hellowpdf.pdf");
// 创建文件输出流
FileOutputStream outputStream = new FileOutputStream(file);
// 创建pdf写入对象
PdfWriter writer = new PdfWriter(outputStream);
// 创建pdf文档对象
PdfDocument pdfDocument = new PdfDocument(writer);
// 创建documen对象(直接与硬盘关联)
Document document = new Document(pdfDocument, PageSize.A4);
// 往document中添加段落
document.add(new Paragraph("hellow pdf"));
// 关闭
document.close();
通过上面的方法,便创建了一个pdf文件在"C:/Users/achao/Desktop/hellowpdf.pdf"路径下,pdf中的内容是一个“hello pdf” 段落。
设置段落格式
// 设置段落的格式
// 对齐方式
paragraph.setTextAlignment(TextAlignment.CENTER); // 居中
paragraph.setTextAlignment(TextAlignment.LEFT); // 靠左
插入表格
File file = new File("C:/Users/achao/Desktop/hellowpdf.pdf");
FileOutputStream outputStream = new FileOutputStream(file);
PdfWriter writer = new PdfWriter(outputStream);
PdfDocument pdfDocument = new PdfDocument(writer);
Document document = new Document(pdfDocument, PageSize.A4);
// 这个数组的大小为表格的列数,里面的数值为列的宽度
float[] columnWidths = new float[]{10.0f, 10.0f, 10.0f, 10.0f};
// 这个布尔值参数的意思是横向拉伸表格,true按比例分配列宽
Table table = new Table(columnWidths, true);
// table和document关联
table.setDocument(document);
// 创建一个单元格
Cell cell = new Cell();
// 单元格添加内容
cell.add("cell1");
// 放进table中
table.addCell(cell);
// 以下的方式可以默认创建cell,但是无法设置更多的样式
table.addCell("string");
table.addCell("string");
table.addCell("string");
table.addCell("string");
table.addCell("string");
table.addCell("string");
table.addCell("string");
// 表格制作完成
table.complete();
document.close();
好了,这就是itext的简单使用,接下来我就要根据工作的具体需求来实现操作表格了。等一下?itex项目中不能用?因为它不是开源的,不能用于商业用途。。。。。
听到上面的话时,心想,那就换一个呗。
三、PdfBox基本使用
反正已经学了itext基础知识,现在学习PdfBox也应该是得心应手的,两者的代码漏记应该是差不多的吧!我略带自信地在网上寻找pdfbox的相关信息。
准备工作
maven依赖:
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.21</version>
</dependency>
创建一个pdf文档
// 创建一个pdf对象
PDDocument document = new PDDocument();
// 向这个pdf对象中添加一个新的页面
PDPage page = new PDPage();
document.addPage(page);
// 获取内容流
PDPageContentStream contentStream = new PDPageContentStream(document, page);
// 往pdf文件中写入字符串
contentStream.showText("hellow pdfbox");
// 内容输出流关闭
contentStream.close();
// 文档保存
document.save("C:/Users/achao/Desktop/pdfbox.pdf");
// 文档关闭
document.close();
写了个hellow pdfbox之后,我开始沾沾自喜,切 ,这么简单,跟那个itext没什么两样嘛,真想不明白那个itext怎么要收费的,只有傻子才会花钱,买它。
后来,咦?啊这,这,这,这表格对象在哪里?我突然一阵懵逼,在网上疯狂搜索信息,就是没有找到它使用表格的类,只找到最最最原始的方法——画线!我一时间腿软了,问问项目组的人上面能不能花钱买个itext,那玩意好啊,真香。结果,贫穷剥削了我们的劳动力,别说了,撸起袖子加油干!
于是乎琢磨了一个下午,按照工作的需求我搞出了下面这些玩意~
绘制表格
public class PdfBox {
public static void main(String[] args) throws Exception {
List<Map<String, String>> mapList = new ArrayList<>();
Map<String, String> map = new LinkedHashMap<>();
map.put("sec_code", "NB");
map.put("ap_code", "AP");
map.put("ARN", "NBSF580002300");
map.put("exp_ARN", "NBSF580002300");
map.put("no_exp_ARN", "NBSF580002300");
Map<String, String> map1 = new LinkedHashMap<>();
map1.put("sec_code", "NB");
map1.put("ap_code", "AP");
map1.put("ARN", "NBSF580002300");
map1.put("exp_ARN", "NBSF580002300");
map1.put("no_exp_ARN", "NBSF580002300");
Map<String, String> map2 = new LinkedHashMap<>();
map2.put("sec_code2", "NB");
map2.put("ap_code2", "AP");
map2.put("ARN2", "NBSF580002300");
map2.put("exp_ARN2", "NBSF580002300");
map2.put("no_exp_ARN2", "NBSF580002300");
Map<String, String> map3 = new LinkedHashMap<>();
map3.put("sec_code3", "NB");
map3.put("ap_code3", "AP");
map3.put("ARN3", "NBSF580002300");
map3.put("exp_ARN3", "NBSF580002300");
map3.put("no_exp_ARN3", "NBSF580002300");
mapList.add(map);
mapList.add(map1);
mapList.add(map2);
mapList.add(map3);
pringPdf(mapList);
}
public static void pringPdf(List<Map<String, String>> mapList) throws Exception {
if (mapList == null || mapList.size() < 0) {
throw new Exception("mapList is null or mapList`s size is not legal");
} else {
// 创建一个pdf对象
PDDocument document = new PDDocument();
// 向这个pdf对象中添加一个新的页面并获取内容流
PDPageContentStream contentStream = insertPage(document);
// 默认的表格宽度和高度
float tableWidth = 500;
float tableHeight = 700;
// 计算map中的表头数量,确定表格的列数, 添加表头数据
tableBodyText(document,contentStream, tableWidth, tableHeight, mapList);
// 内容输出流关闭
contentStream.close();
// 文档保存
document.save("C:/Users/achao/Desktop/pdfbox.pdf");
// 文档关闭
document.close();
}
}
// 绘制表头并填充信息
private static void tableBodyText(PDDocument document,PDPageContentStream contentStream, float tableWidth,
float tableHeight,
List<Map<String, String>> mapList) throws Exception {
if (mapList == null || mapList.size() <= 0){
throw new Exception("mapList is null or mapList`s size is not legal");
}
// 绘制表格外部方框
drawTable(contentStream, 50, 50, tableWidth, tableHeight);
// 表格横数
// int rows = (int) tableHeight / 50;
int rows = 3; // test
// 计算map中的表头数量,确定表格的列数, 添加表头数据
Map<String, String> map = mapList.get(0);
Set<String> keys = map.keySet();
String[] strings = keys.toArray(new String[keys.size()]);
// 按字符串的长度比例分配列宽度
float[] wids = distributeWids(map, tableWidth);
// 相邻两列的和一半
float[] halfWids = new float[wids.length];
for (int i = 0; i < wids.length; i++) {
if (i==0) {
halfWids[i] = wids[i] / 2;
}else {
halfWids[i] = (wids[i] + wids[i-1]) / 2;
}
}
// System.out.println(halfWids.length == strings.length);
int size = mapList.size();
int residue = 0;
if (rows < size) {
residue = size - rows;
size = rows;
}
for (int i = 0; i < size; i ++){
// 列的绘制坐标增量
float a = 0;
// 插入文字的横坐标增量
float b = 0;
if (i == 0){
// 这个循环添加表头
for (int j = 0; j < strings.length; j++) {
a += wids[j];
b += halfWids[j];
// 这里的50是表格边缘的宽度
drawLine(contentStream, 50 + a, 50, 50 + a, 50 + tableHeight);
// 获取字符串的长度,能使它居中
int len = strings[i].length();
// 注意这里字体大小12的时候,是len*6/2
insertText(contentStream, 50 + b - len * 6 / 2, 50 + tableHeight - 30, strings[j]);
}
drawLine(contentStream, 50, tableHeight, 50 + tableWidth, tableHeight);
}
String[] cells = mapList.get(i).values().toArray(new String[mapList.get(i).values().size()]);
b = 0;
// 插入每行的数据
for (int k = 0; k < cells.length; k++) {
b += halfWids[k];
int len = cells[k].length();
insertText(contentStream, 50 + b- len * 6 / 2, tableHeight-i*50-30, cells[k]);
}
drawLine(contentStream, 50, tableHeight-i*50, 50 + tableWidth, tableHeight-i*50);
}
if (residue > 0){
contentStream.close();
residue = mapList.size() - residue;
// 对于剩下的数据,新创建page递归填充
List<Map<String, String>> newMapList = new ArrayList<>();
while (residue < mapList.size()){
newMapList.add(mapList.get(residue));
residue ++;
}
PDPageContentStream newcontentStream = insertPage(document);
tableBodyText(document, newcontentStream, tableWidth, tableHeight, newMapList);
}
contentStream.close();
}
// 绘制表格,返回每条边的长度
private static void drawTable(PDPageContentStream contentStream, float x, float y, float tableWidth,
float tableHeight) {
try {
contentStream.setStrokingColor(Color.GRAY);
// 左边
drawLine(contentStream, x, y, x, y + tableHeight);
// 上边
drawLine(contentStream, x, y + tableHeight, x + tableWidth, y + tableHeight);
// 右边
drawLine(contentStream, x + tableWidth, y + tableHeight, x + tableWidth, y);
// 下边
drawLine(contentStream, x + tableWidth, y, x, y);
} catch (IOException e) {
e.printStackTrace();
}
}
// 绘制线段
private static void drawLine(PDPageContentStream contentStream, float startX, float startY, float endX, float endY) {
try {
// 这里是确定了画线的两个端点
contentStream.moveTo(startX, startY);
contentStream.lineTo(endX, endY);
contentStream.stroke();
} catch (IOException e) {
e.printStackTrace();
}
}
// 插入文字
private static void insertText(PDPageContentStream contentStream, float x, float y, String text) throws IOException {
PDFont font = PDType1Font.HELVETICA_BOLD;
contentStream.setFont(font, 12);
contentStream.beginText();
contentStream.newLineAtOffset(x, y);
contentStream.showText(text);
contentStream.endText();
}
// 插入一个新的page
private static PDPageContentStream insertPage(PDDocument document) throws IOException {
PDPage page = new PDPage();
document.addPage(page);
return new PDPageContentStream(document, page);
}
// 按照比例返回每列的宽度的占比
private static float[] distributeWids(Map<String, String> map, float tableWidth){
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
List<Float> wids = new ArrayList<>();
// 总的字符串长度
float sumLen = 0;
while (iterator.hasNext()){
Map.Entry<String, String> next = iterator.next();
if (next.getValue().length() > next.getKey().length()){
sumLen += next.getValue().length();
wids.add((float) next.getValue().length());
}else {
sumLen += next.getKey().length();
wids.add((float) next.getKey().length());
}
}
float[] widscol = new float[map.size()];
for (int i = 0; i < wids.size(); i++) {
widscol[i] = tableWidth * wids.get(i) / sumLen;
}
return widscol;
}
}
虽然只有一百行的代码,但是字字透露着我的无奈,我恨不得我直接手绘表格,因为这东西真的是太慢了。效果如下:
我想摊牌了,不想搞下去了,这玩意之后要合并表格岂不是要把我累死。我在想有没有开源的包可以操作doc文档的,然后在转换成pdf。一起负责的同事告诉我说,之前他用过一个模板引擎,感觉还不错。啊这,我果断抛弃pdfbox,追寻这个模板引擎去了。
四、PoiTl模板引擎基本使用
去查看了它的官方文档,好家伙,全中文很是友善,http://deepoove.com/poi-tl/。好像这玩意是个人开发的,这样的话就不用考虑版权的问题了。
粗略开了一下入门的教程,他写的还是很清楚的,我就照搬过来了,等到后面看不懂的时候再复制过来。
大概地操作就是:首先你得有一个本地docx文档,然后这文档内容里面有按照它规范化的标签,在代码中开始编译这个docx文档,等到了相应的对象后,使用一些方法将一些字符串或者行为与文档中的标签关联起来。最后开始渲染输出docx文件。
我一开始心想:这玩意要自己先有模板才能操作?那岂不是我要先自己画好表格然后找它填充数据?感觉有点草率,看到后面才发现它的高级用法——自定义插件(策略)。
表格操作
// 新建一个策略继承指定的接口
public class TablePolicy extends DynamicTableRenderPolicy {
@SneakyThrows
// 记住这个data参数,之后弄清楚怎么传递的就好了。第一个参数是自动获取的
public void render(XWPFTable table, Object data) {
// 判空
if (data == null || table == null){
throw new Exception("data or table is null");
}
// 拿到传递过来的每行的数据
List<Map<String, String>> rows = (List<Map<String, String>>) data;
table.removeRow(0);
// 获取表头
Set<String> keySet = rows.get(0).keySet();
String[] heads = keySet.toArray(new String[keySet.size()]);
// 先删除标签行
table.removeRow(0);
// 添加表头行
XWPFTableRow headRow = table.insertNewTableRow(0);
// 往表头行中添加表头数据
for (int i = 0; i < heads.length; i++) {
XWPFTableCell cell = headRow.createCell();
cell.setText(heads[i]);
}
List<String> pre = new ArrayList(2);
List<Integer> a = new LinkedList<>();
// 遍历每一行的数据
for (int i = 0; i < rows.size(); i++) {
List<String> bef = new ArrayList<>(2);
XWPFTableRow row = table.insertNewTableRow(table.getRows().size());
RowRenderData rowRenderData = new RowRenderData();
// 拿出map
Map<String, String> rowMap = rows.get(i);
// 拿出map中的values
for (int j = 0; j < heads.length; j++) {
// 在该行中添加每个单元格的信息
XWPFTableCell cell = row.createCell();
cell.setText(rowMap.get(heads[j]));
// 记录前三个单元格的信息
if (j<2){
bef.add(rowMap.get(heads[j]));
}
}
// 判断前面一行与当前行的条件是否一致
boolean tag = true;
if (bef.size() > 0 && pre.size() > 0){
for (int z = 0; z < bef.size(); z++) {
if (bef.get(z) != pre.get(z)) {
tag = false;
break;
}
}
}
// 将该行数据添加
if (tag && bef.size() > 0 && pre.size() > 0) a.add(table.getRows().size());
pre = bef;
}
// 合并单元格
for (int i = 0; i < a.size(); i++) {
TableTools.mergeCellsVertically(table, 0, a.get(i)-2-i, a.get(i)-1);
TableTools.mergeCellsVertically(table, 1, a.get(i)-2-i, a.get(i)-1);
}
// // 遍历data中每行的行数据
// Iterator<Map<String, String>> iterator = rows.iterator();
// while (iterator.hasNext()){
// // 创建行
// XWPFTableRow row = table.insertNewTableRow(table.getRows().size());
// // 往行中添加数据
// for (String value : iterator.next().values()){
// XWPFTableCell cell = row.createCell();
// cell.setText(value);
// cell.setWidthType(TableWidthType.AUTO);
// }
// }
// // 合并列
// TableTools.mergeCellsVertically(table,1, 1, 2);
table.setTableAlignment(TableRowAlign.CENTER);
}
}
// 数据封装类
public class ReportJob {
public static void tablePolicy() throws IOException {
// 构造好要传递的数据结构
final List<Map<String, String>> data = new ArrayList<Map<String, String>>();
data.add(new LinkedHashMap<String, String>(){
{
put("name", "achao");put("age", "18");put("score", "11");put(
"school", "xinning");}});
data.add(new LinkedHashMap<String, String>(){
{
put("name", "achao");put("age", "18");put("score", "22");put(
"school",
"xinning");}});
data.add(new LinkedHashMap<String, String>(){
{
put("name", "achao");put("age", "18");put("score", "33");put(
"school",
"xinning");}});
data.add(new LinkedHashMap<String, String>(){
{
put("name", "dt");put("age", "18");put("score", "44");put("school",
"xinning");}});
Map map = new HashMap(){
{
put("tablepolicy", data);}};
// 策略和标签进行绑定
Configure configure = Configure.createDefault();
configure.customPolicy("tablepolicy", new TablePolicy());
// 编译
XWPFTemplate template = XWPFTemplate.compile("C:\\Users\\achao\\Desktop\\payment.docx", configure);
template.render(map);
FileOutputStream stream = new FileOutputStream("C:\\Users\\achao\\Desktop\\payment_out.docx");
// 渲染
template.write(stream);
stream.flush();
// 关闭
stream.close();
template.close();
}
}
// 调用上述数据封装类的方法....
先看看效果吧:
在上面的代码中,那个参数的传递是在一个map中的,而且map中的key值要对应这绑定的标签值。
获取到data参数之后,执行了一下操作:
- 根据参数中的字段数量,确定表格的列数
- 根据表格中的数据量,确定行数
- 根据表格中头几个字段属性的情况,合并单元格
至此,对于公司的数据报表的基本功能实现已经完成了,但是后面还是要转换成为pdf呀,这本来觉得很简单的事情再一次搞昏了头脑。。。。