1024程序员节|历经一个月总结使用java实现pdf文件的电子签字+盖章+防伪二维码+水印+PDF文件加密的全套解决方案
今天是1024程序员节,必须得写一篇博客庆祝一下了! 九月中旬到十月底,我和同事参加了某个系统的开发,涉及到对PDF的电子签字+盖章+防伪二维码+水印等,今天将这些功能的核心代码进行开源
🍅程序员小王的博客:程序员小王的博客
🍅CSDN地址:程序员小王java
🍅 欢迎点赞 👍 收藏 ⭐留言 📝
🍅 如有编辑错误联系作者,如果有比较好的文章欢迎分享给我,我会取其精华去其糟粕
🍅java自学的学习路线:java自学的学习路线
一、前言
今天是1024程序员节,必须得写一篇博客庆祝一下了!九月中旬到十月底,我和同事参加了某个系统的开发,涉及到对PDF的电子签字+盖章+防伪二维码+水印等,我最开始选择使用pageoffice实现PDF的盖章和签字,并且也写了一篇博客来进行详细的介绍(pageoffice实现签名盖章:http://t.csdn.cn/nNxpe),但是出现一个问题,PageOffice支持JAVA、ASP.NET、PHP多种编程开发语言,使开发集成简单高效,事半功倍。让集成PAGEOFFICE的协同办公系统更具价值,但是他是卓正软件公司的一个项目,需要收费,并且安全系数不一定能得到保证,而这个项目需要上生产,所以经过多方的研究,学习,总结,今天终于将一套PDF集成线上签字+盖章+防伪二维码+水印的一系列解决方法总结出来,今天写这篇博客进行开源
项目开源地址:https://gitee.com/wanghengjie563135/pdf.git
csdn下载地址:https://download.csdn.net/download/weixin_44385486/86813947
二、使用itextPDF实现PDF电子公章工具类
1、电子公章的制作
我们需要实现电子公章盖章,但是不能使用公司的章,我们这次推荐使用的线上做章工具来模拟电子印章
-
我们选择圆形印章
- 然后输入公司名,输入章名输入编码然后点击395生成,最后点击保存图片,我们的个人专业章就实现了
-
电子公章效果如图:
-
PDF模板图
- 生成PDF效果图
2、itextPDF的相关依赖
<!-- itextpdf依赖 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.10</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<!-- 摘要算法 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.49</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.49</version>
</dependency>
- 使用的是boot项目,所以完整依赖是
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- itextpdf依赖 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.10</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.49</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.49</version>
</dependency>
</dependencies>
3、相关配置及数字签名的配置
(1)摘要算法
- 我们项目启动之后报错
- 需要加这个配置文件就不报错了,这个主要原因是摘要算法没有,需要引入相关依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.49</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.49</version>
</dependency>
- 涉及到加密算法就需要数字签名了(数字签名格式,CMS,CADE),我们就需要一个文件(我的命名是:server.p12)这个东西需要我们自己电脑生成数字签名
(2)java工具keytool生成p12数字证书文件
Keytool是用于管理**和证书的工具,位于%JAVA_HOME%/bin目录。
使用JDK的keytool工具
- keytool在jdk的bin目录下
2. 打开keytool所在的bin目录,然后在上面的路径显示框中输入CMD,然后回车,即可在当前文件夹下打开命令提示符,并且路径是当前文件夹。
- 生成数字文件,在命令行输入
keytool -genkeypair -alias whj -keypass 111111 -storepass 111111 -dname “C=CN,ST=SD,L=QD,O=haier,OU=dev,CN=haier.com” -keyalg RSA -keysize 2048 -validity 3650 -keystore D:\keystore\server.keystore
参数解释:
storepass keystore 文件存储密码
keypass 私钥加解密密码
alias 实体别名(包括证书私钥)
dname 证书个人信息
keyalt 采用公钥算法,默认是DSA keysize **长度(DSA算法对应的默认算法是sha1withDSA,不支持2048长度,此时需指定RSA)
validity 有效期
keystore 指定keystore文件
- 转换为p12格式
在命令行输入
keytool -importkeystore -srckeystore D:\keystore\server.keystore -destkeystore D:\keystore\whj.p12 -srcalias whj -destalias serverkey -srcstoretype jks -deststoretype pkcs12 -srcstorepass 111111 -deststorepass 111111 -noprompt
- 生成的最终文件
4、项目结构及源码
- 工具类(可以直接复制)
package com.whj.pdf;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.security.*;
import com.whj.entity.SignatureInfo;
import java.io.*;
import java.security.GeneralSecurityException;
/**
* @author 王恒杰
* @date 2022/10/13 22:52
* @Description:盖章功能工具类
*/
public class ItextUtil {
public static final char[] PASSWORD = "111111".toCharArray();// keystory密码
/**
* 单多次签章通用
*
* @param src
* @param target
* @param signatureInfo
* @throws GeneralSecurityException
* @throws IOException
* @throws DocumentException
*/
@SuppressWarnings("resource")
public void sign(String src, String target, SignatureInfo signatureInfo) {
InputStream inputStream = null;
FileOutputStream outputStream = null;
ByteArrayOutputStream result = new ByteArrayOutputStream();
try {
inputStream = new FileInputStream(src);
ByteArrayOutputStream tempArrayOutputStream = new ByteArrayOutputStream();
PdfReader reader = new PdfReader(inputStream);
// 创建签章工具PdfStamper ,最后一个boolean参数是否允许被追加签名
// false的话,pdf文件只允许被签名一次,多次签名,最后一次有效
// true的话,pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
PdfStamper stamper = PdfStamper.createSignature(reader,
tempArrayOutputStream, '\0', null, true);
// 获取数字签章属性对象
PdfSignatureAppearance appearance = stamper
.getSignatureAppearance();
appearance.setReason(signatureInfo.getReason());
appearance.setLocation(signatureInfo.getLocation());
// 设置签名的位置,页码,签名域名称,多次追加签名的时候,签名预名称不能一样 图片大小受表单域大小影响(过小导致压缩)
// 签名的位置,是图章相对于pdf页面的位置坐标,原点为pdf页面左下角
// 四个参数的分别是,图章左下角x,图章左下角y,图章右上角x,图章右上角y
//四个参数的分别是,图章左下角x,图章左下角y,图章右上角x,图章右上角y
appearance.setVisibleSignature(new Rectangle(280, 220, 140, 600), 1, "sig1");
// 读取图章图片
Image image = Image.getInstance(signatureInfo.getImagePath());
appearance.setSignatureGraphic(image);
appearance.setCertificationLevel(signatureInfo
.getCertificationLevel());
// 设置图章的显示方式,如下选择的是只显示图章(还有其他的模式,可以图章和签名描述一同显示)
appearance.setRenderingMode(signatureInfo.getRenderingMode());
// 这里的itext提供了2个用于签名的接口,可以自己实现,后边着重说这个实现
// 摘要算法
ExternalDigest digest = new BouncyCastleDigest();
// 签名算法
ExternalSignature signature = new PrivateKeySignature(
signatureInfo.getPk(), signatureInfo.getDigestAlgorithm(),
null);
// 调用itext签名方法完成pdf签章 //数字签名格式,CMS,CADE
MakeSignature.signDetached(appearance, digest, signature,
signatureInfo.getChain(), null, null, null, 0,
MakeSignature.CryptoStandard.CADES);
inputStream = new ByteArrayInputStream(
tempArrayOutputStream.toByteArray());
// 定义输入流为生成的输出流内容,以完成多次签章的过程
result = tempArrayOutputStream;
outputStream = new FileOutputStream(new File(target));
outputStream.write(result.toByteArray());
outputStream.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != outputStream) {
outputStream.close();
}
if (null != inputStream) {
inputStream.close();
}
if (null != result) {
result.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 实体类
package com.whj.entity;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import java.security.PrivateKey;
import java.security.cert.Certificate;
/**
* @author 王恒杰
* @date 2022/10/13 22:52
* @Description:
*/
public class SignatureInfo {
private String reason; //签名的原因,显示在pdf签名属性中
private String location;//签名的地点,显示在pdf签名属性中
private String digestAlgorithm;//摘要算法名称,例如SHA-1
private String imagePath;//图章路径
private String fieldName;//表单域名称
private Certificate[] chain;//证书链
private PrivateKey pk;//签名私钥
private int certificationLevel = 0; //批准签章
private PdfSignatureAppearance.RenderingMode renderingMode;//表现形式:仅描述,仅图片,图片和描述,签章者和描述
//图章属性
private float rectllx;//图章左下角x
private float rectlly;//图章左下角y
private float recturx;//图章右上角x
private float rectury;//图章右上角y
public float getRectllx() {
return rectllx;
}
public void setRectllx(float rectllx) {
this.rectllx = rectllx;
}
public float getRectlly() {
return rectlly;
}
public void setRectlly(float rectlly) {
this.rectlly = rectlly;
}
public float getRecturx() {
return recturx;
}
public void setRecturx(float recturx) {
this.recturx = recturx;
}
public float getRectury() {
return rectury;
}
public void setRectury(float rectury) {
this.rectury = rectury;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getDigestAlgorithm() {
return digestAlgorithm;
}
public void setDigestAlgorithm(String digestAlgorithm) {
this.digestAlgorithm = digestAlgorithm;
}
public String getImagePath() {
return imagePath;
}
public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}
public String getFieldName() {
return fieldName;
}
public void setFieldName(String fieldName) {
this.fieldName = fieldName;
}
public Certificate[] getChain() {
return chain;
}
public void setChain(Certificate[] chain) {
this.chain = chain;
}
public PrivateKey getPk() {
return pk;
}
public void setPk(PrivateKey pk) {
this.pk = pk;
}
public int getCertificationLevel() {
return certificationLevel;
}
public void setCertificationLevel(int certificationLevel) {
this.certificationLevel = certificationLevel;
}
public PdfSignatureAppearance.RenderingMode getRenderingMode() {
return renderingMode;
}
public void setRenderingMode(PdfSignatureAppearance.RenderingMode renderingMode) {
this.renderingMode = renderingMode;
}
}
- 测试
package com.whj.pdf;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.security.DigestAlgorithms;
import com.whj.entity.SignatureInfo;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import static com.whj.pdf.ItextUtil.PASSWORD;
/**
* @author 王恒杰
* @date 2022/10/24 10:42
* @Description: 盖章功能实现
*/
public class PdfStamp {
public static void main(String[] args) {
try {
ItextUtil app = new ItextUtil();
// 将证书文件放入指定路径,并读取keystore ,获得私钥和证书链
String pkPath = "src/main/resources/whj.p12";
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new FileInputStream(pkPath), PASSWORD);
String alias = ks.aliases().nextElement();
PrivateKey pk = (PrivateKey) ks.getKey(alias, PASSWORD);
// 得到证书链
Certificate[] chain = ks.getCertificateChain(alias);
//需要进行签章的pdf
String path = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\程序员小王.pdf";
// 封装签章信息
SignatureInfo signInfo = new SignatureInfo();
signInfo.setReason("理由");
signInfo.setLocation("位置");
signInfo.setPk(pk);
signInfo.setChain(chain);
signInfo.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
signInfo.setDigestAlgorithm(DigestAlgorithms.SHA1);
signInfo.setFieldName("demo");
// 签章图片
signInfo.setImagePath("D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\chapter.png");
signInfo.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
值越大,代表向x轴坐标平移 缩小 (反之,值越小,印章会放大)
signInfo.setRectllx(100);
值越大,代表向y轴坐标向上平移(大小不变)
signInfo.setRectlly(200);
// 值越大 代表向x轴坐标向右平移 (大小不变)
signInfo.setRecturx(150);
// 值越大,代表向y轴坐标向上平移(大小不变)
signInfo.setRectury(150);
//签章后的pdf路径
app.sign(path, "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\out.pdf", signInfo);
} catch (Exception e) {
e.printStackTrace();
}
}
}
5、结果展示
三、thymeleaf+itext签字功能+PDF文件加密实现
-
所以需依赖
-
需要手工导入一个jar包(jar包我放在了源码里面,需要自行下载)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- itextpdf依赖 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.10</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.49</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.49</version>
</dependency>
<!--guava是来自Google的Java核心类库。包含了新的集合类型(例如:复合map、复合set)、
不可变集合,以及一些对于并发、I/O、hashing、缓存、原型、字符串等的通用功能。
guava被广泛使用在Google的项目中,也被广泛的使用在其他公司里。-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.0-jre</version>
</dependency>
</dependencies>
因为这个的相关类太多,只展示了核心代码,全部代码请到博客底部下载源码
1、签字图片上传和签字实现的Controller
package com.whj.controller;
import com.whj.service.SignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @author 王恒杰
* @date 2022/10/24 13:57
* @Description: 实现签字上传和签字功能
*/
@RestController
@RequestMapping("/sign")
public class SignController {
@Autowired
private SignService signService;
@PostMapping(value = "/uploadSign")
@ResponseBody
public String uploadSign(String img) {
return signService.uploadSign(img);
}
@PostMapping(value = "/sign")
@ResponseBody
public String sign() {
return signService.sign(id);
}
}
2、签字上传,签字,PDF文件加密业务层实现
/**
* @author 王恒杰
* @date 2022/10/24 14:00
* @Description:
*/
@Service
public class SignServiceImpl implements SignService{
@Override
public String uploadSign(String img) {
//String idCard="3000";
// 生成jpeg图片 idCard.asString()
String url = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\img\\sign.png";
try {
if (img == null) // 图像数据为空
{
return "no";
}
int i = img.indexOf("base64,") + 7;//获取前缀data:image/gif;base64,的坐标
String newImage = img.substring(i, img.length());//去除前缀
BASE64Decoder decoder = new BASE64Decoder();
// Base64解码
byte[] bytes = decoder.decodeBuffer(newImage);
for (int j = 0; j < bytes.length; ++j) {
if (bytes[j] < 0) {// 调整异常数据
bytes[j] += 256;
}
}
OutputStream out = new FileOutputStream(url);
out.write(bytes);
out.flush();
out.close();
return "yes";
} catch (Exception e) {
return "no";
}
}
@Override
public String sign() {
try {
//初始化文件
String srcPath = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\out.pdf";
//输出文件
String outPath = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\signOut.pdf";
signimg("D:\\Idea\\stamp\\Itext\\src\\main\\resources\\img\\sign.png", srcPath, "批准人", outPath);
//加密
EncryptPDFUtil.encryptPDF(outPath, "EncryptPDF");
return "yes";
} catch (Exception e) {
return "no";
}
}
- 封装签字的方法
public static void signimg(String imgurl, String pdfurl, String keywords, String outPDFPath) throws IOException {
List list = new ArrayList();
SignPDFBean bean1 = new SignPDFBean();
bean1.setKeyStorePass("111111");
bean1.setKeyStorePath("src/main/resources/server.p12");
bean1.setKeyWord(keywords);
bean1.setSealPath(imgurl);
bean1.setSignLocation(keywords);
bean1.setSignReason("计量检定证书签字");
list.add(bean1);
SignPDFRequestBean requestBean = new SignPDFRequestBean();
requestBean.setSrcPDFPath(pdfurl);
requestBean.setOutPDFPath(outPDFPath);
requestBean.setSignPDFBeans(list);
long startTime = System.currentTimeMillis();
// 1.解析pdf文件
Map<Integer, List<KeyWordBean>> map = KeywordPDFUtils.getPDFText(requestBean.getSrcPDFPath());
// 2.获取关键字坐标
List<SignPDFBean> beans = requestBean.getSignPDFBeans();
byte[] fileData = null;
InputStream in = null;
for (int i = 0; i < beans.size(); i++) {
SignPDFBean pdfBean = beans.get(i);
KeyWordBean bean = KeywordPDFUtils.getKeyWordXY1(map, pdfBean.getKeyWord());
if (null == bean) {
System.out.println("未查询到关键字。。。");
}
System.out.println("111" + bean.toString());
long keyTime = System.currentTimeMillis();
if (i == 0) {
in = new FileInputStream(requestBean.getSrcPDFPath());
} else {
in = new ByteArrayInputStream(fileData);
}
// 3.进行盖章
fileData = SignPDFUtils.sign(pdfBean.getKeyStorePass(), pdfBean.getKeyStorePath(), in, pdfBean.getSealPath(), bean.getX(), bean.getY(), bean.getPage(), pdfBean.getSignReason(), pdfBean.getSignLocation());
long signTime = System.currentTimeMillis();
}
// 4.输出盖章后pdf文件
FileOutputStream f = new FileOutputStream(new File(requestBean.getOutPDFPath()));
f.write(fileData);
f.close();
in.close();
long endTime = System.currentTimeMillis();
System.out.println("总时间:" + (endTime - startTime));
}
}
3、封装的实现PDF加密文件的方法
package com.whj.util;
import com.spire.pdf.PdfDocument;
import com.spire.pdf.security.PdfEncryptionKeySize;
import com.spire.pdf.security.PdfPermissionsFlags;
import java.util.EnumSet;
/**
* @author 王恒杰
* @date 2022/10/18 9:26
* @Description: 将PDF加密的工具类
*/
public class EncryptPDFUtil {
public static void encryptPDF(String startFileName,String EncryptPDF) {
//创建PdfDocument实例
PdfDocument doc = new PdfDocument();
//加载PDF文件
doc.loadFromFile(startFileName);
//添加一个空白页,目的为了删除jar包添加的水印,后面再移除这一页
doc.getPages().add();
//加密PDF文件
PdfEncryptionKeySize keySize = PdfEncryptionKeySize.Key_128_Bit;
String openPassword = "123456";//打开文档时,仅用于查看文档
String permissionPassword = "test";//打开文档时,可编辑文档
EnumSet flags = EnumSet.of(PdfPermissionsFlags.Print, PdfPermissionsFlags.Fill_Fields);
doc.getSecurity().encrypt(openPassword, permissionPassword, flags, keySize);
//移除第一个页
doc.getPages().remove(doc.getPages().get(doc.getPages().getCount()-1));
//保存文件
doc.saveToFile("src/main/resources/pdf/EncryptPDF/"+EncryptPDF+".pdf");
doc.close();
}
}
4、签字前端(thymeleaf实现)
- 配置文件
server.port=8081
spring.thymeleaf.prefix=classpath:/templates/
- 前端源码
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!--<meta name="viewport" content="initial-scale=1,width=device-width, height=device-height,user-scalable=no,maximum-scale=1, minimum-scale=1" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="format-detection" content="telephone=no">-->
<!--<script src="http://www.jq22.com/jquery/2.1.1/jquery.min.js"></script>-->
<title>手写签名</title>
<script type="text/javascript" src="../jquery-3.1.1.min.js" ></script>
<script type="text/javascript" src="../ajaxfileupload.js" ></script>
<style type="text/css">
#canvas {}
</style>
</head>
<body id="bb">
<div id="canvasDiv"></div>
<br><br><br>
<button id="btn_clear">清除</button>
<button id="btn_submit">提交</button>
<script language="javascript">
try
{
function onDocumentTouchStart(event) {
if(event.touches.length == 1) {
event.preventDefault();
// Faking double click for touch devices
var now = new Date().getTime();
if(now - timeOfLastTouch < 250) {
reset();
return;
}
timeOfLastTouch = now;
mouseX = event.touches[0].pageX;
mouseY = event.touches[0].pageY;
isMouseDown = true;
}
}
function onDocumentTouchMove(event) {
if(event.touches.length == 1) {
event.preventDefault();
mouseX = event.touches[0].pageX;
mouseY = event.touches[0].pageY;
}
}
function onDocumentTouchEnd(event) {
if(event.touches.length == 0) {
event.preventDefault();
isMouseDown = false;
}
}
var canvasDiv = document.getElementById('canvasDiv');
var canvas = document.createElement('canvas');
var canvasWidth = 1191;
var canvasHeight = 670;
document.addEventListener('touchmove', onDocumentTouchMove, false);
/*document.addEventListener('touchstart', onDocumentTouchStart, true);
document.addEventListener('touchend', onDocumentTouchEnd, true);*/
var point = {};
point.notFirst = false;
canvas.setAttribute('width', canvasWidth);
canvas.setAttribute('height', canvasHeight);
canvas.setAttribute('id', 'canvas');
canvasDiv.appendChild(canvas);
if(typeof G_vmlCanvasManager != 'undefined') {
canvas = G_vmlCanvasManager.initElement(canvas);
}
var context = canvas.getContext("2d");
var img = new Image();
img.src = "./write.jpg";
img.onload = function() {
var ptrn = context.createPattern(img, 'no-repeat');
context.fillStyle = ptrn;
context.fillRect(0, 0, canvas.width, canvas.height);
}
canvas.addEventListener("touchstart", function(e) {
console.log("touchstart");
var mouseX = e.touches[0].pageX - this.offsetLeft;
var mouseY = e.touches[0].pageY - this.offsetTop;
paint = true;
addClick(e.touches[0].pageX - this.offsetLeft, e.touches[0].pageY - this.offsetTop);
redraw();
});
canvas.addEventListener("touchend", function(e) {
console.log("touchend");
paint = false;
});
canvas.addEventListener("touchmove", function(e) {
console.log("touchmove");
console.log(e);
console.log("触摸坐标:"+(e.touches[0].clientX - this.offsetLeft)+","+(e.touches[0].clientY - this.offsetTop));
if(paint) {
addClick(e.touches[0].pageX - this.offsetLeft, e.touches[0].pageY - this.offsetTop, true);
redraw();
}
});
canvas.addEventListener("mousedown", function(e) {
console.log("mousedown");
var mouseX = e.pageX - this.offsetLeft;
var mouseY = e.pageY - this.offsetTop;
paint = true;
addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop);
redraw();
});
canvas.addEventListener("mousemove", function(e) {
console.log(e);
/*console.log(this.offsetLeft);
console.log(this.offsetTop);*/
console.log("鼠标坐标:"+(e.pageX - this.offsetLeft)+","+(e.pageY - this.offsetTop));
console.log("mousemove");
if(paint) {
addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop, true);
redraw();
}
});
canvas.addEventListener("mouseup", function(e) {
console.log("mouseup");
paint = false;
});
canvas.addEventListener("mouseleave", function(e) {
console.log("mouseleave");
paint = false;
});
var clickX = new Array();
var clickY = new Array();
var clickDrag = new Array();
var paint;
function addClick(x, y, dragging)
{
clickX.push(x);
clickY.push(y);
clickDrag.push(dragging);
console.debug(clickDrag);
}
function redraw() {
//canvas.width = canvas.width; // Clears the canvas
context.strokeStyle = "black";
context.lineJoin = "round";
context.lineWidth = 5;
while(clickX.length > 0) {
point.bx = point.x;
point.by = point.y;
point.x = clickX.pop();
point.y = clickY.pop();
console.log(point.x);
console.log(point.y);
/*alert(point.x);
alert(point.y);*/
point.drag = clickDrag.pop();
context.beginPath();
if(point.drag && point.notFirst) {
context.moveTo(point.bx, point.by);
} else {
point.notFirst = true;
context.moveTo(point.x - 1, point.y);
}
context.lineTo(point.x, point.y);
context.closePath();
context.stroke();
}
for(var i=0; i < clickX.length; i++)
{
context.beginPath();
if(clickDrag[i] && i){
context.moveTo(clickX[i-1], clickY[i-1]);
}else{
context.moveTo(clickX[i]-1, clickY[i]);
}
context.lineTo(clickX[i], clickY[i]);
context.closePath();
context.stroke();
}
}
var clear = document.getElementById("btn_clear");
var submit = document.getElementById("btn_submit");
var i =0;
clear.addEventListener("click", function() {
canvas.width = canvas.width;
});
submit.addEventListener("click", function() {
$("#file").attr("src", canvas.toDataURL("image/png"));
/*imgData = canvas.toDataURL("image/png").replace("image/png",'image/octet-stream');
var saveFile = function(data, filename) {
var save_link = document.createElementNS('a');
save_link.href = data;
save_link.download = filename;
var event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
save_link.dispatchEvent(event);
}
i=i+1;
var filename = "测试签名" +i+ '.' + "png";
saveFile(imgData,filename);*/
$.ajax({
url: '/sign/uploadSign',
type: 'POST',
data: {img:canvas.toDataURL("image/png")},
success: function (data){
if(data=="yes"){
alert("上传图片成功")
}
},
error:function(data,status,e){
alert(e);
}
});
});
}
catch(err)
{
txt="本页中存在错误。\n\n"
txt+="错误描述:" + err.description + "\n\n"
txt+="点击“确定”继续。\n\n"
alert(txt)
}
</script>
<img id="file" />
</body>
</html>
4、实现签字功能演示
- 启动springboot项目
- 输入http://localhost:8080/ 打开签字页面,输入姓名点击提交
- 提交成功会显示提交成功
- 服务器上可以看到图片已经保存好了
5、实现PDF的签字和加密
(1)PDF加密实现思路讲解(详细代码看工具类)
- PDF加密的方法
//加密
EncryptPDFUtil.encryptPDF(outPath, "EncryptPDF");
- 然后我们分装了两个权限,如果是需要对这个PDF文件修改一个密码,只能查看PDF一个密码
//加密PDF文件
PdfEncryptionKeySize keySize = PdfEncryptionKeySize.Key_128_Bit;
String openPassword = "123456";//打开文档时,仅用于查看文档
String permissionPassword = "test";//打开文档时,可编辑文档
(2)实现签字的方法的讲解
signimg("src/main/resources/img/sign.png", srcPath, "批准人", outPath);
- 其中第一个参数代表的是签字的图片,第二个参数源文件,第三个参数是“批准人”这个关键字的地方进行签字,第四个参数代表的是输出路径
(3)使用postman测试(因为是post接口,所有只能使用postman进行简单的测试)
(4)最终生成了两个文件
- 一个是没有加密的signOut.pdf,他是签字后生成的文件
-
加密后的文件EncryptPDF.pdf 这个需要输入密码才能打开文件
-
输入只读密码 123456
- 打开文件后,里面的所有文字不能复制
- 使用可以修改权限打开文件 密码:test 文件是可以复制可以修改的
四、PDF实现生成水印并删除源文件
1、生成水印的工具类
package com.whj.util;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Element;
import com.itextpdf.text.pdf.*;
import java.io.File;
import java.io.FileOutputStream;
/**
* @author 王恒杰
* @date 2022/10/21 14:53
* @Description:
*/
public class WaterMark {
/**
* pdf生成水印
*
* @param srcPdfPath 插入前的文件路径
* @param tarPdfPath 插入后的文件路径
* @param WaterMarkContent 水印文案
* @param numberOfPage 每页需要插入的条数
* @throws Exception
*/
public static void addWaterMark(String srcPdfPath, String tarPdfPath, String WaterMarkContent, int numberOfPage) throws Exception {
PdfReader reader = new PdfReader(srcPdfPath);
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(tarPdfPath));
PdfGState gs = new PdfGState();
System.out.println("adksjskalfklsdk");
//设置字体
BaseFont font = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
// 设置透明度
gs.setFillOpacity(0.4f);
int total = reader.getNumberOfPages() + 1;
PdfContentByte content;
for (int i = 1; i < total; i++) {
content = stamper.getOverContent(i);
content.beginText();
content.setGState(gs);
//水印颜色
content.setColorFill(BaseColor.DARK_GRAY);
//水印字体样式和大小
content.setFontAndSize(font, 35);
//插入水印 循环每页插入的条数
for (int j = 0; j < numberOfPage; j++) {
content.showTextAligned(Element.ALIGN_CENTER, WaterMarkContent, 300, 200 * (j + 1), 30);
}
content.endText();
}
stamper.close();
reader.close();
boolean b = deleteFile(srcPdfPath);
System.out.println("PDF水印添加完成!");
}
}
删除源文件
//删除没有用的文件
public static boolean deleteFile(String path) {
boolean result = false;
File file = new File(path);
if (file.isFile() && file.exists()) {
int tryCount = 0;
while (!result && tryCount++ < 10) {
System.gc();
result = file.delete();
}
}
return result;
}
2、测试
(1)加水印之前的源文件
(2)测试代码
public class TestWaterMark {
public static void main(String[] args) {
/**
* pdf生成水印
*
* @param srcPdfPath 插入前的文件路径
* @param tarPdfPath 插入后的文件路径
* @param WaterMarkContent 水印文案
* @param numberOfPage 每页需要插入的条数
* @throws Exception
*/
String srcPdfPath = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\signOut.pdf";
String tarPdfPath = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\pdf\\TestWaterMark.pdf";
String WaterMarkContent = "程序员小王";
Integer numberOfPage = 3;
try {
WaterMark.addWaterMark(srcPdfPath, tarPdfPath, WaterMarkContent, numberOfPage);
} catch (Exception e) {
e.printStackTrace();
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u39T5Cpx-1666600264552)(image/image_XDeFaw_Svq.png)]
- 删除源文件的代码
boolean b = deleteFile(srcPdfPath);
(3)加水印之后的文件
五、PDF使用工具Adobe Acrobat DC+PDF+Itext模板实现二维码功能
1、打开电脑中的Adobe Acrobat pro DC(这个应该win10 都有,搜索一下就出来了),点击 文件→创建→创建表单
-
如果没有自己安装下载一个下载地址:adobe acrobat pro dc2018破解版
-
acrobat pro dc 2018序列号
1118-1629-0753-5166-7814-8217
2、然后导入刚刚生成的pdf(注意签名后的文件不能再进行添加二维码或者文字了)
3、点击图片
4、固定好二维码固定位置然后编辑他为img()
- 击这个阴影部分,将名称改成你要设置的名称,后面要根据这个名称来给他赋值
5、全部设置好以后就可以另存为了,打开后是这个样子
6、实现二维码生成的工具类(仅供测试,详细代码看7)
- jar包
<!-- 条形码、二维码生成 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
- 代码
package com.whj.util.QR;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Hashtable;
/**
* @author 王恒杰
* @date 2022/10/21 16:25
* 二维码生成工具类
*
* 通过Google开源的zxing库来事项生成二维码图片
*/
public class QrCodeUtils {
public static final String QR_CODE_IMAGE_PATH = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\QR\\MyQRCode.png";
public static void generateQRCodeImage(String text, int width, int height, String filePath) throws WriterException, IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height);
Path path = FileSystems.getDefault().getPath(filePath);
MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);
}
/**
* 生成二维码
* @param contents 二维码的内容
* @param width 二维码图片宽度
* @param height 二维码图片高度
*/
public static BufferedImage createQrCodeBufferdImage(String contents, int width, int height){
Hashtable hints= new Hashtable();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
BufferedImage image = null;
try {
BitMatrix bitMatrix = new MultiFormatWriter().encode(
contents,BarcodeFormat.QR_CODE, width, height, hints);
image = toBufferedImage(bitMatrix);
} catch (WriterException e) {
e.printStackTrace();
}
return image;
}
public static BufferedImage toBufferedImage(BitMatrix matrix) {
int width = matrix.getWidth();
int height = matrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, matrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
return image;
}
public static void main(String[] args) {
try {
generateQRCodeImage ("https://www.baidu.com/", 350, 350,QR_CODE_IMAGE_PATH);
} catch (WriterException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- 测试二维码生成
package com.whj.util.QR;
import com.google.zxing.WriterException;
import java.io.IOException;
import static com.whj.util.QR.QrCodeUtils.QR_CODE_IMAGE_PATH;
/**
* @author 王恒杰
* @date 2022/10/24 15:42
* @Description:
*/
public class TestQR {
public static void main(String[] args) {
try {
/**
* 第一个参数:内容可以是二维码 也可以是内容
* 第二第三参数:二维码的宽高
* 第四个参数:二维码生成后的地址
*/
QrCodeUtils.generateQRCodeImage ("https://www.wolai.com/6MjBCdAq3mmGXBcb62V4DE", 350, 350,QR_CODE_IMAGE_PATH);
} catch (WriterException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- 生成后的二维码
7、生成二维码之后将二维码嵌入进PDF综合版本
- 生成二维码并且嵌入PDF工具
package com.whj;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.pdf.*;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author 王恒杰
* @date 2022/10/24 16:06
* @Description:
*/
public class QR {
// 利用模板生成pdf
public static void pdfout(Map<String,Object> map) {
// 模板路径
String templatePath = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\QR\\QR.pdf";
// 生成的新文件路径
String newPDFPath = "D:\\Idea\\stamp\\Itext\\src\\main\\resources\\QR\\testout.pdf";
PdfReader reader;
FileOutputStream out;
ByteArrayOutputStream bos;
PdfStamper stamper;
try {
//给表单添加中文字体 这里采用系统字体。不设置的话,中文可能无法显示
BaseFont bf = BaseFont.createFont("D:\\Idea\\stamp\\Itext\\src\\main\\resources\\Font\\SIMYOU.TTF" , BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
Font FontChinese = new Font(bf, 5, Font.NORMAL);
// 输出流
out = new FileOutputStream(newPDFPath);
// 读取pdf模板
reader = new PdfReader(templatePath);
bos = new ByteArrayOutputStream();
stamper = new PdfStamper(reader, bos);
AcroFields form = stamper.getAcroFields();
Map<String,Object> qrcodeFields=(Map<String, Object>) map.get("qrcodeFields");
//遍历二维码字段
for (Map.Entry<String, Object> entry : qrcodeFields.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// 获取属性的类型
if(value != null && form.getField(key) != null){
//获取位置(左上右下)
AcroFields.FieldPosition fieldPosition = form.getFieldPositions(key).get(0);
//绘制二维码
float width = fieldPosition.position.getRight() - fieldPosition.position.getLeft();
BarcodeQRCode pdf417 = new BarcodeQRCode(value.toString(), (int)width, (int)width, null);
//生成二维码图像
Image image128 = pdf417.getImage();
//绘制在第一页
PdfContentByte cb = stamper.getOverContent(1);
//左边距(居中处理)
float marginLeft = (fieldPosition.position.getRight() - fieldPosition.position.getLeft() - image128.getWidth()) / 2;
//条码位置
image128.setAbsolutePosition(fieldPosition.position.getLeft() + marginLeft, fieldPosition.position.getBottom());
//加入条码
cb.addImage(image128);
}
}
// 如果为false,生成的PDF文件可以编辑,如果为true,生成的PDF文件不可以编辑
stamper.setFormFlattening(true);
stamper.close();
Document doc = new Document();
Font font = new Font(bf, 20);
PdfCopy copy = new PdfCopy(doc, out);
doc.open();
PdfImportedPage importPage = copy.getImportedPage(new PdfReader(bos.toByteArray()), 1);
copy.addPage(importPage);
doc.close();
} catch (IOException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
}
}
}
- 测试
public static void main(String[] args) {
//文本内容map
Map<String,Object> map = new HashMap<String, Object>();
//二维码map
Map<String,Object> qrcodeFields = new HashMap<String, Object>();
qrcodeFields.put("img","https://www.wolai.com/6MjBCdAq3mmGXBcb62V4DE");
//组装map传过去
Map<String,Object> o=new HashMap<String, Object>();
o.put("qrcodeFields",qrcodeFields);
//执行
pdfout(o);
}
项目开源地址:https://gitee.com/wanghengjie563135/pdf.git
csdn下载地址:https://download.csdn.net/download/weixin_44385486/86813947
更多推荐
所有评论(0)