pdf操作之根据关键词签章

pdf操作之根据关键词签章

之前写过一篇pdf签章的文章 java操作pdf制作电子签章, 发现有博友反馈需求根据关键词进行签章,之前的想法时由前端配合,指定pdf位置,java代码执行签名操作,正好最近也有个类似的需求,索性就填下之前的坑吧

需求描述

根据关键词在pdf进行签章,如 pdf尾页盖上xxx机构的电子章
直接上干货

代码实现

package com.taoyuanx.littlepdf.sign;

import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfSignatureAppearance.RenderingMode;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.PdfStream;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import com.itextpdf.text.pdf.parser.TextMarginFinder;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import com.itextpdf.text.pdf.security.*;
import com.itextpdf.text.pdf.security.MakeSignature.CryptoStandard;
import com.taoyuanx.littlepdf.utils.LittlePdfUtil;
import com.taoyuanx.littlepdf.utils.RSAUtil;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.compress.utils.Lists;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;

/**
 * pdf 签章实现类
 */
public class Itext5PdfSign {
    private static final Logger LOG = LoggerFactory.getLogger(Itext5PdfSign.class);

    private SignConfig signConfig;

    public void sign(InputStream waitSignPdf, OutputStream signedPdf) {
        try {
            PdfReader reader = new PdfReader(waitSignPdf);
            /**
             * false的话,pdf文件只允许被签名一次,多次签名,最后一次有效
             * true的话,pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
             */
            PdfStamper stamper = PdfStamper.createSignature(reader, signedPdf, '\0', null, true);
            /**
             * 设定签章为高清章,默认模糊
             */
            stamper.getWriter().setCompressionLevel(PdfStream.BEST_COMPRESSION);
            /**
             * 设定签章属性
             */
            PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
            appearance.setReason(signConfig.getReason());
            appearance.setLocation(signConfig.getLocation());
            appearance.setSignatureCreator(signConfig.getSignername());
            PdfLocationResult locationResult = calcSignLocation(reader, KeyWordFinder.KeyWordMatchType.MATCH_LAST);
            appearance.setVisibleSignature(locationResult.getRectangle(), locationResult.getPageNum(), signConfig.getSignFiledName());
            /**
             * 设定签章图片,签章类别
             */
            appearance.setSignatureGraphic(signConfig.getChapterImg());
            //NOT_CERTIFIED 不会导致pdf上其他签章无效
            appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
            /**
             * 设置图章的显示方式,如下选择的是只显示图章(还有其他的模式,可以图章和签名描述一同显示)
             */
            appearance.setRenderingMode(RenderingMode.GRAPHIC);
            /**
             * 指定摘要算法
             */
            ExternalDigest digest = signConfig.getDigest();
            /**
             * 指定签名对象
             */
            ExternalSignature signature = signConfig.getSignature();
            /**
             * 构造时间时间戳服务器
             */
            TSAClient tsaClient = getTsaClient();
            /**
             * itext 签章
             */
            MakeSignature.signDetached(appearance, digest, signature, signConfig.getChain(), null, null, tsaClient, 0, CryptoStandard.CMS);
        } catch (Exception e) {
            throw new RuntimeException("签章异常", e);
        }
    }


    public Itext5PdfSign(SignConfig signConfig) {
        checkInit(signConfig);
        try {
            BouncyCastleProvider bc = new BouncyCastleProvider();
            Security.addProvider(bc);
            if (StringUtils.isNotEmpty(signConfig.getSignP12Path())) {
                String p12Path = signConfig.getSignP12Path();
                String password = signConfig.getSignP12Password();
                KeyStore ks = RSAUtil.getKeyStore(p12Path, password);
                String alias = ks.aliases().nextElement();
                PrivateKey privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
                signConfig.setChain(ks.getCertificateChain(alias));
                signConfig.setPrivateKey(privateKey);
            }
            signConfig.setDigest(new BouncyCastleDigest());
            signConfig.setSignature(new PrivateKeySignature(signConfig.getPrivateKey(), DigestAlgorithms.SHA256, BouncyCastleProvider.PROVIDER_NAME));
            if (Objects.isNull(signConfig.getChapterImg())) {
                signConfig.setChapterImg(Image.getInstance(signConfig.getChapterImgPath()));
            }
            Image chapterImg = signConfig.getChapterImg();
            signConfig.setStampWidth(chapterImg.getWidth());
            signConfig.setStampHeight(chapterImg.getHeight());
            this.signConfig = signConfig;
        } catch (Exception e) {
            throw new RuntimeException("init error", e);
        }
    }

