最近因工作需要,调研了一下java怎么用代码方式生成带有电子签名和签章的PDF文件,发现网上已经有很多例子了,也参考了很多。发现其中有些细节都没有提到,因此写个博客记录一下。
本文会比较全面,从电子签章封面图片的制作,到电子签名证书的生成,再到绘制PDF以及最后对PDF文件加上电子签章等步骤。
经过测试,生成后的PDF电子签章在Windows系统中支持WPS、福昕、Adobe Acrobat等软件识别。Mac系统WPS无电子证书功能,福昕可以识别,Adobe Acrobat需要花钱就没测。移动端也只有福昕可以识别到电子签章。
相关工具类jar包版本:
1.PDF工具jar包:com.itextpdf:itextpdf:5.5.13.3
2.解决pdf中文字体问题:com.itextpdf:font-asian:7.2.3
3.pdf富文本拓展:com.itextpdf.tool:xmlworker:5.5.13 (好像没用到^o^)
4.hutool工具类jar: cn.hutool.core.io:5.8.25
一、电子签章封面图的制作
注意:已经有封面图(电子公章图片)的朋友们可以略过;
参考视频:在WPS里如果制作电子印章_哔哩哔哩_bilibili
本人制作的效果图:
二、本地生成自测用电子证书
在后面的加签过程中,我们需要用到 .p12格式的证书文件,这里可以用本地自带的jdk生成测试用的。
网上有非常多的方式方法和博客可以参考,这里就不再赘述了。
三、绘制自定义样式的PDF文件并生成带有电子签章的PDF文件
我用的方法是与前端同学约定好入参字段及格式,做成简易版的PDF自定义样式。大家也可以用富文本直接做入参,但是有些样式可能不好调试,看大家的使用习惯吧。
Controller层代码
package com.xxxxx.user.controller.pdf.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import com.xxxxx.common.annotation.Anonymous;
import com.xxxxx.common.response.BizResponseUtil;
import com.xxxxx.user.base.BaseController;
import com.xxxxx.user.controller.pdf.constants.Constant;
import com.xxxxx.user.controller.pdf.pojo.param.MatchItem;
import com.xxxxx.user.controller.pdf.pojo.param.PdfParam;
import com.xxxxx.user.controller.pdf.pojo.param.SignatureInfo;
import com.xxxxx.user.controller.pdf.util.PDFUtil;
import com.tencent.ssv.techinfra.spring.boot.response.pojo.Response;
import jakarta.validation.Valid;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* PDF制作 控制器
*/
@Slf4j
@RestController
@RequestMapping("/pdfMake")
public class PdfMakeController extends BaseController {
/**
* 制作pdf
*/
@PostMapping("/createPdf")
public Response<Void> createPdf(@Valid @RequestBody PdfParam param) {
String tempFilePath = null;
try {
//1.生成临时pdf文件名(年月日时分秒微秒)
String tempFileName = DateUtil.format(DateUtil.date(), "yyyyMMddHHmmssSSS") + "_temp" + Constant.PDF_SUFFIX;
//2.临时文件路径:区分操作系统类型
if (FileUtil.isWindows()) {
tempFilePath = Constant.PDF_PATH_WINDOWS + "/" + tempFileName;
} else {
tempFilePath = Constant.PDF_PATH_LINUX + "/" + tempFileName;
}
//3.创建临时文件及其父目录,如果这个文件存在,直接返回这个文件
File tempFile = FileUtil.touch(tempFilePath);
//4.生成不带电子签章的PDF文件
PDFUtil.createTempPDF(tempFile, param);
// 5-根据关键字获取需要签章的位置
MatchItem matchItem = PDFUtil.getKeyWordsByPath(tempFilePath, Constant.INSCRIBE_NAME);
// 6-获取和封装签章所需信息
SignatureInfo signatureInfo = PDFUtil.getSignatureInfo();
// 7-加盖电子签章,数字签名,并将图片插入到pdf中
PDFUtil.sign(tempFilePath, matchItem, signatureInfo);
return BizResponseUtil.success();
} catch (Exception e) {
log.error("pdf制作失败:{}", e);
return BizResponseUtil.fail("PdfMakeFailed", "pdf制作失败");
} finally {
try {
// 8-删除临时pdf文件
// 8.1-调用GC,强制删除
//显式调用gc会造成性能黑洞,但是临时文件又没有存在的必要,仍需要删除,可以考虑做个定时器每日0点扫描文件夹删除
// System.gc();
// 8.2-删除临时文件
FileUtil.del(tempFilePath);
} catch (Exception e) {
log.error("删除临时文件失败:{}", e);
}
}
}
}
PDFUtil相关代码
package com.xxxxx.user.controller.pdf.util;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.xxxxx.user.controller.pdf.constants.Constant;
import com.xxxxx.user.controller.pdf.controller.PdfMakeController;
import com.xxxxx.user.controller.pdf.helper.ContentEventHelper;
import com.xxxxx.user.controller.pdf.listener.CustomRenderListener;
import com.xxxxx.user.controller.pdf.pojo.param.ContentStyle;
import com.xxxxx.user.controller.pdf.pojo.param.ItemTable;
import com.xxxxx.user.controller.pdf.pojo.param.ItemTableRow;
import com.xxxxx.user.controller.pdf.pojo.param.MatchItem;
import com.xxxxx.user.controller.pdf.pojo.param.PDFColor;
import com.xxxxx.user.controller.pdf.pojo.param.PDFFont;
import com.xxxxx.user.controller.pdf.pojo.param.PdfParam;
import com.xxxxx.user.controller.pdf.pojo.param.SignatureInfo;
import com.itextpdf.text.Chunk;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Element;
import com.itextpdf.text.Image;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import com.itextpdf.text.pdf.security.BouncyCastleDigest;
import com.itextpdf.text.pdf.security.DigestAlgorithms;
import com.itextpdf.text.pdf.security.ExternalDigest;
import com.itextpdf.text.pdf.security.ExternalSignature;
import com.itextpdf.text.pdf.security.MakeSignature;
import com.itextpdf.text.pdf.security.MakeSignature.CryptoStandard;
import com.itextpdf.text.pdf.security.PrivateKeySignature;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.LinkedList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
@Slf4j
public class PDFUtil {
/**
* 生成不带电子签章的PDF文件
*
* @param tempFile 临时文件
* @param param 参数
*/
public static void createTempPDF(File tempFile, PdfParam param) {
//将tempFile转为输出流
try (OutputStream tempOutputStream = new FileOutputStream(tempFile)) { // 创建输出流
// 1-创建文本对象 Document
Document document = new Document(PageSize.A4, Constant.DOCUMENT_MARGIN_LEFT, Constant.DOCUMENT_MARGIN_RIGHT,
Constant.DOCUMENT_MARGIN_TOP, Constant.DOCUMENT_MARGIN_BOTTOM);
// 2-初始化 pdf输出对象 PdfWriter
PdfWriter pdfWriter = PdfWriter.getInstance(document, tempOutputStream);
// 2.1-设置背景图
pdfWriter.setPageEvent(new ContentEventHelper());
// 3-打开 Document
document.open();
// 4-往 Document 添加内容
// 4.1-自定义的标题
Chunk inviteTile1 = new Chunk(Constant.INVITE_TITLE_PREFIX, PDFFont.content1Font);
Chunk inviteTile2 = new Chunk(param.getInviteTitle(), PDFFont.contentBlueFont);
Chunk inviteTile3 = new Chunk(Constant.INVITE_TITLE_SUFFIX, PDFFont.content1Font);