最近接了个需求,要往生成好的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}