SpringBoot集成文件 - 如何集成itextpdf導出PDF?itext的變遷?

除了處理word, excel等文件外,最爲常見的就是PDF的導出了。在java技術棧中,PDF創建和操作最爲常用的itext了,但是使用itext一定要了解其版本歷史和License問題,在早前版本使用的是MPL和LGPL雙許可協議,在5.x以上版本中使用的是AGPLv3(這個協議意味着,只有個人用途和開源的項目才能使用itext這個庫,否則是需要收費的)。本文主要介紹通過SpringBoot集成itextpdf實現PDF導出功能。@pdai

知識準備

需要了解itext,以及itext歷史版本變遷,以及license的問題。

什麼是itext

來源於百度百科:iText是著名的開放源碼的站點sourceforge一個項目(由Bruno Lowagie編寫),是一個用Java和.NET語言寫的庫,用來創建和修改PDF文件。通過iText不僅可以生成PDF或rtf的文檔,而且可以將XML、Html文件轉化爲PDF文件。 iText的安裝非常方便,下載iText.jar文件後,只需要在系統的CLASSPATH中加入iText.jar的路徑,在程序中就可以使用iText類庫了。

iText提供除了基本的創建、修改PDF文件外的其他高級的PDF特性,例如基於PKI的簽名,40位和128位加密,顏色校正,帶標籤的PDF,PDF表單(AcroForms),PDF/X,通過ICC配置文件和條形碼進行顏色管理。這些特性被一些產品和服務中使用,包括Eclipse BIRT,Jasper Reports,JBoss Seam,Windward Reports和pdftk。

一般情況下,iText使用在有以下一個要求的項目中:

  • 內容無法提前利用:取決於用戶的輸入或實時的數據庫信息。
  • 由於內容,頁面過多,PDF文檔不能手動生成。
  • 文檔需在無人蔘與,批處理模式下自動創建。
  • 內容被定製或個性化;例如,終端客戶的名字需要標記在大量的頁面上。

itext的歷史版本和License問題

使用itext一定要了解其版本歷史,和License問題,在早前版本使用的是MPL和LGPL雙許可協議,在5.x以上版本中使用的是AGPLv3(這個協議意味着,只有個人用途和開源的項目才能使用itext這個庫,否則是需要收費的)

  • iText 0.x-2.x/iTextSharp 3.x-4.x
    • 更新時間是2000-2009
    • 使用的是MPL和LGPL雙許可協議
    • 最近的更新是2009年,版本號是iText 2.1.7/iTextSharp 4.1.6.0
    • 此時引入包的GAV版本如下:
<dependency>
  <groupId>com.lowagie</groupId>
  <artifactId>itext</artifactId>
  <version>2.1.7</version>
</dependency>
  • iText 5.x和iTextSharp 5.x
    • 更新時間是2009-2016, 公司化運作,並標準化和提高性能
    • 開始使用AGPLv3協議
      • 只有個人用途和開源的項目才能使用itext這個庫,否則是需要收費的
    • iTextSharp被設計成iText庫的.NET版本,並且與iText版本號同步,iText 5.0.0和iTextSharp5.0.0同時發佈
    • 新功能不在這裏面增加,但是官方會修復重要的bug
    • 此時引入包的GAV版本如下:
<dependency>
  <groupId>com.itextpdf</groupId>
  <artifactId>itextpdf</artifactId>
  <version>5.5.13.3</version>
</dependency>
  • iText 7.x
    • 更新時間是2016到現在
    • AGPLv3協議
    • 完全重寫,重點關注可擴展性和模塊化
    • 不適用iTextSharp這個名稱,都統稱爲iText,有Java和.Net版本
    • JDK 1.7+
    • 此時引入包的GAV版本如下:
<dependency>
  <groupId>com.itextpdf</groupId>
  <artifactId>itext7-core</artifactId>
  <version>7.2.2</version>
  <type>pom</type>
</dependency>

注:iText變化後,GitHub上有團隊基於4.x版本(MPL和LGPL雙許可協議)fork了一個分支成爲OpenPDF,並繼續維護該項目。

標準的itextpdf導出的步驟

itextpdf導出pdf主要包含如下幾步:

