最近有個需求須要我用Java手動寫一份PDF報告,通過考察幾種pdf開源代碼,最終選取了itext7,此版本爲7.1.11
,因爲發現網上關於該工具的博文比較少,特別是實戰博文幾乎沒有,在我踩完各類坑,最終把PDF成型後,打算把經驗分享出來,本文經過摘錄解釋來講明,內容來自本人GitHub itext-pdfcss
項目採用了Spring Cloud config
因此配置在git上,僅僅研究itext7不須要用到數據庫等功能,請直接運行PdfMain
類的main
方法,便可生成模擬的PDF報告html
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>
itext7語義自己和前端css很像,因此有點前端基礎仍是比較容易掌握的java
Image indexImage = new Image(ImageDataFactory.create(GenoReportBuilder.class.getClassLoader().getResource("image/gene.png"))); indexImage.setMargins(-50, -60, -60, -60); indexImage.scale(1, 1.05f);
pdf.addNewPage(2).flush();
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.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
,個人理解該方法設值行高絕對值,官方解釋是兩行文字中間基線之間的距離useAllAvailableWidth
表示頁面有多寬,我就有多寬table.startNewRow();
表示新起一行,table每畫一行都要新起一行new Cell().setTextAlignment(TextAlignment.CENTER)
setHeight
,若更大沒有問題,若高度小於或接近字體大小文字可能就消失了,若想讓Cell高度更接近文字高度,請設置Cell
的padding
,即cell.setPadding(-2)
,設置負值便可itext7中若是要表示段落前的空格,不能使用\t
,但換行可使用\n
git
若要實現Tab
效果能夠有多個方法github
\u00a0
符號,大概七、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));
我經常使用的換頁方法爲以下,該方法可保證當即換頁數據庫
doc.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
固然PdfDocument
有addNewPage
其實也能夠用,但有時候你沒把握好刷新時間可能致使某些混亂ide
能畫出多麼複雜的圖形看是誰畫了,在個人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();
咱們可能遇到把一段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);
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); }
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); } } }
在編寫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);
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);
我沒有找到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); } }
通過上述總結,我基本上把項目中的大多基本點和難點都歸納進去了,初次用itext7寫PDF的同窗基本會遇到的問題基本都在上述這些,不理解的就把項目下下來運行Main方法慢慢調試,理解透我這個項目,還有其它問題那基本只能翻官網了
項目Github: https://github.com/tzxylao/onegeno-itext-pdf
itext7官網:https://itextpdf.com/