一、前言
事情是這個樣子的,小農的公司,之前有個功能需要簽署來進行一系列的操作,於是我們引入了一個三方平臺的簽署——上上籤,但是有一個比較尷尬的點就是,它不支援合同在瀏覽器上和附件一起預覽的,我們想要的是需要將附件拼接在合同主檔案中一起展示,但是它不支援,於是我們就開了一個需求會。。。
產品說,我們要做一個線上合同簽署的功能,不依靠第三方來完成,可以瀏覽器上預覽和下載合同,小農,你這邊能做嗎?
我一聽,這個啊,這個有點難度啊(我需要時間),不太好做,之前我們接入的第三方就沒有完全完成瀏覽器預覽的功能,相當於我們做一個和這個第三方一模一樣的東西,而且還要比它那個相容更多的功能,不太好做(確實有點不太好做),加上之前也沒有做過,心裡沒有底。
產品說,這個沒有辦法(你做也得做,不做也得做),是領導要求的(上面要求的,你只能做),你看下完成這些功能大概需要多久?
於是只能硬著頭皮上了,於是給了一個大概的時間後,就開始研究了,什麼是快樂星球,如果你想知道的話,那我就帶你研究,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的時候就說明我們大功告成了
我們可以看到對應的簽署資訊已經被我們新增上去了,除了沒有第三方認證,該有的功能都有了,太優秀了啊!
這個時候我難免想起了,李白那句 “仰天大笑出門去,我輩豈是蓬蒿人”,天晴了,雨停了,我覺得我又行了,我都已經聯想到產品小姐姐崇拜的小眼神了,魅力放光芒,請你不要再迷戀哥!別光喝酒啊,吃點菜啊,幾個菜喝成這樣
總結
這個功能,花費了小農三天的時候,如果這個功能已經能夠在公司專案中正常使用了,以上就是這個功能的詳細程式碼和說明,如果有疑問或者也在做這個功能的小夥伴可以留言,小農看到了會第一時間回覆
如果大家覺得不錯,記得一鍵三連~
點贊過百,我就是懷雙胞胎也出下一篇
我是牧小農,怕什麼真理無窮,進一步有進一步的歡喜,大家加油!