You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Spring Boot与PDFBox为多签名PDF添加含文字图片的Parafe域

实现类似DocuSign的Parafe域(PDFBox 2.0.28 + Spring Boot)

核心逻辑

DocuSign的Parafe域本质是带复合可视化外观的PDF签名域,同时承载签名图像、签名人信息(姓名、日期)等内容。结合你已实现多用户签名不破坏完整性的基础,核心要做的是:

  1. 为签名域生成包含图片+文字的自定义外观
  2. 始终使用PDF增量更新机制保存修改,避免覆盖已有签名

关键实现步骤

  1. 定位/创建签名域:从PDF的AcroForm中获取目标签名域,若不存在则动态创建并绑定到指定页面位置。
  2. 生成复合外观流:通过PDFBox的PDAppearanceStream绘制签名图片、姓名、日期等元素,组合成Parafe域的可视化内容。
  3. 增量保存文档:调用saveIncremental方法保存修改,确保原有签名的完整性不受影响。

代码示例

1. Parafe域处理工具类

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.util.Matrix;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Calendar;

public class ParafeSignatureHandler {

    /**
     * 填充Parafe域并生成文字+图片的签名外观
     * @param doc 待处理PDF文档
     * @param fieldName 目标签名域名称
     * @param signImagePath 签名图片本地路径
     * @param signerName 签名人姓名
     * @param signDate 签名日期
     * @throws IOException
     */
    public void populateParafeField(PDDocument doc, String fieldName, String signImagePath, String signerName, Calendar signDate) throws IOException {
        PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
        if (acroForm == null) {
            throw new IllegalArgumentException("目标PDF未包含AcroForm表单");
        }

        // 获取或创建签名域
        PDSignatureField signatureField = (PDSignatureField) acroForm.getField(fieldName);
        if (signatureField == null) {
            // 动态创建签名域(示例绑定到第一页,位置需根据实际需求调整)
            PDPage targetPage = doc.getPage(0);
            signatureField = new PDSignatureField(acroForm);
            signatureField.setPartialName(fieldName);

            PDAnnotationWidget widget = signatureField.getWidgets().get(0);
            // 设置域位置:x1,y1,x2,y2(PDF坐标原点在左下角,单位为点)
            widget.setRectangle(new float[]{100, 100, 350, 160});
            targetPage.getAnnotations().add(widget);
            acroForm.getFields().add(signatureField);
        }

        // 生成Parafe复合外观
        generateParafeAppearance(signatureField, signImagePath, signerName, signDate);
    }

    /**
     * 生成包含图片和文字的签名外观
     */
    private void generateParafeAppearance(PDSignatureField signatureField, String signImagePath, String signerName, Calendar signDate) throws IOException {
        PDAnnotationWidget widget = signatureField.getWidgets().get(0);
        float[] rect = widget.getRectangle().getRectangle();
        float width = rect[2] - rect[0];
        float height = rect[3] - rect[1];

        // 创建外观流
        PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
        PDAppearanceStream appearanceStream = new PDAppearanceStream(signatureField.getAcroForm().getDocument());
        appearanceStream.setBBox(widget.getRectangle());
        appearanceStream.setMatrix(Matrix.getTranslateInstance(rect[0], rect[1]));

        try (var contentStream = appearanceStream.createOutputStream()) {
            // 1. 绘制签名图片(自适应缩放)
            PDImageXObject signImage = PDImageXObject.createFromFile(signImagePath, signatureField.getAcroForm().getDocument());
            float imgScale = Math.min(width * 0.55f / signImage.getWidth(), height * 0.9f / signImage.getHeight());
            float imgX = 15;
            float imgY = (height - signImage.getHeight() * imgScale) / 2;
            contentStream.drawImage(signImage, imgX, imgY, signImage.getWidth() * imgScale, signImage.getHeight() * imgScale);

            // 2. 绘制签名人姓名
            contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
            contentStream.beginText();
            contentStream.newLineAtOffset(imgX + signImage.getWidth() * imgScale + 20, height - 25);
            contentStream.showText("签名人: " + signerName);
            contentStream.endText();

            // 3. 绘制签名日期
            contentStream.setFont(PDType1Font.HELVETICA, 10);
            contentStream.beginText();
            contentStream.newLineAtOffset(imgX + signImage.getWidth() * imgScale + 20, height - 45);
            contentStream.showText("签署日期: " + String.format("%tF %tT", signDate, signDate));
            contentStream.endText();
        }

        appearanceDict.setNormalAppearance(appearanceStream);
        widget.setAppearance(appearanceDict);
    }

    /**
     * 增量保存PDF,确保原有签名不被破坏
     */
    public void saveIncremental(PDDocument doc, File outputFile) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
            doc.saveIncremental(fos);
        }
    }
}

2. Spring Boot中结合RabbitMQ消费逻辑

import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.File;
import java.util.Calendar;

@Component
public class SignatureRequestConsumer {

    private final ParafeSignatureHandler signatureHandler = new ParafeSignatureHandler();

    @RabbitListener(queues = "signature-request-queue")
    public void handleParafeSignatureRequest(SignatureRequest request) {
        try {
            // 加载源PDF(实际场景可从OSS/本地存储获取)
            File sourcePdf = new File(request.getSourcePdfPath());
            try (PDDocument doc = PDDocument.load(sourcePdf)) {
                // 填充Parafe域
                signatureHandler.populateParafeField(
                        doc,
                        request.getParafeFieldName(),
                        request.getSignImagePath(),
                        request.getSignerName(),
                        Calendar.getInstance()
                );

                // 增量保存到目标路径
                File targetPdf = new File(request.getTargetPdfPath());
                signatureHandler.saveIncremental(doc, targetPdf);
            }
        } catch (Exception e) {
            // 实际场景需添加日志记录、重试机制等
            e.printStackTrace();
        }
    }

    // 签名请求DTO
    public static class SignatureRequest {
        private String sourcePdfPath;
        private String parafeFieldName;
        private String signImagePath;
        private String signerName;
        private String targetPdfPath;

        // Getters & Setters
        public String getSourcePdfPath() { return sourcePdfPath; }
        public void setSourcePdfPath(String sourcePdfPath) { this.sourcePdfPath = sourcePdfPath; }
        public String getParafeFieldName() { return parafeFieldName; }
        public void setParafeFieldName(String parafeFieldName) { this.parafeFieldName = parafeFieldName; }
        public String getSignImagePath() { return signImagePath; }
        public void setSignImagePath(String signImagePath) { this.signImagePath = signImagePath; }
        public String getSignerName() { return signerName; }
        public void setSignerName(String signerName) { this.signerName = signerName; }
        public String getTargetPdfPath() { return targetPdfPath; }
        public void setTargetPdfPath(String targetPdfPath) { this.targetPdfPath = targetPdfPath; }
    }
}

重要注意事项

  • 必须使用增量保存:绝对不能用doc.save(),必须调用saveIncremental,否则会完全重写PDF,导致原有签名全部失效。
  • 域权限配置:确保AcroForm设置acroForm.setNeedAppearances(false),避免PDF阅读器自动重置外观;同时签名域需设置正确的权限标志(如允许填写)。
  • 字体与图片兼容性:使用嵌入字体避免显示异常,图片支持PNG、JPG等格式,需确保图片路径可访问。
  • 多签名顺序:若需多个用户依次填写Parafe域,需保证每个用户的操作都基于上一次增量更新后的PDF文件。

内容的提问来源于stack exchange,提问作者rahim soufiane

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 17:42:02