# 表单签名字段

签名字段是 PDF 文档中特殊的表单字段类型,专用于电子签名的添加与管理。本章将详细介绍如何使用 Foxit PDF SDK for Web 处理表单签名字段,包括添加、编辑、验证签名,以及对签名过程和用户界面(UI)的自定义。

针对签名功能,Foxit PDF SDK for Web 提供以下支持:

  • 创建、编辑、删除签名域和字段
  • 签名和验证签名
  • 自定义签署人配置信息和签名处理程序
  • 自定义验签处理程序
  • 自定义签名相关的 UI,包括签名对话框、签名验证对话框和签名信息对话框

# 在 PDF 文档中添加、编辑和删除签名字段

# 创建签名字段

使用 PDFForm (opens new window) 类的 createSignature (opens new window) 方法可以向 PDF 文档添加新的签名字段。该方法需要传入一个 CreateSignatureOptions (opens new window) 对象,用于配置签名字段的属性。

const options = {
    pdfRect: {left: 100, top: 100, right: 300, bottom: 200}, // 设置签名字段的位置和大小
    pageIndex: 0, // 指定签名字段添加到的页面索引
    rotate: 0, // 可选,设置 Widget 的旋转角度,可选值为 0、1、2 或 3,分别表示 0°、90°、180° 和 270°
    fieldName: 'SignatureField1' // 可选,指定签名字段的名称;若未指定,将自动生成一个唯一的字段名
};  

pdfForm.createSignature(options).then(widget => {
    console.log("签名字段已成功创建:", widget);
}).catch(error => {
    console.error("创建签名字段时发生错误:", error);
});

签名字段创建成功后,Foxit PDF SDK for Web 会在页面中插入一个新的签名域,并触发 ViewerEvents.annotationAdded (opens new window) 事件。在处理该事件时,可通过 Annot.getType (opens new window)PDFFormField.getType (opens new window) 接口判断是否添加了签名域。

pdfui.addViewerEventListener(PDFViewCtrl.ViewerEvents.annotationAdded, (annotations) => {
    const signatureWidgets = annotations.filter(it => {
        return it.getType() === PDF.annots.constant.Annot_Type.widget && it.getField().getType() === PDF.form.constant.FieldType.signature
    });
    // 使用 signatureWidgets 进行后续操作
})

# 删除签名

这里的删除签名是指删除签名域,而非直接删除签名字段。删除签名域的方式与删除注释(Annot)的方法相同,均通过 PDFPage.removeAnnotByObjectNumber (opens new window) 方法实现。

pdfPage.removeAnnotByObjectNumber(widget.getObjectNumber()).then((removedAnnots) => {
    if(removedAnnots.length === 1) {
        console.log("签名域已成功删除");
    } else {
        console.log("未找到要删除的签名域");
    }
}, reason => {
    console.error("删除签名域时发生错误:", reason);
});

签名域被成功删除后,会返回一个包含已删除签名域的数组。如果未找到要删除的签名域,则返回一个空数组。因此,在删除操作后需要判断返回的数组长度是否为 1 来确认删除是否成功。

此外,签名域删除成功后,如果其所属的表单字段不再包含其他表单域,该字段会被自动清除,并且会触发 DataEvents.formFieldRemoved (opens new window) 事件。

如果您不熟悉表单字段和表单域的关系,可以参考 表单基础概念介绍 章节。

# 开始签名

签名字段创建成功后,可以通过调用 PDFDoc.sign (opens new window) 方法进行签名。签名相关的信息(如签名者身份、签名原因、签名位置等)可以在 signInfo 对象中进行详细定义。

const signInfo = {
    filter: 'Adobe.PPKLite',
    subfilter: 'adbe.pkcs7.sha1',
    flag: 0x1f0,
    distinguishName: '[email protected]',
    location: 'bj',
    reason: 'TestBJ',
    signer: 'web sdk11',
    showTime: true,
    signTime: Date.now(),
    defaultContentsLength: 7942,
    image: 'data:image/png;base64,iVBORw0...',
    rotation: 90,
    timeFormat: {
        format: 'YYYY-MM-DD HH:mm:ss Z',
        timeZoneOptions: { prefix: 'GMT' }
    },
};
// 示例的签名服务地址
const signature_server = 'https://webviewer-demo.foxitsoftware.com/signature'; // 或者使用您自己部署的签名服务地址  