    private void checkInit(SignConfig signConfig) {
        if (StringUtils.isEmpty(signConfig.getSignP12Path())) {
            throw new RuntimeException("缺少签章用的公私钥对信息");
        }
        if (StringUtils.isNotEmpty(signConfig.getSignP12Path()) && StringUtils.isEmpty(signConfig.getSignP12Password())) {
            throw new RuntimeException("签章证书密码配置缺少");
        }
        if (Objects.isNull(signConfig.getChapterImg()) && StringUtils.isEmpty(signConfig.getChapterImgPath())) {
            throw new RuntimeException("缺少签章图片地址信息");
        }
        if (StringUtils.isEmpty(signConfig.getSignername())) {
            throw new RuntimeException("缺少签章者名称信息");
        }
        if (StringUtils.isEmpty(signConfig.getReason())) {
            throw new RuntimeException("缺少签章原因信息");
        }
        if (StringUtils.isEmpty(signConfig.getLocation())) {
            throw new RuntimeException("缺少签章位置信息");
        }
        if (StringUtils.isEmpty(signConfig.getSignFiledName())) {
            throw new RuntimeException("缺少签章域名称信息");
        }
        if (Objects.nonNull(signConfig.getTsaClientFactory())) {
            signConfig.setNoTsa(false);
        }
        if (Objects.nonNull(signConfig.getTsaUrl())) {
            signConfig.setNoTsa(false);
            signConfig.tsaClientFactory = new TsaClientBCFactory(signConfig.getTsaUrl());
        }
    }

    private TSAClient getTsaClient() {
        if (signConfig.isNoTsa()) {
            return null;
        }
        return signConfig.tsaClientFactory.newTSAClient();

    }

    @Data
    public static class SignConfig {
        /**
         * 签章hash 及签名实现
         */
        private ExternalDigest digest;
        private ExternalSignature signature;
        /**
         * 证书链及私钥
         */
        private Certificate[] chain;
        private PrivateKey privateKey;
        /**
         * 签章原因
         */
        private String reason;
        /**
         * 签章位置
         */
        private String location;
        /**
         * 签章者名称
         */
        private String signername;
        /**
         * 签章图片地址
         */
        private Image chapterImg;
        private String chapterImgPath;
        /**
         * 签章域名称可根据域查找章的位置
         */
        private String signFiledName;

        /**
         * 签章公私钥文件
         */
        private String signP12Path;
        private String signP12Password;
        /**
         * 签章关键词及关键词所在的页码
         */
        private String signKeyWord;
        private Integer signKeyWordPageNum;
        /**
         * 签章图片的宽高
         */
        private float stampHeight;
        private float stampWidth;

        /**
         * 签章时间服务
         */
        private boolean noTsa = true;
        private TsaClientFactory tsaClientFactory;
        private String tsaUrl;

    }