@Override
public Document generateItextPdfDocument(OutputStream os) throws Exception {
    // 1. 創建文檔
    Document document = new Document(PageSize.A4);

    // 2. 綁定輸出流(通過pdfwriter)
    PdfWriter.getInstance(document, os);

    // 3. 打開文檔
    document.open();

    // 4. 往文檔中添加內容
    document.add(xxx);

    // 5. 關閉文檔
    document.close();
    return document;
}

document中添加的Element有哪些呢?

需要說明下如下概念之前的差別:

  • Chunk:文檔的文本的最小塊單位
  • Phrase:一系列以特定間距(兩行之間的距離)作爲參數的塊
  • Paragraph:段落是一系列塊和(或)短句。同短句一樣,段落有確定的間距。用戶還可以指定縮排;在邊和(或)右邊保留一定空白,段落可以左對齊、右對齊和居中對齊。添加到文檔中的每一個段落將自動另起一行。

(其它從字面上就可以看出,所以這裏具體就不做解釋了)

實現案例

這裏展示SpringBoot集成itext5導出PDF的例子。

Pom依賴

引入poi的依賴包

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.5.13.3</version>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-asian</artifactId>
    <version>5.2.0</version>
</dependency>

導出PDF

UserController中導出的方法

package tech.pdai.springboot.file.word.poi.controller;


import java.io.OutputStream;

import javax.servlet.http.HttpServletResponse;

import io.swagger.annotations.ApiOperation;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.pdai.springboot.file.word.poi.service.IUserService;

