java-电子签章方案
本文主要描述如何对已有的word文档进行字段填充后,进行电子签章(CA证书)生成pdf文件
废话不多数,上代码(涉及的工具类较多,有不全的评论即可,看到会及时补充)
1.替换word变量为业务数据并签章
@Transactional
@Override
public void generatePdf(业务数据对象 info,boolean isNew) {
try {
//
String generateeLetterUrl = info.getGenerateeLetterUrl();
// 判断PDF文件的url为空,生成对应url
if (StringUtils.isBlank(generateeLetterUrl) || isNew) {
// 获取配置的模板变量值
// 这里是我自己建了一个本地的表对常用的一些模板变量进行管理,也可以对
Map<String, String> variableMap = this.getLetterVariableMap(info);
log.info("根据业务数据组装 map为{}",variableMap);
// 签章文件中参数配置
variableMap.put(CommonConstant.GUARANTOR_NAME, "业务数据");
variableMap.put(CommonConstant.GUARANTOR_ADDRESS, "业务数据");
variableMap.put(CommonConstant.LETTER_ADDRESS,"业务数据");
variableMap.put(CommonConstant.LETTER_BAOZHENGREN, "业务数据");
variableMap.put(CommonConstant.LETTER_PHONE, "业务数据");//联系电话
// 创建文件夹
if (!FileUtil.exist(letterTempFolder)) {
FileUtil.mkdir(letterTempFolder);
}
// 定义路径,流是不可重复读取的,所以这边用了笨方法每次对文件操作都使用新的临时文件地址,最后在进行删除
String tempPdfFileUrl = letterTempFolder + File.separator + info.getGenerateeLetterNo() + CommonConstant.TEMPLATE_PDF;
// docx临时文件地址
String docxPdfFilePath = letterTempFolder + File.separator + info.getGenerateeLetterNo() + RandomUtil.randomNumbers(4) + CommonConstant.TEMPLATE_DOCX;
//签章之后的 文件生成
String targetPdfFilePath = letterTempFolder + File.separator + info.getGenerateeLetterNo() + RandomUtil.randomNumbers(4) + CommonConstant.TEMPLATE_PDF;
// 电子签章临时文件地址
String tempSignPngPath = letterTempFolder + File.separator + info.getGenerateeLetterNo() + CommonConstant.SIGN_PIC;
InputStream templateStream = null;
FileOutputStream pdfOutputStream = null;
InputStream certInputStream = null;
try {
// 这里是因为我这个项目有不同的签章模板,弄了版本号进行区分
log.info("第一步:获取模板文件流");
Resource resource;
if(CommonConstant.LETTERTEMPLATE_VERSION.equals(info.getGenerateeLetterVersion())){
resource = new ClassPathResource("letterTemplate.docx");
}else if(CommonConstant.LETTERTEMPLATE_VERIFY_VERSION.equals(info.getGenerateeLetterVersion())){
resource = new ClassPathResource("letterTemplate3.docx");
} else {
resource = new ClassPathResource("letterTemplate1.docx");
}
// 获取word模板的文件流
templateStream = resource.getInputStream();
if (null != templateStream) {
// org.apache.poi.xwpf.usermodel下的
XWPFDocument document = new XWPFDocument(templateStream);
//模本文件变量分割格式化
WordUtils.formatDocument(document);
//解析替换文本对象
WordUtils.changeText(document, variableMap);
try (FileOutputStream outputStream=new FileOutputStream(docxPdfFilePath)){
document.write(outputStream);
}
//生成 模板变量替换之后的pdf文件
log.info("第二步:生成替换变量之后的pdf文件");
AsposseWordUtils.convertDocxToPdf(docxPdfFilePath,tempPdfFileUrl);
// 第三步:生成签章图片
log.info("第三步:开始生成签章图片");
log.info("第三步:开始生成签章图片");
String SignName=CommonConstant.SIGNED_NAME_VALUE;
String SignBottomName=CommonConstant.SIGNED_BOTTOM_TITLE;
if(系统配置信息!=null && StringUtils.isNotBlank(系统配置信息.getDeptName())){
SignName=系统配置信息.getDeptName();
}
if(系统配置信息!=null && StringUtils.isNotBlank(系统配置信息.getSignCode())){
SignBottomName=系统配置信息.getSignCode();
}
SealUtil.generateDefaultSeal(SignName, SignBottomName, tempSignPngPath);
log.info("第三步:生成签章图片成功");
// 读取CA证书文件
Resource certResource = new ClassPathResource("cert.pfx");
certInputStream = certResource.getInputStream();
// 参数为 目标签章文件地址 、签章机构名称、目标pdf存放地址、
log.info("第四步:PDF进行签章");
String certPassword="CA证书密码";
PdfUtil.signPdf(tempPdfFileUrl, 系统配置信息.getDeptName(), targetPdfFilePath, tempSignPngPath, certInputStream, certPassword, signKeyword, 3);
log.info("第四步:PDF进行签章成功,地址為{}",targetPdfFilePath);
File file = new File(targetPdfFilePath);
// 文件上传minio分布式文件存储器
log.info("第五步:PDF进行上传");
String saveUrl = fileClient.upload(file, FileTypeUrlEnum.LETTER_OF_GUARANTEE.getCode());
log.info("第五步:PDF上传成功,地址為{}",saveUrl);
// 业务上的自定义操作
try {
修改主业务信息方法(info);
// 文件信息入库
} catch (Exception e) {
log.error("保存附件信息失败,参数:{},错误:{}", JSONUtil.toJsonStr(guaranteeFile),e);
}
}
} catch (Exception e) {
throw new ServiceException(e.getMessage());
} finally {
IoUtil.close(templateStream);
IoUtil.close(pdfOutputStream);
IoUtil.close(certInputStream);
log.debug("关闭文件流");
//临时文件删除
FileUtil.del(tempPdfFileUrl);
FileUtil.del(docxPdfFilePath);
FileUtil.del(targetPdfFilePath);
FileUtil.del(tempSignPngPath);
log.debug("删除临时文件");
}
}
}catch (Exception e) {
log.error(" 生成pdf失败, 信息:{},异常信息:{}",info,e);
}
}
2.相关工具类代码
WordUtils
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 通过word模板生成新的word工具类
*/
@Slf4j
public class WordUtils {
/*public static void main(String[] args) throws IOException {
FileInputStream inputStream=new FileInputStream(new File("E:\\letter\\doc\\source\\保函样例模板.docx"));
XWPFDocument document = new XWPFDocument(inputStream);
formatDocument(document);
FileOutputStream outputStream=new FileOutputStream(new File("E:\\letter\\doc\\format\\保函样例模板.docx"));
document.write(outputStream);
}*/
public static boolean hasKeyWord(XWPFDocument document, String keyWord){
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
// 获取段落文本
String text = paragraph.getText();
if(text.contains(keyWord)){
return true;
}
}
return false;
}
/***
*@description:XWPFDocument文档格式化处理
* 变量前缀、后缀、变量名称被拆分,需要合并为单一XWPFRun内容
*[org.apache.poi.xwpf.usermodel.XWPFDocument]
*void
*@author: zhaowenchao
*@date: 2023/6/26 10:54
*
*/
public static void formatDocument(XWPFDocument document){
log.info("=== docment format begin");
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
// 判断此段落时候需要进行替换
String text = paragraph.getText(); // 获取段落文本
if (checkText(text)) {
List<XWPFRun> runs = paragraph.getRuns(); // 获取文本的公共属性集
forwardFormat(runs);
reverseFormat(runs);
}
}
log.info("=== docment format end");
}
public static void forwardFormat(List<XWPFRun> runs ){
//XWPFRun内容以$或者${结尾,向后合并
String preRunValue=null;
for (int i= 0 ;i<runs.size();i++) { // run 为文本对象
XWPFRun run = runs.get(i);
String runText = run.getText(0);
if(!StringUtils.isEmpty(preRunValue)){
runText=preRunValue+runText;
run.setText(runText,0);
}
if(runText.endsWith("$") ||runText.endsWith("${") ){
preRunValue=runText;
run.setText("",0);
}else{
preRunValue=null;
}
}
}
public static void reverseFormat(List<XWPFRun> runs ){
//XWPFRun内容以}开头,向前合并
String nextRunValue=null;
for (int j = runs.size()-1;j>0;j--) { // run 为文本对象
XWPFRun run = runs.get(j);
String runText = run.getText(0);
if(!StringUtils.isEmpty(nextRunValue)){
runText=runText+nextRunValue;
run.setText(runText,0);
}
if(runText.startsWith("}")){
nextRunValue=runText;
run.setText("",0);
}else{
nextRunValue=null;
}
}
}
public static void changeText(XWPFDocument document, Map<String, String> textMap) {
log.info("=== changeText begin=== ");
log.info("=== textMap参数 === : " + textMap);
// 获取段落集合
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
// 判断此段落时候需要进行替换
String text = paragraph.getText(); // 获取段落文本
if (checkText(text)) {
List<XWPFRun> runs = paragraph.getRuns(); // 获取文本的公共属性集
for (XWPFRun run : runs) { // run 为文本对象
String runText = run.getText(0);
run.setText(changeValue(run.toString(), textMap), 0);
}
}
}
log.info("=== changeText end=== ");
}
public static boolean checkText(String text) {
boolean check = false;
if (text.indexOf("$") != -1) {
check = true;
}
return check;
}
public static String changeValue(String value, Map<String, String> textMap) {
Set<Map.Entry<String, String>> textSets = textMap.entrySet();
for (Map.Entry<String, String> textSet : textSets) {
String key =textSet.getKey();
if (value.indexOf(key) != -1) {
value = value.replace(key,String.valueOf(textSet.getValue()));
}
}
return value;
}
}
AsposseWordUtils
import com.aspose.words.Document;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@Slf4j
public class AsposseWordUtils {
private static License license = null;
static {
try {
InputStream inputStream = AsposseWordUtils.class.getResourceAsStream("/license.xml");
license = new License();
license.setLicense(inputStream);
}
catch (Exception e){
throw new RuntimeException("自动加载aspose证书文件失败");
}
}
public static void convertToPdf(Document document, String targetFile) throws IOException {
try {
long old = System.currentTimeMillis();
File file = new File(targetFile);
FileOutputStream fileOutputStream = new FileOutputStream(file);
document.save(fileOutputStream, SaveFormat.PDF);
fileOutputStream.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void convertDocxToPdf(String docxPath, String pdfPath) throws IOException {
try {
Document document = new Document(docxPath);
document.save(pdfPath,SaveFormat.PDF);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {
Document document = new Document("/home/temp/output.docx");
document.save("/home/temp/out.pdf",SaveFormat.PDF);
}
}
SealUtil
import com.ruoyi.common.utils.letter.seal.SealCircle;
import com.ruoyi.common.utils.letter.seal.SealConfiguration;
import com.ruoyi.common.utils.letter.seal.SealFont;
import com.sddbjt.boot.framework.common.enums.ErrorCodeEnum;
import com.sddbjt.boot.framework.common.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.*;
/**
* @Description: 印章工具类
* @Author
* @Date:
*/
@Slf4j
public abstract class SealUtil {
public static void generateDefaultSeal(String mainName,String viceName,String storeUrl){
SealConfiguration sealConfiguration = new SealConfiguration();
sealConfiguration.setMainFont(new SealFont(mainName, true, "宋体", 38, 38d, 10))
.setCenterFont(new SealFont("★", false, "宋体", 60, 60d, 0))
.setViceFont(new SealFont(viceName, false, "宋体", 15, 15d, 10))
// 甲方沟通去掉电子签章title
/*.setTitleFont(new SealFont("电子签章", true, "宋体", 30, 30d, 40))*/
.setBackgroudColor(Color.RED).setImageSize(320).setBorderCircle(new SealCircle(8, 150, 150));
SealUtil.buildAndStoreSeal(sealConfiguration,storeUrl);
};
/**
* 默认从10x10的位置开始画,防止左上部分画布装不下
*/
private final static int INIT_BEGIN = 10;
/**
* 生成私人印章图片,并保存到指定路径
*
* @param lineSize 边线宽度
* @param font 字体对象
* @param addString 追加字符
* @param fullPath 保存全路径
*
*/
public static void buildAndStorePersonSeal(int imageSize, int lineSize, SealFont font, String addString,
String fullPath) throws Exception {
storeBytes(buildBytes(buildPersonSeal(imageSize, lineSize, font, addString)), fullPath);
}
/**
* 生成印章图片,并保存到指定路径
*
* @param conf 配置文件F
* @param fullPath 保存全路径
*
*/
public static void buildAndStoreSeal(SealConfiguration conf, String fullPath) {
try {
storeBytes(buildBytes(buildSeal(conf)), fullPath);
} catch (Exception e) {
log.error("buildAndStoreSeal e :{}",e.getMessage(),e);
throw new ServiceException(ErrorCodeEnum.SYSTEM_ERROR.getCode(),"build store seal error");
}
}
/**
* 生成印章图片的inputstream
* @param image BufferedImage对象
* @return
* @throws Exception
*/
public static InputStream buildInputStream(BufferedImage image) throws Exception {
return new ByteArrayInputStream(buildBytes(image));
}
/**
* 生成印章图片的inputstream
* @param conf 配置文件
* @return
* @throws Exception
*/
public static InputStream buildInputStream(SealConfiguration conf) throws Exception {
return buildInputStream(buildSeal(conf));
}
/**
* 生成印章图片的byte数组
*
* @param image BufferedImage对象
*
* @return byte数组
*
* @throws IOException 异常
*/
public static byte[] buildBytes(BufferedImage image) throws Exception {
try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
//bufferedImage转为byte数组
ImageIO.write(image, "png", outStream);
return outStream.toByteArray();
}
}
/**
* 生成印章图片
*
* @param conf 配置文件
*
* @return BufferedImage对象
*
* @throws Exception 异常
*/
public static BufferedImage buildSeal(SealConfiguration conf) throws Exception {
//1.画布
BufferedImage bi = new BufferedImage(conf.getImageSize(), conf.getImageSize(), BufferedImage.TYPE_4BYTE_ABGR);
//2.画笔
Graphics2D g2d = bi.createGraphics();
//2.1抗锯齿设置
//文本不抗锯齿,否则圆中心的文字会被拉长
RenderingHints hints = new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
//其他图形抗锯齿
hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHints(hints);
//2.2设置背景透明度
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1f));
//2.3填充矩形
g2d.fillRect(0, 0, conf.getImageSize(), conf.getImageSize());
//2.4重设透明度,开始画图
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
//2.5设置画笔颜色
g2d.setPaint(conf.getBackgroudColor());
//3.画边线圆
if (conf.getBorderCircle() != null) {
drawCicle(g2d, conf.getBorderCircle(), INIT_BEGIN, INIT_BEGIN);
} else {
throw new Exception("BorderCircle can not null!");
}
int borderCircleWidth = conf.getBorderCircle().getWidth();
int borderCircleHeight = conf.getBorderCircle().getHeight();
//4.画内边线圆
if (conf.getBorderInnerCircle() != null) {
int x = INIT_BEGIN + borderCircleWidth - conf.getBorderInnerCircle().getWidth();
int y = INIT_BEGIN + borderCircleHeight - conf.getBorderInnerCircle().getHeight();
drawCicle(g2d, conf.getBorderInnerCircle(), x, y);
}
//5.画内环线圆
if (conf.getInnerCircle() != null) {
int x = INIT_BEGIN + borderCircleWidth - conf.getInnerCircle().getWidth();
int y = INIT_BEGIN + borderCircleHeight - conf.getInnerCircle().getHeight();
drawCicle(g2d, conf.getInnerCircle(), x, y);
}
//6.画弧形主文字
if (borderCircleHeight != borderCircleWidth) {
drawArcFont4Oval(g2d, conf.getBorderCircle(), conf.getMainFont(), true);
} else {
drawArcFont4Circle(g2d, borderCircleHeight, conf.getMainFont(), true);
}
//7.画弧形副文字
if (borderCircleHeight != borderCircleWidth) {
drawArcFont4Oval(g2d, conf.getBorderCircle(), conf.getViceFont(), false);
} else {
drawArcFont4Circle(g2d, borderCircleHeight, conf.getViceFont(), false);
}
//8.画中心字
drawFont(g2d, (borderCircleWidth + INIT_BEGIN) * 2, (borderCircleHeight + INIT_BEGIN) * 2,
conf.getCenterFont());
//9.画抬头文字
drawFont(g2d, (borderCircleWidth + INIT_BEGIN) * 2, (borderCircleHeight + 22) * 2, conf.getTitleFont());
g2d.dispose();
return bi;
}
/**
* 生成私人印章图片
*
* @param lineSize 线条粗细
* @param font 字体对象
* @param addString 是否添加文字,如“印”
*
* @return BufferedImage对象
*
* @throws Exception 异常
*/
public static BufferedImage buildPersonSeal(int imageSize, int lineSize, SealFont font, String addString)
throws Exception {
if (font == null || font.getFontText().length() < 2 || font.getFontText().length() > 4) {
throw new Exception("FontText.length illegal!");
}
int fixH = 18;
int fixW = 2;
//1.画布
BufferedImage bi = new BufferedImage(imageSize, imageSize / 2, BufferedImage.TYPE_4BYTE_ABGR);
//2.画笔
Graphics2D g2d = bi.createGraphics();
//2.1设置画笔颜色
g2d.setPaint(Color.RED);
//2.2抗锯齿设置
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//3.写签名
int marginW = fixW + lineSize;
float marginH;
FontRenderContext context = g2d.getFontRenderContext();
Rectangle2D rectangle;
Font f;
if (font.getFontText().length() == 2) {
if (addString != null && addString.trim().length() > 0) {
bi = drawThreeFont(bi, g2d, font.setFontText(font.getFontText() + addString), lineSize, imageSize, fixH,
fixW, true);
} else {
f = new Font(font.getFontFamily(), Font.BOLD, font.getFontSize());
g2d.setFont(f);
rectangle = f.getStringBounds(font.getFontText().substring(0, 1), context);
marginH = (float) (Math.abs(rectangle.getCenterY()) * 2 + marginW) + fixH - 4;
g2d.drawString(font.getFontText().substring(0, 1), marginW, marginH);
marginW += Math.abs(rectangle.getCenterX()) * 2 + (font.getFontSpace() == null ?
INIT_BEGIN :
font.getFontSpace());
g2d.drawString(font.getFontText().substring(1), marginW, marginH);
//拉伸
BufferedImage nbi = new BufferedImage(imageSize, imageSize, bi.getType());
Graphics2D ng2d = nbi.createGraphics();
ng2d.setPaint(Color.RED);
ng2d.drawImage(bi, 0, 0, imageSize, imageSize, null);
//画正方形
ng2d.setStroke(new BasicStroke(lineSize));
ng2d.drawRect(0, 0, imageSize, imageSize);
ng2d.dispose();
bi = nbi;
}
} else if (font.getFontText().length() == 3) {
if (addString != null && addString.trim().length() > 0) {
bi = drawFourFont(bi, font.setFontText(font.getFontText() + addString), lineSize, imageSize, fixH,
fixW);
} else {
bi = drawThreeFont(bi, g2d, font.setFontText(font.getFontText()), lineSize, imageSize, fixH, fixW,
false);
}
} else {
bi = drawFourFont(bi, font, lineSize, imageSize, fixH, fixW);
}
return bi;
}
/**
* 将byte数组保存为本地文件
*
* @param buf byte数组
* @param fullPath 文件全路径
*
* @throws IOException 异常
*/
private static void storeBytes(byte[] buf, String fullPath) throws IOException {
File file = new File(fullPath);
try (FileOutputStream fos = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
//1.如果父目录不存在,则创建
File dir = file.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
//2.写byte数组到文件
bos.write(buf);
}
}
/**
* 画三字
*
* @param bi 图片
* @param g2d 原画笔
* @param font 字体对象
* @param lineSize 线宽
* @param imageSize 图片尺寸
* @param fixH 修复膏
* @param fixW 修复宽
* @param isWithYin 是否含有“印”
*/
private static BufferedImage drawThreeFont(BufferedImage bi, Graphics2D g2d, SealFont font, int lineSize,
int imageSize, int fixH, int fixW, boolean isWithYin) {
fixH -= 9;
int marginW = fixW + lineSize;
//设置字体
Font f = new Font(font.getFontFamily(), Font.BOLD, font.getFontSize());
g2d.setFont(f);
FontRenderContext context = g2d.getFontRenderContext();
Rectangle2D rectangle = f.getStringBounds(font.getFontText().substring(0, 1), context);
float marginH = (float) (Math.abs(rectangle.getCenterY()) * 2 + marginW) + fixH;
int oldW = marginW;
if (isWithYin) {
g2d.drawString(font.getFontText().substring(2, 3), marginW, marginH);
marginW += rectangle.getCenterX() * 2 + (font.getFontSpace() == null ? INIT_BEGIN : font.getFontSpace());
} else {
marginW += rectangle.getCenterX() * 2 + (font.getFontSpace() == null ? INIT_BEGIN : font.getFontSpace());
g2d.drawString(font.getFontText().substring(0, 1), marginW, marginH);
}
//拉伸
BufferedImage nbi = new BufferedImage(imageSize, imageSize, bi.getType());
Graphics2D ng2d = nbi.createGraphics();
ng2d.setPaint(Color.RED);
ng2d.drawImage(bi, 0, 0, imageSize, imageSize, null);
//画正方形
ng2d.setStroke(new BasicStroke(lineSize));
ng2d.drawRect(0, 0, imageSize, imageSize);
ng2d.dispose();
bi = nbi;
g2d = bi.createGraphics();
g2d.setPaint(Color.RED);
g2d.setFont(f);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (isWithYin) {
g2d.drawString(font.getFontText().substring(0, 1), marginW, marginH += fixH);
rectangle = f.getStringBounds(font.getFontText(), context);
marginH += Math.abs(rectangle.getHeight());
g2d.drawString(font.getFontText().substring(1), marginW, marginH);
} else {
g2d.drawString(font.getFontText().substring(1, 2), oldW, marginH += fixH);
rectangle = f.getStringBounds(font.getFontText(), context);
marginH += Math.abs(rectangle.getHeight());
g2d.drawString(font.getFontText().substring(2, 3), oldW, marginH);
}
return bi;
}
/**
* 画四字
*
* @param bi 图片
* @param font 字体对象
* @param lineSize 线宽
* @param imageSize 图片尺寸
* @param fixH 修复膏
* @param fixW 修复宽
*/
private static BufferedImage drawFourFont(BufferedImage bi, SealFont font, int lineSize, int imageSize, int fixH,
int fixW) {
int marginW = fixW + lineSize;
//拉伸
BufferedImage nbi = new BufferedImage(imageSize, imageSize, bi.getType());
Graphics2D ng2d = nbi.createGraphics();
ng2d.setPaint(Color.RED);
ng2d.drawImage(bi, 0, 0, imageSize, imageSize, null);
//画正方形
ng2d.setStroke(new BasicStroke(lineSize));
ng2d.drawRect(0, 0, imageSize, imageSize);
ng2d.dispose();
bi = nbi;
Graphics2D g2d = bi.createGraphics();
g2d.setPaint(Color.RED);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
FontRenderContext context = g2d.getFontRenderContext();
Font f = new Font(font.getFontFamily(), Font.BOLD, font.getFontSize());
g2d.setFont(f);
Rectangle2D rectangle = f.getStringBounds(font.getFontText().substring(0, 1), context);
float marginH = (float) (Math.abs(rectangle.getCenterY()) * 2 + marginW) + fixH;
g2d.drawString(font.getFontText().substring(2, 3), marginW, marginH);
int oldW = marginW;
marginW +=
Math.abs(rectangle.getCenterX()) * 2 + (font.getFontSpace() == null ? INIT_BEGIN : font.getFontSpace());
g2d.drawString(font.getFontText().substring(0, 1), marginW, marginH);
marginH += Math.abs(rectangle.getHeight());
g2d.drawString(font.getFontText().substring(3, 4), oldW, marginH);
g2d.drawString(font.getFontText().substring(1, 2), marginW, marginH);
return bi;
}
/**
* 绘制圆弧形文字
*
* @param g2d 画笔
* @param circleRadius 弧形半径
* @param font 字体对象
* @param isTop 是否字体在上部,否则在下部
*/
private static void drawArcFont4Circle(Graphics2D g2d, int circleRadius, SealFont font, boolean isTop) {
if (font == null) {
return;
}
//1.字体长度
int fontTextLen = font.getFontText().length();
//2.字体大小,默认根据字体长度动态设定 TODO
int fontSize = font.getFontSize() == null ? (55 - fontTextLen * 2) : font.getFontSize();
//3.字体样式
int fontStyle = font.isBold() ? Font.BOLD : Font.PLAIN;
//4.构造字体
Font f = new Font(font.getFontFamily(), fontStyle, fontSize);
FontRenderContext context = g2d.getFontRenderContext();
Rectangle2D rectangle = f.getStringBounds(font.getFontText(), context);
//5.文字之间间距,默认动态调整
double fontSpace;
if (font.getFontSpace() != null) {
fontSpace = font.getFontSpace();
} else {
if (fontTextLen == 1) {
fontSpace = 0;
} else {
fontSpace = rectangle.getWidth() / (fontTextLen - 1) * 0.9;
}
}
//6.距离外圈距离
int marginSize = font.getMarginSize() == null ? INIT_BEGIN : font.getMarginSize();
//7.写字
double newRadius = circleRadius + rectangle.getY() - marginSize;
double radianPerInterval = 2 * Math.asin(fontSpace / (2 * newRadius));
double fix = 0.04;
if (isTop) {
fix = 0.18;
}
double firstAngle;
if (!isTop) {
if (fontTextLen % 2 == 1) {
firstAngle = Math.PI + Math.PI / 2 - (fontTextLen - 1) * radianPerInterval / 2.0 - fix;
} else {
firstAngle = Math.PI + Math.PI / 2 - ((fontTextLen / 2.0 - 0.5) * radianPerInterval) - fix;
}
} else {
if (fontTextLen % 2 == 1) {
firstAngle = (fontTextLen - 1) * radianPerInterval / 2.0 + Math.PI / 2 + fix;
} else {
firstAngle = (fontTextLen / 2.0 - 0.5) * radianPerInterval + Math.PI / 2 + fix;
}
}
for (int i = 0; i < fontTextLen; i++) {
double theta;
double thetaX;
double thetaY;
if (!isTop) {
theta = firstAngle + i * radianPerInterval;
thetaX = newRadius * Math.sin(Math.PI / 2 - theta);
thetaY = newRadius * Math.cos(theta - Math.PI / 2);
} else {
theta = firstAngle - i * radianPerInterval;
thetaX = newRadius * Math.sin(Math.PI / 2 - theta);
thetaY = newRadius * Math.cos(theta - Math.PI / 2);
}
AffineTransform transform;
if (!isTop) {
transform = AffineTransform.getRotateInstance(Math.PI + Math.PI / 2 - theta);
} else {
transform = AffineTransform.getRotateInstance(Math.PI / 2 - theta + Math.toRadians(8));
}
Font f2 = f.deriveFont(transform);
g2d.setFont(f2);
g2d.drawString(font.getFontText().substring(i, i + 1), (float) (circleRadius + thetaX + INIT_BEGIN),
(float) (circleRadius - thetaY + INIT_BEGIN));
}
}
/**
* 绘制椭圆弧形文字
*
* @param g2d 画笔
* @param circle 外围圆
* @param font 字体对象
* @param isTop 是否字体在上部,否则在下部
*/
private static void drawArcFont4Oval(Graphics2D g2d, SealCircle circle, SealFont font, boolean isTop) {
if (font == null) {
return;
}
float radiusX = circle.getWidth();
float radiusY = circle.getHeight();
float radiusWidth = radiusX + circle.getLineSize();
float radiusHeight = radiusY + circle.getLineSize();
//1.字体长度
int fontTextLen = font.getFontText().length();
//2.字体大小,默认根据字体长度动态设定
int fontSize = font.getFontSize() == null ? 25 + (10 - fontTextLen) / 2 : font.getFontSize();
//3.字体样式
int fontStyle = font.isBold() ? Font.BOLD : Font.PLAIN;
//4.构造字体
Font f = new Font(font.getFontFamily(), fontStyle, fontSize);
//5.总的角跨度
float totalArcAng = (float) (font.getFontSpace() * fontTextLen);
//6.从边线向中心的移动因子
float minRat = 0.90f;
double startAngle = isTop ? -90f - totalArcAng / 2f : 90f - totalArcAng / 2f;
double step = 0.5;
int alCount = (int) Math.ceil(totalArcAng / step) + 1;
double[] angleArr = new double[alCount];
double[] arcLenArr = new double[alCount];
int num = 0;
double accArcLen = 0.0;
angleArr[num] = startAngle;
arcLenArr[num] = accArcLen;
num++;
double angR = startAngle * Math.PI / 180.0;
double lastX = radiusX * Math.cos(angR) + radiusWidth;
double lastY = radiusY * Math.sin(angR) + radiusHeight;
for (double i = startAngle + step; num < alCount; i += step) {
angR = i * Math.PI / 180.0;
double x = radiusX * Math.cos(angR) + radiusWidth, y = radiusY * Math.sin(angR) + radiusHeight;
accArcLen += Math.sqrt((lastX - x) * (lastX - x) + (lastY - y) * (lastY - y));
angleArr[num] = i;
arcLenArr[num] = accArcLen;
lastX = x;
lastY = y;
num++;
}
double arcPer = accArcLen / fontTextLen;
for (int i = 0; i < fontTextLen; i++) {
double arcL = i * arcPer + arcPer / 2.0;
double ang = 0.0;
for (int p = 0; p < arcLenArr.length - 1; p++) {
if (arcLenArr[p] <= arcL && arcL <= arcLenArr[p + 1]) {
ang = (arcL >= ((arcLenArr[p] + arcLenArr[p + 1]) / 2.0)) ? angleArr[p + 1] : angleArr[p];
break;
}
}
angR = (ang * Math.PI / 180f);
Float x = radiusX * (float) Math.cos(angR) + radiusWidth;
Float y = radiusY * (float) Math.sin(angR) + radiusHeight;
double qxang = Math.atan2(radiusY * Math.cos(angR), -radiusX * Math.sin(angR));
double fxang = qxang + Math.PI / 2.0;
int subIndex = isTop ? i : fontTextLen - 1 - i;
String c = font.getFontText().substring(subIndex, subIndex + 1);
//获取文字高宽
FontMetrics fm = new JLabel().getFontMetrics(f);
int w = fm.stringWidth(c), h = fm.getHeight();
if (isTop) {
x += h * minRat * (float) Math.cos(fxang);
y += h * minRat * (float) Math.sin(fxang);
x += -w / 2f * (float) Math.cos(qxang);
y += -w / 2f * (float) Math.sin(qxang);
} else {
x += (h * minRat ) * (float) Math.cos(fxang);
y += (h * minRat) * (float) Math.sin(fxang);
x += w / 2f * (float) Math.cos(qxang);
y += w / 2f * (float) Math.sin(qxang);
}
// 旋转
AffineTransform affineTransform = new AffineTransform();
affineTransform.scale(0.8, 1);
if (isTop) {
affineTransform.rotate(Math.toRadians((fxang * 180.0 / Math.PI - 90)), 0, 0);
} else {
affineTransform.rotate(Math.toRadians((fxang * 180.0 / Math.PI + 180 - 90)), 0, 0);
}
Font f2 = f.deriveFont(affineTransform);
g2d.setFont(f2);
g2d.drawString(c, x.intValue() + INIT_BEGIN, y.intValue() + INIT_BEGIN);
}
}
/**
* 画文字
*
* @param g2d 画笔
* @param circleWidth 边线圆宽度
* @param circleHeight 边线圆高度
* @param font 字体对象
*/
private static void drawFont(Graphics2D g2d, int circleWidth, int circleHeight, SealFont font) {
if (font == null) {
return;
}
//1.字体长度
int fontTextLen = font.getFontText().length();
//2.字体大小,默认根据字体长度动态设定
int fontSize = font.getFontSize() == null ? (55 - fontTextLen * 2) : font.getFontSize();
//3.字体样式
int fontStyle = font.isBold() ? Font.BOLD : Font.PLAIN;
//4.构造字体
Font f = new Font(font.getFontFamily(), fontStyle, fontSize);
g2d.setFont(f);
FontRenderContext context = g2d.getFontRenderContext();
String[] fontTexts = font.getFontText().split("\n");
if (fontTexts.length > 1) {
int y = 0;
for (String fontText : fontTexts) {
y += Math.abs(f.getStringBounds(fontText, context).getHeight());
}
//5.设置上边距
float marginSize = INIT_BEGIN + (float) (circleHeight / 2f - y / 2f);
for (String fontText : fontTexts) {
Rectangle2D rectangle2D = f.getStringBounds(fontText, context);
g2d.drawString(fontText, (float) (circleWidth / 2f - rectangle2D.getCenterX() + 1f), marginSize);
marginSize += Math.abs(rectangle2D.getHeight());
}
} else {
Rectangle2D rectangle2D = f.getStringBounds(font.getFontText(), context);
//5.设置上边距,默认在中心
float marginSize = font.getMarginSize() == null ?
(float) (circleHeight / 2f - rectangle2D.getCenterY()) :
(float) (circleHeight / 2f - rectangle2D.getCenterY()) + (float) font.getMarginSize();
g2d.drawString(font.getFontText(), (float) (circleWidth / 2f - rectangle2D.getCenterX() + 1), marginSize);
}
}
/**
* 画圆
*
* @param g2d 画笔
* @param circle 圆配置对象
*/
private static void drawCicle(Graphics2D g2d, SealCircle circle, int x, int y) {
if (circle == null) {
return;
}
//1.圆线条粗细默认是圆直径的1/35
int lineSize = circle.getLineSize() == null ? circle.getHeight() * 2 / (35) : circle.getLineSize();
//2.画圆
g2d.setStroke(new BasicStroke(lineSize));
g2d.drawOval(x, y, circle.getWidth() * 2, circle.getHeight() * 2);
}
/* public static void main(String[] args) throws Exception {
SealConfiguration sealConfiguration = new SealConfiguration();
sealConfiguration.setMainFont(new SealFont("山东省投融资担保集团有限公司", true, "宋体", 38, 38d, 10))
.setCenterFont(new SealFont("★", false, "宋体", 60, 60d, 0))
.setTitleFont(new SealFont("电子签章", true, "宋体", 30, 30d, 40))
*//* .setViceFont(new SealFont("3701027650943", false, "宋体", 69, 48d, 0))*//*
.setBackgroudColor(Color.RED).setImageSize(320).setBorderCircle(new SealCircle(8, 150, 150));
SealUtil.buildAndStoreSeal(sealConfiguration,"E:\\letter\\sign\\sign.png");
}*/
}
印章配置类 SealConfiguration
import java.awt.*;
/**
* @Description: 印章配置类
* @Author
* @Date:
*/
public class SealConfiguration {
/**
* 主文字
*/
private SealFont mainFont;
/**
* 副文字
*/
private SealFont viceFont;
/**
* 抬头文字
*/
private SealFont titleFont;
/**
* 中心文字
*/
private SealFont centerFont;
/**
* 边线圆
*/
private SealCircle borderCircle;
/**
* 内边线圆
*/
private SealCircle borderInnerCircle;
/**
* 内线圆
*/
private SealCircle innerCircle;
/**
* 背景色,默认红色
*/
private Color backgroudColor = Color.RED;
/**
* 图片输出尺寸,默认300
*/
private Integer imageSize = 30;
public SealConfiguration setMainFont(SealFont mainFont) {
this.mainFont = mainFont;
return this;
}
public SealConfiguration setViceFont(SealFont viceFont) {
this.viceFont = viceFont;
return this;
}
public SealConfiguration setTitleFont(SealFont titleFont) {
this.titleFont = titleFont;
return this;
}
public SealConfiguration setCenterFont(SealFont centerFont) {
this.centerFont = centerFont;
return this;
}
public SealConfiguration setBorderCircle(SealCircle borderCircle) {
this.borderCircle = borderCircle;
return this;
}
public SealConfiguration setBorderInnerCircle(SealCircle borderInnerCircle) {
this.borderInnerCircle = borderInnerCircle;
return this;
}
public SealConfiguration setInnerCircle(SealCircle innerCircle) {
this.innerCircle = innerCircle;
return this;
}
public SealConfiguration setBackgroudColor(Color backgroudColor) {
this.backgroudColor = backgroudColor;
return this;
}
public SealConfiguration setImageSize(Integer imageSize) {
this.imageSize = imageSize;
return this;
}
public SealFont getMainFont() {
return mainFont;
}
public SealFont getViceFont() {
return viceFont;
}
public SealFont getTitleFont() {
return titleFont;
}
public SealFont getCenterFont() {
return centerFont;
}
public SealCircle getBorderCircle() {
return borderCircle;
}
public SealCircle getBorderInnerCircle() {
return borderInnerCircle;
}
public SealCircle getInnerCircle() {
return innerCircle;
}
public Color getBackgroudColor() {
return backgroudColor;
}
public Integer getImageSize() {
return imageSize;
}
}
PdfUtil PDF工具类
import cn.hutool.core.util.NumberUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.json.JSONUtil;
import com.google.code.appengine.awt.Color;
import com.lowagie.text.Font;
import com.lowagie.text.pdf.BaseFont;
import com.ruoyi.common.utils.letter.sign.EPortDigitalSignInfo;
import com.ruoyi.common.utils.letter.sign.EPortDigitalSignPosition;
import com.ruoyi.common.utils.letter.sign.ElectronicSignUtil;
import com.sddbjt.boot.framework.common.enums.ErrorCodeEnum;
import com.sddbjt.boot.framework.common.exception.ServiceException;
import fr.opensagres.poi.xwpf.converter.pdf.PdfConverter;
import fr.opensagres.poi.xwpf.converter.pdf.PdfOptions;
import fr.opensagres.xdocreport.itext.extension.font.IFontProvider;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import java.io.*;
import java.util.Map;
/**
* @description:
*/
@Slf4j
public class PdfUtil {
public static void signPdf(String sourcePdfPath , String signName, String signedPdfPath, String signPicPath,
InputStream certInputStream, String certPassword, String keyword,int page) {
//获取签章位置
Map<String,String> positionMap=ItextUtil.findKeywordPosition(sourcePdfPath,keyword,false);
log.info("signPdf positionMap:{}", JSONUtil.toJsonStr(positionMap));
double x= NumberUtil.parseDouble(positionMap.get("x"));
double y= NumberUtil.parseDouble(positionMap.get("y"));
int signPage = NumberUtil.parseInt(positionMap.get("page"));
try(FileInputStream picInputStream= new FileInputStream(signPicPath)){
//设置签章位置及缩放比例
EPortDigitalSignPosition signPosition = new EPortDigitalSignPosition(x-20,y-60,signPage, -65);
//设置签章属性信息
EPortDigitalSignInfo signInfo = new EPortDigitalSignInfo(signName, null, null);
ElectronicSignUtil.sign(certPassword, certInputStream,picInputStream, new File(sourcePdfPath),
new File(signedPdfPath),signInfo, signPosition);
} catch (Exception e) {
log.error("signPdf e:{}",e.getMessage(),e);
throw new ServiceException(ErrorCodeEnum.SYSTEM_ERROR.getCode(),"pdf签章异常");
}
}
/* public static void main(String[] args) throws Exception {
String sourcePdfPath ="E:\\letter\\sign\\b.pdf";
String signName ="山东省投融资担保集团有限公司";
String signedPdfPath="E:\\letter\\signed\\b.pdf";
String signPicPath ="E:\\letter\\sign\\sign.png";
String certPath ="E:\\letter\\cert\\sdph.pfx";
String certPassword ="Sddb@@12pq##q";
String keyword ="签章";
signPdf(sourcePdfPath,signName,signedPdfPath,signPicPath,new FileInputStream(new File(certPath)),certPassword,keyword);
}*/
/***
*@description:生成pdf文件
*[org.apache.poi.xwpf.usermodel.XWPFDocument, java.io.OutputStream]
*void
*@author: zhaowenchao
*@date: 2023/6/26 14:12
*
*/
public static void createPdf(XWPFDocument document , OutputStream pdfOutputStream){
log.info("===createPdf begin");
PdfOptions options = PdfOptions.create();
options.fontProvider(new IFontProvider() {
public Font getFont(String familyName, String encoding, float size, int style, Color color) {
try {
BaseFont bfChinese = BaseFont.createFont("static/fonts/simsun.ttc,1", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
Font fontChinese = new Font(bfChinese, size, style, color);
if (familyName != null)
fontChinese.setFamily(familyName);
return fontChinese;
} catch (Exception e) {
log.error("e:{}",e.getMessage(),e);
throw new RuntimeException();
}
}
});
try {
PdfConverter.getInstance().convert(document, pdfOutputStream, options);
log.info("===createPdf end");
} catch (IOException e) {
log.error("PdfUtils createPdf e :{}",e.getMessage(),e);
throw new ServiceException(ErrorCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage());
}
}
}
ItextUtil
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.json.JSONUtil;
import com.itextpdf.awt.geom.Rectangle2D;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.parser.*;
import com.ruoyi.common.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import java.awt.geom.Arc2D;
import java.io.*;
import java.util.*;
/**
*
*/
@Slf4j
public class ItextUtil {
/* public static void main(String[] args) throws IOException, DocumentException {
String templatePath="E:\\letter\\sign\\b.pdf";
String targetPath="E:\\letter\\sign\\c.pdf";
String fieldName="签章";
String imagePath="E:\\letter\\sign\\sign.png";
signSealAtSignPosition(templatePath,targetPath,imagePath,fieldName);
}*/
/**
* 在pdf文件签章关键字位置添加悬浮签章图片
* @param pdfPath 原始pdf文件
* @param targetPath 目标文件地址
* @param imagePath 图片地址
* @param keyword 关键字
*/
public static void signSealAtSignPosition(String pdfPath,String targetPath,String imagePath,String keyword){
Map<String,String> positionMap=findKeywordPosition(pdfPath,keyword,true);
log.info(" pdf sign position :{}", JSONUtil.toJsonStr(positionMap));
signSeal(pdfPath,targetPath,imagePath, NumberUtil.parseFloat(positionMap.get("x")),NumberUtil.parseFloat(positionMap.get("y")));
}
/**
* 在pdf指定位置添加悬浮图片
* @param pdfPath
* @param targetPath
* @param imagePath
* @param x
* @param y
*/
public static void signSeal(String pdfPath,String targetPath,String imagePath,float x, float y){
log.info("=== signSeal begin");
try(InputStream inputStream=new FileInputStream(new File(pdfPath));
OutputStream outputStream= new FileOutputStream(new File(targetPath))) {
PdfReader reader = new PdfReader(inputStream);
PdfStamper stamper = new PdfStamper(reader,outputStream);
Image image =Image.getInstance(imagePath);
PdfContentByte under = stamper.getOverContent(1);
image.scaleToFit(130,130);
image.setAbsolutePosition(x-120,y-80);
under.addImage(image);
stamper.close();
reader.close();
log.info("=== signSeal end");
} catch (IOException e) {
log.error("signSeal e:{}",e.getMessage(),e);
throw new ServiceException("io异常");
} catch (DocumentException e) {
log.error("signSeal e:{}",e.getMessage(),e);
throw new ServiceException("Document异常");
}
}
/**
* 获取pdf中关键字位置
* @param pdfPath pdf文件路径
* @param keyword 关键字
* @param reverse 高度根据pdf高度反转
* @return
*/
public static Map<String,String> findKeywordPosition(String pdfPath,String keyword,boolean reverse){
float pdfHeight=getPdfHeight(pdfPath);
try(FileInputStream pdfInputStream=new FileInputStream(new File(pdfPath))){
Map<String, List<float[]>> map = findKeywordPostions(pdfInputStream, Arrays.asList(keyword));
if(CollectionUtil.isEmpty(map)){
throw new ServiceException("关键字位置获取异常");
}
List<float[]> list = map.get(keyword);
if(CollectionUtil.isEmpty(list)){
throw new ServiceException("关键字位置集合中不包含指定关键字");
}
float[] position = list.get(0);
float x= position[1];
float y= position[2];
float page = position[0];
log.info("findKeywordPosition x:{},y:{},page:{}",x,y,page);
Map<String,String> positionMap=new HashMap();
positionMap.put("page",String.valueOf(page));
positionMap.put("x",String.valueOf(x));
if(reverse){
positionMap.put("y",String.valueOf(pdfHeight-y));
}else{
positionMap.put("y",String.valueOf(y));
}
return positionMap;
} catch (Exception e) {
log.error("findKeywordPosition e:{}",e.getMessage(),e);
throw new ServiceException("findKeyword异常");
}
}
/**
* 获取pdf单页高度
* @param pdfPath
* @return
*/
public static float getPdfHeight(String pdfPath){
PdfReader reader=null;
try {
reader= new PdfReader(pdfPath);
} catch (IOException e) {
log.error("getPdfHeight IOException:{} ",e.getMessage(),e);
}
Rectangle rectangle =reader.getPageSizeWithRotation(1);
log.debug("pdfPath:{} Width:",pdfPath,rectangle.getWidth());
log.debug("pdfPath:{} Height:",pdfPath,rectangle.getHeight());
reader.close();
return rectangle.getHeight();
}
/**
* findKeywordPostions
*
* @param inputStream
* @param keywords
* 关键字
* @return List<float [ ]> : float[0]:pageNum float[1]:x float[2]:y
* @throws IOException
*/
public static Map<String, List<float[]>> findKeywordPostions(InputStream inputStream, List<String> keywords) {
List<PdfPageContentPositions> pdfPageContentPositions = null;
try {
pdfPageContentPositions = getPdfContentPostionsList(inputStream);
} catch (IOException e) {
log.error("findKeywordPostions e:{}",e.getMessage(),e);
throw new ServiceException("关键字位置获取异常");
}
Map<String, List<float[]>> resultMap = new HashMap<>();
if(CollectionUtil.isNotEmpty(pdfPageContentPositions)){
for (String keyword : keywords) {
List<float[]> result = new ArrayList<>();
for (PdfPageContentPositions pdfPageContentPosition : pdfPageContentPositions) {
List<float[]> charPositions = findPositions(keyword, pdfPageContentPosition);
if (charPositions == null || charPositions.size() < 1) {
continue;
}
result.addAll(charPositions);
}
resultMap.put(keyword, result);
}
}
return resultMap;
}
public static List<PdfPageContentPositions> getPdfContentPostionsList(InputStream inputStream) throws IOException {
PdfReader reader = new PdfReader(inputStream);
List<PdfPageContentPositions> result = new ArrayList<>();
int pages = reader.getNumberOfPages();
for (int pageNum = 1; pageNum <= pages; pageNum++) {
float width = reader.getPageSize(pageNum).getWidth();
float height = reader.getPageSize(pageNum).getHeight();
PdfRenderListener pdfRenderListener = new PdfRenderListener(pageNum, width, height);
// 解析pdf,定位位置
PdfContentStreamProcessor processor = new PdfContentStreamProcessor(pdfRenderListener);
PdfDictionary pageDic = reader.getPageN(pageNum);
PdfDictionary resourcesDic = pageDic.getAsDict(PdfName.RESOURCES);
try {
processor.processContent(ContentByteUtils.getContentBytesForPage(reader, pageNum), resourcesDic);
} catch (IOException e) {
reader.close();
throw e;
}
String content = pdfRenderListener.getContent();
List<CharPosition> charPositions = pdfRenderListener.getcharPositions();
List<float[]> positionsList = new ArrayList<>();
for (CharPosition charPosition : charPositions) {
float[] positions = new float[] { charPosition.getPageNum(), charPosition.getX(), charPosition.getY() };
positionsList.add(positions);
}
PdfPageContentPositions pdfPageContentPositions = new PdfPageContentPositions();
pdfPageContentPositions.setContent(content);
pdfPageContentPositions.setPostions(positionsList);
result.add(pdfPageContentPositions);
}
reader.close();
return result;
}
private static List<float[]> findPositions(String keyword, PdfPageContentPositions pdfPageContentPositions) {
List<float[]> result = new ArrayList<>();
String content = pdfPageContentPositions.getContent();
List<float[]> charPositions = pdfPageContentPositions.getPositions();
for (int pos = 0; pos < content.length();) {
int positionIndex = content.indexOf(keyword, pos);
if (positionIndex == -1) {
break;
}
float[] postions = charPositions.get(positionIndex);
result.add(postions);
pos = positionIndex + 1;
}
return result;
}
private static class PdfPageContentPositions {
private String content;
private List<float[]> positions;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public List<float[]> getPositions() {
return positions;
}
public void setPostions(List<float[]> positions) {
this.positions = positions;
}
}
private static class PdfRenderListener implements RenderListener {
private int pageNum;
private float pageWidth;
private float pageHeight;
private StringBuilder contentBuilder = new StringBuilder();
private List<CharPosition> charPositions = new ArrayList<>();
public PdfRenderListener(int pageNum, float pageWidth, float pageHeight) {
this.pageNum = pageNum;
this.pageWidth = pageWidth;
this.pageHeight = pageHeight;
}
public void beginTextBlock() {
}
public void renderText(TextRenderInfo renderInfo) {
List<TextRenderInfo> characterRenderInfos = renderInfo.getCharacterRenderInfos();
for (TextRenderInfo textRenderInfo : characterRenderInfos) {
String word = textRenderInfo.getText();
if (word.length() > 1) {
word = word.substring(word.length() - 1, word.length());
}
Rectangle2D.Float rectangle = textRenderInfo.getAscentLine().getBoundingRectange();
float x = (float) rectangle.getX();
float y = pageHeight - (float) rectangle.getY();
// 这两个是关键字在所在页面的XY轴的百分比
float xPercent = Math.round(x / pageWidth * 10000) / 10000f;
float yPercent = Math.round((1 - y / pageHeight) * 10000) / 10000f;
CharPosition charPosition = new CharPosition(pageNum, (float) x, (float) y);
charPositions.add(charPosition);
contentBuilder.append(word);
}
}
public void endTextBlock() {
}
public void renderImage(ImageRenderInfo renderInfo) {
}
public String getContent() {
return contentBuilder.toString();
}
public List<CharPosition> getcharPositions() {
return charPositions;
}
}
private static class CharPosition {
private int pageNum = 0;
private float x = 0;
private float y = 0;
public CharPosition(int pageNum, float x, float y) {
this.pageNum = pageNum;
this.x = x;
this.y = y;
}
public int getPageNum() {
return pageNum;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
@Override
public String toString() {
return "[pageNum=" + this.pageNum + ",x=" + this.x + ",y=" + this.y + "]";
}
}
}
3.印章相关工具类
SealCircle
import lombok.Getter;
import lombok.Setter;
/**
* @Description: 印章圆圈类
* @Author
* @Date:
*/
@Getter
@Setter
public class SealCircle {
public SealCircle(Integer lineSize, Integer width,Integer height) {
this.lineSize = lineSize;
this.width = width;
this.height = height;
}
public SealCircle(){}
/**
* 线宽度
*/
private Integer lineSize;
/**
* 半径
*/
private Integer width;
/**
* 半径
*/
private Integer height;
public Integer getLineSize() {
return lineSize;
}
public Integer getHeight() {
return height;
}
public Integer getWidth() {
return width;
}
}
SealConfiguration 印章配置类
import java.awt.*;
/**
* @Description: 印章配置类
* @Author
* @Date:
*/
public class SealConfiguration {
/**
* 主文字
*/
private SealFont mainFont;
/**
* 副文字
*/
private SealFont viceFont;
/**
* 抬头文字
*/
private SealFont titleFont;
/**
* 中心文字
*/
private SealFont centerFont;
/**
* 边线圆
*/
private SealCircle borderCircle;
/**
* 内边线圆
*/
private SealCircle borderInnerCircle;
/**
* 内线圆
*/
private SealCircle innerCircle;
/**
* 背景色,默认红色
*/
private Color backgroudColor = Color.RED;
/**
* 图片输出尺寸,默认300
*/
private Integer imageSize = 30;
public SealConfiguration setMainFont(SealFont mainFont) {
this.mainFont = mainFont;
return this;
}
public SealConfiguration setViceFont(SealFont viceFont) {
this.viceFont = viceFont;
return this;
}
public SealConfiguration setTitleFont(SealFont titleFont) {
this.titleFont = titleFont;
return this;
}
public SealConfiguration setCenterFont(SealFont centerFont) {
this.centerFont = centerFont;
return this;
}
public SealConfiguration setBorderCircle(SealCircle borderCircle) {
this.borderCircle = borderCircle;
return this;
}
public SealConfiguration setBorderInnerCircle(SealCircle borderInnerCircle) {
this.borderInnerCircle = borderInnerCircle;
return this;
}
public SealConfiguration setInnerCircle(SealCircle innerCircle) {
this.innerCircle = innerCircle;
return this;
}
public SealConfiguration setBackgroudColor(Color backgroudColor) {
this.backgroudColor = backgroudColor;
return this;
}
public SealConfiguration setImageSize(Integer imageSize) {
this.imageSize = imageSize;
return this;
}
public SealFont getMainFont() {
return mainFont;
}
public SealFont getViceFont() {
return viceFont;
}
public SealFont getTitleFont() {
return titleFont;
}
public SealFont getCenterFont() {
return centerFont;
}
public SealCircle getBorderCircle() {
return borderCircle;
}
public SealCircle getBorderInnerCircle() {
return borderInnerCircle;
}
public SealCircle getInnerCircle() {
return innerCircle;
}
public Color getBackgroudColor() {
return backgroudColor;
}
public Integer getImageSize() {
return imageSize;
}
}
SealFont 印章字体类
import java.awt.*;
/**
* @Description: 印章字体类
* @Author
* @Date:
*/
public class SealFont {
public SealFont(String fontText, Boolean isBold, String fontFamily, Integer fontSize, Double fontSpace, Integer marginSize) {
this.fontText = fontText;
this.isBold = isBold;
this.fontFamily = fontFamily;
this.fontSize = fontSize;
this.fontSpace = fontSpace;
this.marginSize = marginSize;
}
public SealFont() {
}
/**
* 字体内容
*/
private String fontText;
/**
* 是否加粗
*/
private Boolean isBold = true;
/**
* 字形名,默认为宋体
*/
private String fontFamily = "宋体";
/**
* 字体大小
*/
private Integer fontSize;
/**
* 字距
*/
private Double fontSpace;
/**
* 边距(环边距或上边距)
*/
private Integer marginSize;
/**
* 获取系统支持的字形名集合
*/
public static String[] getSupportFontNames() {
return GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
}
public SealFont setFontSpace(Double fontSpace) {
this.fontSpace = fontSpace;
return this;
}
public SealFont setMarginSize(Integer marginSize) {
this.marginSize = marginSize;
return this;
}
public SealFont setFontFamily(String fontFamily) {
this.fontFamily = fontFamily;
return this;
}
public SealFont setFontText(String fontText) {
this.fontText = fontText;
return this;
}
public SealFont setFontSize(Integer fontSize) {
this.fontSize = fontSize;
return this;
}
public SealFont setBold(Boolean bold) {
isBold = bold;
return this;
}
public String getFontText() {
return fontText;
}
public String getFontFamily() {
return fontFamily;
}
public Integer getFontSize() {
return fontSize;
}
public Double getFontSpace() {
return fontSpace;
}
public Integer getMarginSize() {
return marginSize;
}
public Boolean isBold() {
return isBold;
}
}
4.签章相关工作类
CMSProcessableInputStream
import org.apache.pdfbox.io.IOUtils;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSTypedData;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Wraps a InputStream into a CMSProcessable object for bouncy castle. It's a memory saving
* alternative to the {@link org.bouncycastle.cms.CMSProcessableByteArray CMSProcessableByteArray}
* class.
*
* @author Thomas Chojecki
*/
class CMSProcessableInputStream implements CMSTypedData
{
private InputStream in;
private final ASN1ObjectIdentifier contentType;
CMSProcessableInputStream(InputStream is)
{
this(new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()), is);
}
CMSProcessableInputStream(ASN1ObjectIdentifier type, InputStream is)
{
contentType = type;
in = is;
}
@Override
public Object getContent()
{
return in;
}
@Override
public void write(OutputStream out) throws IOException, CMSException
{
// read the content only one time
IOUtils.copy(in, out);
in.close();
}
@Override
public ASN1ObjectIdentifier getContentType()
{
return contentType;
}
}
CreateSignature
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import java.io.*;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Calendar;
/**
* An example for singing a PDF with bouncy castle.
* A keystore can be created with the java keytool, for example:
*
* {@code keytool -genkeypair -storepass 123456 -storetype pkcs12 -alias test -validity 365
* -v -keyalg RSA -keystore keystore.p12 }
*
* @author Thomas Chojecki
* @author Vakhtang Koroghlishvili
* @author John Hewson
*/
public class CreateSignature extends CreateSignatureBase
{
/**
* Initialize the signature creator with a keystore and certficate password.
*
* @param keystore the pkcs12 keystore containing the signing certificate
* @param pin the password for recovering the key
* @throws KeyStoreException if the keystore has not been initialized (loaded)
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
* @throws UnrecoverableKeyException if the given password is wrong
* @throws CertificateException if the certificate is not valid as signing time
* @throws IOException if no certificate could be found
*/
public CreateSignature(KeyStore keystore, char[] pin)
throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, CertificateException, IOException
{
super(keystore, pin);
}
/**
* Signs the given PDF file. Alters the original file on disk.
* @param file the PDF file to sign
* @throws IOException if the file could not be read or written
*/
public void signDetached(File file) throws IOException
{
signDetached(file, file, null);
}
/**
* Signs the given PDF file.
* @param inFile input PDF file
* @param outFile output PDF file
* @throws IOException if the input file could not be read
*/
public void signDetached(File inFile, File outFile) throws IOException
{
signDetached(inFile, outFile, null);
}
/**
* Signs the given PDF file.
* @param inFile input PDF file
* @param outFile output PDF file
* @param tsaClient optional TSA client
* @throws IOException if the input file could not be read
*/
public void signDetached(File inFile, File outFile, TSAClient tsaClient) throws IOException
{
if (inFile == null || !inFile.exists())
{
throw new FileNotFoundException("Document for signing does not exist");
}
FileOutputStream fos = new FileOutputStream(outFile);
// sign
try (PDDocument doc = PDDocument.load(inFile))
{
signDetached(doc, fos, tsaClient);
}
}
public void signDetached(PDDocument document, OutputStream output, TSAClient tsaClient)
throws IOException
{
setTsaClient(tsaClient);
int accessPermissions = getMDPPermission(document);
if (accessPermissions == 1)
{
throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
}
// create signature dictionary
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("Example User");
signature.setLocation("Los Angeles, CA");
signature.setReason("Testing");
// the signing date, needed for valid signature
signature.setSignDate(Calendar.getInstance());
// Optional: certify
if (accessPermissions == 0)
{
setMDPPermission(document, signature, 2);
}
if (isExternalSigning())
{
System.out.println("Sign externally...");
document.addSignature(signature);
ExternalSigningSupport externalSigning =
document.saveIncrementalForExternalSigning(output);
// invoke external signature service
byte[] cmsSignature = sign(externalSigning.getContent());
// set signature bytes received from the service
externalSigning.setSignature(cmsSignature);
}
else
{
// register signature dictionary and sign interface
document.addSignature(signature, this);
// write incremental (only for signing purpose)
document.saveIncremental(output);
}
}
/*public static void main(String[] args) throws IOException, GeneralSecurityException
{
if (args.length < 3)
{
usage();
System.exit(1);
}
String tsaUrl = null;
boolean externalSig = false;
for (int i = 0; i < args.length; i++)
{
if (args[i].equals("-tsa"))
{
i++;
if (i >= args.length)
{
usage();
System.exit(1);
}
tsaUrl = args[i];
}
if (args[i].equals("-e"))
{
externalSig = true;
}
}
// load the keystore
KeyStore keystore = KeyStore.getInstance("PKCS12");
char[] password = args[1].toCharArray();
keystore.load(new FileInputStream(args[0]), password);
// TSA client
TSAClient tsaClient = null;
if (tsaUrl != null)
{
MessageDigest digest = MessageDigest.getInstance("SHA-256");
tsaClient = new TSAClient(new URL(tsaUrl), null, null, digest);
}
// sign PDF
CreateSignature signing = new CreateSignature(keystore, password);
signing.setExternalSigning(externalSig);
File inFile = new File(args[2]);
String name = inFile.getName();
String substring = name.substring(0, name.lastIndexOf('.'));
File outFile = new File(inFile.getParent(), substring + "_signed.pdf");
signing.signDetached(inFile, outFile, tsaClient);
}*/
private static void usage()
{
System.err.println("usage: java " + CreateSignature.class.getName() + " " +
"<pkcs12_keystore> <password> <pdf_to_sign>\n" + "" +
"options:\n" +
" -tsa <url> sign timestamp using the given TSA server\n" +
" -e sign using external signature creation scenario");
}
}
CreateSignatureBase
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.Attributes;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.*;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.util.Store;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
public abstract class CreateSignatureBase implements SignatureInterface
{
private PrivateKey privateKey;
private Certificate[] certificateChain;
private TSAClient tsaClient;
private boolean externalSigning;
/**
* Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the
* signature.
*
* @param keystore is a pkcs12 keystore.
* @param pin is the pin for the keystore / private key
* @throws KeyStoreException if the keystore has not been initialized (loaded)
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
* @throws UnrecoverableKeyException if the given password is wrong
* @throws CertificateException if the certificate is not valid as signing time
* @throws IOException if no certificate could be found
*/
public CreateSignatureBase(KeyStore keystore, char[] pin)
throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException
{
// grabs the first alias from the keystore and get the private key. An
// alternative method or constructor could be used for setting a specific
// alias that should be used.
Enumeration<String> aliases = keystore.aliases();
String alias;
Certificate cert = null;
while (aliases.hasMoreElements())
{
alias = aliases.nextElement();
setPrivateKey((PrivateKey) keystore.getKey(alias, pin));
Certificate[] certChain = keystore.getCertificateChain(alias);
if (certChain == null)
{
continue;
}
setCertificateChain(certChain);
cert = certChain[0];
if (cert instanceof X509Certificate)
{
// avoid expired certificate
((X509Certificate) cert).checkValidity();
}
break;
}
if (cert == null)
{
throw new IOException("Could not find certificate");
}
}
public final void setPrivateKey(PrivateKey privateKey)
{
this.privateKey = privateKey;
}
public final void setCertificateChain(final Certificate[] certificateChain)
{
this.certificateChain = certificateChain;
}
public void setTsaClient(TSAClient tsaClient)
{
this.tsaClient = tsaClient;
}
public TSAClient getTsaClient()
{
return tsaClient;
}
/**
* We just extend CMS signed Data
*
* @param signedData Generated CMS signed data
* @return CMSSignedData Extended CMS signed data
* @throws IOException
* @throws TSPException
*/
private CMSSignedData signTimeStamps(CMSSignedData signedData)
throws IOException, TSPException
{
SignerInformationStore signerStore = signedData.getSignerInfos();
List<SignerInformation> newSigners = new ArrayList<>();
for (SignerInformation signer : signerStore.getSigners())
{
newSigners.add(signTimeStamp(signer));
}
return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners));
}
/**
* We are extending CMS Signature
*
* @param signer information about signer
* @return information about SignerInformation
*/
private SignerInformation signTimeStamp(SignerInformation signer)
throws IOException, TSPException
{
AttributeTable unsignedAttributes = signer.getUnsignedAttributes();
ASN1EncodableVector vector = new ASN1EncodableVector();
if (unsignedAttributes != null)
{
vector = unsignedAttributes.toASN1EncodableVector();
}
byte[] token = getTsaClient().getTimeStampToken(signer.getSignature());
ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;
ASN1Encodable signatureTimeStamp = new Attribute(oid, new DERSet(ASN1Primitive.fromByteArray(token)));
vector.add(signatureTimeStamp);
Attributes signedAttributes = new Attributes(vector);
SignerInformation newSigner = SignerInformation.replaceUnsignedAttributes(
signer, new AttributeTable(signedAttributes));
if (newSigner == null)
{
return signer;
}
return newSigner;
}
/**
* SignatureInterface implementation.
*
* This method will be called from inside of the pdfbox and create the PKCS #7 signature.
* The given InputStream contains the bytes that are given by the byte range.
*
* This method is for internal use only.
*
* Use your favorite cryptographic library to implement PKCS #7 signature creation.
*
* @throws IOException
*/
@Override
public byte[] sign(InputStream content) throws IOException
{
try
{
List<Certificate> certList = new ArrayList<>();
certList.addAll(Arrays.asList(certificateChain));
Store certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
org.bouncycastle.asn1.x509.Certificate cert = org.bouncycastle.asn1.x509.Certificate.getInstance(certificateChain[0].getEncoded());
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, new X509CertificateHolder(cert)));
gen.addCertificates(certs);
CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
CMSSignedData signedData = gen.generate(msg, false);
if (tsaClient != null)
{
signedData = signTimeStamps(signedData);
}
return signedData.getEncoded();
}
catch (GeneralSecurityException | CMSException | TSPException | OperatorCreationException e)
{
throw new IOException(e);
}
}
/**
* Set if external signing scenario should be used.
* If {@code false}, SignatureInterface would be used for signing.
* <p>
* Default: {@code false}
* </p>
* @param externalSigning {@code true} if external signing should be performed
*/
public void setExternalSigning(boolean externalSigning)
{
this.externalSigning = externalSigning;
}
public boolean isExternalSigning()
{
return externalSigning;
}
/**
* Get the access permissions granted for this document in the DocMDP transform parameters
* dictionary. Details are described in the table "Entries in the DocMDP transform parameters
* dictionary" in the PDF specification.
*
* @param doc document.
* @return the permission value. 0 means no DocMDP transform parameters dictionary exists. Other
* return values are 1, 2 or 3. 2 is also returned if the DocMDP transform parameters dictionary
* is found but did not contain a /P entry, or if the value is outside the valid range.
*/
public int getMDPPermission(PDDocument doc)
{
COSBase base = doc.getDocumentCatalog().getCOSObject().getDictionaryObject(COSName.PERMS);
if (base instanceof COSDictionary)
{
COSDictionary permsDict = (COSDictionary) base;
base = permsDict.getDictionaryObject(COSName.DOCMDP);
if (base instanceof COSDictionary)
{
COSDictionary signatureDict = (COSDictionary) base;
base = signatureDict.getDictionaryObject("Reference");
if (base instanceof COSArray)
{
COSArray refArray = (COSArray) base;
for (int i = 0; i < refArray.size(); ++i)
{
base = refArray.getObject(i);
if (base instanceof COSDictionary)
{
COSDictionary sigRefDict = (COSDictionary) base;
if (COSName.DOCMDP.equals(sigRefDict.getDictionaryObject("TransformMethod")))
{
base = sigRefDict.getDictionaryObject("TransformParams");
if (base instanceof COSDictionary)
{
COSDictionary transformDict = (COSDictionary) base;
int accessPermissions = transformDict.getInt(COSName.P, 2);
if (accessPermissions < 1 || accessPermissions > 3)
{
accessPermissions = 2;
}
return accessPermissions;
}
}
}
}
}
}
}
return 0;
}
public void setMDPPermission(PDDocument doc, PDSignature signature, int accessPermissions)
{
COSDictionary sigDict = signature.getCOSObject();
// DocMDP specific stuff
COSDictionary transformParameters = new COSDictionary();
transformParameters.setItem(COSName.TYPE, COSName.getPDFName("TransformParams"));
transformParameters.setInt(COSName.P, accessPermissions);
transformParameters.setName(COSName.V, "1.2");
transformParameters.setNeedToBeUpdated(true);
COSDictionary referenceDict = new COSDictionary();
referenceDict.setItem(COSName.TYPE, COSName.getPDFName("SigRef"));
referenceDict.setItem("TransformMethod", COSName.getPDFName("DocMDP"));
referenceDict.setItem("DigestMethod", COSName.getPDFName("SHA1"));
referenceDict.setItem("TransformParams", transformParameters);
referenceDict.setNeedToBeUpdated(true);
COSArray referenceArray = new COSArray();
referenceArray.add(referenceDict);
sigDict.setItem("Reference", referenceArray);
referenceArray.setNeedToBeUpdated(true);
// Catalog
COSDictionary catalogDict = doc.getDocumentCatalog().getCOSObject();
COSDictionary permsDict = new COSDictionary();
catalogDict.setItem(COSName.PERMS, permsDict);
permsDict.setItem(COSName.DOCMDP, signature);
catalogDict.setNeedToBeUpdated(true);
permsDict.setNeedToBeUpdated(true);
}
}
ElectronicSignUtil
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.util.Hex;
import java.io.*;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Calendar;
public class ElectronicSignUtil extends CreateSignatureBase {
private PDVisibleSignDesigner visibleSignDesigner;
private final PDVisibleSigProperties visibleSignatureProperties = new PDVisibleSigProperties();
private SignatureOptions signatureOptions;
private boolean lateExternalSigning = false;
/**
* Initialize the signature creator with a keystore (pkcs12) and pin that
* should be used for the signature.
*
* @param keystore is a pkcs12 keystore.
* @param pin is the pin for the keystore / private key
* @throws KeyStoreException if the keystore has not been initialized (loaded)
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
* @throws UnrecoverableKeyException if the given password is wrong
* @throws CertificateException if the certificate is not valid as signing time
* @throws IOException if no certificate could be found
*/
public ElectronicSignUtil(KeyStore keystore, char[] pin)
throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException
{
super(keystore, pin);
}
/**
* 对指定pdf电子签章,并保存到指定位置
* @param password
* @param p12Input
* @param imageStream 由调用方进行流关闭处理
* @param srcPdf
* @param signed
* @param ePortDigitalSignInfo
* @param ePortDigitalSignPosition
* @throws Exception
*/
public static void sign(String password, InputStream p12Input, FileInputStream imageStream, File srcPdf, File signed,
EPortDigitalSignInfo ePortDigitalSignInfo, EPortDigitalSignPosition ePortDigitalSignPosition) throws Exception{
boolean externalSig=false;
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(p12Input, password.toCharArray());
ElectronicSignUtil signing = new ElectronicSignUtil(keystore, password.toCharArray());
int page = ePortDigitalSignPosition.getPageNo();
signing.setVisibleSignDesigner(srcPdf.toString(), ePortDigitalSignPosition.getX(),
ePortDigitalSignPosition.getY(),
ePortDigitalSignPosition.getZoomPercent(), imageStream, page);
signing.setVisibleSignatureProperties(ePortDigitalSignInfo.getSignerName(),
ePortDigitalSignInfo.getCertLocation(), ePortDigitalSignInfo.getReason(),
page, page, true);
signing.setExternalSigning(externalSig);
signing.signPDF(srcPdf, signed, null);
}
/**
* Set visible signature designer for a new signature field.
*
* @param filename
* @param x position of the signature field
* @param y position of the signature field
* @param zoomPercent
* @param imageStream
* @param page the signature should be placed on
* @throws IOException
*/
public void setVisibleSignDesigner(String filename, Double x, Double y, int zoomPercent,
FileInputStream imageStream, int page)
throws IOException
{
visibleSignDesigner = new PDVisibleSignDesigner(filename, imageStream, page);
visibleSignDesigner.xAxis(x.floatValue()).yAxis(y.floatValue()).zoom(zoomPercent).adjustForRotation();
}
/**
* Set visible signature properties for new signature fields.
*
* @param name
* @param location
* @param reason
* @param preferredSize
* @param page
* @param visualSignEnabled
* @throws IOException
*/
public void setVisibleSignatureProperties(String name, String location, String reason, int preferredSize,
int page, boolean visualSignEnabled) throws IOException
{
visibleSignatureProperties.signerName(name).signerLocation(location).signatureReason(reason).
preferredSize(preferredSize).page(page).visualSignEnabled(visualSignEnabled).
setPdVisibleSignature(visibleSignDesigner);
}
/**
* Sign pdf file and create new file that ends with "_signed.pdf".
*
* @param inputFile The source pdf document file.
* @param signedFile The file to be signed.
* @param tsaClient optional TSA client
* @throws IOException
*/
public void signPDF(File inputFile, File signedFile, TSAClient tsaClient) throws IOException
{
this.signPDF(inputFile, signedFile, tsaClient, null);
}
/**
* Sign pdf file and create new file that ends with "_signed.pdf".
*
* @param inputFile The source pdf document file.
* @param signedFile The file to be signed.
* @param tsaClient optional TSA client
* @param signatureFieldName optional name of an existing (unsigned) signature field
* @throws IOException
*/
public void signPDF(File inputFile, File signedFile, TSAClient tsaClient, String signatureFieldName) throws IOException
{
setTsaClient(tsaClient);
if (inputFile == null || !inputFile.exists())
{
throw new IOException("Document for signing does not exist");
}
// creating output document and prepare the IO streams.
try (FileOutputStream fos = new FileOutputStream(signedFile);
PDDocument doc = PDDocument.load(inputFile)){
int accessPermissions = getMDPPermission(doc);
if (accessPermissions == 1)
{
throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
}
// Note that PDFBox has a bug that visual signing on certified files with permission 2
// doesn't work properly, see PDFBOX-3699. As long as this issue is open, you may want to
// be careful with such files.
PDSignature signature;
// sign a PDF with an existing empty signature, as created by the CreateEmptySignatureForm example.
signature = findExistingSignature(doc, signatureFieldName);
if (signature == null)
{
// create signature dictionary
signature = new PDSignature();
}
// Optional: certify
// can be done only if version is at least 1.5 and if not already set
// doing this on a PDF/A-1b file fails validation by Adobe preflight (PDFBOX-3821)
// PDF/A-1b requires PDF version 1.4 max, so don't increase the version on such files.
if (doc.getVersion() >= 1.5f && accessPermissions == 0)
{
setMDPPermission(doc, signature, 2);
}
PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
if (acroForm != null && acroForm.getNeedAppearances())
{
// PDFBOX-3738 NeedAppearances true results in visible signature becoming invisible
// with Adobe Reader
if (acroForm.getFields().isEmpty())
{
// we can safely delete it if there are no fields
acroForm.getCOSObject().removeItem(COSName.NEED_APPEARANCES);
// note that if you've set MDP permissions, the removal of this item
// may result in Adobe Reader claiming that the document has been changed.
// and/or that field content won't be displayed properly.
// ==> decide what you prefer and adjust your code accordingly.
}
else
{
System.out.println("/NeedAppearances is set, signature may be ignored by Adobe Reader");
}
}
// default filter
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
// subfilter for basic and PAdES Part 2 signatures
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
if (visibleSignatureProperties != null)
{
// this builds the signature structures in a separate document
visibleSignatureProperties.buildSignature();
signature.setName(visibleSignatureProperties.getSignerName());
signature.setLocation(visibleSignatureProperties.getSignerLocation());
signature.setReason(visibleSignatureProperties.getSignatureReason());
}
// the signing date, needed for valid signature
signature.setSignDate(Calendar.getInstance());
// do not set SignatureInterface instance, if external signing used
SignatureInterface signatureInterface = isExternalSigning() ? null : this;
// register signature dictionary and sign interface
if (visibleSignatureProperties != null && visibleSignatureProperties.isVisualSignEnabled())
{
signatureOptions = new SignatureOptions();
signatureOptions.setVisualSignature(visibleSignatureProperties.getVisibleSignature());
signatureOptions.setPage(visibleSignatureProperties.getPage() - 1);
doc.addSignature(signature, signatureInterface, signatureOptions);
}
else
{
doc.addSignature(signature, signatureInterface);
}
if (isExternalSigning())
{
System.out.println("Signing externally " + signedFile.getName());
ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
// invoke external signature service
byte[] cmsSignature = sign(externalSigning.getContent());
// Explanation of late external signing (off by default):
// If you want to add the signature in a separate step, then set an empty byte array
// and call signature.getByteRange() and remember the offset signature.getByteRange()[1]+1.
// you can write the ascii hex signature at a later time even if you don't have this
// PDDocument object anymore, with classic java file random access methods.
// If you can't remember the offset value from ByteRange because your context has changed,
// then open the file with PDFBox, find the field with findExistingSignature() or
// PODDocument.getLastSignatureDictionary() and get the ByteRange from there.
// Close the file and then write the signature as explained earlier in this comment.
if (isLateExternalSigning())
{
// this saves the file with a 0 signature
externalSigning.setSignature(new byte[0]);
// remember the offset (add 1 because of "<")
int offset = signature.getByteRange()[1] + 1;
// now write the signature at the correct offset without any PDFBox methods
try (RandomAccessFile raf = new RandomAccessFile(signedFile, "rw"))
{
raf.seek(offset);
raf.write(Hex.getBytes(cmsSignature));
}
}
else
{
externalSigning.setSignature(cmsSignature);
}
}
else
{
doc.saveIncremental(fos);
}
}
IOUtils.closeQuietly(signatureOptions);
}
// Find an existing signature (assumed to be empty). You will usually not need this.
private PDSignature findExistingSignature(PDDocument doc, String sigFieldName)
{
PDSignature signature = null;
PDSignatureField signatureField;
PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
if (acroForm != null)
{
signatureField = (PDSignatureField) acroForm.getField(sigFieldName);
if (signatureField != null)
{
// retrieve signature dictionary
signature = signatureField.getSignature();
if (signature == null)
{
signature = new PDSignature();
// after solving PDFBOX-3524
// signatureField.setValue(signature)
// until then:
signatureField.getCOSObject().setItem(COSName.V, signature);
}
else
{
throw new IllegalStateException("The signature field " + sigFieldName + " is already signed.");
}
}
}
return signature;
}
public boolean isLateExternalSigning()
{
return lateExternalSigning;
}
}
EPortDigitalSignInfo 签名信息
/**
*@description:签名信息
*/
public class EPortDigitalSignInfo {
private String signerName;
private String reason;
private String certLocation;
public EPortDigitalSignInfo(String signerName, String reason, String certLocation) {
this.signerName = signerName;
this.reason = reason;
this.certLocation = certLocation;
}
public String getSignerName() {
return signerName;
}
public void setSignerName(String signerName) {
this.signerName = signerName;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getCertLocation() {
return certLocation;
}
public void setCertLocation(String certLocation) {
this.certLocation = certLocation;
}
}
EPortDigitalSignPosition 签名位置、页码、缩放比例
/**
*@description:签名位置、页码、缩放比例
*/
public class EPortDigitalSignPosition {
private Double x;
private Double y;
private int pageNo;
private int zoomPercent;
public EPortDigitalSignPosition(Double x, Double y, int pageNo, int zoomPercent) {
this.x = x;
this.y = y;
this.pageNo = pageNo;
this.zoomPercent = zoomPercent;
}
public Double getX() {
return x;
}
public void setX(Double x) {
this.x = x;
}
public Double getY() {
return y;
}
public void setY(Double y) {
this.y = y;
}
public int getPageNo() {
return pageNo;
}
public void setPageNo(int pageNo) {
this.pageNo = pageNo;
}
public int getZoomPercent() {
return zoomPercent;
}
public void setZoomPercent(int zoomPercent) {
this.zoomPercent = zoomPercent;
}
}
TSAClient
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.io.IOUtils;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.tsp.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.security.SecureRandom;
/**
* Time Stamping Authority (TSA) Client [RFC 3161].
* @author Vakhtang Koroghlishvili
* @author John Hewson
*/
public class TSAClient
{
private static final Log LOG = LogFactory.getLog(TSAClient.class);
private final URL url;
private final String username;
private final String password;
private final MessageDigest digest;
/**
*
* @param url the URL of the TSA service
* @param username user name of TSA
* @param password password of TSA
* @param digest the message digest to use
*/
public TSAClient(URL url, String username, String password, MessageDigest digest)
{
this.url = url;
this.username = username;
this.password = password;
this.digest = digest;
}
/**
*
* @param messageImprint imprint of message contents
* @return the encoded time stamp token
* @throws IOException if there was an error with the connection or data from the TSA server,
* or if the time stamp response could not be validated
*/
public byte[] getTimeStampToken(byte[] messageImprint) throws IOException
{
digest.reset();
byte[] hash = digest.digest(messageImprint);
// 32-bit cryptographic nonce
SecureRandom random = new SecureRandom();
int nonce = random.nextInt();
// generate TSA request
TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
tsaGenerator.setCertReq(true);
ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm());
TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
// get TSA response
byte[] tsaResponse = getTSAResponse(request.getEncoded());
TimeStampResponse response;
try
{
response = new TimeStampResponse(tsaResponse);
response.validate(request);
}
catch (TSPException e)
{
throw new IOException(e);
}
TimeStampToken token = response.getTimeStampToken();
if (token == null)
{
throw new IOException("Response does not have a time stamp token");
}
return token.getEncoded();
}
// gets response data for the given encoded TimeStampRequest data
// throws IOException if a connection to the TSA cannot be established
private byte[] getTSAResponse(byte[] request) throws IOException
{
LOG.debug("Opening connection to TSA server");
URLConnection connection = url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestProperty("Content-Type", "application/timestamp-query");
LOG.debug("Established connection to TSA server");
if (username != null && password != null && !username.isEmpty() && !password.isEmpty())
{
connection.setRequestProperty(username, password);
}
// read response
OutputStream output = null;
try
{
output = connection.getOutputStream();
output.write(request);
}
finally
{
IOUtils.closeQuietly(output);
}
LOG.debug("Waiting for response from TSA server");
InputStream input = null;
byte[] response;
try
{
input = connection.getInputStream();
response = IOUtils.toByteArray(input);
}
finally
{
IOUtils.closeQuietly(input);
}
LOG.debug("Received response from TSA server");
return response;
}
// returns the ASN.1 OID of the given hash algorithm
private ASN1ObjectIdentifier getHashObjectIdentifier(String algorithm)
{
switch (algorithm)
{
case "MD2":
return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md2.getId());
case "MD5":
return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md5.getId());
case "SHA-1":
return new ASN1ObjectIdentifier(OIWObjectIdentifiers.idSHA1.getId());
case "SHA-224":
return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha224.getId());
case "SHA-256":
return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha256.getId());
case "SHA-384":
return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha384.getId());
case "SHA-512":
return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha512.getId());
default:
return new ASN1ObjectIdentifier(algorithm);
}
}
}