    private PdfLocationResult calcSignLocation(PdfReader reader, KeyWordFinder.KeyWordMatchType matchType) {
        /**
         * 1. 如果关键字存在,则签章在关键字上
         * 2. 如果关键字不存在 则签章在尾页的右下角
         */
        KeyWordLocation keyWordLocation = keyWordLocation(reader, signConfig.getSignKeyWord(), signConfig.getSignKeyWordPageNum(), matchType);
        if (Objects.nonNull(keyWordLocation)) {
            /**
             * 计算规则:
             * 1. 由于获取的关键词文件块宽高过于夸张,所以先计算关键词文本块矩形右下角坐标( keyWordLocation.getUrx(),keyWordLocation.getUry()-文本块高度)
             * 2. 然后将 文本块右下角坐标作为签章矩形的中心点,然后计算签章域的左下角右上角坐标
             * 3.签章域超出pdf页宽高,并做溢出处理
             */
            float keyWordTextBlockHeight = keyWordLocation.getKeyWordTextBlockHeight();
            /**
             * 关键字右下角坐标
             */
            float keyWordLrx = keyWordLocation.getUrx();
            float keyWordLry = keyWordLocation.getUry() - keyWordTextBlockHeight;

            //签章域左下角坐标
            float llx = keyWordLrx - signConfig.getStampWidth() / 2;
            float lly = keyWordLry - signConfig.getStampHeight() / 2;
            /**
             * 横纵坐标向溢出处理
             */
            Rectangle pageSize = reader.getPageSize(keyWordLocation.getPageNum());
            if (llx + signConfig.getStampWidth() > pageSize.getWidth()) {
                llx = pageSize.getWidth() - signConfig.getStampWidth();
            }
            if (lly < 0) {
                llx = 0;
            }
            //签章域右上角坐标
            float urx = llx + signConfig.getStampWidth();
            float ury = lly + signConfig.getStampHeight();


            Rectangle rectangle = new Rectangle(llx, lly, urx, ury);

            LOG.debug("查找到关键词[{}]位置,坐标为{}", keyWordLocation.getText(), keyWordLocation);
            return new PdfLocationResult(keyWordLocation.getPageNum(), rectangle);
        } else {
            int numberOfPages = reader.getNumberOfPages();
            Rectangle lastPageSize = reader.getPageSize(numberOfPages);
            float pageWidth = lastPageSize.getWidth();
            //左下角坐标 lly 表示距离底部的距离
            float llx = pageWidth - signConfig.getStampWidth();
            float lly = 50;
            //右上角坐标
            float urx = llx + signConfig.getStampWidth();
            float ury = lly + signConfig.getStampHeight();
            Rectangle rectangle = new Rectangle(llx, lly, urx, ury);
            LOG.debug("未查找到关键词位置,签章位置默认在尾页", signConfig.getSignKeyWord());
            return new PdfLocationResult(numberOfPages, rectangle);
        }

    }