const signedDocBlobData = await pdfDoc.sign(signInfo, async function sign(signInfo, plainContent) {
    const formdata = new FormData();
    formdata.append("plain", new Blob([plainContent]), "plain");
    const response = await fetch(signature_server + '/digest_and_sign', {
        method: 'POST',
        body: formdata
    });
    return response.arrayBuffer();
});

在签名过程中,Foxit PDF SDK for Web 会调用 sign 回调函数,将签名相关的数据提交至签名服务。签名服务完成签署后,会返回签名后的数据。

关于签名服务的实现,Foxit PDF SDK for Web 提供了 Windows 和 Linux 系统的示例实现。您可以参考发布包中的 /server/signature-server-for-linux/README.md/server/signature-server-for-win/readme.md 两个说明文件。

为了方便您快速验证签名功能,我们提供了如下的测试服务:

注意:以上服务仅用于测试使用,请勿在生产环境中使用。如果我们提供的 signature-server-for-linuxsignature-server-for-win 的 Node.js 实现无法满足您的需求,您可以参考其源码,并根据需要使用其他技术栈重新实现,以确保其在生产环境中的稳定性和可用性。

签名成功后,返回的 blob 对象包含签名后的 PDF 文件。您可以选择将该文件保存到本地,或者直接通过 PDFViewer.openPDFByFile (opens new window) 方法打开:

pdfViewer.openPDFByFile(signedDocBlobData, { fileName: 'signed.pdf' });

# 验证签名

# 验签 API

签名成功后,为确保签名字段的有效性,可以使用 PDFSignature.verify (opens new window) 方法进行验证。

const signatureField = form.getField("Signature field name");
// 示例的签名服务地址
const signature_server = 'https://webviewer-demo.foxitsoftware.com/signature'; // 或者使用您自己部署的签名服务地址

const result = await signatureField.verify({
    force: false,
    handler: async (signatureField, plainContent, signedData, hasDataOutOfScope) => {
        const filter = await signatureField.getFilter();
        const subfilter = await signatureField.getSubfilter();
        const signer = await signatureField.getSigner();
        const formdata = new FormData();
        formdata.append("filter", filter);
        formdata.append("subfilter", subfilter);
        formdata.append("signer", signer);
        formdata.append("plainContent", new Blob([plainContent]), "plainContent");
        formdata.append("signedData", new Blob([signedData]), "signedData");
        
        const response = await fetch(signature_server + '/verify', {
            method: 'POST',
            body: formdata
        });
        const result = parseInt(await response.text());
        return result;
    }
});

验签完成后,将返回一个 result 值,其类型为 Signature_State (opens new window),用于表示签名的验证结果:

  • 签名验证状态:

    • verifyValid (4): 签名验证状态有效
    • verifyInvalid (8): 签名验证状态无效
    • verifyErrorByteRange (64): 非预期的字节范围
    • verifyUnknown (2147483648): 未知签名
  • 文档更改状态:

    • verifyChange (128): 文档已在签名范围内更改(签名无效)
    • verifyIncredible (256): 签名不可信任(包含风险)
    • verifyNoChange (1024): 文档未在签名范围内进行更改
    • verifyChangeLegal (134217728): 文档已在签名范围之外进行了更改,但这种更改是允许的
    • verifyChangeIllegal (268435456): 文档已在签名范围之外进行了更改,且所做的更改使签名无效
  • 颁发者验证状态:

    • verifyIssueUnknown (2048): 颁发者的验证状态未知
    • verifyIssueValid (4096): 颁发者的验证状态有效
    • verifyIssueRevoke (16384): 验证颁发者的证书已吊销
    • verifyIssueExpire (32768): 验证颁发者的证书已过期
    • verifyIssueUncheck (65536): 未检查颁发者
    • verifyIssueCurrent (131072): 已验证的颁发者是当前颁发者
  • 时间戳状态:

    • verifyTimestampNone (262144): 没有时间戳或未检查时间戳
    • verifyTimestampDoc (524288): 该签名是一个时间戳签名
    • verifyTimestampValid (1048576): 时间戳的验证状态有效
    • verifyTimestampInvalid (2097152): 时间戳的验证状态无效
    • verifyTimestampExpire (4194304): 时间戳的验证状态已过期
    • verifyTimestampIssueUnknown (8388608): 时间戳颁发者的验证状态未知
    • verifyTimestampIssueValid (16777216): 时间戳颁发者的验证状态有效
    • verifyTimestampTimeBefore (33554432): 时间戳时间的验证状态有效,因为时间在过期日期之前

