JAVA工具类---PDF电子签章工具类

背景

PDF电子签章可以应用于数字化应用系统中,比如OA系统、ERP系统集成,单独抽出后端工具类可以直接在工作流引擎结束后调用,避免冗余处理,前端可以实现文件预览+签章模板坐标选取,后端实现坐标+实际章加盖,根据数字章的预设大小可以等比例设置对应签章,刚好涉及到此快业务,抽出工具类分享给大家

整体流程

复制代码
*      加载PDF文档
*      计算印章位置和尺寸
*      创建图章注释对象
*      创建外观流并绘制印章图像
*      配置外观字典
*      将注释添加到页面
*      保存文档

依赖引入

java 复制代码
        <!--alibaba json-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.40</version>
        </dependency>

        <!--pdfbox-->
        <dependency>
            <groupId>org.apache.pdfbox</groupId>
            <artifactId>pdfbox</artifactId>
            <version>2.0.29</version>
        </dependency>

示例章

程序展示

以下展示的是测试功能,需要自己嵌入业务中处理

java 复制代码
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationRubberStamp;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.springframework.util.StringUtils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;


/**
 * @author 忧郁的Mr.Li
 */
public class PdfSignUtil {
    private static String signImageFileKey = "core_sign_image_key";//图片key


    /**
     * @author 忧郁的Mr.Li
     * 测试类
     */
    public static void main(String[] args) throws Exception {
        //签章文件
        String inPdfFile = "C:\\xxxx\\xxxx\\xxxx\\测试文件\\测试PDF文件.pdf";

        //输出路径
        String outpdffile = "C:\\xxxx\\xxxx\\xxxx\\测试文件\\测试PDF文件_out.pdf";

        //实际签章文件
        Map<String, File> imageFiles = new HashMap<>();
        File file = new File("C:\\xxxx\\xxxx\\xxxx\\同济xxxx\\hints\\company.png");
        imageFiles.put("company",file);

        //签章坐标信息(包含章id、章类型、第几页、详细坐标、)
        JSONArray jsonArray = new JSONArray();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("rightTopB",107.32477529583333);//页面左下角到印章右上角的Y轴坐标
        jsonObject.put("leftBottomL",36.229693225);//页面左下角到印章左下角的X轴坐标
        jsonObject.put("rightTopL",76.18177655833334);//页面左下角到印章右上角的X轴坐标
        jsonObject.put("leftBottomB",67.3726919625);//页面左下角到印章左下角的Y轴坐标
        jsonObject.put("page",1);//第页
        jsonObject.put("mainid",202);   //用来定义章的大小关联配置表中的章的大小缩放
        jsonObject.put("type","company");   //用来获取实际签章文件,如果同一类型的章需要不同实际章文件,这里可以设置实际文件唯一标识,和imageFiles的Key对应即可

        JSONObject jsonObject1 = new JSONObject();
        jsonObject1.put("rightTopB",107.32477529583333);
        jsonObject1.put("leftBottomL",150.229693225);
        jsonObject1.put("rightTopL",76.18177655833334);
        jsonObject1.put("leftBottomB",130.3726919625);
        jsonObject1.put("page",1);
        jsonObject1.put("mainid",202);
        jsonObject1.put("type","company");
        jsonArray.add(jsonObject);
        jsonArray.add(jsonObject1);


        //文档密码设置的话就可以加密签章完成的文件
        //-------

        //调用签章程序
        File file1 = addPdfSignBySignatures(inPdfFile, outpdffile, imageFiles, jsonArray, "");
        System.out.println("签章完成:生成文件"+file1.getPath());


    }



    /**
     * @author 忧郁的Mr.Li
     * @param :获取(创建)临时文件夹,后续换成自己的
     */
    public static String getTempPathByDayMillis() {
        String tempDir = System.getProperty("java.io.tmpdir");
        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyyMMdd");
        String dateStr = sdf.format(new java.util.Date());
        String path = tempDir + File.separator + "pdfsign" + File.separator + dateStr;
        File dir = new File(path);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return path;
    }





