【玩轉PDF】賊穩,產品要做一個三方合同簽署,我方了!

daxuesheng發表於2021-09-09

一、前言

事情是這個樣子的,小農的公司,之前有個功能需要簽署來進行一系列的操作,於是我們引入了一個三方平臺的簽署——上上籤,但是有一個比較尷尬的點就是,它不支援合同在瀏覽器上和附件一起預覽的,我們想要的是需要將附件拼接在合同主檔案中一起展示,但是它不支援,於是我們就開了一個需求會。。。

產品說,我們要做一個線上合同簽署的功能,不依靠第三方來完成,可以瀏覽器上預覽和下載合同,小農,你這邊能做嗎?

我一聽,這個啊,這個有點難度啊(我需要時間),不太好做,之前我們接入的第三方就沒有完全完成瀏覽器預覽的功能,相當於我們做一個和這個第三方一模一樣的東西,而且還要比它那個相容更多的功能,不太好做(確實有點不太好做),加上之前也沒有做過,心裡沒有底。

產品說,這個沒有辦法(你做也得做,不做也得做),是領導要求的(上面要求的,你只能做),你看下完成這些功能大概需要多久?

於是只能硬著頭皮上了,於是給了一個大概的時間後,就開始研究了,什麼是快樂星球,如果你想知道的話,那我就帶你研究,what???等等,跑偏了,回來回來。
在這裡插入圖片描述

二、那我就帶你研究

研究什麼?什麼是快樂星球[手動狗頭],咳咳,洗腦了,請你立即停止你的傻*行為。

我們知道,如果是想要操作PDF的話(因為簽署合同一般都是用的PDF,同志們為你們解疑了,掌聲可以響起來了),所以一般都是用iText(PDF 操作類庫)操作類庫??? ,咳咳,你怎麼回事?
在這裡插入圖片描述

我們一般都是要使用 Adobe工具設計表單和iText 來進行內容操作,這也是我們今天需要講解的主體,知道了用什麼,我們來講一下我們的需求是什麼?工作後的小夥伴有沒有覺得很可怕,“我們來講一下需求”,首先需要實現的是 通過PDF模板填充我們對應的甲乙方基本資料後,生成PDF檔案,然後再將資料庫的特定資料拼接在PDF裡面一起,通過甲乙方先後簽署後,然後讓該合同進行生效操作!可以預覽和下載。

要求就是這麼個要求,聽著倒是不難,主要是之前沒有做過,心裡不太有譜,但是做完之後,不得不呼自己真是個天才啊,我可真聰明,想起小農從實習的時候就是做的PDF,如今工作這麼久了還是在做PDF的,真是漂(cao)亮(dan),那就做唄,誰怕誰啊!

三、Adobe工具

工欲善其事必先利其器,首先如果想要填充PDF模板裡面的內容的話,我們需要使用到Adobe Acrobat Peo DC這個工具

下載地址:

連結:https://pan.baidu.com/s/1JdeKr7-abc4bajhVxoiYWg
提取碼:6h0i

1、開啟PDF檔案

當我們下載好Adobe Acrobat Peo DC後,用它開啟PDF檔案,然後點選 準備表單
在這裡插入圖片描述

2、新增文字域

點選新增文字域
在這裡插入圖片描述

3、填寫變數名

這個變數名就是我們填充資料的引數,要一一對應
在這裡插入圖片描述

4、填寫完成後,如下所示

在這裡插入圖片描述

3、開始安排

別擔心小夥伴們,專案都給你們準備好了
專案地址:https://github.com/muxiaonong/other/tree/master/pdf_sign_demo

jar檔案:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.5</version>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>layout</artifactId>
            <version>7.1.15</version>
        </dependency>

        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
        </dependency>

        <!--itext生成word文件,需要下面dependency-->
        <dependency>
            <groupId>com.lowagie</groupId>
            <artifactId>iText-rtf</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

