最近接了个需求,要往生成好的PDF格式文书上盖上电子签章。盖章是第三方盖,但盖到第几页以及盖在什么位置需要我这边提供出来。本来位置信息是根据文书模板量的,但由于文字填充后导致页数增加,位置也波动不定,所以就想到通过检索落款单位在文档中的位置来确认盖章的位置。
初次接触PDF处理,这也是自己网上查资料再加上自己思考后做出来的,为了后面类似需求可以方便些,在此小记一下。
maven引入相应PDF处理需要用到的包:
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
第三方签章需要的位置信息类【SignSetting】:
import lombok.Data;
/**
* 签章配置项
*/
@Data
public class SignSetting {
/**
* 位置签章页码/骑缝签章页数
*/
private Integer page;
/**
* 签章X轴坐标
*/
private Integer posX;
/**
* 签章Y轴坐标
*/
private Integer posY;
}
PDF处理监听程序类【PdfRenderListener】 :
import com.itextpdf.awt.geom.Rectangle2D;
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.RenderListener;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import lombok.Data;
import model.TextChunkRenderInfo;
import java.util.ArrayList;
import java.util.List;
/**
* PDF处理监听程序,主要用于文本处理
* Author:
* Date:2022-08-25 00:56
*/
@Data
public class PdfRenderListener implements RenderListener {
/**
* 文本块信息列表
*/
private List<TextChunkRenderInfo> textChunkRenderInfoList = new ArrayList<>();
/**
* 文本块处理开始
*/
@Override
public void beginTextBlock() {
}
/**
* 文本小块处理
*
* @param textRenderInfo 文本信息处理,此处包含一个文本块,该文本块内包含一个字符或多个字符
*/
@Override
public void renderText(TextRenderInfo textRenderInfo) {
// 获取文本内容
String text = textRenderInfo.getText();
if (text != null) {
// 获取文本矩形信息
Rectangle2D.Float rectAscent = textRenderInfo.getAscentLine().getBoundingRectange();
// 计算文字的边框矩形
float minX = (float) rectAscent.getMinX(); // 文本行最左侧
float minY = (float) rectAscent.getMinY() - 1; // 文本行最下方
float maxX = (float) rectAscent.getMaxX(); // 文本最右侧
float maxY = (float) rectAscent.getMaxY() + 1; // 文本最上方
TextChunkRenderInfo textChunkRenderInfo = new TextChunkRenderInfo(text, minX, minY, maxX, maxY);
this.textChunkRenderInfoList.add(textChunkRenderInfo);
}
}
/**
* 文本块处理结束
*/
@Override
public void endTextBlock() {
}
/**
* 图片处理
*
* @param imageRenderInfo 图片预处理信息
*/
@Override
public void renderImage(ImageRenderInfo imageRenderInfo) {
}
}
从PDF文件读取到的文本块信息类【TextChunkRenderInfo】:
import lombok.Data;
/**
* 文本块信息
* Author:
* Date:2022-08-25 01:01
*/
@Data
public class TextChunkRenderInfo {
/**
* 文本内容
*/
private String text;
/**
* 文本左下角横坐标
*/
private float minX;
/**
* 文本左下角纵坐标
*/
private float minY;
/**
* 文本右上角横坐标
*/
private float maxX;
/**
* 文本右上角纵坐标
*/
private float maxY;
/**
* 有参构造函数
*
* @param text 文本内容
* @param minX 文本左下角横坐标
* @param minY 文本左下角纵坐标
* @param maxX 文本右上角横坐标
* @param maxY 文本右上角纵坐标
*/
public TextChunkRenderInfo(String text, float minX, float minY, float maxX, float maxY) {
this.text = text;
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
}
/**
* 获取文本的高度
*
* @return 文本高度
*/
public float getWordHeight() {
return this.maxY - this.minY;
}
/**
* 获取文本文字宽度
*
* @return 文本文字宽度
*/
public float getWordWidth() {
float fullWidth = this.maxX - this.minX;
return fullWidth / this.text.length();
}
/**
* 获取单词在文本内容中的位置及匹配的长度
*
* @param word 待匹配的单词
* @param textStart 从文本的第几个字符开始匹配
* @param wordStart 从单词第几个字符开始匹配
* @return 匹配结果 0:从第几位开始匹配成功,1:word中从第一个字符开始连续匹配成功长度
*/
public int[] getTextContainWordInfo(String word, int textStart, int wordStart) {
int firstMatchIndex = 0; // 第一次匹配成功索引
int matchLength = 0; // 匹配长度
char[] textChars = this.text.toCharArray();
char[] wordChars = word.toCharArray();
for (int i = textStart - 1; i < textChars.length; i++) {
int textIndex = -1;
for (int j = wordStart - 1; j < wordChars.length; j++) {
textIndex = i + j - wordStart + 1;
// 只要存在一个不匹配就匹配不上
if (textChars[textIndex] != wordChars[j]) {
textIndex = -1;
break;
}
// j之前都匹配成功了,如果匹配长度超出了text文本长度,则跳出循环,说明只匹配成功了部分
if (textIndex >= textChars.length - 1) {
break;
}
}
if (textIndex >= 0) { // 说明匹配成功了,不管text是否走完都跳出循环
firstMatchIndex = i;
matchLength = textIndex - i + 1;
break;
}
}
if (matchLength > 0) {
return new int[]{firstMatchIndex + 1, matchLength};
}
return null;
}
}
从PDF文件中匹配落款单位匹配结果信息类【TextChunkMatchResult】:
import lombok.Data;
/**
* PDF文本块关键词匹配结果
* Author:
* Date: 2022-08-25 13:56
*/
@Data
public class TextChunkMatchResult {
/**
* 第一个匹配成功的文本块信息
*/
private TextChunkRenderInfo firstMatchRenderInfo;
/**
* 第一个匹配成功的文本块信息开始字符索引
*/
private int firstMatchStartIndex;
/**
* 第一个匹配成功的文本块信息匹配成功字符长度
*/
private int firstMatchLength;
/**
* 最后一个匹配成功的文本块信息
*/
private TextChunkRenderInfo lastMatchRenderInfo;
}
主要处理程序,PDFUtil类实现代码:
import com.alibaba.fastjson.JSONObject;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import entities.es.SignSetting;
import model.TextChunkMatchResult;
import model.TextChunkRenderInfo;
import java.util.List;
/**
* PDF文件相关操作类
* Author:
* Date:2022-08-24 14:23
*/
public class PdfUtil {
/**
* 根据落款单位获取该落款单位在PDF文件中的位置信息,用来进行电子签章
* Author:
* Date: 2022-08-24 14:32
*
* @param fileName PDF文件路径
* @param unitName 落款单位名称
* @return 电子签章位置信息
*/
public static SignSetting getSignaturePositionBySignedUnit(String fileName, String unitName) {
SignSetting rtnSetting = null;
PdfReader pdfReader = null;
try {
// 初始化PDF文件
pdfReader = new PdfReader(fileName);
// 获取PDF文件总页数
int pdfPage = pdfReader.getNumberOfPages();
// PDF 内容
PdfReaderContentParser parser = new PdfReaderContentParser(pdfReader);
// 从最后一页开始查找落款单位名称,找到后直接跳出循环
// region 获取KeyWordBean
// 根据落款单位定位
for (int i = pdfPage; i >= 1; i--) {
SignSetting finalSignSetting = null;
// PDF当前页文本处理
PdfRenderListener listener = new PdfRenderListener();
parser.processContent(i, listener);
// PDF 获取关键词所在位置信息
finalSignSetting = getKeyWordPosition(listener.getTextChunkRenderInfoList(), unitName);
if (finalSignSetting != null) {
finalSignSetting.setPage(i);
rtnSetting = finalSignSetting;
break;
}
}
// 位置调整,章是从这里开始下方盖章,所以位置上移部分,可以盖在字的上面
if (rtnSetting != null) {
rtnSetting.setPosY(rtnSetting.getPosY() + 50);
}
// endregion
// 如果未找到,就把章盖到最后一页
if (rtnSetting == null) {
rtnSetting = new SignSetting();
rtnSetting.setPage(pdfPage);
}
} catch (Exception ex) {
StringOutPrintlnUtil.writeInfo(ex.getMessage());
} finally {
if (pdfReader != null) {
pdfReader.close();
}
}
return rtnSetting;
}
/**
* 获取关键词所在位置信息
*
* @param textChunkRenderInfoList 文本块信息
* @param keyWord 关键词
* @return 关键词位置信息
*/
private static SignSetting getKeyWordPosition(List<TextChunkRenderInfo> textChunkRenderInfoList, String keyWord) {
SignSetting signSetting = null; // 返回盖章配置信息
TextChunkMatchResult matchResult = null; // 最终匹配结果
keyWord = keyWord.replace(" ", ""); // 去除中间空格
// 第一个匹配到的文本信息
TextChunkRenderInfo firstCharMatch = null;
// 第一个匹配到的文本位置信息
int firstMatchPos = 0;
// 第一个匹配到的字符长度信息
int firstMatchLength = 0;
// 已经匹配成功的字符长度
int matchLength = 0;
for (TextChunkRenderInfo renderInfo : textChunkRenderInfoList) {
int textStart = 1;
while (textStart <= renderInfo.getText().length()) {
int[] matchRes = renderInfo.getTextContainWordInfo(keyWord, textStart, matchLength + 1);
if (matchRes == null) {
firstCharMatch = null;
if (matchLength == 0) {
break;
}
matchLength = 0;
continue;
}
if (matchLength == 0) {
firstCharMatch = renderInfo;
firstMatchPos = matchRes[0];
firstMatchLength = matchRes[1];
}
matchLength += matchRes[1];
if (matchLength >= keyWord.length()) { // 已经完全匹配成功
// 初始化匹配结果
matchResult = new TextChunkMatchResult();
matchResult.setFirstMatchRenderInfo(firstCharMatch);
matchResult.setFirstMatchLength(firstMatchLength);
matchResult.setFirstMatchStartIndex(firstMatchPos - 1);
matchResult.setLastMatchRenderInfo(renderInfo);
firstCharMatch = null;
firstMatchPos = 0;
firstMatchLength = 0;
matchLength = 0;
}
textStart = matchRes[0] + matchRes[1];
}
}
if (matchResult != null) { // 成功匹配到了最后一个
signSetting = new SignSetting();
float posX = matchResult.getFirstMatchRenderInfo().getMinX();
// 将左侧未匹配到的距离加上
posX = posX + matchResult.getFirstMatchRenderInfo().getWordWidth() * matchResult.getFirstMatchStartIndex();
signSetting.setPosX((int) posX);
signSetting.setPosY((int) matchResult.getFirstMatchRenderInfo().getMinY());
}
return signSetting;
}
}
测试如下:
public static void main(String[] args) {
SignSetting signaturePositionBySignedUnit = PdfUtil.getSignaturePositionBySignedUnit("F:\\PDF操作测试.pdf", "单位落款");
System.out.println("查找结果:" + JSONObject.toJSONString(signaturePositionBySignedUnit));
}
测试结果:查找结果:{"page":2,"posX":405,"posY":499}