/**
 * @author pdai
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    @ApiOperation("Download Word")
    @GetMapping("/word/download")
    public void download(HttpServletResponse response) {
        try {
            XWPFDocument document = userService.generateWordXWPFDocument();
            response.reset();
            response.setContentType("application/vnd.ms-excel");
            response.setHeader("Content-disposition",
                    "attachment;filename=user_world_" + System.currentTimeMillis() + ".docx");
            OutputStream os = response.getOutputStream();
            document.write(os);
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

UserServiceImple中導出PDF方法

@Override
public Document generateItextPdfDocument(OutputStream os) throws Exception {
    // document
    Document document = new Document(PageSize.A4);
    PdfWriter.getInstance(document, os);

    // open
    document.open();

    // add content - pdf meta information
    document.addAuthor("pdai");
    document.addCreationDate();
    document.addTitle("pdai-pdf-itextpdf");
    document.addKeywords("pdf-pdai-keyword");
    document.addCreator("pdai");

    // add content -  page content

    // Title
    document.add(createTitle("Java 全棧知識體系"));

    // Chapter 1
    document.add(createChapterH1("1. 知識準備"));
    document.add(createChapterH2("1.1 什麼是POI"));
    document.add(createParagraph("Apache POI 是創建和維護操作各種符合Office Open XML(OOXML)標準和微軟的OLE 2複合文檔格式(OLE2)的Java API。用它可以使用Java讀取和創建,修改MS Excel文件.而且,還可以使用Java讀取和創建MS Word和MSPowerPoint文件。更多請參考[官方文檔](https://poi.apache.org/index.html)"));
    document.add(createChapterH2("1.2 POI中基礎概念"));
    document.add(createParagraph("生成xls和xlsx有什麼區別?POI對Excel中的對象的封裝對應關係?"));

    // Chapter 2
    document.add(createChapterH1("2. 實現案例"));
    document.add(createChapterH2("2.1 用戶列表示例"));
    document.add(createParagraph("以導出用戶列表爲例"));

    // 表格
    List<User> userList = getUserList();
    PdfPTable table = new PdfPTable(new float[]{20, 40, 50, 40, 40});
    table.setTotalWidth(500);
    table.setLockedWidth(true);
    table.setHorizontalAlignment(Element.ALIGN_CENTER);
    table.getDefaultCell().setBorder(1);

    for (int i = 0; i < userList.size(); i++) {
        table.addCell(createCell(userList.get(i).getId() + ""));
        table.addCell(createCell(userList.get(i).getUserName()));
        table.addCell(createCell(userList.get(i).getEmail()));
        table.addCell(createCell(userList.get(i).getPhoneNumber() + ""));
        table.addCell(createCell(userList.get(i).getDescription()));
    }
    document.add(table);

    document.add(createChapterH2("2.2 圖片導出示例"));
    document.add(createParagraph("以導出圖片爲例"));
    // 圖片
    Resource resource = new ClassPathResource("pdai-guli.png");
    Image image = Image.getInstance(resource.getURL());
    // Image image = Image.getInstance("/Users/pdai/pdai/www/tech-pdai-spring-demos/481-springboot-demo-file-pdf-itextpdf/src/main/resources/pdai-guli.png");
    image.setAlignment(Element.ALIGN_CENTER);
    image.scalePercent(60); // 縮放
    document.add(image);

    // close
    document.close();
    return document;
}

private List<User> getUserList() {
    List<User> userList = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
        userList.add(User.builder()
                .id(Long.parseLong(i + "")).userName("pdai" + i).email("[email protected]" + i).phoneNumber(121231231231L)
                .description("hello world" + i)
                .build());
    }
    return userList;
}

在實現時可以將如下創建文檔內容的方法封裝到Util工具類中


private Paragraph createTitle(String content) throws IOException, DocumentException {
    Font font = new Font(getBaseFont(), 24, Font.BOLD);
    Paragraph paragraph = new Paragraph(content, font);
    paragraph.setAlignment(Element.ALIGN_CENTER);
    return paragraph;
}


private Paragraph createChapterH1(String content) throws IOException, DocumentException {
    Font font = new Font(getBaseFont(), 22, Font.BOLD);
    Paragraph paragraph = new Paragraph(content, font);
    paragraph.setAlignment(Element.ALIGN_LEFT);
    return paragraph;
}

private Paragraph createChapterH2(String content) throws IOException, DocumentException {
    Font font = new Font(getBaseFont(), 18, Font.BOLD);
    Paragraph paragraph = new Paragraph(content, font);
    paragraph.setAlignment(Element.ALIGN_LEFT);
    return paragraph;
}

private Paragraph createParagraph(String content) throws IOException, DocumentException {
    Font font = new Font(getBaseFont(), 12, Font.NORMAL);
    Paragraph paragraph = new Paragraph(content, font);
    paragraph.setAlignment(Element.ALIGN_LEFT);
    paragraph.setIndentationLeft(12); //設置左縮進
    paragraph.setIndentationRight(12); //設置右縮進
    paragraph.setFirstLineIndent(24); //設置首行縮進
    paragraph.setLeading(20f); //行間距
    paragraph.setSpacingBefore(5f); //設置段落上空白
    paragraph.setSpacingAfter(10f); //設置段落下空白
    return paragraph;
}

public PdfPCell createCell(String content) throws IOException, DocumentException {
    PdfPCell cell = new PdfPCell();
    cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
    cell.setHorizontalAlignment(Element.ALIGN_CENTER);
    Font font = new Font(getBaseFont(), 12, Font.NORMAL);
    cell.setPhrase(new Phrase(content, font));
    return cell;
}

private BaseFont getBaseFont() throws IOException, DocumentException {
    return BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
}

導出後的PDF

添加頁眉頁腳和水印

在itextpdf 5.x 中可以利用PdfPageEvent來完成頁眉頁腳和水印。

package tech.pdai.springboot.file.pdf.itextpdf.pdf;

import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Document;
import com.itextpdf.text.Element;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.ColumnText;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfGState;
import com.itextpdf.text.pdf.PdfPageEventHelper;
import com.itextpdf.text.pdf.PdfTemplate;
import com.itextpdf.text.pdf.PdfWriter;

/**
 * @author pdai
 */
public class MyHeaderFooterPageEventHelper extends PdfPageEventHelper {

    private String headLeftTitle;

    private String headRightTitle;

    private String footerLeft;

    private String waterMark;

    private PdfTemplate total;

    public MyHeaderFooterPageEventHelper(String headLeftTitle, String headRightTitle, String footerLeft, String waterMark) {
        this.headLeftTitle = headLeftTitle;
        this.headRightTitle = headRightTitle;
        this.footerLeft = footerLeft;
        this.waterMark = waterMark;
    }

    @Override
    public void onOpenDocument(PdfWriter writer, Document document) {
        total = writer.getDirectContent().createTemplate(30, 16);
    }

