作者:fyupeng
技术专栏:☞ https://github.com/fyupeng
项目地址:☞ https://github.com/fyupeng/distributed-blog-system-api
留给读者
一、介绍
根据关键字偏移量水印签字。
允许重复调用,文件安全性高,自动备份并恢复。
二、代码
测试 Demo
PdfUtilTest .java
java
package com.gwssi.common.service;
import com.gwssi.common.util.PDFSigner;
import com.gwssi.torch.v1.dao.TorchDaoManager;
import com.gwssi.torch.v1.dao.api.IBaseDao;
import org.apache.commons.lang.StringUtils;
import java.awt.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Auther: fyp
* @Date: 2024/6/24
* @Description:
* @Package: com.gwssi.common.service
* @Version: 1.0
*/
public class PdfUtilTest {
public String signPdf(String applyno) {
String[] keywords = {"领取人签字", "发照日期", "领取日期"};
Float[] signatureTextOffsetX = {0f ,100f, -20f};
Float[] signatureTextOffsetY = {-50f, 0f, -50f};
//String[] signatureTexts = {"小明", "2024-04-26", "2024-04-28"};
List<String> signatureTexts = new ArrayList<>();
String filepath = "C:/output.pdf";
Map<String,Object> signatureParams = new HashMap<>();
signatureParams.put("keywords", keywords);
signatureParams.put("signatureTexts", signatureTexts);
signatureParams.put("signatureTextOffsetX", signatureTextOffsetX);
signatureParams.put("signatureTextOffsetY", signatureTextOffsetY);
PDFSigner.signPdf(filepath, signatureParams, Color.gray, 18);
return filepath;
}
}
封装类 PDFSigner.java
java
package com.gwssi.common.util;
/**
* @Auther: fyp
* @Date: 2024/4/28
* @Description:
* @Package: com.gwssi.common.util
* @Version: 1.0
*/
import java.awt.*;
import java.io.*;
import java.util.*;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.text.PDFTextStripper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PDFSigner {
private static final Logger LOG = LoggerFactory.getLogger(PDFSigner.class);
private static String filePath;
private static ResourceBundle resourceBundle;
static {
resourceBundle = ResourceBundle.getBundle("torch-v1");
filePath = resourceBundle.getString("project.filePath");
}
//private static final Properties properties = System.getProperties();
// 获取当前用户的工作目录
//private static final String userDir = properties.getProperty("user.dir");
//private static final String rootDirPath = userDir + "/WebContent/";
public static byte[] readFileToBytes(File file) throws IOException {
FileInputStream fis = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
fis.read(bytes);
fis.close();
return bytes;
}
public static void backPdf(String pdfPath) {
// 文件 备份
String backPdfPath = pdfPath + ".bak";
try {
File file = new File(pdfPath);
byte[] fileBytes = null;
try {
if (checkFileSign(pdfPath, backPdfPath)) {
fileBytes = readFileToBytes(file);
backFile(fileBytes, backPdfPath);
}
} catch (Exception e) {
LOG.error("文件备份异常,失败原因:{}", e.getMessage(), e);
throw new RuntimeException("文件备份异常,失败原因:" + e.getMessage());
}
} catch (Exception e) {
LOG.error("文件备份异常,失败原因:{}", e.getMessage(), e);
throw new RuntimeException("文件备份异常,失败原因:" + e.getMessage());
}
}
public static void signPdf(String pdfPath, Map<String ,Object> signParams, Color signatureColor, int signatureFontSize) {
String[] keywords = (String[]) signParams.get("keywords");
List<String> signatureTexts = (List<String>) signParams.get("signatureTexts");
Float[] signatureTextOffsetX = (Float[]) signParams.get("signatureTextOffsetX");
Float[] signatureTextOffsetY = (Float[]) signParams.get("signatureTextOffsetY");
// 文件 备份
String backPdfPath = pdfPath + ".bak";
File file = new File(pdfPath);
byte[] fileBytes = null;
try {
if (checkFileSign(pdfPath, backPdfPath)) {
fileBytes = readFileToBytes(file);
backFile(fileBytes, backPdfPath);
} else {
fileBytes = readFileToBytes(file);
}
PDDocument document = PDDocument.load(new File(pdfPath));
for (int idx = 0; idx < keywords.length; idx++) {
List<KeyWordPosition> keyWordsByByte = PdfMatchKeyword.getKeyWordsByByte(fileBytes, keywords[idx]);
System.out.println(keyWordsByByte);
float[] keyWordsByByte2 = PdfHelper.getKeyWordsByByte(fileBytes, keywords[idx]);
float signatureX = keyWordsByByte2[0]; // 使用关键字的 x 坐标作为签名的 x 坐标
float signatureY = keyWordsByByte2[1] ; // 在关键字的 y 坐标上移 20 个单位作为签名的 y 坐标
int pageNum = findKeywordPage(document, keywords[idx]);
if (pageNum != -1) {
signPage(document, pageNum, signatureTexts.get(idx), signatureX + signatureTextOffsetX[idx], signatureY + signatureTextOffsetY[idx], signatureColor, signatureFontSize);
// PDF 覆盖 (原文件备份)
document.save(pdfPath);
System.out.println("PDF 文件签名成功!");
} else {
System.out.println("未找到关键字,无法签名。");
}
}
document.close();
//deleteFile(pdfPath);
} catch (Exception e) {
LOG.error("文件备份异常,失败原因:{}", e.getMessage(), e);
throw new RuntimeException("文件备份异常,失败原因:" + e.getMessage());
}
}
public static byte[] fileToBytes(File file) throws IOException {
// 创建字节输入流
FileInputStream fis = new FileInputStream(file);
try {
// 获取文件大小
long fileSize = file.length();
// 创建一个与文件大小相同的字节数组
byte[] bytesArray = new byte[(int) fileSize];
// 将文件内容读取到字节数组
fis.read(bytesArray);
// 返回字节数组
return bytesArray;
} finally {
// 关闭文件输入流
fis.close();
}
}
/**
*
* 检查是否第一次生成 第一次 true
* @param pdfPath
* @param backPdfPath
* @return
* @throws IOException
*/
private static boolean checkFileSign(String pdfPath, String backPdfPath) throws IOException {
File pdfFile = new File(pdfPath);
File backPDFFile = new File(backPdfPath);
// 恢复文件 重新生成 (存在备份文件,非第一次生成)
if (backPDFFile.exists()) {
if (pdfFile.exists()) {
pdfFile.delete();
}
byte[] bytes = fileToBytes(backPDFFile);
FileOutputStream fos = new FileOutputStream(pdfFile);
fos.write(bytes);
fos.close();
return false;
}
// 第一次生成
return true;
}
private static void backFile(byte[] fileBytes, String backPdfPath) throws IOException {
File file = new File(backPdfPath);
FileOutputStream fos = new FileOutputStream(file);
fos.write(fileBytes);
fos.close();
}
private static void deleteFile(String pdfPath) throws IOException {
File file = new File(pdfPath);
file.delete();
}
public static void main(String[] args) throws FileNotFoundException {
String[] keywords = {"领取人签字", "发照日期", "领取日期"};
Float[] signatureTextOffsetX = {0f ,100f, -20f};
Float[] signatureTextOffsetY = {-50f, 0f, -50f};
String[] signatureTexts = {"小明", "2024-04-26", "2024-04-28"};
Map<String,Object> signatureParams = new HashMap<>();
signatureParams.put("keywords", keywords);
signatureParams.put("signatureTexts", signatureTexts);
signatureParams.put("signatureTextOffsetX", signatureTextOffsetX);
signatureParams.put("signatureTextOffsetY", signatureTextOffsetY);
signPdf("D:/doc/2023-10-13/14-37-37/珠海陈文路走走杨服装零售店(440003A2300006440).pdf", signatureParams, Color.gray, 18);
}
public static int findKeywordPage(PDDocument document, String keyword) throws IOException {
PDFTextStripper stripper = new PDFTextStripper();
int pageNum = 0;
for (PDPage page : document.getPages()) {
stripper.setStartPage(pageNum + 1);
stripper.setEndPage(pageNum + 1);
String text = stripper.getText(document);
if (text.contains(keyword)) {
return pageNum;
}
pageNum++;
}
return -1;
}
public static void signPage(PDDocument document, int pageNum, String signatureText, float signatureX, float signatureY, Color signatureColor, int fontSize) throws IOException {
PDPage page = document.getPage(pageNum);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true)) {
contentStream.beginText();
//PDFont font = PDType0Font.load(document, new File(filePath, "/app/font/simsun.ttf")); // 替换成实际的字体文件路径
//PDFont font = PDType0Font.load(document, new File(filePath, "/app/font/STSong.ttf")); // 替换成实际的字体文件路径
//PDFont font = PDType0Font.load(document, new File(filePath, "/app/font/simhei.ttf")); // 替换成实际的字体文件路径
PDFont font = PDType0Font.load(document, new File( filePath + "/app/font/hanyizhonghei.ttf")); // 替换成实际的字体文件路径
contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(signatureColor); // 设置文本颜色
contentStream.newLineAtOffset(signatureX, signatureY); // 垂直偏移量减少,签字下移
contentStream.showText(signatureText);
contentStream.endText();
} catch (Exception e) {
LOG.error("签名异常,异常原因:{}", e.getMessage(), e);
throw new RuntimeException("签名异常,异常原因:" + e.getMessage());
}
}
}
PdfMatchKeyword.java
java
package com.gwssi.common.util;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.PdfTextExtractor;
import com.itextpdf.text.pdf.parser.TextExtractionStrategy;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import com.itextpdf.text.pdf.parser.Vector;
public class PdfMatchKeyword {
/**
* 用于供外部类调用获取关键字所在PDF文件坐标
* @param filepath
* @param keyWords
* @return
*/
public static List<KeyWordPosition> getKeyWordsByPath(String filepath, String keyWords) {
List<KeyWordPosition> matchItems = null;
try{
PdfReader pdfReader = new PdfReader(filepath);
matchItems = getKeyWords(pdfReader, keyWords);
} catch (IOException e) {
e.printStackTrace();
}
return matchItems;
}
/**
* 用于供外部类调用获取关键字所在PDF文件坐标
* @param filepath
* @param keyWords
* @return
*/
public static List<KeyWordPosition> getKeyWordsByByte(byte[] pdfIn, String keyWords) {
List<KeyWordPosition> matchItems = null;
try{
PdfReader pdfReader = new PdfReader(pdfIn);
matchItems = getKeyWords(pdfReader, keyWords);
} catch (IOException e) {
e.printStackTrace();
}
return matchItems;
}
/**
* 获取关键字所在PDF坐标
* @param pdfReader
* @param keyWords
* @return
*/
private static List<KeyWordPosition> getKeyWords(PdfReader pdfReader, String keyWords) {
int page = 0;
List<KeyWordPosition> matchItems = new ArrayList<>();
try{
int pageNum = pdfReader.getNumberOfPages();
StringBuilder allText = null;
//遍历页
for (page = 1; page <= pageNum; page++) {
//只记录当页的所有内容,需要记录全部页放在循环外面
List<ItemPosition> allItems = new ArrayList<>();
//扫描内容
MyTextExtractionStrategy myTextExtractionStrategy = new MyTextExtractionStrategy(allItems, page);
PdfTextExtractor.getTextFromPage(pdfReader, page, myTextExtractionStrategy);
//当页的文字内容,用于关键词匹配
allText = new StringBuilder();
//一个字一个字的遍历
for (int i=0; i<allItems.size(); i++) {
ItemPosition item = allItems.get(i);
allText.append(item.getText());
//关键字存在连续多个块中
if(allText.indexOf(keyWords) != -1) {
KeyWordPosition keyWordPosition = new KeyWordPosition();
//记录关键词每个字的位置,只记录开始结束标记时会有问题
List<ItemPosition> listItem = new ArrayList<>();
for(int j=i-keyWords.length()+1; j<=i; j++) {
listItem.add(allItems.get(j));
}
keyWordPosition.setListItem(listItem);
keyWordPosition.setText(keyWords);
matchItems.add(keyWordPosition);
allText.setLength(0);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return matchItems;
}
/**
* 添加矩形标记
* @param oldPath
* @param newPath
* @param matchItems 关键词
* @param color 标记颜色
* @param lineWidth 线条粗细
* @param padding 边框内边距
* @throws DocumentException
* @throws IOException
*/
public static void andRectangleMark(String oldPath, String newPath, List<KeyWordPosition> matchItems, BaseColor color, int lineWidth, int padding) throws DocumentException, IOException{
// 待加水印的文件
PdfReader reader = new PdfReader(oldPath);
// 加完水印的文件
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(newPath));
PdfContentByte content;
// 设置字体
// 循环对每页插入水印
for (KeyWordPosition keyWordPosition:matchItems)
{
//一个关键词的所有字坐标
List<ItemPosition> oneKeywordItems = keyWordPosition.getListItem();
for(int i=0; i<oneKeywordItems.size(); i++) {
ItemPosition item = oneKeywordItems.get(i);
ItemPosition preItem = i==0?null:oneKeywordItems.get(i-1);
// 水印的起始
content = stamper.getOverContent(item.getPage());
// 开始写入水印
content.setLineWidth(lineWidth);
content.setColorStroke(color);
//底线
content.moveTo(item.getRectangle().getLeft()-padding, item.getRectangle().getBottom()-padding);
content.lineTo(item.getRectangle().getRight()+padding, item.getRectangle().getBottom()-padding);
if(i!=0 && preItem!=null && (preItem.getRectangle().getBottom()-padding)==(item.getRectangle().getBottom()-padding) && (preItem.getRectangle().getRight()+padding)!=(item.getRectangle().getLeft()-padding)) {
content.moveTo(preItem.getRectangle().getRight()+padding, preItem.getRectangle().getBottom()-padding);
content.lineTo(item.getRectangle().getLeft()-padding, item.getRectangle().getBottom()-padding);
}
//上线
content.moveTo(item.getRectangle().getLeft()-padding, item.getRectangle().getTop()+padding);
content.lineTo(item.getRectangle().getRight()+padding, item.getRectangle().getTop()+padding);
if(i!=0 && preItem!=null && (preItem.getRectangle().getTop()+padding)==(item.getRectangle().getTop()+padding) && (preItem.getRectangle().getRight()+padding)!=(item.getRectangle().getLeft()-padding)) {
content.moveTo(preItem.getRectangle().getRight()+padding, preItem.getRectangle().getTop()+padding);
content.lineTo(item.getRectangle().getLeft()-padding, item.getRectangle().getTop()+padding);
}
//左线
if(i==0) {
content.moveTo(item.getRectangle().getLeft()-padding, item.getRectangle().getBottom()-padding);
content.lineTo(item.getRectangle().getLeft()-padding, item.getRectangle().getTop()+padding);
}
//右线
if(i==(oneKeywordItems.size()-1)) {
content.moveTo(item.getRectangle().getRight()+padding, item.getRectangle().getBottom()-padding);
content.lineTo(item.getRectangle().getRight()+padding, item.getRectangle().getTop()+padding);
}
content.stroke();
}
}
stamper.close();
}
public static void main(String[] args) throws Exception {
String keyword = "陈雪英";
String sourcePdf = "D:\\SoftPackage\\InterDowloads\\东莞市浩影塑胶模具科技有限公司-登记审核表.pdf";
String watermarkPdf = "D:\\SoftPackage\\InterDowloads\\东莞市浩影塑胶模具科技有限公司-登记审核表2.pdf";
Long start = System.currentTimeMillis();
System.out.println("开始扫描....");
List<KeyWordPosition> matchItems = getKeyWordsByPath(sourcePdf, keyword);
System.out.println("扫描结束["+(System.currentTimeMillis()-start)+"ms],共找到关键字["+keyword+"]出现["+matchItems.size()+"]次");
start = System.currentTimeMillis();
System.out.println("开始添加标记....");
andRectangleMark(sourcePdf
, watermarkPdf
, matchItems
, BaseColor.RED
, 2
, 2);
System.out.println("标记添加完成["+(System.currentTimeMillis()-start)+"ms]");
}
}
/**
* @ClassName: MyTextExtractionStrategy
* @Description: 记录所有位置+字体信息,这种方式获取坐标信息和字体信息方便一点
* @author chenyang-054
* @date 2021-04-09 11:00:31
*/
class MyTextExtractionStrategy implements TextExtractionStrategy{
private List<ItemPosition> positions;
private Integer page;
public MyTextExtractionStrategy() {}
public MyTextExtractionStrategy(List<ItemPosition> positions, Integer page) {
this.positions = positions;
this.page = page;
}
@Override
public void beginTextBlock() {
// TODO Auto-generated method stub
}
@Override
public void renderText(TextRenderInfo renderInfo) {
ItemPosition ItemPosition = new ItemPosition();
Vector bottomLeftPoint = renderInfo.getDescentLine().getStartPoint();
Vector topRightPoint = renderInfo.getAscentLine().getEndPoint();
//记录矩形坐标
Rectangle rectangle = new Rectangle(bottomLeftPoint.get(Vector.I1), bottomLeftPoint.get(Vector.I2),
topRightPoint.get(Vector.I1), topRightPoint.get(Vector.I2));
ItemPosition.setPage(page);
ItemPosition.setRectangle(rectangle);
ItemPosition.setText(renderInfo.getText());
positions.add(ItemPosition);
}
@Override
public void endTextBlock() {
// TODO Auto-generated method stub
}
@Override
public void renderImage(ImageRenderInfo renderInfo) {
// TODO Auto-generated method stub
}
@Override
public String getResultantText() {
// TODO Auto-generated method stub
return null;
}
}
PdfHelper .java
java
package com.gwssi.common.util;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import java.io.IOException;
/**
* @ClassName PdfHelper
* @Description Pdf帮助类
* @Author AlphaJunS
* @Date 2020/3/7 17:40
* @Version 1.0
*/
public class PdfHelper {
/**
* @Author AlphaJunS
* @Date 18:24 2020/3/7
* @Description 用于供外部类调用获取关键字所在PDF文件坐标
* @param filepath
* @param keyWords
* @return float[]
*/
public static float[] getKeyWordsByPath(String filepath, String keyWords) {
float[] coordinate = null;
try{
PdfReader pdfReader = new PdfReader(filepath);
coordinate = getKeyWords(pdfReader, keyWords);
} catch (IOException e) {
e.printStackTrace();
}
return coordinate;
}
public static float[] getKeyWordsByByte(byte[] pdfin, String keyWords) {
float[] coordinate = null;
try{
PdfReader pdfReader = new PdfReader(pdfin);
coordinate = getKeyWords(pdfReader, keyWords);
} catch (IOException e) {
e.printStackTrace();
}
return coordinate;
}
/**
* @Author AlphaJunS
* @Date 18:26 2020/3/7
* @Description 获取关键字所在PDF坐标
* @param pdfReader
* @param keyWords
* @return float[]
*/
private static float[] getKeyWords(PdfReader pdfReader, String keyWords) {
float[] coordinate = null;
int page = 0;
try{
int pageNum = pdfReader.getNumberOfPages();
PdfReaderContentParser pdfReaderContentParser = new PdfReaderContentParser(pdfReader);
CustomRenderListener renderListener = new CustomRenderListener();
renderListener.setKeyWord(keyWords);
for (page = 1; page <= pageNum; page++) {
renderListener.setPage(page);
pdfReaderContentParser.processContent(page, renderListener);
coordinate = renderListener.getPcoordinate();
if (coordinate != null) break;
}
} catch (IOException e) {
e.printStackTrace();
}
return coordinate;
}
public static void main(String[] args) {
float[] keyWordsByPath = getKeyWordsByPath("D:\\SoftPackage\\InterDowloads\\优选(东莞)汽车服务有限公司-登记审核表.pdf", "东莞市");
for(float f : keyWordsByPath){
System.out.println(f);
}
// 238.35
// 141.6
// 1.0
}
}
三、总结
高效、便携、易上手!