1. itext7史上最全實戰總結
1.1. 前言
最近有個需求需要我用Java手動寫一份PDF報告,經過考察幾種pdf開源代碼,最終選取了itext7,此版本為7.1.11
,由於發現網上關於該工具的博文比較少,特別是實戰博文幾乎沒有,在我踩完各種坑,最終把PDF成型后,打算把經驗分享出來,本文通過摘錄解釋來說明,內容來自本人GitHub itext-pdf
1.2. 配置文件
項目采用了Spring Cloud config
所以配置在git上,僅僅研究itext7不需要用到數據庫等功能,請直接運行PdfMain
類的main
方法,即可生成模擬的PDF報告
1.3. 版本POM
itext7相關pom
<properties>
<itext.version>7.1.11</itext.version>
</properties>
<dependencies>
<!-- itext7 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>io</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>forms</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>pdfa</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>pdftest</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>font-asian</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.18</version>
</dependency>
<!--itext7 html轉pdf用到的包-->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
1.4. 干貨
itext7語義本身和前端css很像,所以有點前端基礎還是比較容易掌握的
1.4.1. 添加圖片
- 讀取項目中圖片文件
- 設置邊距
- 設置寬高擴大縮小
Image indexImage = new Image(ImageDataFactory.create(GenoReportBuilder.class.getClassLoader().getResource("image/gene.png")));
indexImage.setMargins(-50, -60, -60, -60);
indexImage.scale(1, 1.05f);
1.4.2. 添加指定空白頁
- 添加第2頁為空白頁,立即刷新后再繼續添加
pdf.addNewPage(2).flush();
1.4.3. Div、Paragraph
Div div = new Div();
div.setWidth(UnitValue.createPercentValue(100));
div.setHeight(UnitValue.createPercentValue(100));
div.setHorizontalAlignment(HorizontalAlignment.CENTER);
Paragraph p1 = new Paragraph();
p1.setHorizontalAlignment(HorizontalAlignment.CENTER);
p1.setMaxWidth(UnitValue.createPercentValue(75));
p1.setMarginTop(180f);
p1.setCharacterSpacing(0.4f);
Style large = new Style();
large.setFontSize(22);
large.setFontColor(GenoColor.getThemeColor());
p1.add(new Text("尊敬的 ").addStyle(large));
...
Paragraph p2 = new Paragraph();
...
div.add(p1);
div.add(p2);
- 整塊的內容用Div包裹,這里整塊包裹的好處是什么?一方面排版分明成體系,另一方面若需求是整塊的內容必須在同一個版面,你可以對Div設置
div.setKeepTogether(true);
,盡量保證若整塊的內容超出了一頁,那這塊內容會自動整塊出現在下一頁,上一頁剩下的就留白了 - 可以看到
Div
,Paragraph
可以設置很多屬性,實際上我們常用的組件除了這兩種,還有Table
,Cell
,List
,他們大部分的屬性都是一樣的,只是部分屬性只在部分組件起效果,所以當你設置某個屬性沒起效果也不用奇怪 Paragraph
需要特別注意的一點,想要段落文字居中,不要用setHorizontalAlignment(HorizontalAlignment.CENTER);
這是組件的居中對段落無效,甚至對段落里你放Text
也無效,需要改用setTextAlignment(TextAlignment.CENTER);
Paragraph
段落的行距也是個高頻問題,這里給出官方我看到的解釋,參考https://itextpdf.com/en/resources/books/itext-7-building-blocks/chapter-4-adding-abstractelement-objects-part-1
,搜關鍵字setFixedLeading
,我的理解該方法設值行高絕對值,官方解釋是兩行文字中間基線之間的距離- 如果想了解詳細的什么屬性哪里能起作用哪里不行,請訪問該地址
1.4.4. Table
useAllAvailableWidth
表示頁面有多寬,我就有多寬table.startNewRow();
表示新起一行,table每畫一行都要新起一行- 同樣table內容需要居中,和段落一樣,請設置
new Cell().setTextAlignment(TextAlignment.CENTER)
- 每個table中cell都有默認高度,會比實際輸入字體高些,此時設置
setHeight
,若更大沒有問題,若高度小於或接近字體大小文字可能就消失了,若想讓Cell高度更接近文字高度,請設置Cell
的padding
,即cell.setPadding(-2)
,設置負值即可
1.4.5. Tab,\t
-
itext7中如果要表示段落前的空格,不能使用
\t
,但換行可以使用\n
-
若要實現
Tab
效果可以有多個方法\u00a0
符號,大概7、8個該符號可表示tab,可能不是很准確
p1.add(new Text("\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0壹基因衷心祝願您身體健康、享受品質生活!"));
p1.setFirstLineIndent(24)
,表示段落前留多少空,需要知道一個字多大,設置成兩倍就行Tab
也是集成AbstractElement
的組件,通過以下方式也可實現相同的效果
p2.add(new Tab()); p2.addTabStops(new TabStop(20, TabAlignment.LEFT));
1.4.6. 換頁
我常用的換頁方法為如下,該方法可保證立即換頁
doc.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
當然PdfDocument
有addNewPage
其實也可以用,但有時候你沒把握好刷新時間可能導致某些混亂
1.4.7. 畫圖或畫文字
能畫出多么復雜的圖形看是誰畫了,在我的PDF中,我畫的最復雜的圖形如下
該圖形由多個弧形區域加線段加文字組成,包括數字上的小箭頭也是畫出來的,畫這個的代碼過多,想要了解詳細的可以自行下載研究,這里介紹API功能
lineTo
畫線段roundRectangle
可用來畫角是弧形的方形,也可以用來畫圓showText
用來畫文字
以上幾種結合填充即可把三角形,多邊形畫出來了
PdfPage page = pdf.getPage(pdf.getNumberOfPages());
pageSize = pdf.getDefaultPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(page);
pdfCanvas.saveState().moveTo(pageSize.getWidth() / 2 - 100 + i * 40, yOffset - 203)
.lineTo(pageSize.getWidth() / 2 - 100 + i * 40, yOffset - 208)
.stroke().restoreState();
pdfCanvas.setLineWidth(2);
pdfCanvas.setStrokeColor(color);
pdfCanvas.roundRectangle(pageSize.getWidth() / 2 - 3 + posXOffset, yOffset - 188, 6, 6, 3)
.stroke();
pdfCanvas.beginText()
.setFontAndSize(font, 12)
.moveText(pageSize.getWidth() / 2 - text.length() * 12 / 2, yOffset - 45);
pdfCanvas.showText(text);
pdfCanvas.endText();
1.4.8. Html段落轉Pdf段落
我們可能遇到把一段Html文本轉換成itext7的段落放進來,此時需要用到它的htmlToPdf模塊,該模塊對應POM
<!--itext7 html轉pdf用到的包-->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>3.0.0</version>
</dependency>
至於使用,設置好配置屬性,使用也很簡單,通常我們需要支持中文,所有配置如下,字體可以自己換
ConverterProperties proper = new ConverterProperties();
//字體設置,解決中文不顯示問題
FontSet fontSet = new FontSet();
fontSet.addFont(GenoReportBuilder.class.getClassLoader().getResource("font/SourceHanSansCN-Regular.ttf").getPath(), PdfEncodings.IDENTITY_H);
FontProvider fontProvider = new FontProvider(fontSet);
proper.setFontProvider(fontProvider);
String content = "html內容";
List<IElement> elements = HtmlConverter.convertToElements(content, proper);
轉換的內容是IElement
集合,而IElement
是什么呢?給張圖就了解了
也就是說只要你的html內容是<div></div>
包裹的,你直接把元素轉成itext7的Div
然后add
到document
就可以實現html內容的添加了,當然你也可以用instanceof
判斷不同內容不同處理
如下是我的處理例子供參考,我把輸入html內容樣式進行了一定修改后轉成itext7組件,這里特別提心,html轉過來的itext7組件可能會不支持部分樣式的修改,所以需要在html中進行css樣式的添加,這里我就把字體和高度統一用css設值了
Div overall = new Div();
java.util.List<IElement> iElements = getFixContent(value);
for (IElement iElement : iElements) {
Style style = new Style();
style.setFontSize(10);
style.setCharacterSpacing(0.7f);
if (iElement instanceof Div) {
Div div = (Div) iElement;
java.util.List<IElement> children = div.getChildren();
// 全部段落改成相同樣式
this.addParagraphStyleCircle(style, children);
overall.add(div);
} else if (iElement instanceof Paragraph) {
Paragraph element = (Paragraph) iElement;
overall.add(element.addStyle(style));
}
}
doc.add(overall);
- getFixContent
private java.util.List<IElement> getFixContent(String content) {
if (content.startsWith("<div>")) {
content = content.replaceAll("<div>", "<div style='line-height:18pt;font-size:16px;'>");
} else {
content = "<div style='line-height:18pt;font-size:16px;'>" + content + "</div>";
}
return HtmlConverter.convertToElements(content, proper);
}
- addParagraphStyleCircle
private void addParagraphStyleCircle(Style style, java.util.List<IElement> children) {
for (IElement child : children) {
if (child instanceof Paragraph) {
Paragraph element = (Paragraph) child;
element.addStyle(style);
java.util.List<IElement> children1 = element.getChildren();
this.addParagraphStyleCircle(style, children1);
}
if (child instanceof Div) {
Div div = (Div) child;
java.util.List<IElement> children1 = div.getChildren();
this.addParagraphStyleCircle(style, children1);
}
if (child instanceof Text) {
Text text = (Text) child;
text.addStyle(style);
}
}
}
1.4.9. 監聽事件
在編寫pdf的時候,比如一篇整體的文章,我們需要在頁眉位置添加關於這篇文章的固定文本或者圖形,類似於打個標簽,表示你翻了這么多頁一直在看這篇文章,當第二篇文章的時候就換一個,舉個例子
- 第一頁
- 第二頁
這種需求我們如何實現呢?思路分析發現,我們需要知道什么時候文章內容一頁寫不起了,換了一頁的時候我們需要添加一個同樣的頁眉。這樣我們就需要知道頁是何時添加的,監聽事件就是處理這種問題的
- pdf是
PdfDocument
,可添加的事件有START_PAGE
,INSERT_PAGE
,REMOVE_PAGE
,END_PAGE
共四個,如上需求我們需要監聽START_PAGE
事件,在事件處理中做相應的處理,我在事件中使用PdfCanvas
畫了頭部內容
HeaderTextEvent headerTextEvent = new HeaderTextEvent(title, font);
pdf.addEventHandler(PdfDocumentEvent.START_PAGE, headerTextEvent);
- HeaderTextEvent類,
Painting
僅僅是封裝了PdfCanvas
public class HeaderTextEvent implements IEventHandler {
private String text;
private PdfFont font;
public HeaderTextEvent(String text,PdfFont font) {
this.text = text;
this.font = font;
}
@Override
public void handleEvent(Event event) {
PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
PdfDocument pdfDoc = docEvent.getDocument();
Painting painting = new Painting(pdfDoc, font);
painting.drawHeader();
painting.drawHeaderText(text);
painting.close();
}
}
在添加內容前添加相應事件,同時需要記得在不需要的時候移除
// 移除監聽器
pdf.removeEventHandler(PdfDocumentEvent.START_PAGE, headerTextEvent);
1.4.10. 添加目錄
我沒有找到itext7原生是否有目錄添加,根據我自己的需求,我用Table
組件來實現了自定義目錄,由於我的PDF是用來打印的,所以我並沒有給目錄添加Link
,也就是頁面跳轉,不過當你徹底理解了我的項目,我想這個需求實現也不難
- 實現效果如下,隨着內容的增長,目錄自動增長
先說下遇到的困難,目錄顧明思意,必須要有內容才會有目錄,所以實際上目錄是最后添加的,但如果我們添加內容到最后再跳轉到前面的頁面來添加目錄,有三個問題:
- 目錄有幾頁如何知道?
- 目錄有幾頁不知道,如何知道內容在第幾頁?
- 由於目錄不確定,所以后續內容的頁碼其實也是不確定的,也就是說頁碼也不是一頁頁可以添加過去的
而經過實踐你會發現,我們不能夠回到前幾頁去修改已存在的頁面,因為會提示你已經flush了,不能修改。
這時我看到了movePage這個方法,也就是可以通過移動頁面,把目錄在內容之后生成,后再移動到前幾頁,但是頁碼還是不能修改,發現腦袋不夠想了只能用上屁股,靈光一閃,不能一遍生成為什么不能二次渲染呢?於是研究讀取原pdf在原pdf上修改,二次渲染的時候填上頁碼及移動頁面,主要代碼如下,包括了讀取中間文件,移動目錄,添加每頁頁碼
PdfReader reader = null;
PdfWriter writer = null;
String inPath = getInPath();
try {
reader = new PdfReader(new File(inPath));
writer = new PdfWriter(new File(outPath));
} catch (IOException e) {
e.printStackTrace();
}
PdfDocument pdf = new PdfDocument(reader, writer);
Document doc = new Document(pdf);
int startPage = 7;
int numberOfPages = pdf.getNumberOfPages();
for (int i = 0; i < catalogSize; i++) {
pdf.movePage(numberOfPages, startPage);
}
String forbidPage = properties.getProperty("forbidPage");
for (int pageNumber = 1; pageNumber < numberOfPages + 1; pageNumber++) {
if (pageNumber > 6 + catalogSize && pageNumber != 8 + catalogSize) {
if (forbidPage != null && (pageNumber - catalogSize) >= Integer.parseInt(forbidPage)) {
continue;
}
PageSize pageSize = pdf.getDefaultPageSize();
doc.showTextAligned(new Paragraph(String.format("- %d -", pageNumber)), pageSize.getWidth() / 2, 30, pageNumber, TextAlignment.CENTER, VerticalAlignment.MIDDLE, 0);
}
}
1.5. 總結
經過上述總結,我基本上把項目中的大多基本點和難點都概括進去了,初次用itext7寫PDF的同學基本會遇到的問題基本都在上述這些,不理解的就把項目下下來運行Main方法慢慢調試,理解透我這個項目,還有其它問題那基本只能翻官網了
項目Github: https://github.com/tzxylao/onegeno-itext-pdf
itext7官網:https://itextpdf.com/