企业级-PDF水印签字

作者: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

    }
}

三、总结

高效、便携、易上手!

相关推荐
join81 小时前
解决vue-pdf的签章不显示问题
javascript·vue.js·pdf
小行星1251 小时前
前端把dom页面转为pdf文件下载和弹窗预览
前端·javascript·vue.js·pdf
穆友航16 小时前
PDF内容提取,MinerU使用
数据分析·pdf
拾荒的小海螺2 天前
JAVA:探索 PDF 文字提取的技术指南
java·开发语言·pdf
村东头老张2 天前
Java 实现PDF添加水印
java·开发语言·pdf
好美啊啊啊啊!2 天前
Thymeleaf模板引擎生成的html字符串转换成pdf
pdf·html
zhentiya2 天前
曼昆《经济学原理》第八版课后答案及英文版PDF
大数据·pdf
三天不学习2 天前
如何解决pdf.js跨域从url动态加载pdf文档
javascript·pdf
吾店云建站2 天前
9个最佳WordPress PDF插件(查看器、嵌入和下载)
程序人生·pdf·创业创新·流量运营·程序员创富·教育电商
007php0072 天前
基于企业微信客户端设计一个文件下载与预览系统
开发语言·python·docker·golang·pdf·php·企业微信