一、前言

事情是这个样子的,小农的公司,之前有个功能需要签署来进行一系列的操作,于是我们引入了一个三方平台的签署——上上签,但是有一个比较尴尬的点就是,它不支持合同在浏览器上和附件一起预览的,我们想要的是需要将附件拼接在合同主文件中一起展示,但是它不支持,于是我们就开了一个需求会。。。

产品说,我们要做一个线上合同签署的功能,不依靠第三方来完成,可以浏览器上预览和下载合同,小农,你这边能做吗?

我一听,这个啊,这个有点难度啊(我需要时间),不太好做,之前我们接入的第三方就没有完全完成浏览器预览的功能,相当于我们做一个和这个第三方一模一样的东西,而且还要比它那个兼容更多的功能,不太好做(确实有点不太好做),加上之前也没有做过,心里没有底。

产品说,这个没有办法(你做也得做,不做也得做),是领导要求的(上面要求的,你只能做),你看下完成这些功能大概需要多久?

于是只能硬着头皮上了,于是给了一个大概的时间后,就开始研究了,什么是快乐星球,如果你想知道的话,那我就带你研究,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的时候就说明我们大功告成了





我们可以看到对应的签署信息已经被我们添加上去了,除了没有第三方认证,该有的功能都有了,太优秀了啊!

这个时候我难免想起了,李白那句 “仰天大笑出门去,我辈岂是蓬蒿人”,天晴了,雨停了,我觉得我又行了,我都已经联想到产品小姐姐崇拜的小眼神了,魅力放光芒,请你不要再迷恋哥!别光喝酒啊,吃点菜啊,几个菜喝成这样

总结

这个功能,花费了小农三天的时候,如果这个功能已经能够在公司项目中正常使用了,以上就是这个功能的详细代码和说明,如果有疑问或者也在做这个功能的小伙伴可以留言,小农看到了会第一时间回复

如果大家觉得不错,记得一键三连~

点赞过百,我就是怀双胞胎也出下一篇

我是牧小农,怕什么真理无穷,进一步有进一步的欢喜,大家加油!

【玩转PDF】贼稳,产品要做一个三方合同签署,我方了!的更多相关文章

  1. 想玩 BGP 路由器么?用 CentOS 做一个

    在之前的教程中,我对如何简单地使用Quagga把CentOS系统变成一个不折不扣地OSPF路由器做了一些介绍.Quagga是一个开源路由软件套件.在这个教程中,我将会重点讲讲如何把一个Linux系统变 ...

  2. 看完你也能独立负责项目!产品经理做APP从头到尾的所有工作流程详解!

    (一)项目启动前 从事产品的工作一年多,但自己一直苦于这样或者那样的困惑,很多人想要从事产品,或者老板自己创业要亲自承担产品一职,但他们对产品这个岗位的认识却不明晰,有的以为是纯粹的画原型,有的是以为 ...

  3. 【转】做产品VS做项目

    相关定义 根据GB/T19000—2008<质量管理体系基础和术语>,有以下定义 过程process 一组将输入转化为输出的相互关联或相互作用的活动 注:一个过程的输入通常是其他过程的输出 ...

  4. 做一个有产品思维的研发:Scrapy安装

    每天10分钟,解决一个研发问题. 如果你想了解我在做什么,请看<做一个有产品思维的研发:课程大纲>传送门:https://www.cnblogs.com/hunttown/p/104909 ...

  5. 做一个有产品思维的研发:部署(Tomcat配置,Nginx配置,JDK配置)

    每天10分钟,解决一个研发问题. 如果你想了解我在做什么,请看<做一个有产品思维的研发:课程大纲>传送门:https://www.cnblogs.com/hunttown/p/104909 ...

  6. 产品相关 做产品VS做项目

    做产品VS做项目 by:授客 QQ:1033553122 相关定义 根据GB/T19000—2008<质量管理体系基础和术语>,有以下定义 过程process 一组将输入转化为输出的相互关 ...

  7. 【To B产品怎么做?】泛用户体验

    目录 - 什么是泛用户体验? - 如何做好泛用户体验? - 泛用户体验有什么用? *预计阅读时间15分钟 不知道你有没有过这种体验,客服妹子的声音软糯,氛围微妙,用词标准,张口就是:给你带来了不好的体 ...

  8. 媳妇儿喜欢玩某音中的动漫特效,那我就用python做一个图片转化软件。

    ​    最近某音上的动漫特效特别火,很多人都玩着动漫肖像,我媳妇儿也不例外.看着她这么喜欢这个特效,我决定做一个图片处理工具,这样媳妇儿的动漫头像就有着落了.编码    为了快速实现我们的目标,我们 ...

  9. 阿里云“网红"运维工程师白金:做一个平凡的圆梦人

    他是阿里云的一位 P8 运维专家,却很有野心得给自己取花名“辟拾(P10)”:他没有华丽的履历,仅凭着 26 年的热爱与坚持,一步一个脚印踏出了属于自己的技术逆袭之路:他爱好清奇,练就了能在 20 秒 ...

随机推荐

  1. 数据结构☞二叉搜索树BST

    二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它可以是一棵空树,也可以是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值: 若它 ...

  2. 【笔记】《Redis设计与实现》chapter2 简单动态字符串

    ------------恢复内容开始------------ 2.1 SDS的定义 struct sdshdr{ // 记录buf数组中已使用字节的数量 // 等于SDS所保存字符串的长度(不含'\0 ...

  3. day13.常用模块

    一.time与datetime模块 1.1time 在Python中,通常有这几种方式来表示时间: 时间戳(timestamp):通常来说,时间戳表示的是从1970年1月1日00:00:00开始按秒计 ...

  4. kubectl cp 从k8s pod 中 拷贝 文件到本地

    请查看官方的说明 kubectl cp --help 官方说使用cp , pod里需要有tar命令 从k8s pod 中 拷贝 文件到本地 这是我使用的命令 kubectl exec redis-6c ...

  5. 漫谈SCA(软件成分分析)测试技术:原理、工具与准确性

    摘要:本文介绍了SCA技术的基本原理.应用场景,业界TOP SCA商用工具的分析说明以及技术发展趋势:让读者对SCA技术有一个基本初步的了解,能更好的准确的应用SCA工具来发现应用软件中一些安全问题, ...

  6. hdu4974 简单题

    题意:       一个人看比赛,这些比赛一共有n个人参与,每一场有两个人比,然后每一场之后这个人都会给比赛的这两个人打分,最多1最少0,比如看完了A,B两人比赛,他可能给这两个人分别的分数是00,1 ...

  7. <JVM中篇:字节码与类的加载篇>03-类的加载过程(类的生命周期)详解

    笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机) 同步更新:https://gitee.com/vectorx/NOTE_JVM https://codechina.cs ...

  8. 【js】Leetcode每日一题-完成所有工作的最短时间

    [js]Leetcode每日一题-完成所有工作的最短时间 [题目描述] 给你一个整数数组 jobs ,其中 jobs[i] 是完成第 i 项工作要花费的时间. 请你将这些工作分配给 k 位工人.所有工 ...

  9. PhpStorm 配置本地文件自动上传至服务器

    目的:本地文件夹下的文件实时同步至指定服务器的文件夹,减少代码移植的成本和风险 添加一个SFTP连接 Tools - Deployment - Browse Remote Host 配置连接参数 Co ...

  10. JVM什么叫安全检测点

    [deerhang] 在JVM的垃圾回收阶段,GC线程首先要进行对象的可达性分析.为了避免多线程对可达性分析的影响引出了安全点检测的概念 当GC线程进行GC前,需要等待其他线程进入安全点.例如JVM调 ...