    /**
     * @author 忧郁的Mr.Li
     * pdf加图片章(根据平台签字工具的配置信息 给pdf文件加章)
     *
     * @param InPdfFile 签章文件
     * @param outpdffile 输出路径
     * @param imageFiles 实际签章文件
     * @param signatures 签章坐标信息(包含章id、章类型、第几页、详细坐标、)
     * @param password 文档密码
     * @throws Exception
     */
    public static File addPdfSignBySignatures(String InPdfFile, String outpdffile, Map<String, File> imageFiles, JSONArray signatures, String password) throws Exception {

        /**
         * 这一行代码确实会将PDF文件加载到内存中
         * 当调用`PDDocument.load(new File(InPdfFile))`时,PDFBox会执行以下操作:
         *
         * 1. **读取文件内容**: 从磁盘读取整个PDF文件的二进制数据
         * 2. **解析PDF结构**: 解析PDF的文档结构、页面对象、字体、图像等资源
         * 3. **构建内存对象树**: 在JVM堆内存中创建完整的文档对象模型,包括所有页面、内容流、注释等
         * 4. **返回文档对象**: 返回一个PDDocument实例,包含所有已解析的内容
         * **内存占用特点:**
         * - 对于小文件(几MB),内存占用相对可控
         * - 对于大文件(几十MB或上百MB),可能会占用大量堆内存
         * - PDF中的嵌入字体、高分辨率图片等资源会显著增加内存消耗
         * - 通常内存占用会是文件大小的数倍
         * **优化建议:**
         * 如果您的应用场景需要处理大型PDF文件,可以考虑:
         * 1. 使用带内存限制参数的load方法,设置最大内存使用量
         * 2. 启用临时文件缓存,让PDFBox将部分数据写入磁盘而非全部保留在内存
         * 3. 处理完成后立即关闭文档对象,释放内存资源
         * 4. 调整JVM堆大小参数,确保有足够的内存空间
         * 您当前的代码使用了try-with-resources语法,这能确保文档使用完毕后自动关闭并释放内存,这是正确的做法。
         */
        try (PDDocument document = PDDocument.load(new File(InPdfFile))) {
            /**
             * 这行代码的作用是移除PDF文档的所有安全限制。
             * 具体功能说明:
             * 解除权限限制: 如果PDF文件设置了权限密码(用户密码或所有者密码),这行代码会告诉PDFBox在保存时移除这些安全限制
             * 必须在保存前设置: 这个设置只在调用document.save()方法保存文档时才生效,它不会立即改变当前加载的文档状态
             * 需要正确密码: 要成功移除安全限制,加载文档时必须提供正确的打开密码或所有者密码。如果文档有密码保护但加载时未提供密码,这行代码可能无法正常工作
             * 典型应用场景:
             * 对受保护的PDF进行编辑后,保存为无密码版本
             * 移除打印、复制、修改等权限限制
             * 在处理加密PDF前解除其安全设置
             * 注意事项:
             * 这行代码不会移除打开密码本身,只是移除各种操作权限的限制
             * 如果PDF有打开密码,仍需在加载时提供密码
             * 只有在保存新文档时,安全设置的变更才会体现出来
             * 对于没有安全设置的PDF,调用此方法不会产生任何影响
             */
            document.setAllSecurityToBeRemoved(true);
            // 获取平台签章配置
            Map<Object, PdfSealConfig> pfsealconfig = getPfSealConfigs();
            //判断签章信息是否存在
            if (signatures != null && signatures.size() >= 0) {
                //开始循环签章信息
                for (int i = 0; i < signatures.size(); i++) {
                    //从签章信息中取出一个签章坐标信息
                    JSONObject itemconfig = signatures.getJSONObject(i);
                    //获取签章信息中的签章主键
                    int mainid = itemconfig.getInteger("mainid");
                    //获取签章信息中的类型
                    String uniquebykey = itemconfig.getString("type");
                    //根据主键获取印章的详细配置信息
                    PdfSealConfig configMap = pfsealconfig.get(mainid);
                    //获取当前印章的类型
                    String typecode = configMap.getType();
                    //印章实际图片
                    File imageFile = null;
                    if (!StringUtils.isEmpty(uniquebykey) && !uniquebykey.equals(typecode)) {
                        imageFile = imageFiles.get(uniquebykey);
                    } else {
                        imageFile = imageFiles.get(typecode);
                    }
                    if (imageFile == null) {
                        imageFile = imageFiles.get(signImageFileKey);
                    }
                    if (imageFile == null || !imageFile.exists()) {
                        continue;
                    }


                    // 获取页面
                    Integer page = itemconfig.getInteger("page");
                    if (page < 1 || page > document.getNumberOfPages()) {
                        continue;
                    }
                    PDPage targetPage = document.getPage(page - 1);
                    PDRectangle box = targetPage.getMediaBox();
                    //偏移坐标值
                    float lowerLeftX = box.getLowerLeftX();
                    float lowerLeftY = box.getLowerLeftY();
                    float width = box.getWidth();
                    float height = box.getHeight();
                    // 获取签章配置尺寸
                    double imagewidth = configMap.getWidth();
                    double imagheight = configMap.getWidth();
                    // 计算位置以图片左下角的位置为主
                    double leftBottomL = itemconfig.getDouble("leftBottomL");
                    double leftBottomB = itemconfig.getDouble("leftBottomB");
                    // 转换为点单位
                    float apartRight = (float) ((leftBottomL * 72) / 25.4f);
                    float apartFloor = (float) ((leftBottomB * 72) / 25.4f);
                    float x, y;
                    //获取PDF页面的旋转角度
                    int rotation = targetPage.getRotation();
                    switch (rotation) {
                        case 90:
                            x = lowerLeftX - apartFloor + width - (float) imagheight * 72 / 25.4f;
                            y = lowerLeftY + apartRight;
                            break;
                        case 180:
                            x = lowerLeftX + width - apartRight - (float) imagewidth * 72 / 25.4f;
                            y = lowerLeftY + height - apartFloor - (float) imagheight * 72 / 25.4f;
                            break;
                        case 270:
                            x = lowerLeftX + apartFloor;
                            y = lowerLeftY - apartRight - (float) imagewidth * 72 / 25.4f + height;
                            break;
                        default:
                            x = lowerLeftX + apartRight;
                            y = lowerLeftY + apartFloor;
                            break;
                    }
                    // 转换尺寸为像素
                    float imagewidthpx = (float) convertMmToPixels(imagewidth);
                    float imagheightpx = (float) convertMmToPixels(imagheight);
                    // 添加注释签章
                    addStampAnnotation(document, targetPage, imageFile, x, y, imagewidthpx, imagheightpx, rotation);
                }
            }
            // 设置密码保护
            if (!StringUtils.isEmpty(password)) {
                AccessPermission ap = new AccessPermission();
                ap.setCanPrint(true);
                ap.setCanExtractForAccessibility(false);
                ap.setCanExtractContent(false);      // 允许复制内容
                ap.setCanPrintDegraded(true);       // 允许降级打印
                ap.setCanModify(false);           // 禁止修改文档内容
                ap.setCanFillInForm(false);       // 禁止填写表单
                ap.setCanModifyAnnotations(false); // 禁止修改注释
                ap.setCanAssembleDocument(false);  // 禁止文档组装
                StandardProtectionPolicy spp = new StandardProtectionPolicy(password, "", ap);
                spp.setEncryptionKeyLength(128);
                document.protect(spp);
            }
            // 保存文档
            document.save(outpdffile);
        }
        return new File(outpdffile);
    }