    /**
     * 根据关键词 计算签章位置
     *
     * @param pdfReader      解析reader
     * @param keyWord        关键词
     * @param keyWordPageNum 关键词所在页码
     * @return
     */
    private KeyWordLocation keyWordLocation(PdfReader pdfReader, String keyWord, Integer keyWordPageNum, KeyWordFinder.KeyWordMatchType matchType) {
        List<KeyWordLocation> keyWordLocationList = Lists.newArrayList();
        try {
            int pageSize = pdfReader.getNumberOfPages();
            PdfReaderContentParser pdfReaderContentParser = new PdfReaderContentParser(pdfReader);
            IntStream pageStream = null;
            if (Objects.nonNull(keyWordPageNum)) {
                pageStream = IntStream.of(keyWordPageNum);
            } else {
                pageStream = IntStream.range(1, pageSize + 1);
            }
            pageStream.forEach(pageNum -> {
                try {
                    pdfReaderContentParser.processContent(pageNum, new KeyWordFinder(keyWord, pageNum, keyWordLocationList));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        KeyWordLocation keyWordLocation = null;
        if (keyWordLocationList.isEmpty()) {
            return null;
        }
        if (keyWordLocationList.size() == 1) {
            keyWordLocation = keyWordLocationList.get(0);
        } else {
            switch (matchType) {
                case MATCH_LAST:
                    keyWordLocation = LittlePdfUtil.getLast(keyWordLocationList);
                    break;
                case MATCH_FIRST:
                    keyWordLocation = keyWordLocationList.get(0);
                    break;
                default:
                    keyWordLocation = LittlePdfUtil.getLast(keyWordLocationList);
                    break;

            }
        }
        if (KeyWordFinder.KEYWORD_MATCH_TYPE_FULL.equals(keyWordLocation.getKeywordMatchType())) {
            return keyWordLocation;
        } else if (KeyWordFinder.KEYWORD_MATCH_TYPE_MIX.equals(keyWordLocation.getKeywordMatchType()) &&
                Objects.nonNull(keyWordLocation.getMixMatchCharList())) {

            /**
             * 如果关键词被分拆,则返回中间字符的坐标
             */
            List<KeyWordCharLocation> mixMatchCharList = keyWordLocation.getMixMatchCharList();
            KeyWordLocation midChar = KeyWordFinder.getMidChar(keyWord, mixMatchCharList);
            keyWordLocation.setLlx(midChar.getLlx());
            keyWordLocation.setLly(midChar.getLly());
            keyWordLocation.setUrx(midChar.getUrx());
            keyWordLocation.setUry(midChar.getUry());
            keyWordLocation.setKeyWordTextBlockHeight(midChar.getKeyWordTextBlockHeight());
            keyWordLocation.setKeyWordTextBlockWidth(midChar.getKeyWordTextBlockWidth());
            return keyWordLocation;
        }
        return keyWordLocation;
    }


    public static class KeyWordFinder extends TextMarginFinder {
        @Getter
        @Setter
        private String keyWord;
        @Getter
        @Setter
        private Integer pageNum;
        @Getter
        @Setter
        private List<KeyWordLocation> keyWordLocationList;
        /**
         * 匹配类型
         * <p>
         * KEYWORD_MATCH_TYPE_FULL 关键词完全匹配
         * KEYWORD_MATCH_TYPE_MIX 关键词混乱匹配
         */
        public static final Integer KEYWORD_MATCH_TYPE_FULL = 1,
                KEYWORD_MATCH_TYPE_MIX = 2;
        /**
         * 当前模式 是否为混乱模式
         */
        private boolean isMixMatch;
        /**
         * 混乱匹配模式,已匹配的字符数(左匹配)
         */
        private Integer keyWordMixMatchCharMatchCount = 0;

        public KeyWordFinder(String keyWord, Integer pageNum, List<KeyWordLocation> keyWordLocationList) {
            this.keyWord = keyWord;
            this.pageNum = pageNum;
            this.keyWordLocationList = keyWordLocationList;
        }

        @Override
        public void renderText(TextRenderInfo renderInfo) {
            super.renderText(renderInfo);
            String text = renderInfo.getText();
            LOG.debug("pdf文本:[{}] 文本宽度{},文本高度", text, this.getWidth(), this.getHeight());
            //查找到关键词,并设置关键词位置
            if (LittlePdfUtil.isNotEmpty(text)) {
                if (text.contains(keyWord)) {
                    keyWordLocationList.add(toKeyWordLocation(text, KEYWORD_MATCH_TYPE_FULL));
                    return;
                } else {
                    handleMatchMix(text);
                    return;
                }
            }


        }


        private void handleMatchMix(String text) {
            if (!isMixMatch) {
                int matchCount = leftMatch(keyWord, text, 0);
                if (matchCount > 0) {
                    isMixMatch = true;
                    keyWordMixMatchCharMatchCount = matchCount;
                    KeyWordLocation keyWordMixMatchLocation = toKeyWordLocation(text, KEYWORD_MATCH_TYPE_MIX);

                    KeyWordCharLocation keyWordCharLocation = toKeyWordCharLocation(text);
                    keyWordCharLocation.setMixMatchCount(matchCount);
                    keyWordCharLocation.setMixMatchStart(0);

                    List<KeyWordCharLocation> mixMatchCharList = new ArrayList<>();
                    mixMatchCharList.add(keyWordCharLocation);

                    keyWordMixMatchLocation.setMixMatchCharList(mixMatchCharList);
                    keyWordLocationList.add(keyWordMixMatchLocation);
                    return;
                }
            }
            if (isMixMatch) {
                int matchCount = leftMatch(keyWord, text, keyWordMixMatchCharMatchCount);
                if (keyWordMixMatchCharMatchCount.equals(keyWord.length())) {
                    //匹配结束
                    isMixMatch = false;
                    keyWordMixMatchCharMatchCount = 0;
                } else if (matchCount > 0) {
                    //继续匹配中
                    List<KeyWordCharLocation> mixMatchCharList = LittlePdfUtil.getLast(keyWordLocationList).getMixMatchCharList();
                    KeyWordCharLocation keyWordCharLocation = toKeyWordCharLocation(text);
                    keyWordCharLocation.setMixMatchCount(matchCount);
                    keyWordCharLocation.setMixMatchStart(keyWordMixMatchCharMatchCount);
                    mixMatchCharList.add(keyWordCharLocation);
                    keyWordMixMatchCharMatchCount += matchCount;
                } else {
                    isMixMatch = false;
                    keyWordMixMatchCharMatchCount = 0;
                    //删除不完全匹配
                    LittlePdfUtil.removeLastList(keyWordLocationList, 1);
                }

            }
        }

        private static int leftMatch(String keyWord, String text, int pos) {
            int matchCount = 0, keyWordLen = keyWord.length();
            for (int i = 0, len = text.length(); i < len; i++) {
                int matchStart = pos + matchCount;
                if (matchStart >= keyWordLen) {
                    break;
                }
                if (Objects.equals(keyWord.charAt(matchStart), text.charAt(i))) {
                    matchCount++;
                }
            }
            if (matchCount > 0) {
                return matchCount;
            }
            return -1;
        }

        public static enum KeyWordMatchType {
            MATCH_LAST,
            MATCH_FIRST;
        }

        private KeyWordLocation toKeyWordLocation(String text, Integer keywordMatchType) {
            KeyWordLocation keyWordLocation = new KeyWordLocation();
            keyWordLocation.setKeyWordTextBlockHeight(this.getHeight());
            keyWordLocation.setKeyWordTextBlockWidth(this.getWidth());
            keyWordLocation.setLlx(this.getLlx());
            keyWordLocation.setLly(this.getLly());
            keyWordLocation.setUrx(this.getUrx());
            keyWordLocation.setUry(this.getUry());
            keyWordLocation.setText(text);
            keyWordLocation.setPageNum(pageNum);
            keyWordLocation.setKeywordMatchType(keywordMatchType);
            return keyWordLocation;
        }

        private KeyWordCharLocation toKeyWordCharLocation(String text) {
            KeyWordCharLocation keyWordCharLocation = new KeyWordCharLocation();
            keyWordCharLocation.setKeyWordTextBlockHeight(this.getHeight());
            keyWordCharLocation.setKeyWordTextBlockWidth(this.getWidth());
            keyWordCharLocation.setLlx(this.getLlx());
            keyWordCharLocation.setLly(this.getLly());
            keyWordCharLocation.setUrx(this.getUrx());
            keyWordCharLocation.setUry(this.getUry());
            keyWordCharLocation.setText(text);
            keyWordCharLocation.setPageNum(pageNum);
            keyWordCharLocation.setKeywordMatchType(KEYWORD_MATCH_TYPE_MIX);
            return keyWordCharLocation;
        }

        public static KeyWordCharLocation getMidChar(String keyWord, List<KeyWordCharLocation> mixMatchCharList) {
            int midIndex = keyWord.length() / 2, count = 0;
            for (int i = 0, len = mixMatchCharList.size(); i < len; i++) {
                KeyWordCharLocation keyWordCharLocation = mixMatchCharList.get(i);
                count += keyWordCharLocation.getMixMatchCount();
                if (count >= midIndex) {
                    return keyWordCharLocation;
                }
            }
            return mixMatchCharList.get(0);
        }
    }

    /**
     * 签章关键词文本位置
     */
    @Data
    public static class KeyWordLocation {
        /**
         * 文本
         */
        private String text;
        /**
         * 关键词所在pdf页码
         */
        private Integer pageNum;
        /**
         * 文本块所在矩形左下角坐标
         */
        private float llx;
        private float lly;
        /**
         * 文本块所在矩形右上角角坐标
         */
        private float urx;
        private float ury;
        /**
         * 文本块宽高
         */
        private float keyWordTextBlockWidth;
        private float keyWordTextBlockHeight;


        /**
         * 关键词匹配类型 参见keyFinder
         */
        private Integer keywordMatchType;

        /**
         * 关键词被解析成多个TextRenderInfo
         */
        private List<KeyWordCharLocation> mixMatchCharList;

    }

    /**
     * 混乱匹配模式下的 打散的关键词字符位置
     */
    @Data
    public static class KeyWordCharLocation extends KeyWordLocation {
        /**
         * 混乱匹配时的起始位置
         */
        private Integer mixMatchStart;

        /**
         * 匹配的字符数
         */
        private Integer mixMatchCount;


    }


    @Data
    public static class PdfLocationResult {
        /**
         * 签章所在pdf页码
         */
        private int pageNum;
        /**
         * 签章矩形
         */
        private Rectangle rectangle;

        public PdfLocationResult(int pageNum, Rectangle rectangle) {
            this.pageNum = pageNum;
            this.rectangle = rectangle;
        }
    }
}

完整代码: github

注意点

pdf文本查找时,一个关键词可能被拆分成多个区块,所以要进行特殊处理

itext pdf 坐标系

page 页的左下角为 坐标原点,定位一个矩形坐标时,根据左下角和右上角两个点坐标即可确定一个矩形的位置

在这里插入图片描述

写在文末:最近看大家对pdf签章反馈较多,csdn很久没有登录了,可能联系不上,大家如有需要可以联系qq:1349226925 ,备注必要信息即可

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值