天天看點

Java PDF生成方案介紹及問題彙總

PDF生成目錄

    • 前言
    • 功能
      • 1. 使用前端抓取列印
      • 2. 使用後端PDF模闆生成
        • 2.1 pdf模闆的概念
        • 2.2 下載下傳連結
        • 2.3 使用方法
          • 1. 首先建立好我們的基礎模闆
          • 2. 打開後就是我們的建立表單域的界面
          • 3. 建立基本的文本域
          • 4. 編寫後端代碼
          • 5. 總結
      • 3. 使用後端itext動态繪制模闆
    • 問題彙總
      • 1.1 為什麼前端不能使用多圖列印及資料量的情況
      • 1.2 什麼時候适合使用前端列印,什麼時候适合後端模闆呢
      • 3.1 PDF檔案中中文無法正常顯示
      • 3.2 想使用自定義字型怎麼辦?
      • 3.3 怎樣給PDF中添加水印功能
      • 3.4 如何給PDF表格中批量添加圖檔?
      • 3.5 動态生成的PDF如何實作分頁?

前言

本文用于介紹實作的方案,具體的代碼層次隻是大概的去涉及些,并提供開發過程中需要下載下傳的aodbe DC工具及字型檔案等,目的是為了有一個PDF的概念,友善後面開發功能能夠知道怎麼開發如何如去開發持續去完善,期間産生的問題,有些記不清了,等後面使用過程中慢慢去補充,也算是一些經驗記錄.也歡迎一塊讨論一塊補充,找出文中存在的Bug

這裡再抛出個問題,目前尚不知道如何去實作.批量PDF中的批量圖檔怎樣短時間内導出呢?

功能

在業務系統中生成PDF檔案有三種方法,分為兩個層次,一個前端列印, 一個後端列印,其中後端列印又分為兩種,一種是使用模闆列印,另外一種使用動态生成PDF檔案

1. 使用前端抓取列印

需要列印的節點,調用方法即可使用,使用于單頁或者圖檔資料量渲染少的情況 否則會産生問題. 見問題彙總[1.1]
預設print列印的是根節點,如果需要列印自定義節點就需要自己去抓取,把不想列印的地方臨時remove掉或者是指定節點,這個具體再百度

2. 使用後端PDF模闆生成

2.1 pdf模闆的概念

後端PDF模闆原理是通過提前準備好一個pdf模闆,布局好pdf模闆的樣式,使用Adobe Acrobat Reader DC 去對模闆進行表單域處理 預填好對應的屬性 ,諸如下圖中被紅框所圈起來的地方就是表單域,這裡統一使用的是文本域 左下角四個圖檔展示的地方使用的是圖檔域
Java PDF生成方案介紹及問題彙總
這裡提供下思路,對這個pdf模闆進行解析下,其中的黑體文字因為是死值,是以提前在模闆中就把這些值寫死了,而灰色框中的文本是為了預覽顯示的預設值,等這裡填充字段了,就會被修改.我們所需要做的就是組織好pdf的樣式,然後把資料填充進去即可,想起來其實是跟前端列印一個道理,最終還是要組織好樣闆去列印生成

2.2 下載下傳連結

免費版DC下載下傳

下載下傳完成後直接點選安裝即可
           

2.3 使用方法

這裡做個大概的示範,具體的使用及出現的問題還是需要自己去排查摸索
1. 首先建立好我們的基礎模闆

來源可以是自己使用DC做的或者是其他方面提供的 點選簽名->建立表單->在下面選擇我們準備好的模闆

Java PDF生成方案介紹及問題彙總
2. 打開後就是我們的建立表單域的界面

紅框所示的就是我們的工具欄,這塊的操作需要慢慢摸索

Java PDF生成方案介紹及問題彙總
3. 建立基本的文本域
Java PDF生成方案介紹及問題彙總
這裡填寫的就是我們後端所需要的的屬性名 
 舉例: 這裡填寫 name
 後端: map.put("name","張三");  
           
4. 編寫後端代碼
<!-- pom檔案 itext 依賴 -->
 <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.4.3</version>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>fr.opensagres.xdocreport</groupId>
            <artifactId>org.apache.poi.xwpf.converter.pdf</artifactId>
            <version>1.0.4</version>
        </dependency>
           
