# # 表单签名字段

签名字段是 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: 'e=foxitsdk@foxitsoftware.cn',
        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` 两个说明文件。

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

- 美区服务：

  - http://webviewer-demo.foxitsoftware.com/signature/digest_and_sign
    (opens new window)
  - http://webviewer-demo.foxitsoftware.com/signature/verify
    (opens new window)
  - https://webviewer-demo.foxitsoftware.com/signature/digest_and_sign
    (opens new window)
  - https://webviewer-demo.foxitsoftware.com/signature/verify
    (opens new window)

- 中国区服务：

  - http://webviewer-demo.foxitsoftware.cn/signature/digest_and_sign
    (opens new window)
  - http://webviewer-demo.foxitsoftware.cn/signature/verify
    (opens new window)

注意：以上服务仅用于测试使用，请勿在生产环境中使用。如果我们提供的 `signature-server-for-linux` 和 `signature-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()` 方法获取。主要包括以下几个接口：

- `ISignDocDialog`
  (opens new window): 用于显示签名文档对话框
- `ISignVerifiedResultDialog`
  (opens new window): 用于显示签名验证结果对话框
- `ISignedSignaturePropertiesDialog`
  (opens new window): 用于显示已签署的签名字段属性对话框

要自定义签名相关的 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` 方法，可以在打开签名对话框时执行特定的自定义逻辑。