    /**
     * @author 忧郁的Mr.Li
     * @param document: PDF在内存中的对象
     * @param targetPage: 待操作的单独页面
     * @param imageFile: 实际印章图片
     * @param x: 页面左下角到印章右下角的X轴坐标
     * @param y: 页面左下角到印章右下角的Y轴坐标
     * @param width: 印章的宽度
     * @param height: 印章的高度
     * @param rotation: 旋转角度
     */
    private static void addStampAnnotation(PDDocument document, PDPage targetPage, File imageFile,
                                           float x, float y, float width, float height, int rotation) throws Exception {
        /**
         * 创建一个PDF的图章注释
         * 橡胶图章注释的特点
         *      视觉呈现: 可以在PDF页面上显示图像内容,非常适合用于电子签章场景
         *      非破坏性: 作为注释层添加,不会修改PDF的原始内容流,便于后续移除或修
         *      可交互: 用户可以点击查看注释属性,某些阅读器中还可以移动或删除
         *      标准支持: 符合PDF规范,大多数PDF阅读器都能正确显示
         */
        PDAnnotationRubberStamp stampAnnotation = new PDAnnotationRubberStamp();
        /**
         * 在页面旋转90度或270度时,交换印章的宽度和高度值。
         *      原始状态(0度): 页面是横向或纵向的正常显示,宽度和高度按常规理解
         *      旋转90度或270度后: 页面相当于从横变竖或从竖变横,原来的宽度变成了视觉上的高度,原来的高度变成了视觉上的宽度
         *      比如说:印章尺寸是40mm宽、30mm高:
         *          在0度页面上: 印章占据40mm的水平空间和30mm的垂直空间
         *          在90度旋转页面上: 如果不交换宽高,印章会按40mm垂直、30mm水平放置,导致印章变形或位置错误
         *          交换后: 印章会正确地在视觉上占据40mm水平、30mm垂直的空间
         *       为什么只在90度和270度时交换:
         *          0度: 页面正常,无需调整
         *          180度: 页面虽然倒置,但宽度和高度的方向关系没有改变,只是起点位置变了(从左下角变成右上角),所以不需要交换宽高
         *          90度和270度: 页面的横纵方向完全互换,必须交换宽高才能保持印章的正确比例和视觉大小
         */
        if (rotation == 90 || rotation == 270) {
            float temp = width;
            width = height;
            height = temp;
        }
        /**
         * 创建一个矩形区域对象,用于定义印章在PDF页面上的精确位置和尺寸。
         * PDRectangle构造函数接收四个参数:
         *      x: 矩形左下角的X坐标(距离页面左边缘的距离)
         *      y: 矩形左下角的Y坐标(距离页面底边缘的距离)
         *      width: 矩形的宽度
         *      height: 矩形的高度
         * 这个矩形对象定义了印章注释的边界框(Bounding Box),决定了:
         *      位置: 印章出现在页面的哪个位置(x和y坐标)
         *      大小: 印章显示为多大(width和height)
         *      显示区域: PDF阅读器会在这个矩形区域内渲染印章图像
         * 单位说明:
         *      这里的坐标和尺寸单位都是"点"(point),这是PDF的标准单位:
         *      1点 = 1/72英寸
         *      1英寸 = 25.4毫米
         *      所以1毫米 ≈ 2.8346点
         * 这也是为什么前面代码中要将毫米值转换为点值的原因。
         */
        PDRectangle position = new PDRectangle(x, y, width, height);
        /**
         * 将之前创建的矩形位置对象设置给图章注释,确定印章在页面上的显示区域。
         * 具体说明:
         *      setRectangle()方法将position矩形对象(包含x、y坐标和宽高信息)绑定到stampAnnotation图章注释对象上。
         * 这一步的重要性:
         *      这是配置图章注释的关键步骤之一,它告诉PDF阅读器:
         *          印章应该出现在页面的哪个位置
         *          印章应该显示为多大的尺寸
         *          印章图像的渲染边界在哪里
         * 完整的配置流程:
         *      在添加印章注释时,通常需要设置多个属性:
         *          设置位置矩形(setRectangle) - 确定位置和大小
         *          设置图像资源 - 确定显示什么内容
         *          设置其他可选属性(如名称、标志等)
         * 只有完成了这些配置后,才能将注释添加到页面上。如果不调用这个方法,图章注释就没有位置信息,无法正确显示。
         */
        stampAnnotation.setRectangle(position);
        /**
         * 设置图章注释为"可打印"状态,确保印章在打印PDF时能够显示出来。
         * setPrinted(true)方法设置注释的"Printed"标志位,这个标志控制注释在打印时的行为:
         *      设置为true: 注释(印章)会在打印PDF文档时被输出到纸张上
         *      设置为false或不设置: 注释只在屏幕上可见,但打印时会隐藏
         * 在签章场景中的重要性:
         * 对于电子签章应用,这通常是必须设置的,因为:
         *      法律效力: 签章文档通常需要打印存档或提交纸质版,如果印章不打印出来,就失去了签章的意义
         *      业务需求: 大多数签章场景要求屏幕显示和打印输出保持一致
         *      用户体验: 用户期望看到的印章效果与实际打印结果一致
         * PDF规范中定义了多种注释显示标志,包括:
         *      Printed: 控制打印时是否显示
         *      Hidden: 控制是否在界面上隐藏
         *      Invisible: 控制是否不可见但仍可交互
         *      ReadOnly: 控制是否只读
         * 对于签章这种需要持久化呈现的场景,设置Printed为true是标准做法
         */
        stampAnnotation.setPrinted(true);
        //获取外观流
        PDAppearanceStream appearanceStream = createAppearanceStream(document, imageFile, width, height, rotation);
        /**
         * 是创建一个外观字典对象,用于管理图章注释的不同状态下的视觉表现。
         * PDAppearanceDictionary是PDFBox中表示PDF外观字典的类。在PDF规范中,外观字典是一个容器,可以存储注释在多种状态下的外观流。
         * 外观字典的作用:
         *  外观字典可以包含以下几种外观:
         *      正常外观(N): 注释在常规状态下如何显示(最常用)
         *      悬停外观(R): 鼠标悬停在注释上时的显示效果(可选)
         *      按下外观(D): 鼠标点击注释时的显示效果(可选)
         * 在印章场景中的应用:
         *  对于电子签章这种静态注释,通常只需要设置正常外观(N):
         *      印章始终以相同的方式显示
         *      不需要交互式的视觉效果变化
         *      简化配置,减少PDF文件大小
         * 为什么需要外观字典:
         *  PDF采用分层设计:
         *      注释对象: 存储元数据和位置信息
         *      外观字典: 管理不同状态的视觉表现
         *      外观流: 具体的绘制指令和图像数据
         * 外观字典作为中间层,提供了灵活性:
         *      可以为同一注释定义多种视觉状态
         *      PDF阅读器根据用户交互选择合适的外观
         *      支持渐进式渲染和缓存优化
         */
        PDAppearanceDictionary appearance = new PDAppearanceDictionary();
        //将之前创建的外观流设置为正常状态的外观
        appearance.setNormalAppearance(appearanceStream);
        //将整个外观字典设置给图章注释对象
        stampAnnotation.setAppearance(appearance);
        //将配置完成的图章注释添加到目标PDF页面的注释列表中,使其正式成为页面的一部分。
        targetPage.getAnnotations().add(stampAnnotation);
    }