根據PDF模板生成檔案和拼接產品的資料

    /**
     * 根據PDF模板生成PDF檔案
     * @return
     */
    @GetMapping("generatePdf")
    public String generatePdf() throws Exception{
//        File file = ResourceUtils.getFile("classpath:"+SAVE_PATH);
        File pdfFile = new File(ResourceUtils.getURL("classpath:").getPath()+SAVE_PATH);
        try {
            PdfReader pdfReader;
            PdfStamper pdfStamper;
            ByteArrayOutputStream baos;

            Document document = new Document();
//

            PdfSmartCopy pdfSmartCopy = new PdfSmartCopy(document,
                    new FileOutputStream(pdfFile));

            document.open();

            File file = ResourceUtils.getFile("classpath:"+templatePath);
            pdfReader = new PdfReader(file.getPath());
            int n = pdfReader.getNumberOfPages();
            log.info("頁數:"+n);
            baos = new ByteArrayOutputStream();
            pdfStamper = new PdfStamper(pdfReader, baos);

            for(int i = 1; i <= n; i++) {
                AcroFields acroFields = pdfStamper.getAcroFields();

                //key statement 1
                acroFields.setGenerateAppearances(true);

                //acroFields.setExtraMargin(5, 5);
                acroFields.setField("customerAddress", "上海市浦東新區田子路520弄1號樓");
                acroFields.setField("customerCompanyName", "上海百度有限公司");
                acroFields.setField("customerName", "張三");
                acroFields.setField("customerPhone", "15216667777");
                acroFields.setField("customerMail", "123456789@sian.com");

                acroFields.setField("vendorAddress", "上海市浦東新區瑟瑟發抖路182號");
                acroFields.setField("vendorCompanyName", "牧小農科技技術有限公司");
                acroFields.setField("vendorName", "王五");
                acroFields.setField("vendorPhone", "15688886666");
                acroFields.setField("vendorMail", "123567@qq.com");

                acroFields.setField("effectiveStartTime", "2021年05月25");
                acroFields.setField("effectiveEndTime", "2022年05月25");

                //true代表生成的PDF檔案不可編輯
                pdfStamper.setFormFlattening(true);

                pdfStamper.close();

                pdfReader = new PdfReader(baos.toByteArray());


                pdfSmartCopy.addPage(pdfSmartCopy.getImportedPage(pdfReader, i));
                pdfSmartCopy.freeReader(pdfReader);
                pdfReader.close();
            }
            pdfReader.close();
            document.close();
        } catch(DocumentException dex) {
            dex.printStackTrace();
        } catch(IOException ex) {
            ex.printStackTrace();
        }
        //建立PDF檔案
        createPdf();


        File file3 = new File(ResourceUtils.getURL("classpath:").getPath()+TEMP_PATH);
        File file1 = new File(ResourceUtils.getURL("classpath:").getPath()+outputFileName);

        List<File> files = new ArrayList<>();
        files.add(pdfFile);
        files.add(file3);

        try {
            PdfUtil pdfUtil = new PdfUtil();
            pdfUtil.mergeFileToPDF(files,file1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //如果你是上傳檔案伺服器上,這裡可以上傳檔案
//        String url = fileServer.uploadPdf(File2byte(file1));

        //刪除總檔案
        //如果是你本地預覽就不要刪除了,刪了就看不到了
//        if(file1.exists()){
//            file1.delete();
//        }
        //刪除模板檔案
        if(pdfFile.exists()){
            System.gc();
            pdfFile.delete();
        }
        //刪除產品檔案
        if(file3.exists()){
            file3.delete();
        }
        return "success";
    }

建立PDF附件資訊拼接到主檔案中:

/**
     * 建立PDF附件資訊
     */
    public static void createPdf() {
        Document doc = null;
        try {
            doc = new Document();
            PdfWriter.getInstance(doc, new FileOutputStream(ResourceUtils.getURL("classpath:").getPath()+TEMP_PATH));
            doc.open();
            BaseFont bfChi = BaseFont.createFont("STSong-Light","UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
            Font fontChi = new Font(bfChi, 8, Font.NORMAL);

            PdfPTable table = new PdfPTable(5);

            Font fontTitle = new Font(bfChi, 15, Font.NORMAL);
            PdfPCell cell = new PdfPCell(new Paragraph("*貨運*運輸服務協議-附件1 運輸費用報價",fontTitle));


            cell.setColspan(5);
            table.addCell(cell);
//			"序號"
            table.addCell(new Paragraph("序號",fontChi));
            table.addCell(new Paragraph("品類",fontChi));
            table.addCell(new Paragraph("名稱",fontChi));
            table.addCell(new Paragraph("計算方式",fontChi));
            table.addCell(new Paragraph("費率",fontChi));

            table.addCell(new Paragraph("1",fontChi));
            table.addCell(new Paragraph("貨運",fontChi));
            table.addCell(new Paragraph("費率1.0",fontChi));
            table.addCell(new Paragraph("算",fontChi));
            table.addCell(new Paragraph("0~100萬-5.7%,上限:500元,下限:20元",fontChi));

            table.addCell(new Paragraph("2",fontChi));
            table.addCell(new Paragraph("貨運",fontChi));
            table.addCell(new Paragraph("費率1.0",fontChi));
            table.addCell(new Paragraph("倒",fontChi));
            table.addCell(new Paragraph("100萬~200萬-5.6%,無上限、下限",fontChi));

            table.addCell(new Paragraph("3",fontChi));
            table.addCell(new Paragraph("貨運",fontChi));
            table.addCell(new Paragraph("費率1.0",fontChi));
            table.addCell(new Paragraph("算",fontChi));
            table.addCell(new Paragraph("200萬~300萬-5.5%,無上限、下限",fontChi));


            doc.add(table);

//			doc.add(new Paragraph("Hello World,看看中文支援不........aaaaaaaaaaaaaaaaa",fontChi));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            doc.close();
        }
    }

合同簽署:

/**
     * 簽署合同
     * @return
     * @throws IOException
     * @throws DocumentException
     */
    @GetMapping("addContent")
    public String addContent() throws IOException, DocumentException {

        BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
        Font font = new Font(baseFont);

        //這裡可以填寫本地地址,也可以是伺服器上的檔案地址
        PdfReader reader = new PdfReader(ResourceUtils.getURL("classpath:").getPath()+outputFileName);
        PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(ResourceUtils.getURL("classpath:").getPath()+endPdf));

//
        PdfContentByte over = stamper.getOverContent(1);
        ColumnText columnText = new ColumnText(over);

        PdfContentByte over1 = stamper.getOverContent(1);
        ColumnText columnText1 = new ColumnText(over1);

        PdfContentByte over2 = stamper.getOverContent(1);
        ColumnText columnText2 = new ColumnText(over2);


        PdfContentByte over3 = stamper.getOverContent(1);
        ColumnText columnText3 = new ColumnText(over3);
        // llx 和 urx  最小的值決定離左邊的距離. lly 和 ury 最大的值決定離下邊的距離
        // llx 左對齊
        // lly 上對齊
        // urx 寬頻
        // ury 高度
        columnText.setSimpleColumn(29, 117, 221, 16);
        Paragraph elements = new Paragraph(0, new Chunk("上海壹站供應鏈有限公司"));

        columnText1.setSimpleColumn(26, 75, 221, 16);
        Paragraph elements1 = new Paragraph(0, new Chunk("2021年03月03日"));

        columnText2.setSimpleColumn(800, 120, 200, 16);
        Paragraph elements2 = new Paragraph(0, new Chunk("壹匯(江蘇)供應鏈管理有限公司蕪湖分公司"));

        columnText3.setSimpleColumn(800, 74, 181, 16);
        Paragraph elements3 = new Paragraph(0, new Chunk("2022年03月03日"));

//            acroFields.setField("customerSigntime", "2021年03月03日");
//                acroFields.setField("vendorSigntime", "2021年03月09日");
        // 設定字型,如果不設定新增的中文將無法顯示
        elements.setFont(font);
        columnText.addElement(elements);
        columnText.go();

        elements1.setFont(font);
        columnText1.addElement(elements1);
        columnText1.go();

        elements2.setFont(font);
        columnText2.addElement(elements2);
        columnText2.go();

        elements3.setFont(font);
        columnText3.addElement(elements3);
        columnText3.go();

        stamper.close();

        File tempFile = new File(ResourceUtils.getURL("classpath:").getPath()+"簽署測試.pdf");

        //如果是你要上傳到伺服器上,填寫伺服器的地址
//        String url = fileServer.uploadPdf(File2byte(tempFile));
//        log.info("url:"+url);

        //如果是上傳伺服器後,要刪除資訊
        //本地不要刪除,否則沒有檔案
//        if(tempFile.exists()){
//            tempFile.delete();
//        }

        return "success";
    }

PDF工具類:

import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.stereotype.Component;

import java.io.*;
import java.util.List;

/***
 * pdf 相關操作
 * @author mxn
 */
@Slf4j
@Component
public class PdfUtil {

    /**
     * 合併PDF檔案
     * @param files 檔案列表
     * @param output 輸出的PDF檔案
     */
    public void mergeFileToPDF(List<File> files, File output) {
        Document document = null;
        PdfCopy copy = null;
        OutputStream os = null;
        try {
            os = new FileOutputStream(output);
            document = new Document();
            copy = new PdfCopy(document, os);
            document.open();
            for (File file : files) {
                if (!file.exists()) {
                    continue;
                }
                String fileName = file.getName();
                if (fileName.endsWith(".pdf")) {
                    PdfContentByte cb = copy.getDirectContent();
                    PdfOutline root = cb.getRootOutline();
                    new PdfOutline(root, new PdfDestination(PdfDestination.XYZ), fileName
                            .substring(0, fileName.lastIndexOf(".")));
                    // 不使用reader來維護檔案,否則刪除不掉檔案,一直被佔用
                    try (InputStream is = new FileInputStream(file)) {
                        PdfReader reader = new PdfReader(is);
                        int n = reader.getNumberOfPages();
                        for (int j = 1; j <= n; j++) {
                            document.newPage();
                            PdfImportedPage page = copy.getImportedPage(reader, j);
                            copy.addPage(page);
                        }
                    } catch(Exception e) {
                        log.warn("error to close file : {}" + file.getCanonicalPath(), e);
//                        e.printStackTrace();
                    }
                } else {
                    log.warn("file may not be merged to pdf. name:" + file.getCanonicalPath());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (document != null) {
                document.close();
            }
            if (copy != null) {
                copy.close();
            }
            if (os != null) {
                IOUtils.closeQuietly(os);
            }
        }
    }


    /**
     * 將檔案轉換成byte陣列
     * @param file
     * @return
     * **/
    public static byte[] File2byte(File file){
        byte[] buffer = null;
        try {
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] b = new byte[1024];
            int n;
            while ((n = fis.read(b)) != -1) {
                bos.write(b, 0, n);
            }
            fis.close();
            bos.close();
            buffer = bos.toByteArray();
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
        return buffer;
    }
}

演示

當我們編寫完成之後,就來到了最關鍵的地方,測試了,心裡還有點小激動,應該不會有BUG的

在這裡插入圖片描述

首先我們輸入http://localhost:8080/generatePdf,生成填充模板,生成新的PDF檔案併合並檔案,生成完成之後我們會在專案的class目錄下看到這個檔案

在這裡插入圖片描述

開啟我們的檔案,就可以看到,對應的資料資訊,到這裡有驚無險,就查最後一步簽署合同了
在這裡插入圖片描述

到這裡如果能夠把簽署的資訊,填寫到合同簽署的位置上,那我們就可以說大功告成了,我們輸入簽署的地址http://localhost:8080/addContent,當我們在目錄下看到 簽署測試.PDF的時候就說明我們大功告成了

在這裡插入圖片描述
在這裡插入圖片描述
我們可以看到對應的簽署資訊已經被我們新增上去了,除了沒有第三方認證,該有的功能都有了,太優秀了啊!

這個時候我難免想起了,李白那句 “仰天大笑出門去,我輩豈是蓬蒿人”,天晴了,雨停了,我覺得我又行了,我都已經聯想到產品小姐姐崇拜的小眼神了,魅力放光芒,請你不要再迷戀哥!別光喝酒啊,吃點菜啊,幾個菜喝成這樣
在這裡插入圖片描述

總結

這個功能,花費了小農三天的時候,如果這個功能已經能夠在公司專案中正常使用了,以上就是這個功能的詳細程式碼和說明,如果有疑問或者也在做這個功能的小夥伴可以留言,小農看到了會第一時間回覆

如果大家覺得不錯,記得一鍵三連~

點贊過百,我就是懷雙胞胎也出下一篇

我是牧小農,怕什麼真理無窮,進一步有進一步的歡喜,大家加油!

相關文章