    @Override
    public void onEndPage(PdfWriter writer, Document document) {
        BaseFont bf = null;
        try {
            bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // page header and footer
        addPageHeaderAndFooter(writer, document, bf);

        // watermark
        if (waterMark!=null) {
            addWaterMark(writer, document, bf);
        }
    }

    private void addPageHeaderAndFooter(PdfWriter writer, Document document, BaseFont bf) {
        PdfContentByte cb = writer.getDirectContent();
        cb.saveState();

        cb.beginText();

        cb.setColorFill(BaseColor.GRAY);
        cb.setFontAndSize(bf, 10);


        // header
        float x = document.top(-10);
        cb.showTextAligned(PdfContentByte.ALIGN_LEFT,
                headLeftTitle,
                document.left(), x, 0);
        cb.showTextAligned(PdfContentByte.ALIGN_RIGHT,
                headRightTitle,
                document.right(), x, 0);

        // footer
        float y = document.bottom(-10);
        cb.showTextAligned(PdfContentByte.ALIGN_LEFT,
                footerLeft,
                document.left(), y, 0);
        cb.showTextAligned(PdfContentByte.ALIGN_CENTER,
                String.format("- %d -", writer.getPageNumber()),
                (document.right() + document.left()) / 2,
                y, 0);

        cb.endText();

        cb.restoreState();
    }

    private void addWaterMark(PdfWriter writer, Document document, BaseFont bf) {
        for (int i = 1; i < 7; i++) {
            for (int j = 1; j < 10; j++) {
                PdfContentByte cb = writer.getDirectContent();
                cb.saveState();
                cb.beginText();
                cb.setColorFill(BaseColor.GRAY);
                PdfGState gs = new PdfGState();
                gs.setFillOpacity(0.1f);
                cb.setGState(gs);
                cb.setFontAndSize(bf, 12);
                cb.showTextAligned(Element.ALIGN_MIDDLE, waterMark, 75 * i,
                        80 * j, 30);
                cb.endText();
                cb.restoreState();
            }
        }
    }

    @Override
    public void onCloseDocument(PdfWriter writer, Document document) {
        ColumnText.showTextAligned(total, Element.ALIGN_LEFT, new Phrase(String.valueOf(writer.getPageNumber() - 1)), 2,
                2, 0);
    }
}

添加水印後導出後的PDF

進一步理解

通過如下幾個問題進一步理解itextpdf。

遇到license問題怎麼辦

如前文所述,使用itext一定要了解其版本歷史和License問題,在早前版本使用的是MPL和LGPL雙許可協議,在5.x以上版本中使用的是AGPLv3。 有兩種選擇:

  1. 使用2.1.7版本
<dependency>
  <groupId>com.lowagie</groupId>
  <artifactId>itext</artifactId>
  <version>2.1.7</version>
</dependency>
  1. 使用OpenPDF

GitHub上有團隊基於itext 4.x版本(MPL和LGPL雙許可協議)fork了一個分支成爲OpenPDF,並繼續維護該項目。

爲何添加頁眉頁腳和水印是通過PdfPageEvent來完成

爲何添加頁眉頁腳和水印是通過PdfPageEvent來完成?

舉個例子,如果我們在上述例子中需要在頁腳中顯示 “Page 1 of 3", 即總頁數怎麼辦呢?而itext是流模式的寫入內容,只有寫到最後,才能知道有多少頁,那麼顯示總頁數必須在內容寫完之後(或者關閉之前)確定;這就是爲什麼在onEndPage方法時纔會寫每頁的頁眉頁腳。

iText僅在調用釋放模板方法後纔將PdfTemplate寫入到OutputStream中,否則對象將一直保存在內存中,直到關閉文檔。所以我們可以在最後關閉文檔前,使用PdfTemplate寫入總頁碼。可以理解成先寫個佔位符,然後統一替換。

示例源碼

https://github.com/realpdai/tech-pdai-spring-demos

參考文章

https://itextpdf.com

https://blog.csdn.net/u012397189/article/details/80196974

更多內容

告別碎片化學習,無套路一站式體系化學習後端開發: Java 全棧知識體系(https://pdai.tech)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章