这周写了一周的需求,是制作一个PDF生成功能,其中用到了Itext来制作PDF的视觉效果。其中一些功能不是很懂,仅作记录,若要学习请仔细甄别正确与否。
开始之前,我还是想说,这傻福需求怎么想出来的,让人手撸PDF,哥们一两页PDF写了快1k行代码,代码量大并且感觉极难维护。这次遇见的难点主要有:
1:对于分页的处理。
这个问题真的困扰了很久,现在也无法良好解决。比如在遇见我写一个动态表格的时候,发生了分页,但我一旦分页我要先添加一些别的表头信息再继续我的分页实现。这个时候我首先想到了用事件监听之类的,在发生分页时自动插入某些函数。
注册事件监听器:
pdfDoc.addEventHandler(PdfDocumentEvent.START_PAGE,
new PageNumberEventHandler(font, boldFont));
具体实现(GPT实现)
private static class PageNumberEventHandler implements IEventHandler {
private final PdfFont font;
private final PdfFont boldFont;
public PageNumberEventHandler(PdfFont font, PdfFont boldFont) {
this.font = font;
this.boldFont = boldFont;
}
@Override
public void handleEvent(Event event) {
PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
PdfDocument pdfDoc = docEvent.getDocument();
PdfPage page = docEvent.getPage();
int pageNumber = pdfDoc.getPageNumber(page);
int totalPages = pdfDoc.getNumberOfPages();
// 更新静态变量
InvoiceGenerator.currentPage = pageNumber;
InvoiceGenerator.totalPages = totalPages;
Rectangle pageSize = page.getPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamBefore(),
page.getResources(), pdfDoc);
// 定义绘制区域(关键修正)
Rectangle rootArea = new Rectangle(36, 36,
pageSize.getWidth() - 72,
pageSize.getHeight() - 72);
// 创建Canvas对象
Canvas canvas = new Canvas(pdfCanvas, rootArea);
// 如果是续页(第2页及以后),添加续篇表头
if (pageNumber > 1) {
try {
addContinuationHeader(canvas, font, boldFont, pageNumber);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 添加页码(右上角)
Paragraph pageInfo = new Paragraph()
.add(String.format(PAGE_X_OF_Y_TEXT, pageNumber, totalPages))
.setFont(font)
.setFontSize(8)
.setFixedPosition(
pageSize.getRight() - 36,
pageSize.getTop() - 20,
100)
.setTextAlignment(TextAlignment.RIGHT);
canvas.add(pageInfo);
// 添加修订信息(左下角)
Paragraph revInfo = new Paragraph(REV_INFO)
.setFont(font)
.setFontSize(7)
.setFixedPosition(36, 20, 100);
canvas.add(revInfo);
canvas.close();
pdfCanvas.release();
}
我后面发现,handler实现出来要写在一个canvas里面,而我开始是把所有内容绘制在一个document里面的,这就导致我把canvas放在document里面的时候document里面的内容无法正确识别出我加入的canvas,然后形成了内容的叠加。遂放弃了用监听器。
手动计算分页
比如计算一页最多可以放多少数据,然后放了这么多条我就新建一页
document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));// 强制分页
然后继续在这一页里面写。这样的缺点就是我要计算出准确的能放多少数据,如果数据的变化太多这个方法就不行了。其实后面就遇见了,因为后面插入的每条数据的宽度不一样。然后我之前想的是。把宽度计算出来,结果我第一页的宽度计算和后面的不一样,缕了半小时思路还是有问题。
实际场景是:
插入items里面的每一行数据,按道理来说每一行setbond是(10),但是里面的详情字段可能会超长,然后导致换行,我这里就把这个item当作了两行,这样确实可以做到准确分页,但是会出现的问题是,如果分页刚好发生在换行这个item,那么我这个item就应该到下面去,我设定的是第一页只存12行,存过了就分页。但这样如果我第4个item的时候前面已经有10行了,那么我第4行就要超过了,所以第4行就应该放到下一个去。然后当时就是设置了一个总行数totalNum和一个真实到了哪一个item的数量currentNum。第一页的逻辑就是。总行数totalNum就是每次calculateLineBreaks(commercialInvoiceDto.getItems().get(i).getDescription()
, font
, 8
, 340)得到的行数,这个函数就是计算我这个详情实际占多少行。
if(i+calculateLineBreaks(commercialInvoiceDto.getItems().get(i).getDescription()
, font
, 8
, 340)<12)
那么在第二页的逻辑的时候,我就传一个currentNum去代表这一页开始遍历的item的index。这一页可以存40行数据,那么我每遍历一个currentNum进去,我的totalNum就加一次calculateLineBreaks,然后如果下一次+calculateLineBreaks会超过40我就提前分页。这样我感觉从逻辑上是能行通的。结果后面告诉我不想要这种换行的效果,要采用字体缩放来解决。
字体缩放
结果这才是最痛苦的,本来打算周五可以摸摸鱼看看八股,在GPT和百度的挣扎下,最开始老板告诉我有一个自适应缩放的,找了半天没找到。我就打算采用百度上通过计算我这个字段,计算出他会占多少行,然后按照比例缩小字体大小。结果他的计算方式是大致计算,不能准确的识别出里面的空格,就导致缩放的有可能会不够小。然后我采用一个处理溢出操作的方法,结果和注释所说一样,我的自适应缩放只能缩放到7f大小就不能更小了。
Paragraph para = new Paragraph(Des)
.setFont(font)
.setFontSize(fontSize)
.setWidth(maxWidth);
para.setProperty(Property.OVERFLOW_X, OverflowPropertyValue.FIT);//只能自适应缩放到字体大小大于7f的,看见有什么MIN_FONT_SIZE字段,但我用的7.2.5没有,我看了好久源码也没找到。
2:布局问题
边框重合
在生成过程中,我发现有的线框会变得很粗,这是因为两个外边框的重合会引起变粗的效果。因此可以通过setBorder来操控,比如说和上面的方框重合了,就把上面的setBorderBottom设置为(NO_Border)或者把下面的setBorderTop。
同时注意,外边框和内部文字的边框,如果只想呈现一个框框里面只有文字没有分割线的效果就只在new Table的时候设置边框,如果文字中间需要分割就在Cell里面设置。
对齐问题
一行有多少内容可以存放是由开始设置的分割布局来决定的,因此如果有两行内容可以放在一个table里面,但是注意存放的数量。
3:常见概念
(这是在下周一写的了,哥们明明功能都写完了,结果组长告诉我周五说我现在用的包不能过甲方审核,heartbreak了。然后后面尝试用openPdf做,我感觉太麻烦了,刚好老板说不用做了,但是都写这么多了,再说点我这次 遇见的常用的一些内容吧)
一、表格 (Table) 操作
-
表格初始化
PdfPTable table = new PdfPTable(3);
:创建一个包含 3 列的表格。table.setWidthPercentage(100);
:设置表格宽度占页面宽度的 100%。table.setTotalWidth(new float[]{30, 20, 50});
:自定义列宽比例,这里三列的宽度比例分别为 30、20、50。
-
表格样式属性
table.setSpacingBefore(10f);
:设置表格上方的间距为 10 个单位。table.setSpacingAfter(10f);
:设置表格下方的间距为 10 个单位。table.setLockedWidth(true);
:锁定列宽,防止列宽在后续操作中自动调整。table.getDefaultCell().setBorder(0);
:设置表格默认单元格无边框。
二、单元格 (Cell) 操作
-
基础单元格
PdfPCell cell = new PdfPCell(new Paragraph("内容"));
:创建一个包含文本内容的单元格。cell.setBackgroundColor(BaseColor.ORANGE);
:设置单元格的背景颜色为橙色。cell.setBorderColor(BaseColor.BLUE);
:设置单元格的边框颜色为蓝色。
-
高级属性
cell.setColspan(2);
:使单元格横向合并 2 列。cell.setRowspan(2);
:使单元格纵向合并 2 行。cell.setHorizontalAlignment(Element.ALIGN_CENTER);
:设置单元格内容水平居中对齐。cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
:设置单元格内容垂直居中对齐。
三、文本与段落
-
段落样式
Paragraph p = new Paragraph("文本", FontFactory.getFont("SIMHEI", 12));
:创建一个包含文本的段落,并设置中文字体为 “黑体”,字号为 12。p.setAlignment(Element.ALIGN_CENTER);
:设置段落内容居中对齐。p.setSpacingBefore(5f);
:设置段落前的间距为 5 个单位。
四、图形与图像
-
插入图片
Image img = Image.getInstance("logo.png");
:从指定路径(这里是 “logo.png”)获取图片。PdfPCell imgCell = new PdfPCell(img, true);
:创建一个包含图片的单元格,true
表示图片将被缩放以适应单元格大小。
五、文档元数据
document.addTitle("标题");
:设置文档的标题。document.addKeywords("关键词");
:添加文档的关键词。document.addCreator("创建者");
:设置文档的创建者信息。