1. 程式人生 > >itext7史上最全實戰總結

itext7史上最全實戰總結

# 1. itext7史上最全實戰總結 ## 1.1. 前言 最近有個需求需要我用Java手動寫一份PDF報告,經過考察幾種pdf開原始碼,最終選取了itext7,此版本為`7.1.11`,由於發現網上關於該工具的博文比較少,特別是實戰博文幾乎沒有,在我踩完各種坑,最終把PDF成型後,打算把經驗分享出來,本文通過摘錄解釋來說明,內容來自本人GitHub [itext-pdf](https://github.com/tzxylao/onegeno-itext-pdf) ## 1.2. 配置檔案 專案採用了`Spring Cloud config`所以配置在git上,僅僅研究itext7不需要用到資料庫等功能,請直接執行`PdfMain`類的`main`方法,即可生成模擬的PDF報告 ## 1.3. 版本POM itext7相關pom ``` 7.1.11 com.itextpdf kernel ${itext.version} com.itextpdf io ${itext.version} com.itextpdf layout ${itext.version} com.itextpdf forms ${itext.version} com.itextpdf
pdfa ${itext.version}
com.itextpdf pdftest ${itext.version} com.itextpdf font-asian ${itext.version} org.slf4j slf4j-log4j12 1.7.18 com.itextpdf html2pdf 3.0.0
``` ## 1.4. 乾貨 itext7語義本身和前端css很像,所以有點前端基礎還是比較容易掌握的 ### 1.4.1. 新增圖片 1. 讀取專案中圖片檔案 2. 設定邊距 3. 設定寬高擴大縮小 ``` 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. 新增指定空白頁 1. 新增第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); ``` 1. 整塊的內容用Div包裹,這裡整塊包裹的好處是什麼?一方面排版分明成體系,另一方面若需求是整塊的內容必須在同一個版面,你可以對Div設定`div.setKeepTogether(true);`,儘量保證若整塊的內容超出了一頁,那這塊內容會自動整塊出現在下一頁,上一頁剩下的就留白了 2. 可以看到`Div`,`Paragraph`可以設定很多屬性,實際上我們常用的元件除了這兩種,還有`Table`,`Cell`,`List`,他們大部分的屬性都是一樣的,只是**部分屬性只在部分元件起效果**,所以當你設定某個屬性沒起效果也不用奇怪 3. `Paragraph`需要**特別注意的一點**,想要**段落文字居中**,不要用`setHorizontalAlignment(HorizontalAlignment.CENTER);`這是元件的居中對段落無效,甚至對段落裡你放`Text`也無效,需要改用`setTextAlignment(TextAlignment.CENTER);` 4. `Paragraph`段落的行距也是個高頻問題,這裡給出官方我看到的解釋,參考`https://itextpdf.com/en/resources/books/itext-7-building-blocks/chapter-4-adding-abstractelement-objects-part-1`,搜關鍵字`setFixedLeading`,我的理解該方法設值行高絕對值,官方解釋是**兩行文字中間基線之間的距離** 5. 如果想了解詳細的什麼屬性哪裡能起作用哪裡不行,請訪問該[地址](https://itextpdf.com/en/resources/books/itext-7-building-blocks/abstractelement-methods) ![UTOOLS1590980957507.png](https://user-gold-cdn.xitu.io/2020/6/1/1726ddacb13ccf7c?w=1234&h=822&f=png&s=139430) ### 1.4.4. Table 1. `useAllAvailableWidth`表示頁面有多寬,我就有多寬 2. `table.startNewRow();`表示新起一行,table每畫一行都要新起一行 3. 同樣table內容需要居中,和段落一樣,請設定`new Cell().setTextAlignment(TextAlignment.CENTER)` 4. 每個table中cell都有預設高度,會比實際輸入字型高些,此時設定`setHeight`,若更大沒有問題,若**高度小於或接近字型大小**文字可能就消失了,若想讓**Cell高度更接近文字高度**,請設定`Cell`的`padding`,即`cell.setPadding(-2)`,設定負值即可 ### 1.4.5. Tab,\t 1. itext7中如果要表示段落前的空格,不能使用`\t`,但換行可以使用`\n` 2. 若要實現`Tab`效果可以有多個方法 1. `\u00a0`符號,大概7、8個該符號可表示tab,可能不是很準確 ``` p1.add(new Text("\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0壹基因衷心祝願您身體健康、享受品質生活!")); ``` 2. `p1.setFirstLineIndent(24)`,表示段落前留多少空,需要知道一個字多大,設定成兩倍就行 3. `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中,我畫的最複雜的圖形如下 ![UTOOLS1590982696170.png](https://user-gold-cdn.xitu.io/2020/6/1/1726df54deb24ea7?w=533&h=120&f=png&s=7113) 該圖形由多個弧形區域加線段加文字組成,包括數字上的小箭頭也是畫出來的,畫這個的程式碼過多,想要了解詳細的可以自行下載研究,這裡介紹API功能 1. `lineTo`畫線段 2. `roundRectangle`可用來畫角是弧形的方形,也可以用來畫圓 3. `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` ``` com.itextpdf
html2pdf 3.0.0
``` 至於使用,設定好配置屬性,使用也很簡單,通常我們需要支援中文,所有配置如下,字型可以自己換 ``` 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 elements = HtmlConverter.convertToElements(content, proper); ``` 轉換的內容是`IElement`集合,而`IElement`是什麼呢?給張圖就瞭解了 ![UTOOLS1590990939918.png](https://user-gold-cdn.xitu.io/2020/6/1/1726e73291b4984d?w=423&h=386&f=png&s=208373) 也就是說只要你的html內容是``包裹的,你直接把元素轉成itext7的`Div`然後`add`到`document`就可以實現html內容的添加了,當然你也可以用`instanceof`判斷不同內容不同處理 如下是我的處理例子供參考,我把輸入html內容樣式進行了一定修改後轉成itext7元件,這裡特別提心,**html轉過來的itext7元件可能會不支援部分樣式的修改**,所以需要在html中進行css樣式的新增,這裡我就把字型和高度統一用css設值了 ``` Div overall = new Div(); java.util.List 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 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 getFixContent(String content) { if (content.startsWith("")) { content = content.replaceAll("", ""); } else { content = "" + content + ""; } return HtmlConverter.convertToElements(content, proper); } ``` - addParagraphStyleCircle ``` private void addParagraphStyleCircle(Style style, java.util.List children) { for (IElement child : children) { if (child instanceof Paragraph) { Paragraph element = (Paragraph) child; element.addStyle(style); java.util.List children1 = element.getChildren(); this.addParagraphStyleCircle(style, children1); } if (child instanceof Div) { Div div = (Div) child; java.util.List children1 = div.getChildren(); this.addParagraphStyleCircle(style, children1); } if (child instanceof Text) { Text text = (Text) child; text.addStyle(style); } } } ``` ### 1.4.9. 監聽事件 在編寫pdf的時候,比如一篇整體的文章,我們需要在頁首位置新增關於這篇文章的固定文字或者圖形,類似於打個標籤,表示你翻了這麼多頁一直在看這篇文章,當第二篇文章的時候就換一個,舉個例子 - 第一頁 ![UTOOLS1590992499119.png](https://user-gold-cdn.xitu.io/2020/6/1/1726e8afa22fc252?w=803&h=156&f=png&s=10565) - 第二頁 ![UTOOLS1590992555997.png](https://user-gold-cdn.xitu.io/2020/6/1/1726e8bd13996207?w=824&h=165&f=png&s=5674) 這種需求我們如何實現呢?思路分析發現,我們需要知道什麼時候文章內容一頁寫不起了,換了一頁的時候我們需要新增一個同樣的頁首。這樣我們就需要知道頁是何時新增的,監聽事件就是處理這種問題的 - 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`,也就是頁面跳轉,不過當你徹底理解了我的專案,我想這個需求實現也不難 - 實現效果如下,隨著內容的增長,目錄自動增長 ![UTOOLS1590993651033.png](https://user-gold-cdn.xitu.io/2020/6/1/1726e9c86c6e4a9e?w=761&h=412&f=png&s=28215) 先說下遇到的困難,目錄顧明思意,必須要有內容才會有目錄,所以實際上目錄是最後新增的,但如果我們新增內容到最後再跳轉到前面的頁面來新增目錄,有三個問題: 1. 目錄有幾頁如何知道? 2. 目錄有幾頁不知道,如何知道內容在第幾頁? 3. 由於目錄不確定,所以後續內容的頁碼其實也是不確定的,也就是說頁碼也不是一頁頁可以新增過去的 而經過實踐你會發現,我們不能夠回到前幾頁去修改已存在的頁面,因為會提示你已經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方法慢慢除錯,理解透我這個專案,還有其它問題那基本只能翻[官網](https://itextpdf.com/)了 > 專案Github: https://github.com/tzxylao/onegeno-itext-pdf > itext7官網:https://itextpdf.com/