注意:在验证签名前,请确认签名字段是否已签署。如果未签署,调用 verify 方法将会报错,因此需先检查签名字段的签署状态。

const isSigned = await signatureField.isSigned();
if (isSigned) {
    // 在这里进行验签
}

# 自定义验证处理器(verify handler)

验证处理器是一个异步函数,用于将数据提交到签名验证服务并返回验证结果。可以通过调用 PDFViewer.getSignatureService (opens new window) 方法获取签名管理服务,并设置一个全局默认的验证处理器。

注意:一旦设置全局默认的验证处理器,Foxit PDF SDK for Web 会自动使用该处理器。在应用层调用 PDFSignature.verify (opens new window) 方法时,如果未显式指定验证处理器(handler)参数,也会自动使用自定义验证处理器。

const signatureService = pdfViewer.getSignatureService();
// 示例的签名服务地址
const signature_server = 'https://webviewer-demo.foxitsoftware.com/signature'; // 或者使用您自己部署的签名服务地址
signatureService.setVerifyHandler(
    async (signatureField, plainContent, signedData, hasDataOutOfScope) => {
        const filter = await signatureField.getFilter();
        const subfilter = await signatureField.getSubfilter();
        const signer = await signatureField.getSigner();
        const formdata = new FormData();
        formdata.append("filter", filter);
        formdata.append("subfilter", subfilter);
        formdata.append("signer", signer);
        formdata.append("plainContent", new Blob([plainContent]), "plainContent");
        formdata.append("signedData", new Blob([signedData]), "signedData");
        
        const response = await fetch(signature_server + '/verify', {
            method: 'POST',
            body: formdata
        });
        return parseInt(await response.text());
    }
)

# 获取签名信息

PDFSignature.getSignInfo (opens new window) 方法用于获取签名信息。如果签名已签署,则返回 SignedFormSignatureInfo (opens new window) 对象, 反之返回 UnsignedFormSignatureInfo (opens new window) 对象。

# 自定义签名 UI

Foxit PDF SDK for Web 提供了一组接口,用于自定义签名相关的 UI 元素。这些接口定义在 ISignatureUI 中,可通过 IViewerUI.getSignatureUI() 方法获取。主要包括以下几个接口:

要自定义签名相关的 UI 元素,可以通过继承 SDK 内置的 Viewer UI 类(如 XViewerUI 或 TinyViewerUI),并重写 getSignatureUI 方法返回自定义的 ISignatureUI 实现。比如:

class CustomSignDocDialog implements ISignDocDialog {
    private signatureField: ISignatureField;
    private _isOpened = false;
    async openWith(
        signature: ISignatureField,
        okCallback: (data: SignDocInfo, sign: DigestSignHandler) => Promise<void> | void,
        cancelCallback?: () => Promise<void> | void
    ): Promise<void> {
        this._isOpened = true;
        this.signatureField = signature;
        // 创建签名对话框界面
        // 显示各项信息
        // 注册 okCallback 和 cancelCallback 回调事件,当用户确认或者取消时会调用该回调
    }
    isOpened(): boolean {
        return this._isOpened;
    }
    getCurrentSignature(): ISignatureField | undefined {
        return this.signatureField;
    }
    hide(): void {
        this._isOpened = false;
        // 隐藏签名对话框
    }
    destroy(): void {
        // 释放资源
    }
}
class CustomSignatureUI implements ISignatureUI {
  // 实现 ISignatureUI 接口中的方法
  getSignDocumentDialog() {
    return Promise.resolve(new CustomSignDocDialog())
  }
  ...
}

class CustomViewerUI extends UIExtension.XViewerUI {
  getSignatureUI() {
    return Promise.resolve(new CustomSignatureUI());
  }
}

// 使用自定义的 ViewerUI
new PDFUI({
  viewerOptions: {
    viewerUI: new CustomViewerUI()
  }
})

在自定义的 ISignatureUI 实现中,可以根据需求定制各个对话框的外观和行为。例如,通过重写 ISignDocDialog.openWith 方法,可以在打开签名对话框时执行特定的自定义逻辑。