package com.zhou.stamp;
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 lombok.Data;
import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Calendar;
/**
* @author lang.zhou
* @since 2023/8/1 13:42
*/
@Data
public class PDFStamper {
private String pdf;
public static void main(String[] args) throws Exception{
PDFStamper stamper = new PDFStamper();
stamper.setPdf("C:\\Users\\zhou\\Desktop\\2.pdf");
StampOption option = new StampOption();
PdfSignDto signDto = new PdfSignDto();
signDto.setContact("zhoulang");
signDto.setLocation("cn");
signDto.setSignDate(Calendar.getInstance());
signDto.setReason("盖章");
option.setSignDto(signDto);
//盖章图片
option.setImgPath("C:\\Users\\zhou\\Desktop\\stamp2.jpeg");
//盖章坐标(中心点)
option.setX(200);
option.setY(200);
//章宽高,为空默认图片实际尺寸
option.setWidth(100);
option.setHeight(100);
option.setPage(1);
stamper.stamp(new FileInputStream("C:\\Users\\zhou\\Desktop\\1.pfx"),"证书密码",option, new FileOutputStream("C:\\Users\\zhou\\Desktop\\3.pdf"));
}
/**
* 计算签章的对角线坐标(左上,右下)
*/
private float[] calcImagePosition(Image image, StampOption stampOption){
float[] pos = new float[4];
float w = stampOption.getWidth();
float h = stampOption.getHeight();
if(w == 0 || h == 0){
w = image.getWidth();
h = image.getHeight();
}
pos[0] = stampOption.getX() - w / 2;
pos[1] = stampOption.getY() - h / 2;
pos[2] = stampOption.getX() + w / 2;
pos[3] = stampOption.getY() + h / 2;
return pos;
}
/**
* 签名盖章
* @param p12 pkcs12/pfx格式证书文件流
* @param password 证书密码
* @param stampOption 盖章选项
* @param out 盖章后输出文件流
*/
@SneakyThrows
public void stamp(InputStream p12, String password, StampOption stampOption, OutputStream out){
//读取keystore ,获得私钥和证书链
KeyStore ks = KeyStore.getInstance("PKCS12");
char[] chars = password.toCharArray();
ks.load(p12, chars);
String alias = ks.aliases().nextElement();
PrivateKey pk = (PrivateKey) ks.getKey(alias, chars);
Certificate[] chain = ks.getCertificateChain(alias);
PdfReader pdfReader = new PdfReader(pdf);
//目标文件输出流
//创建签章工具PdfStamper ,最后一个boolean参数
//false的话,pdf文件只允许被签名一次,多次签名,最后一次有效
//true的话,pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
PdfStamper stamper = PdfStamper.createSignature(pdfReader, out, '\0', null, false);
//读取图章图片,这个image是itext包的image
Image image = Image.getInstance(stampOption.getImgPath());
PdfSignDto signDto = stampOption.getSignDto();
// 获取数字签章属性对象,设定数字签章的属性
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
BeanUtils.copyProperties(signDto,appearance);
//计算对角线坐标(左上,右下)
float[] pos = calcImagePosition(image, stampOption);
//设置签名的位置,页码,签名域名称,多次追加签名的时候,签名预名称不能一样
//签名的位置,是图章相对于pdf页面的位置坐标,原点为pdf页面左下角
//四个参数的分别是,图章左上角x,图章左上角y,图章右下角x,图章右下角y
appearance.setVisibleSignature(new Rectangle(pos[0], pos[1], pos[2], pos[3]), stampOption.getPage(), "sig1");
appearance.setSignatureGraphic(image);
appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
//设置图章的显示方式,如下选择的是只显示图章(还有其他的模式,可以图章和签名描述一同显示)
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
// 这里的itext提供了2个用于签名的接口,可以自己实现
// 摘要算法
ExternalDigest digest = new BouncyCastleDigest();
// 签名算法
ExternalSignature signature = new PrivateKeySignature(pk, DigestAlgorithms.SHA256, null);
// 调用itext签名方法完成pdf签章CryptoStandard.CMS 签名方式,建议采用这种
MakeSignature.signDetached(appearance, digest, signature, chain, null, null, null, 0, MakeSignature.CryptoStandard.CMS);
stamper.close();
pdfReader.close();
}
}
import lombok.Data;
/**
* @author lang.zhou
* @since 2023/8/1 13:47
*/
@Data
public class StampOption {
private float x = 0f;
private float y = 0f;
private float width = 0f;
private float height = 0f;
/**
* 盖章页数
*/
private int page = 1;
/** 签名图片地址 */
private String imgPath;
private PdfSignDto signDto;
}
import lombok.Data;
import java.util.Calendar;
/**
* 签名信息实体类
* @author lang.zhou
* @since 2023/8/1 13:41
*/
@Data
public class PdfSignDto {
private static final long serialVersionUID = 1L;
/** 签名时间 */
private Calendar signDate;
/** 摘要算法 */
private String digestAlgorithm;
/** 原因 */
private String reason;
/** 地点 */
private String location;
/** 签名 */
private String signatureName;
/** 加密算法 */
private String encryptionAlgorithm;
/** 签名者 */
private String signerName;
/** 联系方式 */
private String contact;
/** 修订号 */
private int revisionNumber;
}