    /**
     * @author 忧郁的Mr.Li
     * @param document: PDF文件对象
     * @param imageFile: 印章实际图片的文件
     * @param width: 印章宽度
     * @param height:  印章高度
     * @param pageRotation:  页面旋转角度
     * 封装整个印章图像转换为PDF外观流的完整流程。
     */
    private static PDAppearanceStream createAppearanceStream(PDDocument document, File imageFile, float width, float height, int pageRotation) throws Exception {
        //根据页面对印章进行旋转处理
        File image = rotateImageFile(imageFile, 360 - pageRotation);
        /**
         * 创建一个空的PDF外观流对象,用于承载印章图像的视觉内容。
         * PDAppearanceStream是PDFBox库中的一个类,代表PDF规范中的"外观流"(Appearance Stream)。构造函数传入document参数,将这个外观流关联到指定的PDF文档中。
         * 外观流的作用:
         *  外观流是PDF注释的"渲染模板",它定义了注释在页面上应该如何显示。对于图章注释来说,外观流包含:
         *      印章图像数据
         *      图像的绘制位置和尺寸
         *      可能的变换操作(缩放、旋转等)
         *      其他图形状态信息
         * 为什么需要创建外观流:
         *   PDF的注释机制采用分离设计:
         *      注释对象: 存储注释的元数据(位置、类型、属性等)
         *      外观流: 存储注释的实际视觉内容
         * 这种设计的好处是:
         *      同一个注释可以有多个外观(如正常状态、鼠标悬停状态)
         *      阅读器可以选择性地渲染外观
         *      便于缓存和复用
         * 后续操作:
         *  创建这个空的外观流对象后,后续代码会:
         *      获取其内容流(PDPageContentStream)
         *      将旋转后的印章图像绘制到内容流中
         *      设置边界框(BBox)定义绘制区域
         *      最后将完成的外观流设置给图章注释
         * 这是构建印章视觉效果的核心步骤之一。
         */
        PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
        /**
         * 创建一个边界框矩形对象,用于定义外观流的绘制区域大小
         * PDRectangle构造函数接收宽度和高度两个参数,创建一个以原点(0,0)为左下角、指定宽高的矩形区域。这个矩形定义了:
         *      宽度: 外观流内容区域的横向尺寸
         *      高度: 外观流内容区域的纵向尺寸
         *      坐标系统: 在这个区域内进行绘图操作,原点位于左下角
         * 边界框(BBox)的作用:
         * 边界框是PDF外观流的一个必需属性,它告诉PDF阅读器:
         *      渲染范围: 外观流内容的有效绘制区域有多大
         *      资源分配: 阅读器需要为这个区域分配多少内存和渲染资源
         *      裁剪边界: 超出边界框的内容会被裁剪掉,不会显示
         * 与之前位置矩形的区别:
         *      之前的position矩形: 定义印章在PDF页面上的位置和大小(绝对坐标)
         *      当前的bbox矩形: 定义外观流内部的绘制坐标系大小(相对坐标,从0,0开始)
         * 后续使用:
         * 创建这个bbox对象后,会通过appearanceStream.setBBox(bbox)将其设置给外观流,然后在这个定义的区域内绘制印章图像。
         * 图像的绘制坐标会基于这个边界框的坐标系,通常是从(0,0)点开始绘制整个图像,使其填满整个边界框区域。
         */
        PDRectangle bbox = new PDRectangle(width, height);
        /**
         * 是将之前创建的边界框矩形对象设置给外观流,定义外观流的绘制区域。
         * setBBox()方法将bbox矩形(包含宽度和高度信息)绑定到appearanceStream外观流对象上。这一步是外观流配置的必要步骤。
         * 为什么必须设置边界框:
         *  根据PDF规范,每个外观流都必须有一个边界框(BBox)属性,它是必需字段而非可选字段。如果不设置:
         *      PDF文档可能不符合规范标准
         *      某些PDF阅读器可能无法正确渲染印章
         *      可能导致印章显示异常或完全不显示
         * 设置后的效果:
         *  设置边界框后,外观流就有了明确的绘制空间:
         *      后续的图像绘制操作会在这个定义的区域内进行
         *      PDF阅读器知道需要为这个区域分配渲染资源
         *      确保印章图像按照预期的尺寸显示
         * 完整的配置流程:
         * 在创建外观流时,通常需要完成以下配置:
         *      创建外观流对象
         *      设置边界框(setBBox) - 定义绘制区域大小
         *      获取内容流并绘制图像
         *      关闭内容流
         *      将外观流设置给图章注释
         * 这是构建完整外观流的关键环节之一。
         */
        appearanceStream.setBBox(bbox);
        /**
         * 创建一个PDF资源对象,用于管理和存储外观流中使用的各种资源(如图像、字体等)。
         * PDResources是PDFBox库中的一个类,代表PDF文档中的资源字典。在PDF规范中,资源字典用于集中管理页面或外观流中使用的所有外部资源。
         * 资源对象的作用:
         *  资源对象可以包含多种类型的资源:
         *      图像资源(XObject): 嵌入的图片、照片、印章图像等
         *      字体资源(Font): 文本渲染所需的字体文件
         *      颜色空间(ColorSpace): 定义颜色的表示方式
         *      图案(Pattern): 填充图案和渐变
         *      表单(Form XObject): 可复用的图形元素
         * 在印章场景中的应用:
         *  对于图章注释的外观流,主要需要管理的是图像资源:
         *      将印章图片转换为PDF的XObject格式
         *      添加到resources资源对象中,并分配一个唯一的名称
         *      后续绘制时通过这个名称引用图像
         * 为什么需要资源对象:
         *  PDF采用分离式存储设计:
         *      资源定义: 在资源字典中声明和存储
         *      资源使用: 在内容流中通过名称引用
         *  这种设计的优势:
         *      支持资源复用,同一图像可以被多次引用
         *      便于PDF阅读器预加载和管理资源
         *      符合PDF规范的标准结构
         *
         */
        PDResources resources = new PDResources();
        //将配置好的资源对象设置给外观流,使外观流能够访问和使用其中包含的图像等资源。
        appearanceStream.setResources(resources);
        /**
         * 从文件系统中读取印章图片文件,并将其转换为PDF格式的图像对象。
         * PDImageXObject.createFromFile()是一个静态工厂方法,它执行以下操作:
         *      读取文件: 从image.getPath()指定的路径读取图片文件
         *      格式检测: 自动识别图片格式(JPEG、PNG、TIFF等)
         *      压缩处理: 根据图片格式进行适当的压缩编码
         *      创建对象: 生成一个PDImageXObject对象,这是PDF中用于表示图像的标准对象类型
         *      关联文档: 将图像对象添加到document文档的资源结构中
         * PDImageXObject的作用:
         *  PDImageXObject是PDFBox中表示PDF图像的对象,它:
         *      封装了图像的像素数据
         *      包含图像的元信息(宽度、高度、颜色空间、分辨率等)
         *      可以被嵌入到PDF页面或外观流中
         *      支持多种图像格式的自动转换
         * 为什么需要转换:
         *  原始的图片文件(如PNG、JPG)不能直接绘制到PDF中,必须:
         *      转换为PDF规范的图像格式
         *      嵌入到PDF文档结构中
         *      分配唯一的对象编号和引用名称
         */
        PDImageXObject pdImage = PDImageXObject.createFromFile(image.getPath(), document);
        //添加到之前创建的resources资源对象中
        resources.add(pdImage);

        // 获取图片原始尺寸
        int imageWidth = pdImage.getWidth();
        int imageHeight = pdImage.getHeight();

        // 计算等比例缩放
        float scaleX = width / imageWidth;
        float scaleY = height / imageHeight;

        // 选择较小的缩放比例以保持完整图片
        float scale = Math.min(scaleX, scaleY);

        // 计算缩放后的尺寸
        float scaledWidth = imageWidth * scale;
        float scaledHeight = imageHeight * scale;

        // 计算居中位置
        float x = (width - scaledWidth) / 2;
        float y = (height - scaledHeight) / 2;

        /**
         * 创建一个内容流对象,用于在外观流中执行实际的绘图操作(如绘制印章图像)。
         *  PDPageContentStream是PDFBox中用于向PDF写入图形内容的类。这个构造函数接收两个参数
         *      document: PDF文档对象
         *      appearanceStream: 之前创建的外观流对象
         * 这个构造函数的特殊之处在于,它不是向普通页面写入内容,而是向外观流写入内容。
         * 内容流包含一系列PDF绘图操作符和指令,告诉PDF阅读器如何渲染视觉内容。对于印章场景,内容流会包含:
         *      设置图形状态(如透明度、混合模式)
         *      绘制图像的指令
         *      坐标变换操作(缩放、旋转、平移)
         *      其他图形绘制命令
         * 为什么需要内容流:
         *      PDF采用"指令式"渲染模型:
         *          资源层: 存储图像、字体等原始数据(已完成)
         *          内容流层: 存储如何使用这些资源的指令(当前步骤)
         *          渲染引擎: PDF阅读器执行内容流指令来显示最终效果
         * 内容流就像是给PDF阅读器的"绘画脚本",详细说明每一步该如何绘制。
         */
        try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) {
            /**
             * 核心代码---在外观流的内容流中绘制印章图像,指定图像的绘制位置 and 尺寸。
             *  drawImage()方法执行实际的图像绘制操作,接收四个参数:
             *      pdImage: 要绘制的图像对象(之前从文件转换得到的印章图片)
             *      x: 图像左下角的X坐标(绘制起始位置)
             *      y: 图像左下角的Y坐标(绘制起始位置)
             *      scaledWidth: 图像的目标宽度(会被缩放到这个宽度)
             *      scaledHeight: 图像的目标高度(会被缩放到这个高度)
             * 绘制过程:
             *  当执行这行代码时,PDFBox会:
             *      将图像数据写入内容流
             *      添加必要的绘图操作符
             *      应用坐标变换,将图像放置到指定位置
             *      设置图像的显示尺寸为指定的宽高
             * 坐标系统:
             *  这里的x和y坐标是相对于外观流边界框(BBox)的坐标系:
             *      原点(0,0)位于边界框的左下角
             *      通常印章会从(0,0)开始绘制,填满整个边界框区域
             *      如果需要考虑页面旋转,可能已经在前面的步骤中进行了坐标调整
             * 缩放效果:
             *      如果原始图像尺寸与scaledWidth/scaledHeight不一致,图像会被自动缩放
             *      保持图像的纵横比可能需要额外计算,否则可能出现拉伸变形
             *      在印章场景中,通常会精确计算缩放比例以确保印章显示正确的大小
             *  这行代码是整个印章添加流程中最关键的操作之一,它真正地将印章图像"画"到了PDF文档中。
             *  之前所有的配置(创建外观流、设置资源、创建内容流)都是为了让这一步能够正确执行
             */
            contentStream.drawImage(pdImage, x, y, scaledWidth, scaledHeight);
        }
        return appearanceStream;
    }




    /**
     * @author 忧郁的Mr.Li
     * @param imageFile: 实际印章图片
     * @param rotation: 实际印章图片需要旋转的角度
     */
    private static File rotateImageFile(File imageFile, int rotation) {
        if (rotation == 0) {
            return imageFile;
        }

        try {
            // 使用ImageIO读取图片
            BufferedImage originalImage = ImageIO.read(imageFile);
            int originalWidth = originalImage.getWidth();
            int originalHeight = originalImage.getHeight();

            // 计算旋转后的尺寸
            int rotatedWidth, rotatedHeight;
            switch (rotation) {
                case 90:
                case 270:
                    rotatedWidth = originalHeight;
                    rotatedHeight = originalWidth;
                    break;
                case 180:
                    rotatedWidth = originalWidth;
                    rotatedHeight = originalHeight;
                    break;
                default:
                    return imageFile;
            }

            // 创建新的缓冲图片
            BufferedImage rotatedImage = new BufferedImage(rotatedWidth, rotatedHeight,
                    originalImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : originalImage.getType());

            Graphics2D g2d = rotatedImage.createGraphics();

            // 设置高质量渲染
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

            // 在正确的位置绘制旋转后的图像
            switch (rotation) {
                case 90:
                    g2d.translate(rotatedWidth, 0);
                    g2d.rotate(Math.PI / 2);
                    break;
                case 180:
                    g2d.translate(rotatedWidth, rotatedHeight);
                    g2d.rotate(Math.PI);
                    break;
                case 270:
                    g2d.translate(0, rotatedHeight);
                    g2d.rotate(Math.PI * 3 / 2);
                    break;
            }

            g2d.drawImage(originalImage, 0, 0, null);
            g2d.dispose();

            // 保存旋转后的图片到临时文件
            String tempPath = getTempPathByDayMillis();
            File tempDir = new File(tempPath);
            if (!tempDir.exists()) {
                tempDir.mkdirs();
            }

            File rotatedFile = new File(tempPath, "rotated_" + System.currentTimeMillis() + "_" + imageFile.getName());
            ImageIO.write(rotatedImage, "png", rotatedFile);
            return rotatedFile;
        } catch (Exception e) {
            e.printStackTrace();
            return imageFile;
        }
    }






    /**
     * @author 忧郁的Mr.Li
     * 获取配置信息,后续定义一个表内容,详情参考实体类PdfSealConfig
     */
    public static Map<Object, PdfSealConfig> getPfSealConfigs() throws Exception {
        Map<Object, PdfSealConfig> pdfSealConfigMap = new HashMap<>();

        //后续这里从数据库中读取配置表,转换成Map,做静态配置的话需要提供清除机制

        //圆形公章
        PdfSealConfig pdfSealConfig = new PdfSealConfig();
        pdfSealConfig.setMainid(202);
        pdfSealConfig.setCreatetime(new Date());
        pdfSealConfig.setModifytime(new Date());
        pdfSealConfig.setCreator(1);
        pdfSealConfig.setModifier(1);
        pdfSealConfig.setType("company");
        pdfSealConfig.setShape("circular");
        pdfSealConfig.setWidth(40.00);
        pdfSealConfig.setHeight(40.00);
        pdfSealConfig.setRemark("这是一个圆形公章");
        pdfSealConfigMap.put(pdfSealConfig.getMainid(),pdfSealConfig);
        //

        return pdfSealConfigMap;
    }



    /**
     * @author 忧郁的Mr.Li
     * 毫米转换像素
     */
    public static double convertMmToPixels(double millimeters) {
        double inches = millimeters / 25.4; // 毫米转换为英寸
        double pixels = inches * 72; // 英寸乘以像素密度得到像素
        return pixels;
    }




    //印章配置实体类

    @Data
    public static class PdfSealConfig {

        //主键
        private Integer mainid;

        //创建时间
        private Date createtime;

        //修改时间
        private Date modifytime;

        //创建人
        private Integer creator;

        //修改人
        private Integer modifier;

        //印章类型
        private String type;

        //印章形状
        private String shape;

        //宽度
        private Double width;

        //高度
        private Double height;

        //印章备注
        private String remark;

    }


}

测试结果

测试前pdf文件

测试生成的签章后的PDF文件

相关推荐
零二年的冬2 小时前
epoll详解
java·linux·开发语言·c++·链表
凭君语未可2 小时前
Java 中的接口是什么
java·开发语言
XiYang-DING2 小时前
【Java】二叉树
java·开发语言·数据结构
凌冰_2 小时前
Servlet+Thymeleaf + Fetch 实现无刷新异步请求
java·servlet
AscendKing2 小时前
免费、易用、覆盖全平台的网页转 PDF 工具
pdf·html·网页保存·网页保存为pdf·保存网页位pdf
深蓝轨迹2 小时前
面试常见的jdk---LTS版本新特性梳理
java·面试·jdk
Stella Blog2 小时前
狂神Java基础学习笔记Day01
java·笔记·学习
李白的天不白2 小时前
java处理跨域请求
java
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【11】Spring AI Models 扩展:DashScope
java·人工智能·spring