// pdf txt 測試
		OutputStream os = null;
        ByteArrayOutputStream baos = null;
		String template = "E:/test/ceshi.pdf"; //模闆PDF的位置
        Map<String, Object> treeDto = new HashMap<>(); // 構模組化闆所需要的文本資料
        treeDto.put("titile", "标題");
        treeDto.put("name", "姓名");
        treeDto.put("sex", "性别");
        treeDto.put("idCard", "使用者身份證号");
        /*
         * 構模組化闆中所需要的的圖檔 這裡value都是圖檔的存儲路徑 
         * 但是最終都需要解析為流是以傳參什麼需要自己判斷
         * */
        treeDto.put("stuImg","圖檔位址1"); 
        treeDto.put("img1", "圖檔位址2");
        treeDto.put("img2", "圖檔位址3");
        treeDto.put("img3", "圖檔位址4");
        try {
            os = response.getOutputStream();
            //讀取模闆資源檔案
            PdfReader reader = new PdfReader(template);
            baos = new ByteArrayOutputStream();
            //擷取操作對象
            PdfStamper stamper = new PdfStamper(reader, baos);
            AcroFields form = stamper.getAcroFields();
            //拿到pdf所存在的表單域
            Iterator<String> it = form.getFields().keySet().iterator();
            while (it.hasNext()) {
               // 這裡就是pdf中我們編寫的文本域的名稱,如果相等則比對 賦予相對應的值
                String name = it.next().toString();
                //寫入圖檔
                if ("stuImg".equals(name) || "img1".equals(name) || "img2".equals(name) || "img3".equals(name)) {
                    Image image = Image.getInstance(treeDto.get(name).toString());
                    form.addSubstitutionFont(BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED));
                    //設定圖檔應該顯示的位置 這裡因為隻有一張pdf 是以直接get(0)
                    int pageNo = form.getFieldPositions(name).get(0).page;
                    // 拿到pdf所對應的表單域中的界限 x y
                    Rectangle signRect = form.getFieldPositions(name).get(0).position;
                    float x = signRect.getLeft();
                    float y = signRect.getBottom();
                    PdfContentByte under = stamper.getOverContent(pageNo);
                    image.scaleToFit(signRect.getWidth(), signRect.getHeight());
                    //控制圖檔
                    image.setAbsolutePosition(x, y);
                    //添加圖檔到pdf中
                    under.addImage(image);
                    //寫入文字
                } else {
                    form.setField(name, treeDto.get(name).toString());
                }
            }
            stamper.setFormFlattening(true);
            stamper.close();
            Document doc = new Document();
            PdfCopy copy = new PdfCopy(doc, os);
            doc.open();
            PdfImportedPage importPage = copy.getImportedPage(new PdfReader(baos.toByteArray()), 1);
            copy.addPage(importPage);
            doc.close();
//            response.reset();  這裡本意是清空管道,但是存在一些問題
			//以流的形式傳回給浏覽器 這樣一般的浏覽器就用自帶的工具打開pdf或者下載下傳  特别實用
            logger.info("開始傳回PDF");
            //設定為pdf格式
            response.setContentType("application/pdf");
            response.setHeader("Content-Disposition", "attachment;fileName="
                    + URLEncoder.encode(fileName, "UTF-8"));
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "GET");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
            logger.info("生成預覽檔案成功");
        } catch (Exception e) {
            logger.info("傳回PDF失敗" + e.getMessage());
            e.fillInStackTrace();
        } finally {
        // 不管成功與否關閉流
            IOUtils.closeQuietly(os);
        }
           
5. 總結

隻要能拿到模闆對應的對象,我們就可以操作對象對文本等其他屬性進行指派,隻要能想到的基本都能

夠去操作.功能點很是很多的,不過基本上80%問題使用這些在模闆的固定+資料的指派+代碼的配置上

都能解決了模闆功能及代碼編寫上都能有很多其他的方法去實作更簡潔更實用符合自己業務的功能,

這個就需要後續慢慢去摸索了.

3. 使用後端itext動态繪制模闆

沒錯,這裡又是使用itext來生成pdf檔案,不過和模闆最大的不同是模闆隻能在固定死的模闆上進行縫縫補補的操作

,而這個功能可以實作動态的實作pdf,真正意義上的通過代碼生成pdf檔案

<!-- itextpdf   注意itext 的元件版本保持一緻-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.13.1</version>
        </dependency>
        <!-- itext-asian 語言包 可以獨立出來-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <!-- itextpdf-tool-xmlworker -->
        <dependency>
        <groupId>com.itextpdf.tool</groupId>
        <artifactId>xmlworker</artifactId>
        <version>5.5.13.1</version>
        </dependency>
           
要使用動态生成pdf檔案功能首先要了解幾個概念
  1. 一個PDF 是以Document 為對象 操作的是這個對象裡面的屬性,可以把document對象想象成列印的一張紙,如果需要多頁pdf 那就把内容向下填充
  2. 前期使用會出現一堆問題,如字型 及中文支援上見問題3.1~3.2
  3. 使用pdf建立表格的時候要有一個列的概念,這點在poi excel中也是,在腦袋中表格就是由n行n列的單元格組成,要保證整體是一個長方形
  4. 實作過程是從上到下,從左到右,操作document節點
  5. pdf填充内容的過程是先打開檔案,打開執行寫入程式後,才會關閉

    工具類:

package com.smart.safety.util;

import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * @author MinChang
 * @Date 2021/8/2
 */
public class PdfUtil {

    public static BaseFont bfChinese;
    private static final int defaultSize =12;
    public static BaseFont kaiTiFont;


    static {
        try {
            //指定字型 沒有這個則 createChineseFont 方法無用
            bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H",
                    BaseFont.NOT_EMBEDDED);
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static {
        try {
            //指定字型 沒有這個則 createChineseFont 方法無用
            kaiTiFont = BaseFont.createFont(  "/ttf/KAITIGB2312.ttf",BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    /**
     * @param table  表格
     * @param border  邊框
     * @param rightIndent  占比
     * @param text  文字
     * @param size  size
     * @return
     */
    public static void createCell(PdfPTable table,int border,float rightIndent,String text,int size,int rowSpan,int colSpan){
        PdfPCell pdfPCell = new PdfPCell();
        pdfPCell.setBorder(border); //設定邊框
        pdfPCell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
        pdfPCell.setRightIndent(rightIndent);
        pdfPCell.setPaddingTop(2f);//把字垂直居中
        pdfPCell.setPaddingBottom(8f);//把字垂直居中
        pdfPCell.setRowspan(rowSpan);
        pdfPCell.setPhrase(PdfUtil.createChineseFont(text,size));
        pdfPCell.setColspan(colSpan);
        table.addCell(pdfPCell);
    }
    public static void createCell(PdfPTable table,String text,int size){
        int border=Rectangle.BOX;
        int rowSpan=1;
        int colSpan=1;
        float rightIndent=1;
        PdfPCell pdfPCell = new PdfPCell();
        pdfPCell.setBorder(border); //設定邊框
        pdfPCell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
        pdfPCell.setRightIndent(rightIndent);
        pdfPCell.setPaddingTop(2f);//把字垂直居中
        pdfPCell.setPaddingBottom(8f);//把字垂直居中
        pdfPCell.setRowspan(rowSpan);
        pdfPCell.setPhrase(PdfUtil.createChineseFont(text,size));
        pdfPCell.setColspan(colSpan);
        table.addCell(pdfPCell);
    }
    public static void createCell(PdfPTable table,String text){
        int size = 8;
        int border=Rectangle.BOX;
        int rowSpan=1;
        int colSpan=1;
        float rightIndent=1;
        PdfPCell pdfPCell = new PdfPCell();
        pdfPCell.setBorder(border); //設定邊框
        pdfPCell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
        pdfPCell.setRightIndent(rightIndent);
        pdfPCell.setPaddingTop(2f);//把字垂直居中
        pdfPCell.setPaddingBottom(8f);//把字垂直居中
        pdfPCell.setRowspan(rowSpan);
        pdfPCell.setPhrase(PdfUtil.createChineseFont(text,size));
        pdfPCell.setColspan(colSpan);
        table.addCell(pdfPCell);
    }

    /**
     * 傳回中文Paragraph  預設12
     * @param text
     * @return
     */
    public static Paragraph createChineseFont(String text){
        Font c = new Font(bfChinese, defaultSize, Font.NORMAL);
        return   new Paragraph(text, c);
    }
    /**
     * 傳回中文Paragraph
     * @param text
     * @param size
     * @return
     */
    public static Paragraph createChineseFont(String text,int size){
        if (size==0){
            size=defaultSize;
        }
        Font c = new Font(bfChinese, size, Font.NORMAL);
        return   new Paragraph(text, c);
    }



    /*
     * @Description 藍色背景色标題内容行添加
      
     * @Date  2019/7/12 14:56
     * @param table 表格
     * @param cell  列
     * @param text  文本
     * @return void
     **/
    public static void addTableGroupTitle(PdfPTable table, PdfPCell cell, String text) {
        cell = new PdfPCell(new Phrase(text,getColorFont(BaseColor.WHITE)));
        table.addCell(addTitleCell(cell,25,new BaseColor(69,153,241),2,false));
    }
    /**
     * @Description 藍色背景色标題内容行添加
      
     * @Date  2019/7/12 14:56
     * @param table 表格
     * @param cell  列
     * @param text  文本
     * @param colspan 需要合并的列
     * @return void
     **/
    public static void addTableGroupTitle(PdfPTable table, PdfPCell cell, String text,int colspan) {
        cell = new PdfPCell(new Phrase(text,getColorFont(BaseColor.WHITE)));
        table.addCell(addTitleCell(cell,25,new BaseColor(69,153,241),colspan,false));
    }
    /**
     * @Description 核查建議
      
     * @Date  2019/7/12 14:43
     * @param table 表格
     * @param cell 列
     * @param suggestText 核查建議内容
     * @param fontColor 核查建議内容文字顔色
     * @return com.itextpdf.text.Element
     **/
    public static void addSuggestLine(PdfPTable table,PdfPCell cell,String suggestText,BaseColor fontColor) throws Exception {
        addSuggestLine(table, cell, suggestText, fontColor, 0,10f,30f);
    }
    /**
     * @Description 核查建議
      
     * @Date  2019/7/12 14:43
     * @param table 表格
     * @param cell 列
     * @param suggestText 核查建議内容
     * @param fontColor 核查建議内容文字顔色
     * @param colspan 合并的列
     * @param widths 列所占寬
     * @return com.itextpdf.text.Element
     **/
    public static void addSuggestLine(PdfPTable table,PdfPCell cell,String suggestText,BaseColor fontColor,int colspan,float...widths) throws Exception {
        cell = new PdfPCell(new Phrase("核查建議:",getColorFont()));
        cell.setColspan(1);
        table.addCell(addBaseCell(cell,23,new BaseColor(238,238,238),false));
        cell = new PdfPCell(new Phrase(suggestText,getColorFont(fontColor)));
        if(colspan>0){
            cell.setColspan(colspan);
        }
        table.addCell(addBaseCell(cell,23,new BaseColor(238,238,238),false));
        table.setWidths(getColumnWiths(widths));
    }
    /**
     * @Description 資訊分組table
      
     * @Date  2019/7/12 14:43
     * @param groupText 文本内容
     * @return com.itextpdf.text.Element
     **/
    public static Element addTableGroupLine(String groupText) {
        PdfPTable tableBaseInfoIndex = new PdfPTable(1);
        tableBaseInfoIndex.setWidthPercentage(20);
        PdfPCell cellBaseInfo = new PdfPCell(new Phrase(groupText,getColorFont()));
        cellBaseInfo.setHorizontalAlignment(Element.ALIGN_CENTER);
        tableBaseInfoIndex.addCell(addTitleCell(cellBaseInfo,28,new BaseColor(238,238,238),2,false));
        tableBaseInfoIndex.addCell(addBlankLine(10,1));
        return tableBaseInfoIndex;
    }

    /**
     * @Description 指定顔色字型 預設進行中文顯示
      
     * @Date  2019/7/12 14:05
     * @param color 字型顔色
     * @param fontSize 字型大小
     * @param fontFamily 字型
     * @return com.itextpdf.text.Font
     **/
    public static Font getColorFont(BaseColor color, int fontSize, String fontFamily) {
        Font font = new Font(getFont());
        font.setColor(color);
        if(fontSize>0&&(null!=fontFamily||!"".equals(fontFamily))){
            font.setSize(fontSize);
            font.setFamily(fontFamily);
        }
        return font;
    }
    /**
     * @Description 指定顔色字型 預設進行中文顯示
      
     * @Date  2019/7/12 14:05
     * @param color 字型顔色
     * @return com.itextpdf.text.Font
     **/
    public static Font getColorFont(BaseColor color) {
        return getColorFont(color, 0, null);
    }
    /**
     * @Description  預設進行中文顯示
      
     * @Date  2019/7/12 14:05
     * @return com.itextpdf.text.Font
     **/
    public static Font getColorFont() {
        Font font = new Font(getFont());
        return font;
    }
    /**
     * @Description 指定列寬度
      
     * @Date  2019/7/12 11:59
     * @param widths 一個或多個
     * @return float[]
     **/
    public static float[] getColumnWiths(float...widths){
        float[] columnWidths = new float[widths.length];
        for (int i = 0; i < widths.length; i++) {
            columnWidths[i]=widths[i];
        }
        return columnWidths;
    }
    /**
     * @Description 添加表頭cell
      
     * @Date  2019/7/12 11:36
     * @param titleCell 要操作的cell
     * @param fixedHeight 行高度
     * @param baseColor 背景色
     * @param colspan  合并的列數
     * @param isBottomBorder 是否有下邊框 true 有 fasle 沒有
     * @return com.itextpdf.text.pdf.PdfPCell
     **/
    public static PdfPCell addTitleCell(PdfPCell titleCell,int fixedHeight,BaseColor baseColor,int colspan,boolean isBottomBorder){
        titleCell.setColspan(colspan);
        titleCell.setFixedHeight(fixedHeight);
        titleCell.setUseVariableBorders(true);
        titleCell.setUseAscender(true);
        titleCell.setUseDescender(true);
        titleCell.setBackgroundColor(baseColor);
        if(isBottomBorder){
            titleCell.setBorder(Rectangle.BOTTOM);
            titleCell.setBorderColorBottom(BaseColor.LIGHT_GRAY);
        }else{
            titleCell.setBorder(Rectangle.NO_BORDER);
        }
        titleCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
        return titleCell;
    }

    /**
     * @Description 添加空行
      
     * @Date  2019/7/12 11:36
     * @param fixedHeight 空行高度
     * @param colspan  合并的列數
     * @return com.itextpdf.text.pdf.PdfPCell
     **/
    public static PdfPCell addBlankLine(int fixedHeight,int colspan){
        PdfPCell blankLine = new PdfPCell();
        blankLine.setFixedHeight(fixedHeight);
        blankLine.setBorder(Rectangle.NO_BORDER);
        blankLine.setColspan(colspan);
        return blankLine;
    }
    /**
     * @Description 添加預設cell
      
     * @param baseCell 要操作的cell
     * @Date  2019/7/12 11:36
     * @return com.itextpdf.text.pdf.PdfPCell
     **/
    public static PdfPCell addBaseCell(PdfPCell baseCell){
        baseCell.setFixedHeight(23);
        baseCell.setUseVariableBorders(true);
        baseCell.setUseAscender(true);
        baseCell.setUseDescender(true);
        baseCell.setBorder(Rectangle.BOTTOM);
        baseCell.setBorderColorBottom(BaseColor.LIGHT_GRAY);
        baseCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
        return baseCell;
    }
    /**
     * @Description 添加cell
      
     * @param baseCell 要操作的cell
     * @param isBottomBorder 是否有下邊框 true 有 fasle 沒有
     * @Date  2019/7/12 11:36
     * @return com.itextpdf.text.pdf.PdfPCell
     **/
    public static PdfPCell addBaseCell(PdfPCell baseCell,boolean isBottomBorder){
        baseCell.setFixedHeight(23);
        baseCell.setUseVariableBorders(true);
        baseCell.setUseAscender(true);
        baseCell.setUseDescender(true);
        if(isBottomBorder){
            baseCell.setBorder(Rectangle.BOTTOM);
            baseCell.setBorderColorBottom(BaseColor.LIGHT_GRAY);
        }else{
            baseCell.setBorder(Rectangle.NO_BORDER);
        }
        baseCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
        return baseCell;
    }
    /**
     * @Description 添加cell
     * @param baseCell 要操作的cell
     * @param fixedHeight 行高
     * @param color 背景色
     * @param isBottomBorder 是否有下邊框 true 有 fasle 沒有
     * @Date  2019/7/12 11:36
     * @return com.itextpdf.text.pdf.PdfPCell
     **/
    public static PdfPCell addBaseCell(PdfPCell baseCell,int fixedHeight,BaseColor color,boolean isBottomBorder){
        baseCell.setFixedHeight(fixedHeight);
        baseCell.setUseVariableBorders(true);
        baseCell.setUseAscender(true);
        baseCell.setUseDescender(true);
        if(null!=color){
            baseCell.setBackgroundColor(color);
        }
        if(isBottomBorder){
            baseCell.setBorder(Rectangle.BOTTOM);
            baseCell.setBorderColorBottom(BaseColor.LIGHT_GRAY);
        }else{
            baseCell.setBorder(Rectangle.NO_BORDER);
        }
        baseCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
        return baseCell;
    }
    /**
     * @Description 設定中文支援
      
     * @Date  2019/7/11 10:33
     * @Param []
     * @return com.itextpdf.text.pdf.BaseFont
     **/
    public static BaseFont getFont() {
        BaseFont bf = null;
        try {
            bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
        } catch (Exception e) {
            System.out.println("Exception = " + e.getMessage());
        }
        return bf;
    }
    /**
     * 斜角排列、全屏多個重複的花式文字水印
     *
     * @param input             需要加水印的PDF讀取輸入流
     * @param output            輸出生成PDF的輸出流
     * @param waterMarkString   水印字元
     * @param xAmout            x軸重複數量
     * @param yAmout            y軸重複數量
     * @param opacity           水印透明度
     * @param rotation          水印文字旋轉角度,一般為45度角
     * @param waterMarkFontSize 水印字型大小
     * @param color             水印字型顔色
     */
    public static void stringWaterMark(InputStream input, OutputStream output, String waterMarkString, int xAmout, int yAmout, float opacity, float rotation, int waterMarkFontSize, BaseColor color) {
        try {

            PdfReader reader = new PdfReader(input);
            PdfStamper stamper = new PdfStamper(reader, output);

            // 添加中文字型
            BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);

            int total = reader.getNumberOfPages() + 1;

            PdfContentByte over;
            // 給每一頁加水印
            for (int i = 1; i < total; i++) {
                Rectangle pageRect = stamper.getReader().getPageSizeWithRotation(i);
                // 計算水印每個機關步長X,Y
                float x = pageRect.getWidth() / xAmout;
                float y = pageRect.getHeight() / yAmout;

                over = stamper.getOverContent(i);
                PdfGState gs = new PdfGState();
                // 設定透明度為
                gs.setFillOpacity(opacity);

                over.setGState(gs);
                over.saveState();

                over.beginText();
                over.setColorFill(color);
                over.setFontAndSize(baseFont, waterMarkFontSize);

                for (int n = 0; n < xAmout + 1; n++) {
                    for (int m = 0; m < yAmout + 1; m++) {
                        over.showTextAligned(Element.ALIGN_CENTER, waterMarkString, x * n, y * m, rotation);
                    }
                }

                over.endText();
            }
            stamper.close();
        } catch (Exception e) {
            new Exception("NetAnd PDF add Text Watermark error"+e.getMessage());
        }
    }

    /**
     * 圖檔水印,整張頁面平鋪
     * @param input     需要加水印的PDF讀取輸入流
     * @param output    輸出生成PDF的輸出流
     * @param imageFile 水印圖檔路徑
     */
    public static void imageWaterMark(InputStream input, OutputStream output, String imageFile, float opacity) {
        try {

            PdfReader reader = new PdfReader(input);
            PdfStamper stamper = new PdfStamper(reader, output);
            Rectangle pageRect = stamper.getReader().getPageSize(1);
            float w = pageRect.getWidth();
            float h = pageRect.getHeight();

            int total = reader.getNumberOfPages() + 1;

            Image image = Image.getInstance(imageFile);
            image.setAbsolutePosition(0, 0);// 坐标
            image.scaleAbsolute(w, h);

            PdfGState gs = new PdfGState();
            gs.setFillOpacity(opacity);// 設定透明度

            PdfContentByte over;
            // 給每一頁加水印
            float x, y;
            Rectangle pagesize;
            for (int i = 1; i < total; i++) {
                pagesize = reader.getPageSizeWithRotation(i);
                x = (pagesize.getLeft() + pagesize.getRight()) / 2;
                y = (pagesize.getTop() + pagesize.getBottom()) / 2;
                over = stamper.getOverContent(i);
                over.setGState(gs);
                over.saveState();//沒這個的話,圖檔透明度不起作用,必須在beginText之前,否則透明度不起作用,會被圖檔覆寫了内容而看不到文字了。
                over.beginText();
                // 添加水印圖檔
                over.addImage(image);
            }
            stamper.close();
        } catch (Exception e) {
            new Exception("NetAnd PDF add image Watermark error" + e.getMessage());
        }
    }
    /**
     * @description 頂部表格卡片形式顯示格式資料組裝
     * @param tableMobileHeader 要操作的表格
     * @param cellMobileHeader 要操作的單元格
     * @param clospan 合并列 不需要合并填寫0
     * @param fixedHeight 行高
     * @param padding 間距
     * @param border 邊框
     * @param borderColor 邊框顔色
     * @param backgroundColor 背景色
     * @param vertical 垂直對齊方式
     * @param horizontal  水準對齊方式
     * @return void
     **/
    public static void addTableHeaderData(PdfPTable tableMobileHeader, PdfPCell cellMobileHeader, int clospan, float fixedHeight, int padding, int border, BaseColor borderColor, BaseColor backgroundColor, int vertical, int horizontal) {
        cellMobileHeader.setUseBorderPadding(true);
        cellMobileHeader.setUseAscender(true);
        if(clospan>0){
            cellMobileHeader.setColspan(clospan);
        }
        cellMobileHeader.setUseDescender(true);
        cellMobileHeader.setFixedHeight(fixedHeight);
        cellMobileHeader.setPadding(padding);
        cellMobileHeader.setVerticalAlignment(vertical);
        cellMobileHeader.setHorizontalAlignment(horizontal);
        if(null!=backgroundColor){
            cellMobileHeader.setBackgroundColor(backgroundColor);
        }
        cellMobileHeader.setBorder(border);
        cellMobileHeader.setBorderColor(borderColor);
        tableMobileHeader.addCell(cellMobileHeader);
    }
    /**
     * @description 頂部表格卡片形式顯示格式資料組裝
     * @param tableMobileHeader 要操作的表格
     * @param cellMobileHeader 要操作的單元格
     * @param clospan 合并列 不需要合并填寫0
     * @param backgroundColor 背景色
     * @return void
     **/
    public static void addTableHeaderData(PdfPTable tableMobileHeader, PdfPCell cellMobileHeader, int clospan, BaseColor backgroundColor) {
        addTableHeaderData(tableMobileHeader, cellMobileHeader, clospan, 100, 10, 30, BaseColor.WHITE, backgroundColor, 0, 0);
    }
}

           

生成代碼:

@Async
    public Future<Result> createPdf(List<Map<String, Object>> userMap,String relativePath) {
        ByteArrayOutputStream out = null;
        InputStream pdfStream = null;
        try {
            Document document = new Document(PageSize.A4, 50, 50, 30, 20);
            out = new ByteArrayOutputStream();
            PdfWriter writer = PdfWriter.getInstance(document,out);
            document.open();
            for (Map<String, Object> userInfo : userMap) {
                CompanyStaffDto companyStaffDto = (CompanyStaffDto) userInfo.get("userInfo");
                // 加入水印
                PdfContentByte waterMar = writer.getDirectContentUnder();
                // 開始設定水印
                waterMar.beginText();
                // 設定水印透明度
                PdfGState gs = new PdfGState();
                // 設定填充字型不透明度為0.4f
                gs.setFillOpacity(0.2f);
                try {
                    // 設定水印字型參數及大小                                  (字型參數,字型編碼格式,是否将字型資訊嵌入到pdf中(一般不需要嵌入),字型大小)
                    waterMar.setFontAndSize(PdfUtil.kaiTiFont,10);
                    // 設定透明度
                    waterMar.setGState(gs);
                    // 設定水印對齊方式 水印内容 X坐标 Y坐标 旋轉角度
                    for(int i=0 ; i<3; i++) {
                        for (int j = 0; j < 6; j++) {
                            waterMar.showTextAligned(Element.ALIGN_CENTER,"水印内容", 50.5f + i * 280, 40.0f + j * 100, -15);
                        }
                    }
                    // 設定水印顔色
                    waterMar.setColorFill(BaseColor.GRAY);
                    //結束設定
                    waterMar.endText();
                    waterMar.stroke();

                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    waterMar = null;
                    gs = null;
                }
                //設定标題! 這個标題不是頁面内,而是檔案展示的時候左上角的
                document.addTitle("标題");
          		//建立中文字型,否側不能使用  參數1 内容 參數2 内容大小
                Paragraph title = PdfUtil.createChineseFont("中文", 13);
                //設定居中
                title.setAlignment(Element.ALIGN_CENTER);
                //将内容添加的節點中
                document.add(title);
                //添加QR圖檔
                Image image = Image.getInstance((byte[]) userInfo.get("qrUrl"));
                //設定圖檔所占大小
                image.scaleAbsolute(60f, 60f);
                image.setAlignment(Element.ALIGN_RIGHT);
                image.setPaddingTop(-9f);
                document.add(image);

                //生成橫線  linWidth 寬度
                LineSeparator solidLine = new LineSeparator(2f, 100, BaseColor.BLACK, Element.ALIGN_CENTER, -5f);
                document.add(solidLine);
                LineSeparator dottedLine = new LineSeparator(1f, 100, BaseColor.BLACK, Element.ALIGN_CENTER, -8f);
                document.add(dottedLine);
                //列印時間設定
                Paragraph printTime = PdfUtil.createChineseFont("列印時間" + DateUtil.format(new Date(), "yyyy年MM月dd日 HH:mm:ss"), 9);
                //設定上間距
                printTime.setSpacingBefore(3f);
                //設定下間距
                printTime.setSpacingAfter(3f);
                //設定居中方式
                printTime.setAlignment(Element.ALIGN_RIGHT);
                document.add(printTime);


                //設定基本情況頁面
                Paragraph basicSituation = PdfUtil.createChineseFont("☞ 基本情況", 10);
                basicSituation.setAlignment(Element.ALIGN_LEFT);
                document.add(basicSituation);

                //建立基本情況表格
                //建立表格對象
                PdfPTable table = new PdfPTable(4);
                table.setSpacingBefore(10);//前邊距
                table.setSpacingAfter(10);//後邊距
                table.setWidthPercentage(100);//表格寬占比
                table.setHorizontalAlignment(Element.ALIGN_CENTER);

                table.setHeaderRows(2);
                table.getDefaultCell().setVerticalAlignment(Element.ALIGN_TOP);//單元格中文字垂直對齊方式
                table.getDefaultCell().setBorderColor(BaseColor.BLACK);//單元格線條顔色
                table.getDefaultCell().setMinimumHeight(30);//單元格最小高度
                table.getDefaultCell().setExtraParagraphSpace(5);//段落文字與表格之間的距離,底部距離
                table.getDefaultCell().setLeading(15, 0);//設定行間距
				//這裡使用的是自定義的Util工具類
                PdfUtil.createCell(table, "姓名");
                PdfUtil.createCell(table,"姓名資料");
                PdfUtil.createCell(table, "報告月份");
                PdfUtil.createCell(table, nowDate);
                PdfUtil.createCell(table, "性别");
                PdfUtil.createCell(table, "性别資料");
                PdfUtil.createCell(table, "身份證号");
                PdfUtil.createCell(table, "身份證号資料");
                PdfUtil.createCell(table, "崗位");
                PdfUtil.createCell(table, "崗位資料");
                PdfUtil.createCell(table, "手機号");
                PdfUtil.createCell(table, "手機号資料");
                PdfUtil.createCell(table, "實際時長/計劃時長");
                PdfUtil.createCell(table, "實際時長/計劃時長資料");
                PdfUtil.createCell(table, "是否完成");
                PdfUtil.createCell(table,"已完成" );
                PdfUtil.createCell(table, "所屬企業");
                PdfUtil.createCell(table, Rectangle.BOX, 1, "企業名稱資料", 8, 1, 3);
                document.add(table);
 /*  這裡是尾部簽名及日期的展示,就不加入了  
              //簽名
                Paragraph sign = PdfUtil.createChineseFont("簽字:" + companyStaffDto.getName(), 8);
                sign.setAlignment(Element.ALIGN_RIGHT);
                document.add(sign);
                //日期
                Paragraph date = PdfUtil.createChineseFont(DateUtil.format(new Date(), "yyyy年MM月dd日"), 8);
                date.setAlignment(Element.ALIGN_RIGHT);
                document.add(date);
   */  
            try{
                document.close();
            }catch (Exception e){
                log.debug("列印pdf無内容");
            }
            Result resultUrl =new Result();
                pdfStream = new ByteArrayInputStream(out.toByteArray());
                Result result = ossApiService.upload(pdfStream, relativePath);
                if(result.success){
                    resultUrl = result;
                }
                return AsyncResult.forValue(resultUrl);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                if(pdfStream!=null){
                    pdfStream.close();
                }

            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(out!=null){
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return null;
    }
           

最終的一個基本樣式就是這個樣子

Java PDF生成方案介紹及問題彙總

問題彙總

1.1 為什麼前端不能使用多圖列印及資料量的情況

答: 1. 因為前端如果資料量大的話
       會給浏覽器造成很大的壓力,會産生卡死的情況,這種情況當然是不願意去看到的
    2. 圖檔渲染量過多,會出現圖檔渲染丢失的情況,讀取不到url而導緻控制台頁面報錯,
       越往後的圖檔越有幾率會變成一個 X 
       這種情況下其實也有解決辦法:  将我們的圖檔提前在本地以base64的方式去存儲 ,
       但是又回到第一個回答上,是以酌情使用.
           

1.2 什麼時候适合使用前端列印,什麼時候适合後端模闆呢

答: 1. 什麼時候可以使用前端列印功能: 
		1.1 像列印一般段落文字,承諾書這種,前端簡單粗暴,也好布局
		1.2 列印表格一類的文檔,因為表格後端繪制比較麻煩,是以推薦交給前端使用<table/>去布局
		1.3 輕量列印,資料量不大的前兩種
		1.4 前端樣式設定的很精美,後端做到同等底部太難 
    2. 前端列印和後端列印其實各自能做到的,各自都能做到,而且效率和布局上前端更勝一籌,但是像一些樣式複雜的,随着資料量增大,
    前端導入的效率就無法和後端去相對應了,尤其是帶有圖檔的情況下.而且不友善提供各種檔案格式的
    資料,列印頁面情況下就交給後端去生成固定死的頁面,還能更好的處理性能上問題
           

3.1 PDF檔案中中文無法正常顯示

答: 因為字型的原因,是以如果想使用中文字型的話,需要指定對應中文的編碼 這裡定義了一個工具類用于傳回中文字型

public static BaseFont bfChinese;
    private static final int defaultSize =12;
    public static BaseFont kaiTiFont;  
    static {
        try {
            //指定字型 沒有這個則 createChineseFont 方法無用
            bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H",
                    BaseFont.NOT_EMBEDDED);
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
/**
     * 傳回中文Paragraph  預設12
     * @param text
     * @return
     */
    public static Paragraph createChineseFont(String text){
        Font c = new Font(bfChinese, defaultSize, Font.NORMAL);
        return   new Paragraph(text, c);
    }
    /**
     * 傳回中文Paragraph
     * @param text
     * @param size
     * @return
     */
    public static Paragraph createChineseFont(String text,int size){
        if (size==0){
            size=defaultSize;
        }
        Font c = new Font(bfChinese, size, Font.NORMAL);
        return   new Paragraph(text, c);
    }
           

3.2 想使用自定義字型怎麼辦?

答: 自定義字型,不能通過簡單的使用編碼去調用,這點可以去看官方的Asian編碼jar包,裡面對中文支援少的可憐,更别說是字型,是以想使用,如楷體這類的需要自己去引用,這裡提供一個楷體ttf檔案,其他類型需要自行下載下傳

代碼裡的檔案我放在了resources檔案下直接就可以通過相對路徑去通路,但是excel讀取字型的方式不能通過這樣.excel Font用的是java的,pdf Font類是自己封裝的 後面再說

楷體字型檔案

public static BaseFont kaiTiFont;
static {
        try {
            //指定字型 沒有這個則 createChineseFont 方法無用
            kaiTiFont = BaseFont.createFont(  "/ttf/KAITIGB2312.ttf",BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 傳回中文楷體Paragraph  預設12
     * @param text
     * @return
     */
    public static Paragraph createChineseFont(String text){
        Font c = new Font(kaiTiFont, defaultSize, Font.NORMAL);
        return   new Paragraph(text, c);
    }
           

3.3 怎樣給PDF中添加水印功能

具體PDF中操作水印,還是比較複雜,這個需要自己琢磨,這裡給個簡單的具體的可以看前面工具類中提供的水印方式

// 加入水印
                PdfContentByte waterMar = writer.getDirectContentUnder();
                // 開始設定水印
                waterMar.beginText();
                // 設定水印透明度
                PdfGState gs = new PdfGState();
                // 設定填充字型不透明度為0.4f
                gs.setFillOpacity(0.2f);
                try {
                    // 設定水印字型參數及大小                                  (字型參數,字型編碼格式,是否将字型資訊嵌入到pdf中(一般不需要嵌入),字型大小)
                    waterMar.setFontAndSize(PdfUtil.kaiTiFont,10);
                    // 設定透明度
                    waterMar.setGState(gs);
                    // 設定水印對齊方式 水印内容 X坐标 Y坐标 旋轉角度
                    for(int i=0 ; i<3; i++) {
                        for (int j = 0; j < 6; j++) {
                            waterMar.showTextAligned(Element.ALIGN_CENTER,"水印内容", 50.5f + i * 280, 40.0f + j * 100, -15);
                        }
                    }
                    // 設定水印顔色
                    waterMar.setColorFill(BaseColor.GRAY);
                    //結束設定
                    waterMar.endText();
                    waterMar.stroke();

                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    waterMar = null;
                    gs = null;
                }
           

3.4 如何給PDF表格中批量添加圖檔?

答: 這裡給出一個思路,把表格分為n列, 每列放置一個圖檔 就不用考慮 格式的問題了

for (int i=0;i< size;i++) {
         Image img = Image.getInstance(r.faceRecordDtoList.get(i).getFaceUrl() + "?x-oss-process=image/resize,m_fill,h_80,w_70/quality,q_70");
         img.scaleAbsolute(50f, 60f);
         img.setAlignment(Rectangle.LEFT);
         img.setScaleToFitHeight(false);
         PdfPCell imgCell = new PdfPCell(img);
         imgCell.setColspan(1);
         imgCell.setBorder(Rectangle.BOTTOM);
         //行對象
         safe.addCell(imgCell);
                        }
           

3.5 動态生成的PDF如何實作分頁?

兩種方案
1. 使用     document.newPage(); 就能從内容結束後進行分頁
2. 另外一種,換個思路生成單個的pdf檔案,然後使用工具把pdf拼接成一個pdf檔案