Browse Source

Merge branch 'feature/signature' into preparing

liutian 9 months ago
parent
commit
0d9ce054e3
58 changed files with 4884 additions and 344 deletions
  1. 1 1
      packages/core/constants/index.js
  2. 6 6
      packages/core/rollup.config.js
  3. 9 1
      packages/core/src/TextSelection.ts
  4. 121 0
      packages/core/src/annotation/layer.js
  5. 1 1
      packages/core/src/annotation/stamp.js
  6. 2 1
      packages/core/src/annotation_store.js
  7. 238 0
      packages/core/src/form/add_signature_fields.js
  8. 787 0
      packages/core/src/form/signature_fields.js
  9. 164 28
      packages/core/src/index.js
  10. 212 179
      packages/core/src/ink_sign.js
  11. 580 37
      packages/core/src/worker/compdfkit_worker.js
  12. 117 3
      packages/webview/locales/en.json
  13. 117 3
      packages/webview/locales/zh-CN.json
  14. 29 0
      packages/webview/public/example/Blank Page.pdf
  15. BIN
      packages/webview/public/images/logo-backgroud.png
  16. BIN
      packages/webview/public/images/logo-backgroud@2x.png
  17. 2 0
      packages/webview/src/assets/base.css
  18. 3 0
      packages/webview/src/assets/icons/icon-signature.svg
  19. 4 2
      packages/webview/src/components/App/index.vue
  20. 221 0
      packages/webview/src/components/Dialogs/AddDigitalFileDialog.vue
  21. 325 0
      packages/webview/src/components/Dialogs/CertificationViewerDialog.vue
  22. 38 0
      packages/webview/src/components/Dialogs/DeleteSignatureDialog.vue
  23. 6 1
      packages/webview/src/components/Dialogs/Dialog.vue
  24. 137 0
      packages/webview/src/components/Dialogs/SelectSignTypeDialog.vue
  25. 645 0
      packages/webview/src/components/Dialogs/SignatureAppearanceDialog.vue
  26. 205 0
      packages/webview/src/components/Dialogs/SignatureDetailsDialog.vue
  27. 8 2
      packages/webview/src/components/DocumentContainer/DocumentContainer.vue
  28. 1 0
      packages/webview/src/components/Header/Header.vue
  29. 2 18
      packages/webview/src/components/HeaderItems/HeaderItems.vue
  30. 5 0
      packages/webview/src/components/Icon/AddDigitalSign.vue
  31. 5 0
      packages/webview/src/components/Icon/AddElectronicSign.vue
  32. 5 0
      packages/webview/src/components/Icon/CloseB.vue
  33. 5 0
      packages/webview/src/components/Icon/CreateSignField.vue
  34. 7 0
      packages/webview/src/components/Icon/FailedIcon.vue
  35. 9 0
      packages/webview/src/components/Icon/Logo.vue
  36. 5 0
      packages/webview/src/components/Icon/MoreB.vue
  37. 5 0
      packages/webview/src/components/Icon/Signature.vue
  38. 8 0
      packages/webview/src/components/Icon/SignatureInvalid.vue
  39. 7 0
      packages/webview/src/components/Icon/SignatureUnknown.vue
  40. 7 0
      packages/webview/src/components/Icon/SignatureValid.vue
  41. 5 0
      packages/webview/src/components/Icon/SuccessIcon.vue
  42. 5 0
      packages/webview/src/components/Icon/VerifyDigitalSign.vue
  43. 3 0
      packages/webview/src/components/LeftPanel/LeftPanel.vue
  44. 10 0
      packages/webview/src/components/LeftPanel/LeftPanelTabs.vue
  45. 158 0
      packages/webview/src/components/SignatureContainer/SignatureContainer.vue
  46. 90 0
      packages/webview/src/components/SignatureToolBar/SignatureToolBar.vue
  47. 86 0
      packages/webview/src/components/SignatureToolBar/SignatureVerifyBar.vue
  48. 156 0
      packages/webview/src/components/Signatures/DigitalKeyboardCreatePanel.vue
  49. 146 0
      packages/webview/src/components/Signatures/DigitalSignCreatePanel.vue
  50. 72 43
      packages/webview/src/components/Signatures/SignCreatePanel.vue
  51. 3 1
      packages/webview/src/components/Toolbar/Toolbar.vue
  52. 3 0
      packages/webview/src/core/deleteSignature.js
  53. 3 0
      packages/webview/src/core/getSignature.js
  54. 1 1
      packages/webview/src/core/handleSign.js
  55. 7 1
      packages/webview/src/core/index.js
  56. 3 0
      packages/webview/src/core/loadCertificates.js
  57. 29 2
      packages/webview/src/stores/modules/document.js
  58. 55 13
      packages/webview/src/stores/modules/viewer.js

+ 1 - 1
packages/core/constants/index.js

@@ -177,7 +177,7 @@ const WidgetTypeString = {
   3: 'textfield',
   4: 'combobox',
   5: 'listbox',
-  6: 'signaturefields',
+  6: 'signatureFields',
   0xff: 'unknown',
 }
 

+ 6 - 6
packages/core/rollup.config.js

@@ -34,12 +34,12 @@ const plugins = [
   })
 ]
 
-plugins.push(
-terser({
-  mangle: {
-    reserved
-  }
-}))
+// plugins.push(
+// terser({
+//   mangle: {
+//     reserved
+//   }
+// }))
 
 export default [{
   input: "./src/worker/compdfkit_worker.js",

+ 9 - 1
packages/core/src/TextSelection.ts

@@ -160,7 +160,15 @@ export default class TextSelection {
   handleMouseDown(event: MouseEvent) {
     this.cleanSelection()
     const tool = this.tool
-    if (!markupType.includes(tool) || document.querySelector('.dialog')?.contains(event.target)) return
+
+    const dialogs = document.querySelectorAll('.dialog')
+    if (dialogs.length) {
+      for (let i = 0; i < dialogs.length; i++) {
+        if (dialogs[i].contains(event.target)) return
+      }
+    }
+
+    if (!markupType.includes(tool)) return
     const inPage = this.testPoint(event)
     if (!inPage) return
 

+ 121 - 0
packages/core/src/annotation/layer.js

@@ -22,6 +22,8 @@ import AddComboBox from '../form/add_combo_box.js'
 import MarkupTextAnnotation from '../markup/text_annotation.js'
 import PaintLink from './paint/link'
 import Link from './link.js'
+import AddSignatureFields from '../form/add_signature_fields.js'
+import SignatureFields from '../form/signature_fields.js'
 
 // 注释的画布层
 class ComPDFAnnotationLayer {
@@ -119,6 +121,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
     }
     const shapes = ['line', 'arrow', 'circle', 'square', 'ink']
     if (markup.includes(tool) && color) {
@@ -164,6 +169,10 @@ class ComPDFAnnotationLayer {
       this.linkManager.tool = tool
       this.linkManager.color = color
     }
+    if (this.signFieldManager) {
+      this.signFieldManager.tool = tool
+      this.signFieldManager.color = color
+    }
     if (shapes.includes(tool) && color) {
       document.querySelector('.document').classList.add('annotation-edit')
       if (this.freetextManager) {
@@ -193,6 +202,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.annotateManager) {
         this.annotateManager.init()
         return
@@ -238,6 +250,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.freetextManager) {
         this.freetextManager.init()
         return
@@ -283,6 +298,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.textManager) {
         this.textManager.init()
         return
@@ -328,6 +346,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.textFieldManager) {
         this.textFieldManager.init()
         return
@@ -373,6 +394,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.checkBoxManager) {
         this.checkBoxManager.init()
         return
@@ -418,6 +442,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.radioButtonManager) {
         this.radioButtonManager.init()
         return
@@ -463,6 +490,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.pushButtonManager) {
         this.pushButtonManager.init()
         return
@@ -508,6 +538,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.comboBoxManager) {
         this.comboBoxManager.init()
         return
@@ -553,6 +586,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.listBoxManager) {
         this.listBoxManager.init()
         return
@@ -600,6 +636,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
     } else if (tool === 'link') {
       document.querySelector('.document').classList.add('annotation-edit')
       if (this.annotateManager) {
@@ -629,6 +668,9 @@ class ComPDFAnnotationLayer {
       if (this.listBoxManager) {
         this.listBoxManager.reset()
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.reset()
+      }
       if (this.linkManager) {
         this.linkManager.init()
         return
@@ -645,6 +687,54 @@ class ComPDFAnnotationLayer {
         eventBus: this.eventBus,
         selectedElementName: this.selectedElementName
       })
+    } else if (tool === 'signatureFields') {
+      document.querySelector('.document').classList.add('annotation-edit')
+      if (this.annotateManager) {
+        this.annotateManager.reset()
+      }
+      if (this.freetextManager) {
+        this.freetextManager.reset()
+      }
+      if (this.textManager) {
+        this.textManager.reset()
+      }
+      if (this.textFieldManager) {
+        this.textFieldManager.reset()
+      }
+      if (this.checkBoxManager) {
+        this.checkBoxManager.reset()
+      }
+      if (this.radioButtonManager) {
+        this.radioButtonManager.reset()
+      }
+      if (this.pushButtonManager) {
+        this.pushButtonManager.reset()
+      }
+      if (this.comboBoxManager) {
+        this.comboBoxManager.reset()
+      }
+      if (this.listBoxManager) {
+        this.listBoxManager.reset()
+      }
+      if (this.linkManager) {
+        this.linkManager.reset()
+      }
+      if (this.signFieldManager) {
+        this.signFieldManager.init()
+        return
+      }
+      this.signFieldManager = new AddSignatureFields({
+        tool,
+        color,
+        container: this.pageDiv,
+        svgElement: this.svgElement,
+        layer: this,
+        viewport: this.viewport,
+        scale: this.scale,
+        page: this.page,
+        eventBus: this.eventBus,
+        selectedElementName: this.selectedElementName
+      })
     }
   }
 
@@ -694,6 +784,9 @@ class ComPDFAnnotationLayer {
       if (this.linkManager) {
         this.linkManager.viewport = clonedViewport
       }
+      if (this.signFieldManager) {
+        this.signFieldManager.viewport = clonedViewport
+      }
     }
     if (!this.div) {
       this.renderContainer()
@@ -893,6 +986,19 @@ class ComPDFAnnotationLayer {
           })
           this.annotationsArray.push(link)
         }
+        if (annotation.type === 'signatureFields' && !annotation.isDelete) {
+          const signatureFields = new SignatureFields({
+            container: this.div,
+            annotation,
+            page: this.page,
+            viewport: this.viewport,
+            scale: this.scale,
+            eventBus: this.eventBus,
+            layer: this,
+            messageHandler: this.messageHandler
+          })
+          this.annotationsArray.push(signatureFields)
+        }
       }
     }
 
@@ -969,6 +1075,9 @@ class ComPDFAnnotationLayer {
     if (this.linkManager) {
       this.linkManager.reset()
     }
+    if (this.signFieldManager) {
+      this.signFieldManager.reset()
+    }
     this.eventBus._off('toolChanged', this.onHandleTool)
     this.eventBus._off('toolModeChanged', this.onHandleToolMode)
     this.eventBus._off('setDefaultSelect', this.onHandleDefaultSelect)
@@ -1181,6 +1290,18 @@ class ComPDFAnnotationLayer {
         highlight: this.annotationStore.highlightLink
       })
       this.annotationsArray.push(link)
+    } else if (annotation.type === 'signatureFields' && !annotation.isDelete) {
+      const signatureFields = new SignatureFields({
+        container: this.div,
+        annotation,
+        page: this.page,
+        viewport: this.viewport,
+        scale: this.scale,
+        eventBus: this.eventBus,
+        layer: this,
+        messageHandler: this.messageHandler
+      })
+      this.annotationsArray.push(signatureFields)
     }
   }
 

+ 1 - 1
packages/core/src/annotation/stamp.js

@@ -125,7 +125,7 @@ export default class Stamp extends Base {
 
       const imageArray = await this.messageHandler.sendWithPromise('GetRenderAnnot', {
         annotPtr: this.annotation.annotPtr,
-        annotation: this.annotation,
+        rect: this.annotation.rect,
         scale: this.scale
       })
 

+ 2 - 1
packages/core/src/annotation_store.js

@@ -6,5 +6,6 @@ export default {
   fileData: null,
   $t: null,
   highlightLink: true,
-  highlightForm: true
+  highlightForm: true,
+  signatures: [],
 }

+ 238 - 0
packages/core/src/form/add_signature_fields.js

@@ -0,0 +1,238 @@
+import {
+  getAbsoluteCoordinate,
+  createElement,
+  getInitialPoint,
+} from '../annotation/utils';
+
+export default class AddSignField {
+  constructor ({
+    tool,
+    color,
+    svgElement,
+    layer,
+    container,
+    viewport,
+    page,
+    eventBus
+  }) {
+    this.layer = layer
+    this._tool = tool
+    this.color = color
+    this.svgElement = svgElement
+    this.container = container
+    this.viewport = viewport
+    this.page = page
+    this.eventBus = eventBus
+    this.path = []
+
+    this.initStartPoint = null
+    this.initEndPoint = null
+    this.shapeEle = null
+
+    this.onMousedown = this.handleMouseDown.bind(this)
+    this.onMouseup = this.handleMouseUp.bind(this)
+    this.onMousemove = this.handleMouseMove.bind(this)
+
+    this.init()
+  }
+
+  init () {
+    this.container.removeEventListener('mousedown', this.onMousedown)
+    this.container.removeEventListener('touchstart', this.onMousedown)
+
+    this.container.addEventListener('mousedown', this.onMousedown)
+    this.container.addEventListener('touchstart', this.onMousedown)
+    this.svgElement.style.cursor = 'crosshair'
+    this.container.style.touchAction = 'none'
+    document.removeEventListener('mousemove', this.onMousemove)
+    document.removeEventListener('mouseup', this.onMouseup)
+    document.removeEventListener('touchmove', this.onMousemove)
+    document.removeEventListener('touchend', this.onMouseup)
+  }
+
+  get tool () {
+    return this._tool
+  }
+
+  set tool (toolType) {
+    if (toolType === this._tool) return
+    if (!toolType) {
+      this.reset()
+      this.svgElement.style.cursor = 'default'
+      this.container.style.touchAction = ''
+
+      this.container.removeEventListener('mousedown', this.onMousedown)
+      this.container.removeEventListener('touchstart', this.onMousedown)
+    }
+    this._tool = toolType
+  }
+
+  reset () {
+    this.initStartPoint = null
+    this.initEndPoint = null
+    if (this.shapeEle) this.shapeEle.remove()
+    this.shapeEle = null
+    this.svgElement.innerHTML = ''
+    this.path = []
+    document.removeEventListener('mousemove', this.onMousemove)
+    document.removeEventListener('mouseup', this.onMouseup)
+    
+    document.removeEventListener('touchmove', this.onMousemove)
+    document.removeEventListener('touchend', this.onMouseup)
+
+    this.container.removeEventListener('mousedown', this.onMousedown)
+    this.container.removeEventListener('touchstart', this.onMousedown)
+
+    this.eventBus._off('initTextfieldName', this.initTextfieldName)
+  }
+
+  handleMouseDown (event) {
+    if (this.layer.annotationStore.selectedElementName || document.getElementById("sign-image-save")) return
+    const { x, y } = getAbsoluteCoordinate(this.container, event)
+    this.initStartPoint = { x, y }
+
+    document.addEventListener('mousemove', this.onMousemove)
+    document.addEventListener('mouseup', this.onMouseup)
+    
+    document.addEventListener('touchmove', this.onMousemove)
+    document.addEventListener('touchend', this.onMouseup)
+  }
+
+  handleMouseUp () {
+    let initStartPoint = this.initStartPoint
+    let initEndPoint = this.initEndPoint
+    if (initEndPoint && initStartPoint.x !== initEndPoint.x && initStartPoint.y !== initEndPoint.y) {
+      let start = getInitialPoint(initStartPoint, this.viewport, this.viewport.scale)
+      let end = getInitialPoint(initEndPoint, this.viewport, this.viewport.scale)
+      
+      const newStart = {
+        x: Math.min(start.x, end.x),
+        y: Math.min(start.y, end.y)
+      }
+      const newEnd = {
+        x: Math.max(start.x, end.x),
+        y: Math.max(start.y, end.y)
+      }
+      
+      const rect = {
+        left: newStart.x,
+        top: newStart.y,
+        right: newEnd.x,
+        bottom: newEnd.y
+      }
+
+      const annotationData = {
+        operate: "add-form",
+        type: 'signatureFields',
+        pageIndex: this.page,
+        date: new Date(),
+        rect,
+      }
+
+      let index = 1
+      if (this.layer.annotationStore.annotationsAll) {
+        let annotArr = Object.values(this.layer.annotationStore.annotationsAll).reduce(function(acc, curr) {
+          return acc.concat(curr);
+        }, []);
+    
+        const textfieldArr = annotArr.filter(item => item.type === 'signatureFields')
+        for (let i = 0; i <= textfieldArr.length; i++) {
+          const name = 'SignatureFormField' + (i + 1)
+          if (textfieldArr.every(item => item.fieldName !== name)) {
+            index = i + 1
+            break
+          }
+        }
+      }
+      annotationData.fieldName = 'SignatureFormField' + index
+
+      this.eventBus.dispatch('annotationChange', {
+        type: 'add',
+        annotation: annotationData
+      })
+    }
+    // this.reset()
+    
+    this.initStartPoint = null
+    this.initEndPoint = null
+    if (this.shapeEle) this.shapeEle.remove()
+    this.shapeEle = null
+    this.svgElement.innerHTML = ''
+    this.path = []
+    document.removeEventListener('mousemove', this.onMousemove)
+    document.removeEventListener('mouseup', this.onMouseup)
+    
+    document.removeEventListener('touchmove', this.onMousemove)
+    document.removeEventListener('touchend', this.onMouseup)
+
+    if (this.tool === 'signatureFields') document.getElementById('createSignFieldButton').click()
+  }
+
+  handleMouseMove (event) {
+    let { x, y } = getAbsoluteCoordinate(this.container, event)
+    const { width, height } = this.viewport
+    const scale = this.viewport.scale
+    x = Math.min(width, x)
+    x = Math.max(0, x)
+    y = Math.min(height, y)
+    y = Math.max(0, y)
+    this.initEndPoint = { x, y }
+    if (this.initStartPoint.x !== this.initEndPoint.x && this.initStartPoint.y !== this.initEndPoint.y) {
+      if (!this.shapeEle) {
+        const shapeEle = createElement('div', {
+          'backgroundColor': '#93B9FD',
+          'opacity': '0.3',
+          'position': "absolute"
+        })
+        this.shapeEle = shapeEle
+        this.scene = this.container.querySelector('.annotationLayer')
+        this.scene.append(this.shapeEle)
+      } else {
+        if (event.shiftKey) {
+          const { initStartPoint, initEndPoint } = this
+          const distanceX = Math.abs(initEndPoint.x - initStartPoint.x)
+          const distanceY = Math.abs(initEndPoint.y - initStartPoint.y)
+          const distance = Math.min(distanceX, distanceY)
+          if (initStartPoint.x < initEndPoint.x) {
+            initEndPoint.x = initStartPoint.x + distance
+          } else {
+            initEndPoint.x = initStartPoint.x - distance
+          }
+          if (initStartPoint.y < initEndPoint.y) {
+            initEndPoint.y = initStartPoint.y + distance
+          } else {
+            initEndPoint.y = initStartPoint.y - distance
+          }
+        }
+        const rect = this.calculate(this.initStartPoint, this.initEndPoint)
+
+        const shapeEle = this.shapeEle
+        shapeEle.style.top = `${rect.top}px`;
+        shapeEle.style.left = `${rect.left}px`;
+        shapeEle.style.width = `${Math.abs(rect.width - 1)}px`;
+        shapeEle.style.height = `${Math.abs(rect.height - 1)}px`;
+      }
+    }
+  }
+  
+  rectCalc ({ start, end }) {
+    return {
+      top: start.y < end.y ? start.y : end.y,
+      left: start.x < end.x ? start.x : end.x,
+      width: Math.abs(end.x - start.x),
+      height: Math.abs(end.y - start.y),
+    };
+  };
+
+  calculate (start, end) {
+    const initRect = this.rectCalc({ start, end });
+    const actualbdwidth = 1 * this.viewport.scale
+
+    return {
+      top: initRect.top - actualbdwidth,
+      left: initRect.left - actualbdwidth,
+      width: initRect.width + actualbdwidth * 2,
+      height: initRect.height + actualbdwidth * 2,
+    }
+  }
+}

+ 787 - 0
packages/core/src/form/signature_fields.js

@@ -0,0 +1,787 @@
+import Base from '../annotation/base';
+import { getActualPoint, getClickPoint, createSvg, createElement } from '../annotation/utils';
+import { onClickOutside } from '../ui_utils'
+
+export default class SignatureFields extends Base {
+  
+  constructor ({
+    container,
+    annotation,
+    page,
+    viewport,
+    scale,
+    eventBus,
+    layer,
+    messageHandler
+  }) {
+    super({
+      container,
+      annotation,
+      page,
+      viewport,
+      scale,
+      eventBus
+    })
+
+    this.layer = layer
+    this.messageHandler = messageHandler
+    this.hidden = true
+    this.outline = null
+
+    this.start = null
+    this.end = null
+
+    this.newStart = null
+    this.newEnd = null
+
+    this.startCircle = null
+    this.endCircle = null
+
+    this.ratio = window.devicePixelRatio || 1
+    this.isDigital = 0
+
+    this.onMousedown = this.handleMouseDown.bind(this)
+    this.onMouseup = this.handleMouseUp.bind(this)
+    this.onMousemove = this.handleMouseMove.bind(this)
+    this.onDelete = this.handleDelete.bind(this)
+    this.render()
+  }
+
+  async render () {
+    this.eventBus._on('setProperty', this.setProperty.bind(this))
+    this.eventBus._on('deleteSignature', this.onDelete)
+    this.eventBus._on('updateSignatureFieldAp', this.updateAp.bind(this))
+
+    for (let key in this.annotation) {
+      if (key === 'background-color') {
+        let newName = 'backgroundColor'
+        this.annotation[newName] = this.annotation[key]
+        delete this.annotation[key]
+      }
+    }
+
+    const annotation = this.annotation
+
+    const { start, end } = this.getActualRect(
+      this.viewport,
+      this.scale
+    )
+    this.start = start
+    this.end = end
+
+    const rect = this.calculate(start, end)
+    
+    const annotationContainer = document.createElement('div')
+    annotationContainer.id = annotation.name
+    annotationContainer.className = 'annotation'
+    annotationContainer.style.top = rect.top + 'px'
+    annotationContainer.style.left = rect.left + 'px'
+    annotationContainer.style.width = rect.width + 'px'
+    annotationContainer.style.height = rect.height + 'px'
+    this.annotationContainer = annotationContainer
+
+    let shapeElement = createElement(
+      'div',
+      {
+        left: 0,
+        top: 0,
+        width: `${Math.abs(rect.width)}px`,
+        height: `${Math.abs(rect.height)}px`,
+        backgroundColor: '#93B9FD',
+        opacity: '0.3',
+        position: 'relative'
+      }
+    )
+    this.shapeElement = shapeElement
+
+    const signature = this.layer.annotationStore.signatures.find(item => item.signaturePtr === this.annotation.signaturePtr)
+    if (signature) {
+      this.isDigital = signature.isDigital
+      await this.getSignatureImage()
+    }
+
+    this.annotationContainer.append(this.shapeElement)
+    this.annotationContainer.addEventListener('mousedown', this.handleClick.bind(this))
+    this.container.append(this.annotationContainer)
+
+    this.outerLineContainer = document.createElement('div')
+    this.outerLineContainer.className = 'outline-container'
+    this.deletetButton = createSvg(
+      "svg",
+      {
+        width: "30",
+        height: "30",
+        viewBox: "0 0 30 30",
+        fill: "none",
+        class: 'delete-button'
+      },
+      {
+        width: '30px',
+        height: '30px'
+      }
+    );
+    this.deletetButton.addEventListener('click', this.onDelete)
+    this.deletetButton.innerHTML = this.deleteSvgStr
+
+    const left = (rect.left + rect.width - 30) + 'px'
+    const top = (rect.top + rect.height + 8) + 'px'
+    this.deletetButton.style.left = left
+    this.deletetButton.style.top = top
+    
+    const outerLine = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+
+    outerLine.style.position = 'absolute'
+    outerLine.style.zIndex = 1
+    outerLine.style.left = `${rect.left - 8}px`
+    outerLine.style.top = `${rect.top - 8}px`
+    outerLine.style.width = `${rect.width + 8 * 2}px`
+    outerLine.style.height = `${rect.height + 8 * 2}px`
+    this.outerLine = outerLine
+
+    this.moveRect = createSvg(
+      "rect",
+      {
+        class: "move",
+        'data-id': "move",
+        stroke: "#4982E6",
+        'stroke-width': 1,
+        'fill-opacity': 0,
+        width: rect.width + 8,
+        height: rect.height + 8,
+        x: 4,
+        y: 4
+      }
+    );
+    
+    this.topLeftRect = createSvg(
+      "rect",
+      {
+        class: "nw-resize",
+        'data-id': "nw-resize",
+        fill: "#FFFFFF",
+        stroke: "#4982E6",
+        'stroke-width': 1,
+        width: 6,
+        height: 6,
+        x: 1,
+        y: 1
+      }
+    );
+
+    this.LeftRect = createSvg(
+      "rect",
+      {
+        class: "w-resize",
+        'data-id': "w-resize",
+        fill: "#FFFFFF",
+        stroke: "#4982E6",
+        'stroke-width': 1,
+        width: 6,
+        height: 6,
+        x: 1,
+        y: rect.height / 2 + 5
+      }
+    );
+
+    this.bottomLeftRect = createSvg(
+      "rect",
+      {
+        class: "sw-resize",
+        'data-id': "sw-resize",
+        fill: "#FFFFFF",
+        stroke: "#4982E6",
+        'stroke-width': 1,
+        width: 6,
+        height: 6,
+        x: 1,
+        y: rect.height + 9
+      }
+    );
+
+    this.bottomRect = createSvg(
+      "rect",
+      {
+        class: "s-resize",
+        'data-id': "s-resize",
+        fill: "#FFFFFF",
+        stroke: "#4982E6",
+        'stroke-width': 1,
+        width: 6,
+        height: 6,
+        x: rect.width / 2 + 5,
+        y: rect.height + 9
+      }
+    );
+    
+    this.bottomRightRect = createSvg(
+      "rect",
+      {
+        class: "se-resize",
+        'data-id': "se-resize",
+        fill: "#FFFFFF",
+        stroke: "#4982E6",
+        'stroke-width': 1,
+        width: 6,
+        height: 6,
+        x: rect.width + 9,
+        y: rect.height + 9
+      }
+    );
+
+    this.rightRect = createSvg(
+      "rect",
+      {
+        class: "e-resize",
+        'data-id': "e-resize",
+        fill: "#FFFFFF",
+        stroke: "#4982E6",
+        'stroke-width': 1,
+        width: 6,
+        height: 6,
+        x: rect.width + 9,
+        y: rect.height / 2 + 5
+      }
+    );
+
+    this.topRightRect = createSvg(
+      "rect",
+      {
+        class: "ne-resize",
+        'data-id': "ne-resize",
+        fill: "#FFFFFF",
+        stroke: "#4982E6",
+        'stroke-width': 1,
+        width: 6,
+        height: 6,
+        x: rect.width + 9,
+        y: 1
+      }
+    );
+
+    this.topRect = createSvg(
+      "rect",
+      {
+        class: "n-resize",
+        'data-id': "n-resize",
+        fill: "#FFFFFF",
+        stroke: "#4982E6",
+        'stroke-width': 1,
+        width: 6,
+        height: 6,
+        x: rect.width / 2 + 5,
+        y: 1
+      }
+    );
+
+    this.outerLine.append(this.moveRect)
+    this.outerLine.append(this.topLeftRect)
+    this.outerLine.append(this.LeftRect)
+    this.outerLine.append(this.bottomLeftRect)
+    this.outerLine.append(this.bottomRect)
+    this.outerLine.append(this.bottomRightRect)
+    this.outerLine.append(this.rightRect)
+    this.outerLine.append(this.topRightRect)
+    this.outerLine.append(this.topRect)
+    this.outerLineContainer.append(this.outerLine)
+    this.outerLineContainer.append(this.deletetButton)
+  }
+
+  getActualRect (viewport, s,) {
+    const { left: x1, top: y1, right: x2, bottom: y2 } = this.annotation.rect
+    
+    const start = getActualPoint(
+      {
+        x: x1,
+        y: y1
+      },
+      viewport,
+      s
+    )
+    const end = getActualPoint(
+      {
+        x: x2,
+        y: y2
+      },
+      viewport,
+      s
+    )
+    return {
+      start,
+      end
+    }
+  }
+
+  rectCalc ({ start, end }) {
+    return {
+      top: start.y < end.y ? start.y : end.y,
+      left: start.x < end.x ? start.x : end.x,
+      width: Math.abs(end.x - start.x),
+      height: Math.abs(end.y - start.y),
+    };
+  };
+
+  calculate (start, end) {
+    const initRect = this.rectCalc({ start, end })
+    this.initRect = initRect
+    const actualbdwidth = (this.annotation.borderWidth || 2) * this.scale
+
+    return {
+      top: initRect.top - actualbdwidth,
+      left: initRect.left - actualbdwidth,
+      width: initRect.width + actualbdwidth * 2,
+      height: initRect.height + actualbdwidth * 2,
+    }
+  }
+
+  getInitialPoint () {
+    const s = this.scale
+    const startX = this.start.x
+    const startY = this.start.y
+    const endX = this.end.x
+    const endY = this.end.y
+
+    let x1, y1, x2, y2;
+    x1 = startX / s;
+    y1 = startY / s; 
+    x2 = endX / s;
+    y2 = endY / s;
+    return {
+      start: {
+        x: x1,
+        y: y1,
+      },
+      end: {
+        x: x2,
+        y: y2,
+      },
+    };
+  }
+
+  updateTool () {
+    if (this.hidden) {
+      if (this.layer.annotationStore.selectedElementName === this.annotation.name) {
+        this.layer.annotationStore.selectedElementName = null
+      }
+      this.outerLineContainer.remove()
+    } else {
+      if (this.layer.annotationStore.selectedElementName !== this.annotation.name) {
+        this.layer.annotationStore.selectedElementName = this.annotation.name
+      }
+      this.container.append(this.outerLineContainer)
+      this.outerLine.addEventListener('mousedown', this.onMousedown)
+      this.outerLine.addEventListener('touchstart', this.onMousedown)
+    }
+  }
+
+  handleClick () {
+    if (!this.hidden || document.fullscreenElement || this.layer.annotationStore.creating || this.layer.toolMode === 'editor' || document.getElementById("sign-image-save")) return
+    if (this.isDigital) return
+    if (this.layer.pageDiv.querySelector('.annotationLayer svg').style.cursor === 'crosshair' && this.layer.toolMode !== 'sign') return
+
+    if (this.layer.tool === 'signatureFields') {
+      this.hidden = false
+      this.updateTool()
+
+      onClickOutside([this.annotationContainer, this.outerLine, this.deletetButton], this.handleOutside.bind(this))
+    } else {
+      this.eventBus.dispatch('openSelectSignTypeDialog', this.annotation)
+    }
+  }
+
+  handleOutside () {
+    if (!this.hidden && this.layer.annotationStore.selectedElementName === this.annotation.name) {
+      this.layer.annotationStore.selectedElementName = null
+    }
+
+    this.hidden = true
+
+    if (this.layer.annotationStore.selectedElementName === this.annotation.name) {
+      this.layer.annotationStore.selectedElementName = null
+    }
+    this.outerLine.removeEventListener('mousedown', this.onMousedown)
+    this.outerLine.removeEventListener('touchstart', this.onMousedown)
+    this.outerLineContainer.remove()
+  }
+
+  handleMouseDown (event) {
+    if (event.button !== 0 && event.type === 'mousedown') return
+    if (this.layer.tool) {
+      event.stopPropagation()
+    }
+    const operatorId = event.target.getAttribute('data-id')
+  
+    const { pageX, pageY } = getClickPoint(event)
+    this.startState = {
+      operator: operatorId,
+      clickX: pageX,
+      clickY: pageY,
+    }
+
+    document.addEventListener('mousemove', this.onMousemove)
+    document.addEventListener('mouseup', this.onMouseup)
+    document.addEventListener('touchmove', this.onMousemove)
+    document.addEventListener('touchend', this.onMouseup)
+  }
+
+  handleMouseMove (event) {
+    if (event.button !== 0 && event.type === 'mousemove') return
+    if (event.type === 'touchmove') {
+      document.body.style.overscrollBehavior = 'none';
+    }
+    this.moving = true
+    const { pageX, pageY } = getClickPoint(event)
+    const start = this.start
+    const end = this.end
+    const startState = this.startState
+
+    const { width, height } = this.viewport
+    if (startState.operator === 'nw-resize') {
+      let x1 = pageX - (startState.clickX - this.start.x);
+      let y1 = pageY - (startState.clickY - this.start.y);
+      x1 = Math.min(end.x - 5, x1)
+      x1 = Math.max(0, x1)
+      y1 = Math.min(end.y - 5, y1)
+      y1 = Math.max(0, y1)
+
+      this.update({
+        start: { x: x1, y: y1 },
+        end,
+      });
+    } else if (startState.operator === 'w-resize') {
+      let x1 = pageX - (startState.clickX - this.start.x);
+      x1 = Math.min(end.x - 5, x1)
+      x1 = Math.max(0, x1)
+
+      this.update({
+        start: { x: x1, y: start.y },
+        end,
+      });
+    } else if (startState.operator === 'sw-resize') {
+      let x1 = pageX - (startState.clickX - this.start.x);
+      let y1 = pageY - (startState.clickY - this.end.y);
+      x1 = Math.min(end.x - 5, x1)
+      x1 = Math.max(0, x1)
+      y1 = Math.min(height, y1)
+      y1 = Math.max(start.y + 5, y1)
+
+      this.update({
+        start: { x: x1, y: start.y },
+        end: { x: end.x, y: y1 },
+      });
+    } else if (startState.operator === 's-resize') {
+      let y1 = pageY - (startState.clickY - this.end.y);
+      y1 = Math.min(height, y1)
+      y1 = Math.max(start.y + 5, y1)
+      this.update({
+        start,
+        end: { x: end.x, y: y1 },
+      });
+    } else if (startState.operator === 'se-resize') {
+      let x = pageX - (startState.clickX - this.end.x);
+      let y = pageY - (startState.clickY - this.end.y);
+      x = Math.min(width, x)
+      x = Math.max(start.x + 5, x)
+      y = Math.min(height, y)
+      y = Math.max(start.y + 5, y)
+      this.update({
+        start,
+        end: { x, y },
+      });
+    } else if (startState.operator === 'e-resize') {
+      let x = pageX - (startState.clickX - this.end.x);
+      x = Math.min(width, x)
+      x = Math.max(start.x + 5, x)
+      this.update({
+        start,
+        end: { x, y: end.y },
+      });
+    } else if (startState.operator === 'ne-resize') {
+      let x = pageX - (startState.clickX - this.end.x);
+      let y = pageY - (startState.clickY - this.start.y);
+      x = Math.min(width, x)
+      x = Math.max(start.x + 5, x)
+      y = Math.min(end.y - 5, y)
+      y = Math.max(0, y)
+      this.update({
+        start: { x: start.x, y },
+        end: { x, y: end.y },
+      });
+    } else if (startState.operator === 'n-resize') {
+      let y = pageY - (startState.clickY - this.start.y);
+      y = Math.min(end.y - 5, y)
+      y = Math.max(0, y)
+      this.update({
+        start: { x: start.x, y },
+        end,
+      });
+    } else if (startState.operator === 'move') {
+      let x1 = pageX - (startState.clickX - this.start.x);
+      let y1 = pageY - (startState.clickY - this.start.y);
+      let x2 = pageX - (startState.clickX - this.end.x);
+      let y2 = pageY - (startState.clickY - this.end.y);
+
+      const rect = {
+        width: Math.abs(start.x -end.x),
+        height: Math.abs(start.y -end.y)
+      }
+      if (x1 < x2) {
+        x1 = Math.max(0, x1)
+        x1 = Math.min(width - rect.width, x1)
+        x2 = Math.max(rect.width, x2)
+        x2 = Math.min(width, x2)
+      } else {
+        x2 = Math.max(0, x2)
+        x2 = Math.min(width - rect.width, x2)
+        x1 = Math.max(rect.width, x1)
+        x1 = Math.min(width, x1)
+      }
+
+      if (y1 < y2) {
+        y1 = Math.max(0, y1)
+        y1 = Math.min(height - rect.height, y1)
+        y2 = Math.max(rect.height, y2)
+        y2 = Math.min(height, y2)
+      } else {
+        y2 = Math.max(0, y2)
+        y2 = Math.min(height - rect.height, y2)
+        y1 = Math.max(rect.height, y1)
+        y1 = Math.min(height, y1)
+      }
+
+      this.update({
+        start: { x: x1, y: y1 },
+        end: { x: x2, y: y2 },
+      });
+    }
+  }
+
+  handleDelete (data) {
+    const event = data instanceof Event ? data : null
+    if (!this.annotationContainer) return
+    if (event || (!event && data.signaturePtr !== this.annotation.signaturePtr)) return
+    if (this.layer.tool && event) {
+      event.stopPropagation()
+    }
+    if (event) this.handleOutside()
+    this.annotationContainer.remove()
+
+    this.annotation.isDelete = true
+    const annotationData = {
+      type: 'delete',
+      annotation: {
+        operate: "del-annot",
+        name: this.annotation.name,
+        pageIndex: this.page
+      }
+    }
+    this.annotation.annotPtr && (annotationData.annotation.annotPtr = this.annotation.annotPtr)
+    this.eventBus.dispatch('annotationChange', annotationData)
+  }
+
+  handleMouseUp (event) {
+    if (event.button !== 0 && event.type === 'mouseup') return
+    if (this.layer.tool) {
+      event.stopPropagation()
+    }
+    if (event.type === 'touchend') {
+      document.body.style.overscrollBehavior = 'auto';
+    }
+    this.moving = false
+    document.removeEventListener('mousemove', this.onMousemove)
+    document.removeEventListener('mouseup', this.onMouseup)
+    document.removeEventListener('touchmove', this.onMousemove)
+    document.removeEventListener('touchend', this.onMouseup)
+    
+    const { pageX, pageY } = getClickPoint(event)
+    if (pageX === this.startState.clickX && pageY === this.startState.clickY) return
+
+    this.start = this.newStart
+    this.end = this.newEnd
+
+    const annotation = this.annotation
+    const { start, end } = this.getInitialPoint()
+
+    const rect = {
+      left: start.x,
+      top: start.y,
+      right: end.x,
+      bottom: end.y
+    }
+
+    this.eventBus.dispatch('annotationChange', {
+      type: 'modify',
+      annotation: {
+        operate: "mod-form",
+        name: annotation.name,
+        pageIndex: this.page,
+        pagePtr: annotation.pagePtr,
+        annotPtr: annotation.annotPtr,
+        rect
+      }
+    })
+  }
+
+  update ({ start, end }) {
+    const rect = this.calculate(start, end)
+
+    this.newStart = start
+    this.newEnd = end
+
+    this.annotationContainer.style.top = rect.top + 'px'
+    this.annotationContainer.style.left = rect.left + 'px'
+    this.annotationContainer.style.width = rect.width + 'px'
+    this.annotationContainer.style.height = rect.height + 'px'
+    
+    this.annotationContainer.style.width = `${rect.width}px`;
+    this.annotationContainer.style.height = `${rect.height}px`;
+
+    this.shapeElement.style.width = `${rect.width}px`;
+    this.shapeElement.style.height = `${rect.height}px`;
+
+    this.outerLine.style.left = `${rect.left - 8}px`
+    this.outerLine.style.top = `${rect.top - 8}px`
+    this.outerLine.style.width = `${rect.width + 8 * 2}px`
+    this.outerLine.style.height = `${rect.height + 8 * 2}px`
+
+    const left = (rect.left + rect.width - 30) + 'px'
+    const top = (rect.top + rect.height + 8) + 'px'
+    this.deletetButton.style.left = left
+    this.deletetButton.style.top = top
+    
+    this.moveRect.setAttribute('width',  rect.width + 9)
+    this.moveRect.setAttribute('height',  rect.height + 9)
+    
+    this.LeftRect.setAttribute("y", rect.height / 2 + 5)
+
+    this.bottomLeftRect.setAttribute("y", rect.height + 9)
+
+    this.bottomRect.setAttribute("x", rect.width / 2 + 5)
+    this.bottomRect.setAttribute("y", rect.height + 9)
+
+
+    this.bottomRightRect.setAttribute("x", rect.width + 9)
+    this.bottomRightRect.setAttribute("y", rect.height + 9)
+
+
+    this.rightRect.setAttribute("x", rect.width + 9)
+    this.rightRect.setAttribute("y", rect.height / 2 + 5)
+
+    
+    this.topRightRect.setAttribute("x", rect.width + 9)
+
+    this.topRect.setAttribute("x", rect.width / 2 + 5)
+  }
+
+  async handlePropertyPanel (props) {
+    if (this.layer.annotationStore.selectedElementName !== this.annotation.name) return
+    const annotation = this.annotation
+    let updatePropsNum = 0
+    for (const key in props) {
+      for (const item in annotation) {
+        if (item === key && annotation[item] !== props[key]) {
+          annotation[item] = props[key]
+          updatePropsNum ++
+        }
+      }
+    }
+
+    if (updatePropsNum > 0) {
+      const annotationData = {
+        operate: "mod-form",
+        name: annotation.name,
+        pageIndex: this.page,
+        pagePtr: annotation.pagePtr,
+        annotPtr: annotation.annotPtr,
+        url: annotation.url,
+        destPage: annotation.destPage
+      }
+      const updateAnnot = Object.assign({}, annotationData, props)
+      await this.eventBus.dispatch('annotationChange', {
+        type: 'modify',
+        annotation: updateAnnot
+      })
+    }
+
+    this.handleOutside()
+  }
+
+  async setProperty (props) {
+    if (this.layer.annotationStore.selectedElementName !== this.annotation.name) return
+    const annotation = this.annotation
+    let updatePropsNum = 0
+    for (const key in props) {
+      for (const item in annotation) {
+        if (item === key && annotation[item] !== props[key]) {
+          annotation[item] = props[key]
+          updatePropsNum ++
+        }
+      }
+    }
+
+    if (updatePropsNum > 0) {
+      const annotationData = {
+        operate: "mod-form",
+        name: annotation.name,
+        pageIndex: this.page,
+        pagePtr: annotation.pagePtr,
+        annotPtr: annotation.annotPtr,
+        url: annotation.url,
+        destPage: annotation.destPage
+      }
+      const updateAnnot = Object.assign({}, annotationData, props)
+      await this.eventBus.dispatch('annotationChange', {
+        type: 'modify',
+        annotation: updateAnnot
+      })
+    }
+  }
+
+  async getSignatureImage() {
+    const rect = this.initRect
+    const imgRect = {
+      width: parseInt(rect.width * this.ratio + 1),
+      height: parseInt(rect.height * this.ratio + 1)
+    }
+
+    const imageArray = await this.messageHandler.sendWithPromise('GetRenderAnnot', {
+      annotPtr: this.annotation.annotPtr,
+      rect: this.annotation.rect,
+      scale: this.scale * this.ratio
+    })
+
+    const canvas = document.createElement('canvas')
+    canvas.style.width = imgRect.width + 'px'
+    canvas.style.height = imgRect.height + 'px'
+    canvas.width = imgRect.width
+    canvas.height = imgRect.height
+    let drawContext = canvas.getContext("2d")
+    drawContext.clearRect(0, 0, canvas.width, canvas.height)
+    let imageData = drawContext.createImageData(imgRect.width, imgRect.height)
+    imageData.data.set(imageArray)
+    drawContext.putImageData(imageData, 0, 0)
+    const imgSrc = canvas.toDataURL("image/png", 1)
+
+    if (this.imgEle) {
+      this.imgEle.src = imgSrc
+    } else {
+      let imgEle = document.createElement('img')
+      imgEle.style.width = '100%'
+      imgEle.style.height = '100%'
+      imgEle.style.pointerEvents = 'none'
+
+      if (this.isDigital) this.annotationContainer.style.zIndex = 1
+      this.shapeElement.style.position = 'absolute'
+      this.shapeElement.style.zIndex = -1
+
+      imgEle.src = imgSrc
+      this.imgEle = imgEle
+
+      this.annotationContainer.append(this.imgEle)
+    }
+  }
+
+  async updateAp(annotPtr) {
+    if (this.annotation.annotPtr !== annotPtr) return
+    await this.getSignatureImage()
+  }
+}

+ 164 - 28
packages/core/src/index.js

@@ -16,7 +16,7 @@ import { EventBus, isValidRotation, isValidScrollMode, shadow, isValidSpreadMode
 import { ALIGNMAP, AnnotationTypeString, AnnotationState, AnnotationStateString } from "../constants";
 import { PDFPresentationMode } from "./pdf_presentation_mode.js";
 import annotationStore from "./annotation_store"
-import { InkSign } from "./ink_sign"
+import { Signatures } from "./signatures"
 import MessageHandler from "./message_handler"
 import JSZip from 'jszip'
 import Outline from './Outline'
@@ -256,11 +256,6 @@ class ComPDFKitViewer {
         const blob = await response.blob()
         this.#fonFile = new File([blob], 'font.ff', { type: blob.type })
       }
-      const doc = await this.messageHandler.sendWithPromise('InitDocument', {
-        fontJson: this.#fonFile
-      })
-
-      this.doc = doc
     }
     return verified
   }
@@ -411,16 +406,16 @@ class ComPDFKitViewer {
   }
 
   addAnnotations(annotation) {
-    this.handleAnnotationChange({
-      type: 'add',
-      annotation: [annotation]
+    const annotations = Array.isArray(annotation) ? annotation : [annotation]
+    annotations.forEach(item => {
+      this.handleAnnotationChange({ type: 'add', annotation: item })
     })
   }
 
   delAnnotations(annotation) {
-    this.handleAnnotationChange({
-      type: 'delete',
-      annotation: [annotation]
+    const annotations = Array.isArray(annotation) ? annotation : [annotation]
+    annotations.forEach(item => {
+      this.handleAnnotationChange({ type: 'delete', annotation: item })
     })
   }
 
@@ -446,6 +441,7 @@ class ComPDFKitViewer {
     this.distanceChangedCallback = options.distanceChangedCallback || this.distanceChangedCallback
     this.annotations = null
     annotationStore.annotationsAll = null
+    annotationStore.signatures = null
     annotationStore.selectedName = null
     this.fontFileInited = false
     this.saveAction = options.saveAction || null
@@ -508,12 +504,16 @@ class ComPDFKitViewer {
           }
 
           let buffer = new Uint8Array(binaryData)
-          await this.messageHandler.sendWithPromise('LoadFile', {
+          const doc = await this.messageHandler.sendWithPromise('LoadFile', {
+            doc: this.doc,
+            pagesPtr: this.pagesPtr,
             buffer,
             fontFile: this.#fonFile,
             fontName: 'DroidSansFallbackFull'
           })
 
+          this.doc = doc
+
           const loadRes = await this.messageHandler.sendWithPromise('LoadDocumentByStream', {
             doc: this.doc,
             fileId: 0,
@@ -582,6 +582,19 @@ class ComPDFKitViewer {
 
           await this.initPage()
 
+          const response = await fetch('./lib/result_sign.pfx')
+          const blob = await response.blob()
+          const certFile = new File([blob], 'result_sign.pfx', { type: blob.type })
+          this.certFile = certFile
+          // const a = await this.messageHandler.sendWithPromise('AddSignature', {
+          //   doc: this.doc,
+          //   pagePtr: this.pagesPtr[0],
+          //   certFile,
+          // })
+
+          // console.log(a)
+          // saveAs(a, 'test.pdf')
+
           if (this.webviewerServer) {
             if (this.controller) {
               this.controller.abort()
@@ -665,6 +678,15 @@ class ComPDFKitViewer {
     )
   }
 
+  async addSignature(ptr) {
+    const a = await this.messageHandler.sendWithPromise('AddSignature', {
+      doc: this.doc,
+      pagePtr: this.pagesPtr[0],
+      certFile,
+      ptr
+    })
+  }
+
   getSelectedText(pageNumber) {
     if (pageNumber) {
       const page = this.pdfViewer._pages[pageNumber - 1]
@@ -1088,6 +1110,8 @@ class ComPDFKitViewer {
   }
 
   async getAnnotations() {
+    const signaturePtrList = await this.getSignatures()
+
     const annotations = []
     const pagesPtr = this.pagesPtr
     for (let pageIndex = 0; pageIndex < this.pagesCount; pageIndex++) {
@@ -1109,7 +1133,8 @@ class ComPDFKitViewer {
             doc: this.doc,
             annotPtr: annotation.annotPtr,
             pagePtr: annotation.pagePtr,
-            typeInt
+            typeInt,
+            signaturePtrList
           })
         } else {
           attr = await this.messageHandler.sendWithPromise('GetAnnotation', {
@@ -1486,6 +1511,7 @@ class ComPDFKitViewer {
     eventBus._on("createSignature", this.handleCreateSignature.bind(this));
     eventBus._on("handleField", this.handleField.bind(this));
     eventBus._on("pageNumberChanged", this.pageNumberChanged.bind(this));
+    eventBus._on("updateSignatureAp", this.updateSignatureAp.bind(this));
   }
 
   bindWindowEvents() {
@@ -1576,7 +1602,7 @@ class ComPDFKitViewer {
     let annotation = data.annotation
     this.annotationHistory.push(annotation)
     let annotateHandles = []
-    const formTypes = ['textfield', 'checkbox', 'radiobutton', 'listbox', 'combobox', 'pushbutton']
+    const formTypes = ['textfield', 'checkbox', 'radiobutton', 'listbox', 'combobox', 'pushbutton', 'signatureFields']
     if (data.type === 'add') {
       if (!Array.isArray(annotation)) {
         annotation = [annotation]
@@ -2001,6 +2027,11 @@ class ComPDFKitViewer {
     }
   }
 
+  async loadCertificates(data) {
+    const result = await this.messageHandler.sendWithPromise('LoadCertificates', data)
+    return result
+  }
+
   async download(download = false, data, type) {
     if (data) {
       data.forEach(file => {
@@ -2544,9 +2575,7 @@ class ComPDFKitViewer {
 
       case "Print":
         const url = await this.download(false, null, 3)
-        this.triggerPrinting(url);
         return url
-        break;
 
       case "SaveAs":
         this.downloadOrSave();
@@ -2812,26 +2841,43 @@ class ComPDFKitViewer {
     return targetObj
   }
 
-  handleSign(flag, param) {
-    if (flag === 'create' && !this.InkSign) {
-      this.InkSign = new InkSign({
+  handleSign(data) {
+    const { type = 0, flag, param } = data
+
+    if (flag === 'create' && !this.Signatures) {
+      this.Signatures = new Signatures({
         pdfViewer: this.pdfViewer,
         eventBus: this.eventBus,
+        type
       })
     }
-    if (!this.InkSign) return;
+    if (flag === 'save' && type) {
+      const { pkcs12Buffer, password } = data
+      this.updateSignatureAp({
+        type,
+        annotPtr: data.annotation.annotPtr,
+        pkcs12Buffer,
+        password,
+        ...param
+      })
+    }
+    if (!this.Signatures) return
+
     if (flag === 'clear' || flag === 'create') {
-      this.InkSign.clear()
+      this.Signatures.clear()
     }
     if (flag === 'save') {
-      this.InkSign.toImage(param)
+      this.Signatures.toImage(param)
     }
     if (flag === 'reset') {
-      this.InkSign.reset()
-      this.InkSign = null
+      this.Signatures.reset()
+      this.Signatures = null
     }
-    if (flag === "update") {
-      this.InkSign.update(param)
+    if (flag === 'update') {
+      this.Signatures.update(param)
+    }
+    if (flag === 'image') {
+      return this.Signatures.getImageBase64(param)
     }
   }
 
@@ -3404,7 +3450,7 @@ class ComPDFKitViewer {
       }
 
       const { status } = result
- 
+
       if(!status) console.warn('move', status)
     }
 
@@ -3907,6 +3953,96 @@ class ComPDFKitViewer {
   setAnnotator(name) {
     this.annotator = name
   }
+
+  async getSignature(data) {
+    const ratio = window.devicePixelRatio || 1
+
+    const rect = data.rect
+    const width = parseInt(rect.width * ratio)
+    const height = parseInt(rect.height * ratio)
+    rect.width = width
+    rect.height = height
+
+    const imageArray = await this.messageHandler.sendWithPromise('CreateSignatureAP', {
+      rect,
+      content: data.content,
+      text: data.text ? data.text : '',
+      isContentAlginLeft: data.isContentAlginLeft ? data.isContentAlginLeft : false,
+      isDrawOnlyContent: data.isDrawOnlyContent ? data.isDrawOnlyContent : false,
+      isDrowLogo: data.isDrowLogo ? data.isDrowLogo : false,
+      logoBase64: data.logoBase64 ? data.logoBase64 : '',
+      imageBase64: data.imageBase64 ? data.imageBase64 : ''
+    })
+
+    const canvas = document.createElement('canvas')
+    canvas.style.width = width + 'px'
+    canvas.style.height = height + 'px'
+    canvas.width = width
+    canvas.height = height
+    let drawContext = canvas.getContext("2d")
+    drawContext.clearRect(0, 0, canvas.width, canvas.height)
+    let imageData = drawContext.createImageData(width, height)
+    imageData.data.set(imageArray)
+    drawContext.putImageData(imageData, 0, 0)
+    const imgSrc = canvas.toDataURL("image/png", 1)
+    return imgSrc
+  }
+
+  async deleteSignature(signature) {
+    this.messageHandler.sendWithPromise('RemoveSignature', {
+      doc: this.doc,
+      signaturePtr: signature.signaturePtr
+    })
+
+    this.eventBus.dispatch('deleteSignature', signature)
+
+    annotationStore.signatures.splice(signature.index, 1)
+    await this.getSignatures()
+  }
+
+  async getSignatures() {
+    const { signatures, signaturePtrList } = await this.messageHandler.sendWithPromise('GetSignatures', {
+      doc: this.doc
+    })
+    annotationStore.signatures = signatures
+    this.eventBus.dispatch('getSignatures', annotationStore.signatures)
+    return signaturePtrList
+  }
+
+  async updateSignatureAp(data) {
+    const { type, pagePtr, rect, annotPtr, imageArray, imageBase64 } = data
+
+    if (type) {
+      const { content, isDrawOnlyContent = false, isDrowLogo = false, isContentAlginLeft, text, logoBase64, pkcs12Buffer, password, } = data
+      const blobData = await this.messageHandler.sendWithPromise('UpdateDigitalSignAp', {
+        doc: this.doc,
+        annotPtr,
+        content,
+        isDrawOnlyContent,
+        isDrowLogo,
+        isContentAlginLeft,
+        text,
+        logoBase64,
+        imageBase64,
+        pkcs12Buffer,
+        password,
+      })
+      const newUrl = URL.createObjectURL(blobData)
+      const filename = this._docName
+      await this.loadDocument(newUrl, { filename, notUpdatePwd: true })
+      URL.revokeObjectURL(blobData)
+    } else {
+      await this.messageHandler.sendWithPromise('UpdateSignatureAP', {
+        annotPtr,
+        pagePtr,
+        rect,
+        imageArray,
+        imageBase64
+      })
+      this.eventBus.dispatch('updateSignatureFieldAp', annotPtr)
+    }
+
+  }
 }
 
 class PDFWorker {

+ 212 - 179
packages/core/src/ink_sign.js

@@ -1,20 +1,20 @@
 import {
   getInitialPoint
 } from './annotation/utils';
-class InkSign {
-  constructor({ container, pdfViewer, eventBus }) {
-    this.container = container
+class Signatures {
+  constructor({ pdfViewer, eventBus, type }) {
     this.pdfViewer = pdfViewer
     this.eventBus = eventBus
+    this.type = type // 0: electronic signature, 1: digital signature
 
     this.path = []
-    this.inkPath=[]
-    this.currentPath=[]
-    this.areaBound=[NaN,NaN,NaN,NaN]
+    this.inkPath = []
+    this.currentPath = []
+    this.areaBound = [NaN, NaN, NaN, NaN]
     this.inkPadding = { x: 10, y: 10 }
     this.strokeWidth = 5
     this.strokColor = "#000"
-    
+
     this.onMousedown = this.handleMouseDown.bind(this)
     this.onMousemove = this.handleMouseMove.bind(this)
     this.onMouseup = this.handleMouseUp.bind(this)
@@ -23,8 +23,11 @@ class InkSign {
     this.init()
   }
 
-  init () {
-    this.canvas = document.getElementById('trackpadCanvas')
+  init() {
+    const digitalTrackpadCanvas = document.getElementById('digitalTrackpadCanvas')
+    const electronicTrackpadCanvas = document.getElementById('electronicTrackpadCanvas')
+
+    this.canvas = this.type ? digitalTrackpadCanvas : electronicTrackpadCanvas
     this.ctx = this.canvas.getContext('2d')
     this.canvas.style.cursor = 'crosshair'
 
@@ -38,94 +41,88 @@ class InkSign {
     this.eventBus._on('initCursorStyle', this.onHandleInitCursorStyle)
   }
 
-  handleMouseDown (e) {
+  handleMouseDown(e) {
     if (e.changedTouches && e.changedTouches.length !== 1) return
     const clientX = e.clientX || e.changedTouches[0].clientX
     const clientY = e.clientY || e.changedTouches[0].clientY
     //计算鼠标在画布的距离
-    var disX = clientX - this.canvas.offsetLeft
-    var disY = clientY - this.canvas.offsetTop
+    var disX = clientX - this.canvas.getBoundingClientRect().left
+    var disY = clientY - this.canvas.getBoundingClientRect().top
     //设置画线的宽,与颜色
     this.ctx.lineWidth = this.strokeWidth
     this.ctx.strokeStyle = this.strokColor
-    this.ctx.lineJoin = 'round';
-    this.ctx.lineCap = 'round';
-   
-    if(this.currentPath.length>0)
-    {
+    this.ctx.lineJoin = 'round'
+    this.ctx.lineCap = 'round'
+
+    if (this.currentPath.length > 0) {
       this.inkPath.push(this.currentPath)
-      this.currentPath=[]
+      this.currentPath = []
     }
-    this.ctx.beginPath();
+    this.ctx.beginPath()
     //设置画的起始点
     this.ctx.moveTo(disX, disY)
-    this.mousedown=true
-    this.pressPoint={x:disX,y:disY}
+    this.mousedown = true
+    this.pressPoint = { x: disX, y: disY }
     document.addEventListener('mousemove', this.onMousemove)
     document.addEventListener('mouseup', this.onMouseup)
     document.addEventListener('touchmove', this.onMousemove)
     document.addEventListener('touchend', this.onMouseup)
   }
-  
-  handleMouseMove (e) {
+
+  handleMouseMove(e) {
     const clientX = e.clientX || e.changedTouches[0].clientX
     const clientY = e.clientY || e.changedTouches[0].clientY
-    var disX = clientX - this.canvas.offsetLeft
-    var disY = clientY - this.canvas.offsetTop
+    const disX = clientX - this.canvas.getBoundingClientRect().left
+    const disY = clientY - this.canvas.getBoundingClientRect().top
 
     if (!(disX > this.canvasWidth || disY > this.canvasHeight || disX < 0 || disY < 0)) {
-      //移动时设置画线的结束位置。并且显示
-      let savePoint=true
-      if(this.mousedown)
-      {
-        if( this.pressPoint && (disX==this.pressPoint.x && disY==this.pressPoint.y))
-        {
-          //避免保存只单击一个点
-          savePoint=false
+      // 移动时设置画线的结束位置并显示
+      let savePoint = true
+      if (this.mousedown) {
+        if (this.pressPoint && (disX == this.pressPoint.x && disY == this.pressPoint.y)) { // 避免保存只单击一个点
+          savePoint = false
         }
       }
-      
-      if(savePoint)
-      {
-        this.ctx.lineTo(disX, disY) //鼠标点下去的位置
+
+      if (savePoint) {
+        this.ctx.lineTo(disX, disY) // 鼠标点下去的位置
         this.ctx.stroke()
         this.path.push([disX, disY])
-        this.currentPath.push({x:disX,y:disY})
+        this.currentPath.push({ x: disX, y: disY })
       }
     }
   }
 
-  handleMouseUp () {
+  handleMouseUp() {
     document.removeEventListener('mousemove', this.onMousemove)
     document.removeEventListener('mouseup', this.onMouseup)
     document.removeEventListener('touchmove', this.onMousemove)
     document.removeEventListener('touchend', this.onMouseup)
-    this.mousedown=false
-    this.pressPoint={x:-1,y:-1}
+    this.mousedown = false
+    this.pressPoint = { x: -1, y: -1 }
     const rect = this.getRect()
     this.rect = rect
-    if(this.currentPath.length>0)
-    {
+    if (this.currentPath.length > 0) {
       this.inkPath.push(this.currentPath)
-      this.currentPath=[]
+      this.currentPath = []
     }
   }
 
-  clear () {
+  clear() {
     this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
     this.path = []
-    this.inkPath=[]
-    this.currentPath=[]
-    this.areaBound=[NaN,NaN,NaN,NaN]
+    this.inkPath = []
+    this.currentPath = []
+    this.areaBound = [NaN, NaN, NaN, NaN]
     this.initCursorStyle()
   }
 
-  reset () {
+  reset() {
     this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
     this.path = []
-    this.inkPath=[]
-    this.currentPath=[]
-    this.areaBound=[NaN,NaN,NaN,NaN]
+    this.inkPath = []
+    this.currentPath = []
+    this.areaBound = [NaN, NaN, NaN, NaN]
     this.onMousedown = null
     this.onMousemove = null
     this.onMouseup = null
@@ -133,17 +130,17 @@ class InkSign {
     this.onHandleInitCursorStyle = null
   }
 
-  getRect () {
-    const xArray = [];
-    const yArray = [];
+  getRect() {
+    const xArray = []
+    const yArray = []
     this.path.forEach((point) => {
-      xArray.push(point[0]);
-      yArray.push(point[1]);
-    });
-    const top = Math.min(...yArray);
-    const left = Math.min(...xArray);
-    const bottom = Math.max(...yArray);
-    const right = Math.max(...xArray);
+      xArray.push(point[0])
+      yArray.push(point[1])
+    })
+    const top = Math.min(...yArray)
+    const left = Math.min(...xArray)
+    const bottom = Math.max(...yArray)
+    const right = Math.max(...xArray)
     // this.addPath = this.addPath.map((ele) => {
     //   let x = ele[0] - left
     //   let y = ele[1] - top
@@ -159,88 +156,120 @@ class InkSign {
     }
   }
 
-  toImage (param) {
-    if (this.path.length === 0) return
-    //const finalCanvas = this.paintFinalCanvas()
-    //const dataURL = finalCanvas.toDataURL('image/jpeg')
-    let finalCanvas = this.getInkPathData()
-    let dataURL = finalCanvas.toDataURL('image/png')
-    let isSaveInk=true
-    if(!finalCanvas)
-    {
-      inalCanvas = this.paintFinalCanvas()
-      dataURL = finalCanvas.toDataURL('image/jpeg')
-      isSaveInk=false
+  getImageBase64() {
+    let imageBase64 = null
+    if (this.path.length !== 0){
+      let finalCanvas = this.getInkPathData()
+      imageBase64 = finalCanvas.toDataURL('image/png')
+      if (!finalCanvas) {
+        finalCanvas = this.paintFinalCanvas()
+        imageBase64 = finalCanvas.toDataURL('image/jpeg')
+      }
     }
-    this.dataURL = dataURL
+    return imageBase64
+  }
 
-    // 创建一个Image元素
-    var img = new Image();
-    img.src = dataURL;
-    img.id = 'sign-image-save'
-
-    // 设置样式和光标
-    img.style.position = "fixed";
-    img.style.pointerEvents = "none";
-    img.style.zIndex = "5";
-    img.classList.add('sign-ink-save')
-
-    if(isSaveInk) {
-     Object.defineProperty(img,'inkPath',{value:this.inkPath})
-     Object.defineProperty(img,'inkParam',{value:param})
+  async toImage(param) {
+    let { annotation, imageBase64 } = param
+    const { annotPtr, rect, pagePtr } = annotation
+    let dataURL = null
+
+    if (this.path.length !== 0){
+      let finalCanvas = this.getInkPathData()
+      let imageUrl = finalCanvas.toDataURL('image/png')
+      if (!finalCanvas) {
+        finalCanvas = this.paintFinalCanvas()
+        imageUrl = finalCanvas.toDataURL('image/jpeg')
+      }
+      dataURL = imageUrl
+    }
+    this.dataURL = dataURL || imageBase64
+    const imageArray = dataURL ? this.dataURLToArrayBuffer(dataURL) : null
+
+    if (annotPtr && (imageArray || imageBase64) && !this.type) {
+      this.eventBus.dispatch('updateSignatureAp', {
+        type: this.type,
+        annotPtr,
+        pagePtr,
+        rect,
+        imageArray,
+        imageBase64
+      })
     }
-   
-    let documentContainer = document.querySelector('.document-container')
 
-    const rect = this.rect
+    // if (this.type) {
+    //   const previewContainer = document.querySelector('#digitalSignaturePreview')
+    //   previewContainer.querySelector('img').src = dataURL
+    //   return
+    // }
 
-    documentContainer.addEventListener("mousemove", function(event) {
-      // 获取光标的位置
-      var x = event.clientX;
-      var y = event.clientY;
+    // 创建一个Image元素
+    // var img = new Image()
+    // img.src = dataURL
+    // img.id = 'sign-image-save'
+
+    // // 设置样式和光标
+    // img.style.position = "fixed"
+    // img.style.pointerEvents = "none"
+    // img.style.zIndex = "5"
+    // img.classList.add('sign-ink-save')
+
+    // if (isSaveInk) {
+    //   Object.defineProperty(img, 'inkPath', { value: this.inkPath })
+    //   Object.defineProperty(img, 'inkParam', { value: param })
+    // }
 
-      let offsetX = rect.width / 2
-      let offsetY = rect.height / 2
+    // let documentContainer = document.querySelector('.document-container')
 
-      // 设置图片的位置
-      img.style.left = (x - offsetX) + "px";
-      img.style.top = (y - offsetY) + "px";
-    });
+    // const rect = this.rect
 
-    // 移动端 添加图片至页面中心
-    var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
-    var targetPage = document.querySelector('.page.page-number' + this.pdfViewer.currentPageNumber)
-    if (isMobile) {
-      var targetPageRect = targetPage.getBoundingClientRect()
+    // documentContainer.addEventListener("mousemove", function (event) {
+    //   // 获取光标的位置
+    //   var x = event.clientX
+    //   var y = event.clientY
 
-      var x = targetPageRect.left + targetPageRect.width / 2;
-      var y = targetPageRect.top + targetPageRect.height / 2;
+    //   let offsetX = rect.width / 2
+    //   let offsetY = rect.height / 2
 
-      let offsetX = rect.width / 2
-      let offsetY = rect.height / 2
+    //   // 设置图片的位置
+    //   img.style.left = (x - offsetX) + "px"
+    //   img.style.top = (y - offsetY) + "px"
+    // })
 
-      img.style.left = (x - offsetX) + "px";
-      img.style.top = (y - offsetY) + "px";
-    }
+    // // 移动端 添加图片至页面中心
+    // var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
+    // var targetPage = document.querySelector('.page.page-number' + this.pdfViewer.currentPageNumber)
+    // if (isMobile) {
+    //   var targetPageRect = targetPage.getBoundingClientRect()
 
-    this.img = img;
+    //   var x = targetPageRect.left + targetPageRect.width / 2
+    //   var y = targetPageRect.top + targetPageRect.height / 2
 
-    // 添加图片到页面
-    documentContainer.appendChild(this.img);
-    this.documentContainer = documentContainer
+    //   let offsetX = rect.width / 2
+    //   let offsetY = rect.height / 2
 
-    // 移动端 调用点击事件直接添加到页面
-    setTimeout(() => {
-      if (isMobile) {
-        var event = new PointerEvent('click')
-        targetPage.dispatchEvent(event)
-      }
-    }, 10)
+    //   img.style.left = (x - offsetX) + "px"
+    //   img.style.top = (y - offsetY) + "px"
+    // }
+
+    // this.img = img
+
+    // // 添加图片到页面
+    // documentContainer.appendChild(this.img)
+    // this.documentContainer = documentContainer
+
+    // // 移动端 调用点击事件直接添加到页面
+    // setTimeout(() => {
+    //   if (isMobile) {
+    //     var event = new PointerEvent('click')
+    //     targetPage.dispatchEvent(event)
+    //   }
+    // }, 10)
   }
-  
-  getData (layerData) {
+
+  getData(layerData) {
     if (this.path.length === 0) return
-    this.layerData = layerData;
+    this.layerData = layerData
     // const annotation = {
     //   operate: "add-annot",
     //   type: 'ink',
@@ -299,7 +328,7 @@ class InkSign {
       }
 
       if (!isNaN(this.areaBound[0])) {
-       
+
         let offset = { x: this.inkPadding.x - this.areaBound[0], y: this.inkPadding.y - this.areaBound[1] }
 
         for (let i = 0; i < this.inkPath.length; i++) {
@@ -312,36 +341,31 @@ class InkSign {
     }
   }
 
-  getInkPathData()
-  {
+  getInkPathData() {
     this.calcInkPath()
-    if(!isNaN(this.areaBound[0]))
-    {
-      let drawCanvas=document.createElement("canvas")
-      let drawContext=drawCanvas.getContext("2d")
-
-      drawCanvas.width=this.areaBound[2]-this.areaBound[0]+this.inkPadding.x*2
-      drawCanvas.height=this.areaBound[3]-this.areaBound[1]+this.inkPadding.y*2
-      drawContext.strokeStyle=this.strokColor
-      drawContext.lineWidth=this.strokeWidth
-     
+    if (!isNaN(this.areaBound[0])) {
+      let drawCanvas = document.createElement("canvas")
+      let drawContext = drawCanvas.getContext("2d")
+
+      drawCanvas.width = this.areaBound[2] - this.areaBound[0] + this.inkPadding.x * 2
+      drawCanvas.height = this.areaBound[3] - this.areaBound[1] + this.inkPadding.y * 2
+      drawContext.strokeStyle = this.strokColor
+      drawContext.lineWidth = this.strokeWidth
+
       for (let i = 0; i < this.inkPath.length; i++) {
-        if(this.inkPath[i].length==1)
-        {
+        if (this.inkPath[i].length == 1) {
           drawContext.beginPath()
-          drawContext.fillStyle=this.strokColor
-          drawContext.ellipse(this.inkPath[i][0].x,this.inkPath[i][0].y,this.strokeWidth/2,this.strokeWidth/2,0,0,360,false)
+          drawContext.fillStyle = this.strokColor
+          drawContext.ellipse(this.inkPath[i][0].x, this.inkPath[i][0].y, this.strokeWidth / 2, this.strokeWidth / 2, 0, 0, 360, false)
           drawContext.fill()
           drawContext.closePath()
         }
 
-        if(this.inkPath[i].length>1)
-        {
+        if (this.inkPath[i].length > 1) {
           drawContext.beginPath()
-          drawContext.moveTo(this.inkPath[i][0].x,this.inkPath[i][0].y)
-          for (let j = 1; j < this.inkPath[i].length; j++)
-          {
-            drawContext.lineTo(this.inkPath[i][j].x,this.inkPath[i][j].y)
+          drawContext.moveTo(this.inkPath[i][0].x, this.inkPath[i][0].y)
+          for (let j = 1; j < this.inkPath[i].length; j++) {
+            drawContext.lineTo(this.inkPath[i][j].x, this.inkPath[i][j].y)
           }
           drawContext.stroke()
           drawContext.closePath()
@@ -352,10 +376,10 @@ class InkSign {
     return null
   }
 
-  formatPath () {
+  formatPath() {
     let path = ''
     const length = this.path.length
-    
+
     const xArray = [];
     const yArray = [];
 
@@ -365,7 +389,7 @@ class InkSign {
       xArray.push(point.x);
       yArray.push(point.y);
 
-      path+=(point.x.toFixed(2) + ',' + point.y.toFixed(2))
+      path += (point.x.toFixed(2) + ',' + point.y.toFixed(2))
       if (i + 1 < length) {
 
         const point = this.handlePoint(this.path[i])
@@ -373,7 +397,7 @@ class InkSign {
         xArray.push(point.x);
         yArray.push(point.y);
 
-        path+=(';' + point.x.toFixed(2) + ',' + point.y.toFixed(2))
+        path += (';' + point.x.toFixed(2) + ',' + point.y.toFixed(2))
         if (i + 2 < length) {
           path += ';'
         }
@@ -391,7 +415,7 @@ class InkSign {
     }
   }
 
-  handlePoint (point) {
+  handlePoint(point) {
     const newPoint = getInitialPoint(
       {
         x: point[0] + this.layerData.clickX - this.rect.width / 2,
@@ -406,13 +430,13 @@ class InkSign {
     }
   }
 
-  initCursorStyle () {
+  initCursorStyle() {
     if (this.img) {
       this.img.remove()
     }
   }
 
-  paintFinalCanvas () {
+  paintFinalCanvas() {
     let finalCanvas = document.createElement("canvas")
     let ctx = finalCanvas.getContext('2d')
 
@@ -429,38 +453,34 @@ class InkSign {
     return finalCanvas
   }
 
-  update(param)
-  {
-    if(param)
-    {
-      this.strokeWidth=param.width
-      this.strokColor=param.color
+  async update(param) {
+    if (this.type) {
+      await this.toImage(param)
     }
 
-    if(this.inkPath)
-    {
+    const { width, color } = param
+    width && (this.strokeWidth = width)
+    color && (this.strokColor = color)
+
+    if (this.inkPath) {
       this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
       this.ctx.lineWidth = this.strokeWidth
       this.ctx.strokeStyle = this.strokColor
 
-      for(let i=0;i<this.inkPath.length;i++)
-      {
-        if(this.inkPath[i].length==1)
-        {
+      for (let i = 0; i < this.inkPath.length; i++) {
+        if (this.inkPath[i].length == 1) {
           this.ctx.beginPath()
-          this.ctx.fillStyle=this.strokColor
-          this.ctx.ellipse(this.inkPath[i][0].x,this.inkPath[i][0].y,this.strokeWidth/2,this.strokeWidth/2,0,0,360,false)
+          this.ctx.fillStyle = this.strokColor
+          this.ctx.ellipse(this.inkPath[i][0].x, this.inkPath[i][0].y, this.strokeWidth / 2, this.strokeWidth / 2, 0, 0, 360, false)
           this.ctx.fill()
           this.ctx.closePath()
         }
 
-        if(this.inkPath[i].length>1)
-        {
+        if (this.inkPath[i].length > 1) {
           this.ctx.beginPath()
-          this.ctx.moveTo(this.inkPath[i][0].x,this.inkPath[i][0].y)
-          for (let j = 1; j < this.inkPath[i].length; j++)
-          {
-            this.ctx.lineTo(this.inkPath[i][j].x,this.inkPath[i][j].y)
+          this.ctx.moveTo(this.inkPath[i][0].x, this.inkPath[i][0].y)
+          for (let j = 1; j < this.inkPath[i].length; j++) {
+            this.ctx.lineTo(this.inkPath[i][j].x, this.inkPath[i][j].y)
           }
           this.ctx.stroke()
           this.ctx.closePath()
@@ -468,6 +488,19 @@ class InkSign {
       }
     }
   }
+
+  dataURLToArrayBuffer(dataURL) {
+    var binary = atob(dataURL.split(',')[1])
+    var mime = dataURL.split(';')[0].split(':')[1]
+    var arrayBuffer = new ArrayBuffer(binary.length)
+    var uintArray = new Uint8Array(arrayBuffer)
+    for (var i = 0; i < binary.length; i++) {
+      uintArray[i] = binary.charCodeAt(i)
+    }
+    return uintArray
+  }
+
+  updateSignature(signature) {}
 }
 
-export { InkSign }
+export { Signatures }

File diff suppressed because it is too large
+ 580 - 37
packages/core/src/worker/compdfkit_worker.js


+ 117 - 3
packages/webview/locales/en.json

@@ -63,7 +63,12 @@
     "addText": "Add Text",
     "addImage": "Add Image",
     "undo": "Undo",
-    "redo": "Redo"
+    "redo": "Redo",
+
+    "createSignField": "Create Signature Field",
+    "addDigitalSign": "Add Digital Signature",
+    "addElectronicSign": "Add Electronic Signature",
+    "verifyDigitalSign": "Verify Digital Signature"
   },
 
   "nextPage": "Next",
@@ -105,7 +110,12 @@
     "delete": "Delete",
     "replyPlaceholder": "Reply or add thoughts",
     "reply": "Reply",
-    "confirm": "Confirm"
+    "confirm": "Confirm",
+    "signature": "Signature",
+    "signatureList": "Signature List",
+    "signatureDetails": "Signature Details",
+    "certificationDetails": "Certification Details",
+    "noSignatures": "No Signatures"
   },
 
   "pageModePanel": {
@@ -203,6 +213,7 @@
   },
   "ok": "OK",
   "cancel": "Cancel",
+  "continue": "Continue",
 
   "signatures": {
     "trackpad": "Trackpad",
@@ -214,9 +225,109 @@
     "image": "Image",
     "selectFile": "Select a File",
 
+    "none": "None",
+
     "clear": "Clear",
     "save": "Save and Apply",
-    "uploadError": "Please upload images less than 1MB"
+    "uploadError": "Please upload images less than 1MB",
+
+    "selectTypeDialog": {
+      "selectType": "Select Signature Type",
+      "signWithElectronic": "Sign with Electronic Signatures",
+      "signWithElectronicDesc": "Draw, type, or upload image signatures to sign files.",
+      "signWithDigital": "Sign with Digital Signatures",
+      "signWithDigitalDesc": "Create or upload a digital certificate with a unique digital ID to sign files.",
+      "startSigning": "Start Signing"
+    },
+
+    "detailsDialog": {
+      "title": "Digital Signature Details",
+      "signBy": "Valid signature, signed by ",
+      "signingTime": "Signing Time",
+      "validitySummary": "Validity Summary",
+      "validIdentity": "The signer's identity is valid.",
+      "invalidIdentity": "The signer's identity is invalid.",
+      "unknownIdentity": "Signature validity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates.",
+      "expiredCertificate": "The file was signed with a certificate that has expired. If you acquired this file recently, it may not be authentic.",
+      "validSignature": "The signature is valid.",
+      "invalidSignature": "The signature is invalid.",
+      "notModifiedDoc": "The document has not been modified since this signature was applied.",
+      "alteredDoc": "The document has been altered or corrupted since it was signed by the current user.",
+      "viewCertificateDetails": "View Certificate Details",
+      
+      "unknownSignature": "Signature validity is unknown.",
+      "atLeastOneInvalid": "At least one signature is invalid.",
+      "viewAllSignatures": "View All Signatures"
+    },
+
+    "certificateViewerDialog": {
+      "title": "Certificate Viewer",
+      "abstracts": "Abstracts",
+      "summary": "Summary",
+      "issuedTo": "Issued to:",
+      "issuer": "Issuer:",
+      "validFrom": "Valid from:",
+      "validTo": "Valid to:",
+      "intendedUsage": "Intended Usage:",
+
+      "details": "Details",
+      "version": "Version",
+      "algorithm": "Algorithm",
+      "subject": "Subject",
+      "serialNumber": "Serial Number",
+      "certificatePolicy": "Certificate Policy:",
+      "crlDistributionPoints": "CRL Distribution Points:",
+      "issuerInfoAccess": "Issuer Information Access:",
+      "issuerKeyIdentifier": "Issuer‘s Key Identifier:",
+      "subjectKeyIdentifier": "Subject‘s Key Identifier:",
+      "basicConstraints": "Basic Constraints:",
+      "keyUsage": "Key Usage:",
+      "publicKey": "Public Key:",
+      "X509": "X.509 Data:",
+      "SHA1Digest": "SHA1 Digest:",
+      "MD5Digest": "MD5 Digest:",
+
+      "trust": "Trust",
+      "trustedTo": "This Certificate Is Trusted to:",
+      "addToTrust": "Add to Trusted Certificates",
+      "done": "Done"
+    },
+
+    "deleteConfirm": "Are you sure to delete it?",
+
+    "addDigitalFileDialog": {
+      "title": "Add a Digital ID",
+      "desc": "Browse a digital ID file. Digital IDs are password-protected. If you do not know the password, you cannot obtain a digital ID.",
+      "certificateFile": "Certificate File",
+      "uploadFile": "Upload your certificate file",
+      "upload": "Upload",
+      "password": "Password",
+      "enterPassword": "Enter the password of the certificate file",
+      "invalidPassword": "Invalid Password"
+    },
+
+    "appearanceDialog": {
+      "title": "Customize the Signature Appearance",
+      "signHere": "Sign Here!",
+
+      "includeText": "Include Text",
+      "name": "Name",
+      "dName": "Distinguishable name",
+      "date": "Date",
+      "ComPDFKitVersion": "ComPDFKit Version",
+      "logo": "Logo",
+      "location": "Location",
+      "reason": "Reason",
+      "signReason": "Reason",
+      "docOwner": "I am the owner of the document",
+      "approvingDoc": "I am approving the document",
+      "reviewedDoc": "I have reviewed this document",
+      "labels": "Labels",
+
+      "text": "Text",
+      "type": "Type",
+      "textAlignment": "Text Alignment"
+    }
   },
   "color": "Color",
   "opacity": "Opacity",
@@ -225,6 +336,9 @@
   "oblique": "Oblique",
   "bold": "Bold",
   "boldOblique": "Bold Oblique",
+  "delete": "Delete",
+  "warning": "Warning",
+  "colon": ": ",
 
   "compare": {
     "startCompare": "Start to Compare",

+ 117 - 3
packages/webview/locales/zh-CN.json

@@ -63,7 +63,12 @@
     "addText": "添加文字",
     "addImage": "添加图片",
     "undo": "撤销",
-    "redo": "重做"
+    "redo": "重做",
+
+    "createSignField": "添加签名域",
+    "addDigitalSign": "添加数字签名",
+    "addElectronicSign": "添加电子签名",
+    "verifyDigitalSign": "验证数字签名"
   },
 
   "nextPage": "下一页",
@@ -105,7 +110,12 @@
     "delete": "删除",
     "replyPlaceholder": "回复",
     "reply": "保存",
-    "confirm": "保存"
+    "confirm": "保存",
+    "signature": "签名",
+    "signatureList": "签名列表",
+    "signatureDetails": "查看签名详情",
+    "certificationDetails": "查看证书详情",
+    "noSignatures": "没有签名"
   },
 
   "pageModePanel": {
@@ -203,6 +213,7 @@
   },
   "ok": "确定",
   "cancel": "取消",
+  "continue": "继续",
 
   "signatures": {
     "trackpad": "触摸板",
@@ -214,9 +225,109 @@
     "image": "图片",
     "selectFile": "选择文件",
 
+    "none": "无",
+
     "clear": "清除",
     "save": "保存",
-    "uploadError": "请上传1M以内的图片"
+    "uploadError": "请上传1M以内的图片",
+
+    "selectTypeDialog": {
+      "selectType": "请选择签名类型",
+      "signWithElectronic": "电子签名",
+      "signWithElectronicDesc": "通过绘制、图片、或输入文字进行文件签署。",
+      "signWithDigital": "数字签名(数字证书签署)",
+      "signWithDigitalDesc": "通过创建或上传带有唯一数字ID的数字证书签署文件。",
+      "startSigning": "开始签名"
+    },
+
+    "detailsDialog": {
+      "title": "数字签名详细信息",
+      "signBy": "签名有效,签署者为",
+      "signingTime": "签署时间",
+      "validitySummary": "有效性小结",
+      "validIdentity": "签名者的身份有效。",
+      "invalidIdentity": "签名者的身份无效。",
+      "unknownIdentity": "签名有效性未知,因为它未包含在您的受信任证书列表中,并且它的父证书都不是受信任证书。",
+      "expiredCertificate": "该文件是用已过期的证书签名的。如果您最近获取了此文件,则该文件可能不真实。",
+      "invalidSignature": "签名无效。",
+      "validSignature": "签名有效。",
+      "notModifiedDoc": "自应用此签名以来,文档未被修改。",
+      "alteredDoc": "自当前用户签署文档以来,该文档已被更改或损坏。",
+      "viewCertificateDetails": "查看证书细节",
+      
+      "unknownSignature": "签名有效性未知。",
+      "atLeastOneInvalid": "至少一个签名无效,文档可能被篡改。",
+      "viewAllSignatures": "查看所有签名"
+    },
+
+    "certificateViewerDialog": {
+      "title": "证书查看程序",
+      "abstracts": "摘要",
+      "summary": "小结",
+      "issuedTo": "颁发给",
+      "issuer": "颁发者",
+      "validFrom": "有效起始日期",
+      "validTo": "有效截止日期",
+      "intendedUsage": "预期用途",
+
+      "details": "详细信息",
+      "version": "版本",
+      "algorithm": "签名算法",
+      "subject": "主题",
+      "serialNumber": "序列号",
+      "certificatePolicy": "证书策略",
+      "crlDistributionPoints": "CRL分发点",
+      "issuerInfoAccess": "颁发机构信息访问",
+      "issuerKeyIdentifier": "颁发机构密钥标识符",
+      "subjectKeyIdentifier": "主体密钥标识符",
+      "basicConstraints": "基本约束",
+      "keyUsage": "密钥用法",
+      "publicKey": "公钥",
+      "X509": "X.509",
+      "SHA1Digest": "SHA1",
+      "MD5Digest": "MD5",
+
+      "trust": "信任",
+      "trustedTo": "本证书信任于:",
+      "addToTrust": "添加到可信任证书",
+      "done": "关闭"
+    },
+
+    "deleteConfirm": "确定删除该内容?",
+
+    "addDigitalFileDialog": {
+      "title": "添加数字身份证",
+      "desc": "浏览数字身份证文件。数字身份证受密码保护。如果你不知道密码,你就无法获取数字身份证。",
+      "certificateFile": "证书文件",
+      "uploadFile": "上传你的证书文件",
+      "upload": "浏览",
+      "password": "证书密码",
+      "enterPassword": "请输入证书密码",
+      "invalidPassword": "密码错误,请重试"
+    },
+
+    "appearanceDialog": {
+      "title": "自定义签名外观",
+      "signHere": "请在此处输入签名!",
+
+      "includeText": "包含文本",
+      "name": "名称",
+      "dName": "辨别名",
+      "date": "日期",
+      "ComPDFKitVersion": "ComPDFKit 版本",
+      "logo": "徽标",
+      "location": "位置",
+      "reason": "原因",
+      "signReason": "签署原因",
+      "docOwner": "我是该文档的作者",
+      "approvingDoc": "我正在批准该文档",
+      "reviewedDoc": "我已审阅该文档",
+      "labels": "标签",
+
+      "text": "字体",
+      "type": "字形",
+      "textAlignment": "对齐方式"
+    }
   },
   "color": "颜色",
   "opacity": "不透明度",
@@ -225,6 +336,9 @@
   "oblique": "斜体",
   "bold": "粗体",
   "boldOblique": "粗斜体",
+  "delete": "删除",
+  "warning": "注意",
+  "colon": ":",
 
   "compare": {
     "startCompare": "开始对比",

+ 29 - 0
packages/webview/public/example/Blank Page.pdf

@@ -0,0 +1,29 @@
+%PDF-1.7
+%¡³Å×
+1 0 obj
+<</Pages 2 0 R /Type/Catalog>>
+endobj
+2 0 obj
+<</Count 1/Kids[ 4 0 R ]/Type/Pages>>
+endobj
+3 0 obj
+<</CreationDate(D:20240422104039)>>
+endobj
+4 0 obj
+<</MediaBox[ 0 0 595 842]/Parent 2 0 R /Resources<<>>/Rotate 0/Type/Page>>
+endobj
+xref
+0 5
+0000000000 65535 f
+0000000017 00000 n
+0000000066 00000 n
+0000000122 00000 n
+0000000176 00000 n
+trailer
+<<
+/Root 1 0 R
+/Info 3 0 R
+/Size 5/ID[<A4C601DFA068C500E02FD2A979A55577><A4C601DFA068C500E02FD2A979A55577>]>>
+startxref
+269
+%%EOF

BIN
packages/webview/public/images/logo-backgroud.png


BIN
packages/webview/public/images/logo-backgroud@2x.png


+ 2 - 0
packages/webview/src/assets/base.css

@@ -32,6 +32,7 @@
   --c-bg: var(--c-black-11);
 
   --c-text: var(--c-black-5);
+  --c-black-text: var(--c-black);
   
   --c-divider: rgba(0, 0, 0, 0.12);
 
@@ -139,6 +140,7 @@ html.dark {
   --c-bg: var(--c-black-3);
   
   --c-text: var(--c-white);
+  --c-black-text: var(--c-white);
 
   --c-divider: rgba(255, 255, 255, 0.2);
 

File diff suppressed because it is too large
+ 3 - 0
packages/webview/src/assets/icons/icon-signature.svg


+ 4 - 2
packages/webview/src/components/App/index.vue

@@ -44,6 +44,7 @@
   const downloading = computed(() => useViewer.getDownloading)
   const downloadError = computed(() => useViewer.getDownloadError)
   const activeSignCreatePanel = computed(() => useViewer.isElementOpen('signCreatePanel'))
+  const activeDigitalSignCreatePanel = computed(() => useViewer.isElementOpen('digitalSignCreatePanel'))
   const popoverChanged = computed(() => useViewer.getPopoverChanged)
   const toolMode = computed(() => useViewer.getToolMode)
 
@@ -55,8 +56,9 @@
     const openFileInput = document.getElementById("fileInput")
     openFileInput.click()
 
-    if (activeSignCreatePanel.value) {
-      useViewer.toggleElement('signCreatePanel')
+    if (activeSignCreatePanel.value || activeDigitalSignCreatePanel.value) {
+      useViewer.closeElement('signCreatePanel')
+      useViewer.closeElement('digitalSignCreatePanel')
       useViewer.setActiceToolMode('view')
     }
     if (!popoverChanged.value) useViewer.setPopoverChanged(true)

+ 221 - 0
packages/webview/src/components/Dialogs/AddDigitalFileDialog.vue

@@ -0,0 +1,221 @@
+<template>
+  <div class="add-digital-file-dialog" v-if="show">
+    <Dialog :show="show" :dialogName="dialogName" :close="true">
+      <template #header>{{ $t('signatures.addDigitalFileDialog.title') }}</template>
+
+      <p>{{ $t('signatures.addDigitalFileDialog.desc') }}</p>
+      <div class="input-file row">
+        <span>{{ $t('signatures.addDigitalFileDialog.certificateFile') }}</span>
+        <div class="addition">
+          <div class="file-input-underbox" @click="uploadFile">
+            <span :class="{ 'uploaded': inputFile?.name }">{{ inputFile?.name || $t('signatures.addDigitalFileDialog.uploadFile') }}</span>
+            <FileFolder />
+          </div>
+          <input ref="uploadFileEl" type="file" @change="handleFile" accept=".pfx">
+        </div>
+      </div>
+      <div class="input-pwd row">
+        <span>{{ $t('signatures.addDigitalFileDialog.password') }}</span>
+        <div>
+          <input v-model="password" type="password" :placeholder="$t('signatures.addDigitalFileDialog.enterPassword')">
+          <p v-if="isError" class="error-text">{{ $t('signatures.addDigitalFileDialog.invalidPassword') }}</p>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="rect-button white" @click="closeDialog">{{ $t('cancel') }}</div>
+        <div class="rect-button blue" @click="next">{{ $t('continue') }}</div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script setup>
+import { computed, ref, watch, getCurrentInstance } from 'vue'
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import core from '@/core'
+
+const dialogName = 'addDigitalFileDialog'
+
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+const instance = getCurrentInstance().appContext.app.config.globalProperties
+const $t = instance.$t
+
+const show = computed(() => useViewer.isElementOpen(dialogName))
+
+const inputFile = ref(null)
+const isError = ref(false)
+const password = ref('')
+
+const uploadFileEl = ref(null)
+
+watch(() => show.value, (newVal, oldVal) => {
+  if (newVal) {
+    inputFile.value = null
+    isError.value = false
+  }
+})
+
+const closeDialog = () => {
+  useViewer.closeElement(dialogName)
+}
+
+const next = async () => {
+  const result = await validatePwd()
+  if (!result) return
+
+  const subject = result.subject
+
+  const nowDate= new Date()
+  const year = nowDate.getFullYear()
+  const month = nowDate.getMonth() + 1 < 10 ? `0${nowDate.getMonth() + 1}` : nowDate.getMonth() + 1
+  const day = nowDate.getDate() < 10 ? `0${nowDate.getDate()}` : nowDate.getDate()
+  const hour = nowDate.getHours() < 10 ? `0${nowDate.getHours()}` : nowDate.getHours()
+  const minute = nowDate.getMinutes() < 10 ? `0${nowDate.getMinutes()}` : nowDate.getMinutes()
+  const second = nowDate.getSeconds() < 10 ? `0${nowDate.getSeconds()}` : nowDate.getSeconds()
+  const nowDateStr = `${year}.${month}.${day}\n${hour}:${minute}:${second}`
+  const content = `${$t('signatures.appearanceDialog.name')}: ${subject.CN}\n${$t('signatures.appearanceDialog.date')}: ${nowDateStr}`
+  const src = await core.getSignature({
+    rect: {
+      width: 582,
+      height: 187,
+      top: 0,
+      left: 0,
+      right: 582,
+      bottom: 187
+    },
+    content,
+    text: subject.CN
+  })
+  useViewer.setCertSubject(subject)
+  useViewer.setDigitalSignaturePreview(src)
+
+  closeDialog()
+  useViewer.openElement('signatureAppearanceDialog')
+}
+
+const validatePwd = async () => {
+  const fileReader = new FileReader()
+  const arrayBufferPromise = new Promise((resolve, reject) => {
+    fileReader.addEventListener('load', async (e) => {
+      resolve(new Uint8Array(e.target.result))
+    })
+    fileReader.addEventListener('error', () => {
+      reject('Error reading the local certificate')
+    })
+
+    fileReader.readAsArrayBuffer(inputFile.value)
+  })
+
+  const pkcs12Buffer = await arrayBufferPromise
+
+  const result = await core.loadCertificates({
+    pkcs12Buffer,
+    password: password.value
+  })
+
+  isError.value = !result
+
+  if (result) {
+    useDocument.setPkcs12({
+      pkcs12Buffer,
+      password: password.value
+    })
+  }
+  return result
+}
+
+const uploadFile = () => {
+  const el = uploadFileEl.value
+  el.click()
+}
+
+const handleFile = (e) => {
+  const file = e.target.files[0]
+  inputFile.value = file
+  uploadFileEl.value.value = ''
+}
+</script>
+
+<style lang="scss">
+.add-digital-file-dialog {
+
+  .dialog-container {
+    width: 450px;
+
+    .close {
+      float: right;
+      margin-top: 4px;
+    }
+  }
+
+  .row {
+    display: flex;
+    justify-content: space-between;
+  }
+
+  .input-file {
+    margin: 20px 0;
+
+    .addition {
+      position: relative;
+      display: flex;
+      align-items: center;
+
+      .file-input-underbox {
+        display: flex;
+        align-items: center;
+        color: #999;
+        cursor: pointer;
+
+        .uploaded {
+          color: var(--c-text);
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+
+        svg {
+          position: absolute;
+          right: 4px;
+          pointer-events: none;
+          color: var(--c-side-outline-text);
+        }
+      }
+    }
+  }
+
+  input,
+  .file-input-underbox {
+    padding: 0 20px 0 8px;
+    width: 300px;
+    height: 24px;
+    background: var(--c-right-side-content-fillbox-bg);
+    border: 1px solid var(--c-right-side-content-fillbox-border);
+    border-radius: 1px;
+    font-size: 14px;
+    line-height: 16px;
+  }
+
+  input[type="file"] {
+    display: none;
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    opacity: 0;
+    cursor: pointer;
+
+    &::file-selector-button {
+      cursor: pointer;
+    }
+  }
+  
+  .input-pwd {
+
+    .error-text {
+      color: red;
+    }
+  }
+}
+</style>

+ 325 - 0
packages/webview/src/components/Dialogs/CertificationViewerDialog.vue

@@ -0,0 +1,325 @@
+<template>
+  <div class="certification-details-dialog" v-if="show">
+    <Dialog :show="show" :dialogName="dialogName">
+      <template #header>
+        <p class="title">{{ $t('signatures.certificateViewerDialog.title') }}</p>
+        <p class="desc">{{ $t('signatures.certificateViewerDialog.title') }}</p>
+      </template>
+
+      <div class="container">
+        <div class="left">
+          <p v-for="(signer, index) in signature.signerList" :key="index"
+            :class="{ 'highlighted': selectedSignerIndex === index }"
+            @focus="selectedSignerIndex = index"
+            @blur="selectedSignerIndex = 0"
+            tabindex="-1">
+            {{ signer.certificateList[0].subject.CN }}
+          </p>
+        </div>
+
+        <div class="right">
+          <div class="tabs">
+            <div @click="activePanel = 1" :class="{ active: activePanel === 1 }">{{ $t('signatures.certificateViewerDialog.abstracts') }}</div>
+            <div @click="activePanel = 2" :class="{ active: activePanel === 2 }">{{ $t('signatures.certificateViewerDialog.details') }}</div>
+            <div @click="activePanel = 3" :class="{ active: activePanel === 3 }">{{ $t('signatures.certificateViewerDialog.trust') }}</div>
+          </div>
+          <div class="panel">
+            <div class="summary">
+              <div class="title">
+                <div class="line-left"></div>
+                <span>{{ activePanel === 3 ? $t('signatures.certificateViewerDialog.trustedTo') : $t('signatures.certificateViewerDialog.summary') }}</span>
+                <div class="line-right"></div>
+              </div>
+              <div class="content">
+                <div v-show="activePanel === 1" class="abstracts">
+                  <p>{{ $t('signatures.certificateViewerDialog.issuedTo') }} <span>{{ selectedSigner.issuer.CN }} &lt;{{ selectedSigner.issuer.email }}&gt;</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.issuer') }} <span>{{ selectedSigner.issuer.CN }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.validFrom') }} <span>{{ formatDate(selectedSigner.validDateStart) }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.validTo') }} <span>{{ formatDate(selectedSigner.validDateEnd) }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.intendedUsage') }} <span>Digital Signature, Non-Repudiation</span></p>
+                </div>
+
+                <div v-show="activePanel === 2" class="details">
+                  <p>{{ $t('signatures.certificateViewerDialog.version') }} <span>{{ selectedSigner.version }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.algorithm') }} <span>{{ selectedSigner.signAlgOidType }} ({{ selectedSigner.signAlgOid }})</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.subject') }} <span>C={{ selectedSigner.subject.C }},ST={{ selectedSigner.subject.ST }},L={{ selectedSigner.subject.L }},O={{ selectedSigner.subject.O }},OU={{ selectedSigner.subject.OU }},CN={{ selectedSigner.subject.CN }},emailAddress={{ selectedSigner.subject.email }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.issuer') }} <span>C={{ selectedSigner.issuer.C }},ST={{ selectedSigner.issuer.ST }},L={{ selectedSigner.issuer.L }},O={{ selectedSigner.issuer.O }},OU={{ selectedSigner.issuer.OU }},CN={{ selectedSigner.issuer.CN }},emailAddress={{ selectedSigner.issuer.email }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.serialNumber') }} <span>{{ selectedSigner.serialNumber }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.validFrom') }} <span>{{ formatDate(selectedSigner.validDateStart) }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.validTo') }} <span>{{ formatDate(selectedSigner.validDateEnd) }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.certificatePolicy') }} <span>{{ selectedSigner.certificatePolicies[0] }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.crlDistributionPoints') }} <span>{{ selectedSigner.CRLDistributionPoints }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.issuerInfoAccess') }} <span v-for="(info, index) in selectedSigner.sccessInfo" :key="index">{{ info.Key }}={{ info.AccessInfo }}{{ index === selectedSigner.sccessInfo.length - 1 ? '' : ',' }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.issuerKeyIdentifier') }} <span>{{ selectedSigner.authorityKeyIdentifier }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.subjectKeyIdentifier') }} <span>{{ selectedSigner.subjectKeyIdentifier }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.basicConstraints') }} <span>{{ selectedSigner.basicConstraints }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.keyUsage') }} <span>{{ selectedSigner.keyUsage }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.publicKey') }} <span>{{ selectedSigner.publicKey }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.X509') }} <span>{{ selectedSigner.X509Data }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.SHA1Digest') }} <span>{{ selectedSigner.SHA1Digest }}</span></p>
+                  <p>{{ $t('signatures.certificateViewerDialog.MD5Digest') }} <span>{{ selectedSigner.MD5Digest }}</span></p>
+                </div>
+
+                <div v-show="activePanel === 3" class="trust">
+                  <p>
+                    <SuccessIcon v-if="selectedSigner.isTrusted" />
+                    <FailedIcon v-else />
+                    <span>Sign document or data</span>
+                  </p>
+                  <p>
+                    <SuccessIcon v-if="selectedSigner.isTrusted" />
+                    <FailedIcon v-else />
+                    <span>Certify document</span>
+                  </p>
+                </div>
+              </div>
+            </div>
+
+            <div v-show="activePanel === 3" class="trust-button">
+              <div @click="">{{ $t('signatures.certificateViewerDialog.addToTrust') }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="rect-button blue" @click="close">{{ $t('signatures.certificateViewerDialog.done') }}</div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+
+const dialogName = 'certificateViewerDialog'
+
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const show = computed(() => useViewer.isElementOpen(dialogName))
+const signature = computed(() => useDocument.getSelectedSignature)
+const selectedSigner = computed(() => signature.value.signerList[0].certificateList[selectedSignerIndex.value])
+
+const activePanel = ref(1)
+const selectedSignerIndex = ref(0)
+
+const close = () => {
+  useViewer.closeElement(dialogName)
+}
+
+const formatDate = (dateStr) => {
+    // const isoString = dateStr.replace(/Z$/, '')
+
+    // const utcDate = new Date(isoString)
+    // const localDate = new Date(utcDate.getTime() + (utcDate.getTimezoneOffset() * 60000))
+
+    // const year = localDate.getFullYear()
+    // const month = String(localDate.getMonth() + 1).padStart(2, '0')
+    // const day = String(localDate.getDate()).padStart(2, '0')
+    // const hours = String(localDate.getHours()).padStart(2, '0')
+    // const minutes = String(localDate.getMinutes()).padStart(2, '0')
+    // const seconds = String(localDate.getSeconds()).padStart(2, '0')
+
+    // const timeZoneOffset = '+0800'
+    // return `${year}/${month}/${day} ${hours}:${minutes}:${seconds} ${timeZoneOffset}`
+
+    let year = dateStr.slice(0, 4)
+    let month = dateStr.slice(4, 6)
+    let day = dateStr.slice(6, 8)
+    let hours = dateStr.slice(8, 10)
+    let minutes = dateStr.slice(10, 12)
+    let seconds = dateStr.slice(12, 14)
+    
+    return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`
+
+}
+</script>
+
+<style lang="scss">
+.certification-details-dialog {
+
+  .dialog-container {
+    padding: 20px;
+    width: 1070px;
+    border-radius: 0;
+
+    header {
+      
+      .title {
+        font-size: 16px;
+        line-height: 24px;
+        font-weight: 700;
+        color: var(--c-right-side-header-text);
+      }
+
+      .desc {
+        margin-top: 20px;
+        font-size: 14px;
+        line-height: 16px;
+        font-weight: 400;
+      }
+    }
+
+    main {
+      margin: 32px 0;
+    }
+
+    footer {
+      .rect-button {
+        border-radius: 4px;
+      }
+    }
+  }
+
+  .container {
+    display: flex;
+
+    .left {
+      margin-right: 32px;
+      padding: 2px;
+      width: 246px;
+      border: 1px solid var(--c-header-border);
+
+      p {
+        padding: 4px;
+        font-size: 14px;
+        line-height: 16px;
+        color: var(--c-black-text);
+        cursor: default;
+
+        &.highlighted {
+          background: #1460F3;
+          color: white;
+        }
+      }
+    }
+
+    .right {
+      flex: 1;
+      border: 1px solid var(--c-header-border);
+      height: 700px;
+
+      .tabs {
+        display: flex;
+        justify-content: center;
+        border-bottom: 1px solid var(--c-header-border);
+
+        > div {
+          margin: 10px 12px -1px 12px;
+          padding-bottom: 4px;
+          font-size: 16px;
+          line-height: 24px;
+          cursor: pointer;
+
+          &.active {
+            color: var(--c-right-side-header-text);
+            font-weight: 700;
+            border-bottom: 2px solid #4982E6;
+          }
+        }
+      }
+
+      .panel {
+        padding: 20px;
+
+        .summary {
+          margin-top: 10px;
+          border: 1px solid var(--c-header-border);
+          border-top-width: 0px;
+
+          .title {
+            display: flex;
+
+            span {
+              margin: -12px 4px 0 4px;
+              font-size: 14px;
+              line-height: 20px;
+              font-weight: 600;
+              color: var(--c-black-text);
+            }
+
+            .line-left {
+              width: 12px;
+              border-top: 1px solid var(--c-header-border);
+            }
+
+            .line-right {
+              flex: 1;
+              border-top: 1px solid var(--c-header-border);
+            }
+          }
+
+          .content {
+            margin: 20px 20px 16px;
+            
+            p {
+              font-size: 14px;
+              line-height: 20px;
+
+              & + p {
+                margin-top: 12px;
+              }
+            }
+
+            span {
+              color: var(--c-side-annotation-text);
+            }
+
+            .details {
+              overflow-y: auto;
+              max-height: 564px;
+
+              p {
+                color: var(--c-black-text);
+                word-break: break-all;
+              }
+            }
+
+            .trust p {
+              display: flex;
+              align-items: center;
+              
+              span {
+                color: var(--c-right-side-header-text);
+              }
+
+              svg {
+                max-width: 20px;
+                width: 20px;
+                height: 20px;
+                margin-right: 4px;
+              }
+            }
+          }
+        }
+
+        .trust-button {
+          margin-top: 24px;
+          text-align: right;
+
+          > div {
+            padding: 0 12px;
+            display: inline-block;
+            height: 32px;
+            border: 1px solid var(--c-header-border);
+            border-radius: 1px;
+            background: var(--c-header-button-active);
+            cursor: pointer;
+            font-size: 14px;
+            line-height: 32px;
+            color: var(--c-right-side-header-text);
+
+            &:hover {
+              background: var(--c-popup-bg-hover);
+              color: white;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 38 - 0
packages/webview/src/components/Dialogs/DeleteSignatureDialog.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="delete-page-popup" v-if="show">
+    <Dialog :show="show" :dialogName="dialogName">
+      <Warning />
+      <p>{{ $t('signatures.deleteConfirm') }}</p>
+
+      <template #footer>
+        <div class="rect-button white" @click="closeDialog">{{ $t('cancel') }}</div>
+        <div class="rect-button blue" @click="handleDelete">{{ $t('ok') }}</div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import core from '@/core'
+
+const dialogName = 'deleteConfirmDialog'
+
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const show = computed(() => useViewer.isElementOpen(dialogName))
+const selectedSignature = computed(() => useDocument.getSelectedSignature)
+
+const closeDialog = () => {
+  useViewer.closeElement(dialogName)
+  useDocument.setSelectedSignature(null)
+}
+
+const handleDelete = () => {
+  core.deleteSignature(JSON.parse(JSON.stringify(selectedSignature.value)))
+  closeDialog()
+}
+</script>

+ 6 - 1
packages/webview/src/components/Dialogs/Dialog.vue

@@ -1,7 +1,12 @@
 <template>
   <div v-if="show" class="dialog">
     <div class="dialog-container">
-      <div class="close" v-if="close"><Close @click="closeDialog" /></div>
+      <div class="close" v-if="close" @click="closeDialog">
+        <CloseA v-if="close === 'A'" />
+        <CloseB v-if="close === 'B'" />
+        <Close v-else />
+      </div>
+
       <header>
         <slot name="header"></slot>
       </header>

+ 137 - 0
packages/webview/src/components/Dialogs/SelectSignTypeDialog.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="move-page-popup" v-if="show">
+    <Dialog :show="show" :dialogName="dialogName" :close="true">
+      <p class="title title-color">{{ $t('signatures.selectTypeDialog.selectType') }}</p>
+
+      <div class="select-content">
+        <div @click="type = 'electronic'" class="option">
+          <RadioBtnSel v-if="type === 'electronic'" /><RadioBtnDis v-else />
+          <div class="content">
+            <p class="title">{{ $t('signatures.selectTypeDialog.signWithElectronic') }}</p>
+            <p>{{ $t('signatures.selectTypeDialog.signWithElectronicDesc') }}</p>
+          </div>
+        </div>
+        <div @click="type = 'digital'" class="option">
+          <RadioBtnSel v-if="type === 'digital'" /><RadioBtnDis v-else />
+          <div class="content">
+            <p class="title">{{ $t('signatures.selectTypeDialog.signWithDigital') }}</p>
+            <p>{{ $t('signatures.selectTypeDialog.signWithDigitalDesc') }}</p>
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="rect-button blue" @click="toSign">{{ $t('signatures.selectTypeDialog.startSigning') }}</div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import core from '@/core'
+
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const dialogName = 'selectSignTypeDialog'
+const show = computed(() => useViewer.isElementOpen(dialogName))
+const activeTool = computed(() => useDocument.getActiveTool)
+
+const type = ref('electronic')
+
+const showDialog = (data) => {
+  useViewer.setSelectedSignatureField(data)
+  if (activeTool.value === 'addDigitalSign') {
+    useViewer.openElement('addDigitalFileDialog')
+  }
+  else if (activeTool.value === 'addElectronicSign') openSignCreatePanel()
+  else useViewer.openElement('selectSignTypeDialog')
+}
+core.addEvent('openSelectSignTypeDialog', showDialog)
+
+const toSign = () => {
+  useViewer.closeElement(dialogName)
+  if (type.value === 'digital') useViewer.openElement('addDigitalFileDialog')
+  else openSignCreatePanel()
+}
+
+const openSignCreatePanel = () => {
+  useViewer.toggleElement('signCreatePanel')
+
+  useViewer.setActiceToolMode('sign')
+
+  useViewer.toggleActiveHand(false)
+  core.switchTool(0)
+  
+  core.switchAnnotationEditorMode(0)
+  
+  document.body.style.overflow = 'hidden'
+}
+</script>
+
+<style lang="scss">
+.move-page-popup {
+
+  .dialog-container {
+    width: 372px;
+
+    .close {
+      float: right;
+      margin-top: 3px;
+    }
+
+    main {
+      margin-top: 32px;
+    }
+
+    footer {
+      .rect-button {
+        width: 100%;
+        border-radius: 4px;
+      }
+    }
+  }
+
+  .title {
+    font-size: 16px;
+    font-weight: 700;
+    line-height: 24px;
+  }
+
+  .title-color {
+    color: var(--c-right-side-header-text);
+  }
+
+  .select-content {
+    margin-top: 16px;
+    margin-bottom: 24px;
+
+    .option {
+      align-items: flex-start;
+
+      & + .option {
+        margin-top: 16px;
+      }
+
+      svg {
+        margin-top: 3.5px;
+        margin-right: 8px;
+        width: 16px;
+        height: 16px;
+      }
+
+      .content {
+        display: flex;
+        flex-direction: column;
+
+        .title {
+          margin-bottom: 8px;
+        }
+      }
+    }
+  }
+}
+</style>

File diff suppressed because it is too large
+ 645 - 0
packages/webview/src/components/Dialogs/SignatureAppearanceDialog.vue


+ 205 - 0
packages/webview/src/components/Dialogs/SignatureDetailsDialog.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="signature-details-dialog" v-if="show">
+    <Dialog :show="show" :dialogName="dialogName">
+      <template #header>
+        <p>{{ $t('signatures.detailsDialog.title') }}</p>
+        <CloseB class="close" @click="close" />
+      </template>
+
+      <div class="info">
+        <SignatureValid v-if="signature.signerList[0].isCertTrusted && signature.signerList[0].isSignVerified" />
+        <SignatureInvalid v-else-if="signature.signerList[0].isCertTrusted || signature.signerList[0].isSignVerified" />
+        <SignatureUnknown v-else />
+        <div>
+          <p class="title">{{ $t('signatures.detailsDialog.signBy') }} "{{ signature.signerList[0].certificateList[0].subject.CN }} &lt;{{ signature.signerList[0].certificateList[0].subject.email }}&gt;".</p>
+          <p class="desc">{{ $t('signatures.detailsDialog.signingTime') }} {{ dayjs(signature.date).format('YYYY-MM-DD HH:mm:ss') }}</p>
+        </div>
+      </div>
+
+      <div class="summary">
+        <div class="title">
+          <div class="line-left"></div>
+          <span>{{ $t('signatures.detailsDialog.validitySummary') }}</span>
+          <div class="line-right"></div>
+        </div>
+        <div class="content">
+          <p>{{ signature.signerList[0].isCertTrusted ? $t('signatures.detailsDialog.validIdentity') : $t('signatures.detailsDialog.invalidIdentity') }} </p>
+          <p>{{ signature.signerList[0].isSignVerified ? $t('signatures.detailsDialog.validSignature') : $t('signatures.detailsDialog.invalidSignature') }}</p>
+          <p>{{ signature.signerList[0].length ? $t('signatures.detailsDialog.alteredDoc') : $t('signatures.detailsDialog.notModifiedDoc') }}</p>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="view-button" @click="openDialog">{{ $t('signatures.detailsDialog.viewCertificateDetails') }}</div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import dayjs from 'dayjs'
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+
+const dialogName = 'signatureDetailsDialog'
+
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const show = computed(() => useViewer.isElementOpen(dialogName))
+const signature = computed(() => useDocument.getSelectedSignature)
+
+const close = () => {
+  useViewer.closeElement(dialogName)
+}
+
+const openDialog = () => {
+  close()
+  useViewer.openElement('certificateViewerDialog')
+}
+
+const formatDate = (date) => {
+  return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${formatAMPM(date.getHours())}:${addZero(date.getMinutes())}:${addZero(date.getSeconds())} ${getESTorEDT(date)}`
+}
+const formatAMPM = (hours) => {
+  return hours >= 12 ? 'PM' : 'AM'
+}
+const addZero = (num) => {
+  return num < 10 ? '0' + num : num
+}
+const getESTorEDT = (date) => {
+  let jan = new Date(date.getFullYear(), 0, 1)
+  let jul = new Date(date.getFullYear(), 6, 1)
+  if (date.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset())) {
+    return 'EST'
+  } else {
+    return 'EDT'
+  }
+}
+</script>
+
+<style lang="scss">
+.signature-details-dialog {
+
+  .dialog-container {
+    padding: 0;
+    width: 552px;
+    border-radius: 0;
+    position: relative;
+
+    header {
+      
+      p {
+        margin-top: 6px;
+        margin-left: 12px;
+        font-size: 14px;
+        line-height: 20px;
+        font-weight: 400;
+        color: var(--c-black-text);
+      }
+
+      .close {
+        position: absolute;
+        top: 8px;
+        right: 8px;
+        margin: 0;
+        cursor: pointer;
+      }
+    }
+
+    main {
+      margin-top: 40px;
+      margin-bottom: 15px;
+
+      .info {
+        margin: 0 26px 26px;
+        display: flex;
+        align-items: center;
+
+        svg {
+          margin-right: 16px;
+          min-width: 32px;
+          width: 32px;
+          height: 32px;
+        }
+
+        .title {
+          font-weight: 600;
+          font-size: 16px;
+          line-height: 24px;
+        }
+
+        .desc {
+          margin-top: 8px;
+          font-size: 14px;
+          line-height: 20px;
+        }
+      }
+
+      .summary {
+        margin: auto 24px;
+        height: 345px;
+        border: 1px solid var(--c-header-border);
+        border-top-width: 0px;
+        font-size: 14px;
+        line-height: 20px;
+        color: var(--c-black-text);
+
+        .title {
+          display: flex;
+
+          span {
+            margin: -10px 4px 0 4px;
+            font-weight: 600;
+          }
+
+          .line-left {
+            width: 12px;
+            border-top: 1px solid var(--c-header-border);
+          }
+
+          .line-right {
+            flex: 1;
+            border-top: 1px solid var(--c-header-border);
+          }
+        }
+
+        .content {
+          display: flex;
+          flex-direction: column;
+          padding: 0 20px;
+          margin-top: 10px;
+          margin-bottom: 20px;
+          height: calc(100% - 40px);
+          overflow: auto;
+          p + p {
+            margin-top: 12px;
+          }
+        }
+      }
+    }
+
+    footer {
+      .view-button {
+        margin-right: 26px;
+        margin-bottom: 20px;
+        padding: 6px 8px;
+        height: 32px;
+        border: 1px solid var(--c-header-border);
+        border-radius: 1px;
+        background: var(--c-header-button-active);
+        cursor: pointer;
+        font-size: 14px;
+        line-height: 20px;
+        color: var(--c-black-text);
+
+        &:hover {
+          background: var(--c-popup-bg-hover);
+          color: white;
+        }
+      }
+    }
+  }
+}
+</style>

+ 8 - 2
packages/webview/src/components/DocumentContainer/DocumentContainer.vue

@@ -48,8 +48,14 @@
   <SettingsDialog />
   <PreventDialog />
   <MeasurePop />
+  <SelectSignTypeDialog />
+  <SignatureAppearanceDialog />
+  <SignatureDetailsDialog />
+  <CertificationViewerDialog />
+  <DeleteSignatureDialog />
+  <AddDigitalFileDialog />
   <div v-if="loading && loadingPercent < 100" class="loading-state">{{ $t('loading') }}...</div>
-  <div v-show="!load && loadingPercent <= 0 && activePanelTab !== 'COMPARISON' && ['compare', 'document'].includes(toolMode)" class="upload-container">
+  <div v-show="loadingPercent <= 0 && !load && activePanelTab !== 'COMPARISON' && ['compare', 'document'].includes(toolMode)" class="upload-container">
     <input id="fileInput" type="file" accept=".pdf" @change="handleUpload" />
     <label for="fileInput">{{ $t('upload') }}</label>
   </div>
@@ -81,7 +87,7 @@ const rightPanelSpace = computed(() => {
 const toolMode = computed(() => useViewer.getToolMode)
 const compareStatus = computed(() => useViewer.getCompareStatus)
 const topSpace = computed(() => {
-  return ['view', 'sign', 'document'].includes(useViewer.getToolMode) || (toolMode.value === 'compare' && compareStatus.value !== 'finished') ? 0 : 44
+  return ['view', 'document'].includes(useViewer.getToolMode) || (toolMode.value === 'compare' && compareStatus.value !== 'finished') ? 0 : 44
 })
 const loading = computed(() => useViewer.getUploadLoading && useViewer.getUpload)
 const activePanelTab = computed(() => useViewer.getActiveElementTab('leftPanelTab'))

+ 1 - 0
packages/webview/src/components/Header/Header.vue

@@ -3,6 +3,7 @@
     <HeaderItems :items="items" :rightItems="rightItems" :toolMode="toolMode" />
   </div>
   <Toolbar :toolMode="toolMode" />
+  <SignatureVerifyBar />
 </template>
 
 <script setup>

+ 2 - 18
packages/webview/src/components/HeaderItems/HeaderItems.vue

@@ -34,7 +34,7 @@
             <div v-if="item.element === 'view' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'view' }" @click="changeToolMode('view')">{{ $t('header.viewer') }}</div>
             <div v-else-if="item.element === 'annotation' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'annotation' }" @click="changeToolMode('annotation')">{{ $t('header.annotations') }}</div>
             <div v-else-if="item.element === 'form' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'form' }" @click="changeToolMode('form')">{{ $t('header.forms') }}</div>
-            <div v-else-if="item.element === 'sign' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'sign' }" @click="openSignCreatePanel()">{{ $t('header.signatures') }}</div>
+            <div v-else-if="item.element === 'sign' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'sign' }" @click="changeToolMode('sign')">{{ $t('header.signatures') }}</div>
             <div v-else-if="item.element === 'security' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'security' }" @click="changeToolMode('security')">{{ $t('header.security') }}</div>
             <div v-else-if="item.element === 'compare' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'compare' }" @click="changeToolMode('compare')">{{ $t('header.compare') }}</div>
             <div v-else-if="item.element === 'editor' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'editor' }" @click="changeToolMode('editor')">{{ $t('header.editor') }}</div>
@@ -49,7 +49,7 @@
           <div v-if="item.element === 'view' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'view' }" @click="changeToolMode('view')">{{ $t('header.viewer') }}</div>
           <div v-else-if="item.element === 'annotation' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'annotation' }" @click="changeToolMode('annotation')">{{ $t('header.annotations') }}</div>
           <div v-else-if="item.element === 'form' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'form' }" @click="changeToolMode('form')">{{ $t('header.forms') }}</div>
-          <div v-else-if="item.element === 'sign' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'sign' }" @click="openSignCreatePanel()">{{ $t('header.signatures') }}</div>
+          <div v-else-if="item.element === 'sign' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'sign' }" @click="changeToolMode('sign')">{{ $t('header.signatures') }}</div>
           <div v-else-if="item.element === 'security' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'security' }" @click="changeToolMode('security')">{{ $t('header.security') }}</div>
           <div v-else-if="item.element === 'compare' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'compare' }" @click="changeToolMode('compare')">{{ $t('header.compare') }}</div>
           <div v-else-if="item.element === 'editor' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'editor' }" @click="changeToolMode('editor')">{{ $t('header.editor') }}</div>
@@ -151,22 +151,6 @@ const changeToolMode = (mode) => {
     useViewer.resetPanels()
   }
 }
-
-const openSignCreatePanel = () => {
-  useViewer.toggleElement('signCreatePanel')
-  useViewer.setActiceToolMode('sign')
-
-  useDocument.setToolState('')
-    
-  useViewer.toggleActiveHand(false)
-  switchTool(0)
-  
-  switchAnnotationEditorMode(0)
-  
-  popoverMode.value.setShow(false)
-  
-  document.body.style.overflow = 'hidden'
-}
 </script>
 
 <style lang="scss">

File diff suppressed because it is too large
+ 5 - 0
packages/webview/src/components/Icon/AddDigitalSign.vue


File diff suppressed because it is too large
+ 5 - 0
packages/webview/src/components/Icon/AddElectronicSign.vue


+ 5 - 0
packages/webview/src/components/Icon/CloseB.vue

@@ -0,0 +1,5 @@
+<template>
+  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path fill-rule="evenodd" clip-rule="evenodd" d="M12.6466 13.2531L13.0001 13.6066L13.7072 12.8995L13.3537 12.546L8.7072 7.8995L13.2532 3.35352L13.6067 2.99997L12.8996 2.29286L12.5461 2.64642L8.00009 7.19239L3.45418 2.64648L3.10063 2.29292L2.39352 3.00003L2.74708 3.35358L7.29299 7.8995L2.64657 12.5459L2.29302 12.8995L3.00013 13.6066L3.35368 13.253L8.0001 8.6066L12.6466 13.2531Z" fill="currentColor"/>
+  </svg>
+</template>

File diff suppressed because it is too large
+ 5 - 0
packages/webview/src/components/Icon/CreateSignField.vue


+ 7 - 0
packages/webview/src/components/Icon/FailedIcon.vue

@@ -0,0 +1,7 @@
+<template>
+  <svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M17.5 10.5C17.5 14.6421 14.1421 18 10 18C5.85786 18 2.5 14.6421 2.5 10.5C2.5 6.35786 5.85786 3 10 3C14.1421 3 17.5 6.35786 17.5 10.5Z" fill="#FF6666"/>
+    <path d="M12 8.5L8 12.5" stroke="white" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
+    <path d="M8 8.5L12 12.5" stroke="white" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
+  </svg>
+</template>

+ 9 - 0
packages/webview/src/components/Icon/Logo.vue

@@ -0,0 +1,9 @@
+<template>
+  <svg width="105" height="105" viewBox="0 0 105 105" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <g opacity="0.4">
+      <path d="M0.408203 9.65476C0.408203 4.73679 4.395 0.75 9.31297 0.75H71.6464C76.5644 0.75 80.5512 4.7368 80.5512 9.65477V85.3452H9.31295C4.39499 85.3452 0.408203 81.3584 0.408203 76.4404V9.65476Z" fill="#3863F1"/>
+      <path fill-rule="evenodd" clip-rule="evenodd" d="M56.8051 20.0437C38.7725 20.0437 24.1543 34.662 24.1543 52.6945C24.1543 70.727 38.7726 85.3452 56.8051 85.3452H80.5512V20.0437H56.8051ZM56.805 40.8214C50.2477 40.8214 44.932 46.1371 44.932 52.6944C44.932 59.2517 50.2477 64.5674 56.805 64.5674H80.5511V40.8214H56.805Z" fill="white"/>
+      <path fill-rule="evenodd" clip-rule="evenodd" d="M95.3925 20.0437H80.5512V85.3452L24.1543 85.3452V95.7341C24.1543 100.652 28.1411 104.639 33.059 104.639H95.3925C100.31 104.639 104.297 100.652 104.297 95.7341V28.9485C104.297 24.0305 100.31 20.0437 95.3925 20.0437ZM44.932 52.6944C44.932 46.1371 50.2477 40.8214 56.805 40.8214H80.5511V64.5674H56.805C50.2477 64.5674 44.932 59.2517 44.932 52.6944Z" fill="#31BC98"/>
+    </g>
+  </svg>
+</template>

+ 5 - 0
packages/webview/src/components/Icon/MoreB.vue

@@ -0,0 +1,5 @@
+<template>
+  <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path fill-rule="evenodd" clip-rule="evenodd" d="M11 6C11 6.55228 10.5523 7 10 7C9.44772 7 9 6.55228 9 6C9 5.44772 9.44772 5 10 5C10.5523 5 11 5.44772 11 6ZM11 10C11 10.5523 10.5523 11 10 11C9.44772 11 9 10.5523 9 10C9 9.44772 9.44772 9 10 9C10.5523 9 11 9.44772 11 10ZM10 15C10.5523 15 11 14.5523 11 14C11 13.4477 10.5523 13 10 13C9.44772 13 9 13.4477 9 14C9 14.5523 9.44772 15 10 15Z" fill="currentColor"/>
+  </svg>
+</template>

File diff suppressed because it is too large
+ 5 - 0
packages/webview/src/components/Icon/Signature.vue


File diff suppressed because it is too large
+ 8 - 0
packages/webview/src/components/Icon/SignatureInvalid.vue


File diff suppressed because it is too large
+ 7 - 0
packages/webview/src/components/Icon/SignatureUnknown.vue


File diff suppressed because it is too large
+ 7 - 0
packages/webview/src/components/Icon/SignatureValid.vue


+ 5 - 0
packages/webview/src/components/Icon/SuccessIcon.vue

@@ -0,0 +1,5 @@
+<template>
+  <svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path fill-rule="evenodd" clip-rule="evenodd" d="M10 18C14.1421 18 17.5 14.6421 17.5 10.5C17.5 6.35786 14.1421 3 10 3C5.85786 3 2.5 6.35786 2.5 10.5C2.5 14.6421 5.85786 18 10 18ZM9.74283 13.4801L13.9095 8.48014L12.7572 7.51986L9.11627 11.8889L7.197 9.96967L6.13634 11.0303L8.63634 13.5303L9.21707 14.1111L9.74283 13.4801Z" fill="#3CCD75"/>
+  </svg>
+</template>

File diff suppressed because it is too large
+ 5 - 0
packages/webview/src/components/Icon/VerifyDigitalSign.vue


+ 3 - 0
packages/webview/src/components/LeftPanel/LeftPanel.vue

@@ -25,6 +25,9 @@
       <div class="compare-container" :class="{ hidden: activePanelTab !== 'COMPARISON' }">
         <Compare />
       </div>
+      <div class="signature-container" :class="{ hidden: activePanelTab !== 'SIGNATURE' }">
+        <SignatureContainer />
+      </div>
     </div>
     <div class="findbar-container" :class="{ hidden: activePanelTab !== 'SEARCH' }">
       <SearchContainer />

+ 10 - 0
packages/webview/src/components/LeftPanel/LeftPanelTabs.vue

@@ -49,6 +49,16 @@
     :isActive="activePanelTab === 'COMPARISON'"
     @click="setActiveLeftPanelTab('COMPARISON')"
   />
+  <Button
+    :className="activePanelTab === 'SIGNATURE' ? 'active' : ''"
+    img="icon-signature"
+    :title="$t('leftPanel.signature')"
+    dataElement="Signature"
+    :isActive="activePanelTab === 'SIGNATURE'"
+    @click="setActiveLeftPanelTab('SIGNATURE', 'none')"
+  >
+    <Signature />
+  </Button>
   <Button
     :className="activePanelTab === 'SEARCH' ? 'active' : ''"
     img="icon-search"

+ 158 - 0
packages/webview/src/components/SignatureContainer/SignatureContainer.vue

@@ -0,0 +1,158 @@
+<template>
+  <div class="signature-container">
+    <div class="signature-title">{{ $t('leftPanel.signatureList') }}</div>
+    <div v-if="signatures.length" class="signatures">
+      <template v-for="(signature, index) in signatures" :key="index">
+        <div class="signature" :class="{ 'focus': selectedSignature && selectedSignature.name === signature.name }" @click="goToPage(signature.pageIndex)">
+          <template v-for="(signer, sIndex) in signature.signerList">
+            <SignatureValid v-if="signer.isCertTrusted && signer.isSignVerified" />
+            <SignatureInvalid v-else-if="signer.isCertTrusted || signer.isSignVerified" />
+            <SignatureUnknown v-else />
+            <span :title="signature.name">{{ (signer.certificateList[0].subject.CN ? signer.certificateList[0].subject.CN : '') + '\'s signature' }}</span>
+            <n-popover
+              placement="bottom-start"
+              trigger="click"
+              :show-arrow="false"
+              to="#outerContainer"
+              :raw="true"
+              :z-index="96"
+              class="option-popover"
+              :show="selectedSignature?.name === signature.name && showPopover"
+              @clickoutside="onOutsidePopover"
+            >
+              <template #trigger>
+                <Button class="more" @click.stop="selectSign(signature, index)">
+                  <MoreB />
+                </Button>
+              </template>
+              <div class="drop-down">
+                <div class="drop-item" @click="openDialog('signatureDetailsDialog')">{{ $t('leftPanel.signatureDetails') }}</div>
+                <div class="drop-item" @click="openDialog('certificateViewerDialog')">{{ $t('leftPanel.certificationDetails') }}</div>
+                <div class="drop-item" @click="openDialog('deleteConfirmDialog')">{{ $t('delete') }}</div>
+              </div>
+            </n-popover>
+          </template>
+        </div>
+      </template>
+    </div>
+    <div v-else class="no-signatures">
+      {{ $t('leftPanel.noSignatures') }}
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed, ref, getCurrentInstance } from 'vue'
+import core from '@/core'
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import { NPopover } from 'naive-ui'
+
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const signatures = computed(() => useDocument.getSignatures)
+const selectedSignature = computed(() => useDocument.getSelectedSignature)
+
+const showPopover = ref(false)
+
+const setSignaturesList = (signatures) => {
+  useDocument.initSignatures(signatures)
+  const instance = getCurrentInstance()
+  instance?.proxy?.$forceUpdate()
+}
+core.addEvent('getSignatures', setSignaturesList)
+
+const goToPage = (page) => {
+  if (page < 0) return
+  core.pageNumberChanged({
+    value: (page * 1 + 1).toString()
+  })
+}
+
+const onOutsidePopover = () => {
+  showPopover.value = false
+  useDocument.setSelectedSignature(null)
+}
+
+const openDialog = (name) => {
+  showPopover.value = false
+  useViewer.openElement(name)
+}
+
+const selectSign = (signature, index) => {
+  showPopover.value = true
+  signature.index = index
+  useDocument.setSelectedSignature(signature)
+}
+</script>
+
+<style lang="scss">
+.signature-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+
+  .signature-title {
+    margin: 0;
+    padding: 9px 16px;
+    line-height: 17px;
+    font-size: 14px;
+    font-weight: bold;
+    color: var(--c-side-title);
+  }
+
+  .signatures {
+    flex-grow: 1;
+    overflow-y: auto;
+
+    .signature {
+      display: flex;
+      align-items: center;
+      padding: 6px 16px;
+      // height: 32px;
+      cursor: default;
+
+      &:hover, &.focus {
+        background-color: var(--c-side-header-active);
+      }
+
+      span {
+        margin-left: 8px;
+        text-wrap: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        font-size: 14px;
+        line-height: 16px;
+        color: var(--c-right-side-header-text);
+      }
+
+      svg {
+        min-width: 20px;
+      }
+
+      .more {
+        margin-left: auto;
+        padding: 0;
+        margin-right: 0;
+        border-radius: 2px;
+      }
+    }
+  }
+
+  .no-signatures {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, 50%);
+    text-align: center;
+    font-weight: 700;
+    color: var(--c-side-text);
+    font-size: 14px;
+    line-height: 16px;
+  }
+}
+.option-popover .drop-down .drop-item {
+  padding: 6px 8px;
+}
+</style>

+ 90 - 0
packages/webview/src/components/SignatureToolBar/SignatureToolBar.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="signature-tool">
+    <Button id="createSignFieldButton" :class="{ active: activeTool === 'signatureFields' }" @click="changeActiveTool('signatureFields')" :title="$t('header.createSignField')">
+      <CreateSignField />
+      <span>{{ $t('header.createSignField') }}</span>
+    </Button>
+    <Button :class="{ active: activeTool === 'addDigitalSign' }" @click="changeActiveTool('addDigitalSign')" :title="$t('header.addDigitalSign')">
+      <AddDigitalSign />
+      <span>{{ $t('header.addDigitalSign') }}</span>
+    </Button>
+    <Button :class="{ active: activeTool === 'addElectronicSign' }" @click="changeActiveTool('addElectronicSign')" :title="$t('header.addElectronicSign')">
+      <AddElectronicSign />
+      <span>{{ $t('header.addElectronicSign') }}</span>
+    </Button>
+    <Button @click="verifyDigitalSign" :title="$t('header.verifyDigitalSign')">
+      <VerifyDigitalSign />
+      <span>{{ $t('header.verifyDigitalSign') }}</span>
+    </Button>
+  </div>
+</template>
+
+<script setup>
+  import { computed } from 'vue'
+  import { useViewerStore } from '@/stores/modules/viewer'
+  import { useDocumentStore } from '@/stores/modules/document'
+
+  import core from '@/core'
+  const { switchTool, switchAnnotationEditorMode } = core
+
+  const useDocument = useDocumentStore()
+  const useViewer = useViewerStore()
+
+  const activeTool = computed(() => useDocument.getActiveTool)
+
+  const changeActiveTool = (tool) => {
+    useDocument.setToolState(tool)
+
+    useViewer.toggleActiveHand(false)
+    switchTool(0)
+    
+    switchAnnotationEditorMode(0)
+  }
+
+  const openSignCreatePanel = () => {
+    useViewer.toggleElement('signCreatePanel')
+    useViewer.setActiceToolMode('sign')
+
+    useViewer.toggleActiveHand(false)
+    switchTool(0)
+    
+    switchAnnotationEditorMode(0)
+    
+    document.body.style.overflow = 'hidden'
+  }
+
+  const verifyDigitalSign = () => {
+    changeActiveTool('')
+    useViewer.setSignatureVerify(3)
+  }
+</script>
+
+<style lang="scss" scoped>
+.signature-tool {
+  display: flex;
+  align-items: center;
+
+  button {
+    display: flex;
+    align-items: center;
+    font-family: Helvetica;
+    font-size: 12px;
+    font-weight: 400;
+    line-height: 16px;
+
+    &:last-child {
+      margin-right: 0;
+    }
+
+    svg {
+      min-width: 20px;
+      margin-right: 8px;
+    }
+
+    span {
+      text-wrap: nowrap;
+    }
+  }
+}
+</style>
+

+ 86 - 0
packages/webview/src/components/SignatureToolBar/SignatureVerifyBar.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="signature-verify" v-if="signatureVerify">
+    <div v-if="signatureVerify === 1">
+      <SignatureValid />
+      <span>{{ $t('signatures.detailsDialog.validSignature') }}</span>
+    </div>
+    <div v-else-if="signatureVerify === 2">
+      <SignatureInvalid />
+      <span>{{ $t('signatures.detailsDialog.invalidSignature') }}</span>
+    </div>
+    <div v-else>
+      <SignatureUnknown />
+      <span>{{ $t('signatures.detailsDialog.unknownSignature') }}</span>
+    </div>
+    <div class="view-button" @click="viewAll">{{ $t('signatures.detailsDialog.viewAllSignatures') }}</div>
+  </div>
+</template>
+
+<script setup>
+  import { computed } from 'vue'
+  import { useViewerStore } from '@/stores/modules/viewer'
+  import { useDocumentStore } from '@/stores/modules/document'
+  import core from '@/core'
+
+  const useViewer = useViewerStore()
+  const useDocument = useDocumentStore()
+
+  const signatureVerify = computed(() => useViewer.getSignatureVerify)
+
+  const viewAll = () => {
+    useViewer.openElement('leftPanel')
+    useViewer.setActiveElementTab('leftPanelTab', 'SIGNATURE')
+    useViewer.setSignatureVerify(0)
+  }
+</script>
+
+<style lang="scss" scoped>
+.signature-verify {
+  padding: 8px 16px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  z-index: 71;
+  height: 48px;
+  background: #DDE9FF;
+  animation: ease-out 1s;
+
+  div:first-child {
+    display: flex;
+    align-items: center;
+  }
+
+  svg {
+    width: 32px;
+    height: 32px;
+  }
+
+  span {
+    margin-left: 8px;
+    font-size: 14px;
+    line-height: 16px;
+    color: #001A4E;
+  }
+
+  .view-button {
+    padding: 8px 12px;
+    height: 32px;
+    border: 1px solid #1460F3;
+    cursor: pointer;
+    font-size: 14px;
+    line-height: 16px;
+    color: #1460F3;
+
+    &:hover {
+      background: #1460F3;
+      color: white;
+    }
+  }
+}
+
+@keyframes ease-out {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+</style>
+

+ 156 - 0
packages/webview/src/components/Signatures/DigitalKeyboardCreatePanel.vue

@@ -0,0 +1,156 @@
+<template>
+  <Dialog class="digital-signature-dialog" :show="true" :dialogName="dialogName">
+    <template #header>
+      <CloseB class="close" @click="close" />
+    </template>
+
+    <div class="paint-container">
+      <div class="keyboard-paint" style=""><input v-model="textContent" type="text" :placeholder="$t('signatures.appearanceDialog.signHere')"></div>
+    </div>
+
+    <template #footer>
+      <div class="rect-button white" @click="clear">{{ $t('signatures.clear') }}</div>
+      <div class="rect-button white" @click="close">{{ $t('cancel') }}</div>
+      <div class="rect-button blue" @click="save">{{ $t('ok') }}</div>
+    </template>
+  </Dialog>
+</template>
+
+<script setup>
+import { computed, watch, ref } from 'vue'
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import core from '@/core'
+
+const { text } = defineProps(['text'])
+
+const emits = defineEmits(['modifyText'])
+
+const dialogName = 'digitalKeyboardCreatePanel'
+
+const textContent = ref(text)
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const show = computed(() => useViewer.isElementOpen(dialogName))
+
+const close = () => {
+  useViewer.closeElement(dialogName)
+  clear()
+}
+
+const clear = () => {
+  textContent.value = ''
+}
+
+const save = () => {
+  emits('modifyText', textContent.value)
+  close()
+}
+</script>
+
+<style lang="scss">
+.digital-signature-dialog {
+
+  .dialog-container {
+    padding: 15px 23px;
+    width: 628px;
+    border-radius: 0;
+    position: relative;
+
+    header {
+      
+      p {
+        margin-left: -3px;
+        font-size: 14px;
+        line-height: 16px;
+        font-weight: 400;
+        color: var(--c-right-side-header-text);
+      }
+
+      .close {
+        position: absolute;
+        top: 8px;
+        right: 8px;
+        margin: 0;
+        cursor: pointer;
+      }
+    }
+
+    main {
+      margin-top: 24px;
+      margin-bottom: 20px;
+    }
+
+    footer {
+
+      .rect-button {
+        border-radius: 4px;
+      }
+    }
+  }
+
+  .dialog-container {
+    padding: 16px 24px;
+
+    main {
+      margin-top: 40px;
+      margin-bottom: 12px;
+    }
+
+    footer {
+
+      .rect-button.white {
+        border-radius: 2px;
+      }
+
+      .rect-button:first-child {
+        margin-right: auto;
+      }
+    }
+  }
+
+  .paint-container {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 240px;
+    background-color: #F2F3F5;
+
+    .keyboard-paint {
+      flex: 1;
+
+      input {
+        width: 100%;
+        font-size: 46px;
+        line-height: 64px;
+        font-weight: 400;
+        color: #000;
+        border: none;
+        background: transparent;
+        text-align: center;
+        font-family: 'Courier New', Courier, monospace;
+      }
+    }
+
+    p {
+      position: absolute;
+      font-size: 14px;
+      line-height: 16px;
+      color: #757780;
+      pointer-events: none;
+    }
+  }
+}
+
+@media screen and (max-width: 628px) {
+  .digital-signature-dialog .dialog-container {
+    width: 100%;
+
+    footer {
+      margin-top: 0;
+    }
+  }
+}
+</style>

+ 146 - 0
packages/webview/src/components/Signatures/DigitalSignCreatePanel.vue

@@ -0,0 +1,146 @@
+<template>
+  <Dialog class="digital-signature-dialog" :show="true" :dialogName="dialogName">
+    <template #header>
+      <CloseB class="close" @click="close" />
+    </template>
+
+    <div class="paint-container">
+      <canvas id="digitalTrackpadCanvas" width="578" height="240"></canvas>
+    </div>
+
+    <template #footer>
+      <div class="rect-button white" @click="clear">{{ $t('signatures.clear') }}</div>
+      <div class="rect-button white" @click="close">{{ $t('cancel') }}</div>
+      <div class="rect-button blue" @click="save">{{ $t('ok') }}</div>
+    </template>
+  </Dialog>
+</template>
+
+<script setup>
+import { computed, watch, toRaw } from 'vue'
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import core from '@/core'
+
+const emits = defineEmits(['modifyImage'])
+const dialogName = 'digitalSignCreatePanel'
+
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const show = computed(() => useViewer.isElementOpen(dialogName))
+
+const close = () => {
+  useViewer.closeElement(dialogName)
+  clear()
+}
+
+const clear = () => {
+  core.handleSign({
+    flag: 'clear'
+  })
+}
+
+const save = () => {
+  const imageBase64 = core.handleSign({
+    type: 1,
+    flag: 'image',
+  })
+  emits('modifyImage', imageBase64)
+  close()
+}
+</script>
+
+<style lang="scss">
+.digital-signature-dialog {
+
+  .dialog-container {
+    padding: 15px 23px;
+    width: 628px;
+    border-radius: 0;
+    position: relative;
+
+    header {
+      
+      p {
+        margin-left: -3px;
+        font-size: 14px;
+        line-height: 16px;
+        font-weight: 400;
+        color: var(--c-right-side-header-text);
+      }
+
+      .close {
+        position: absolute;
+        top: 8px;
+        right: 8px;
+        margin: 0;
+        cursor: pointer;
+      }
+    }
+
+    main {
+      margin-top: 24px;
+      margin-bottom: 20px;
+    }
+
+    footer {
+
+      .rect-button {
+        border-radius: 4px;
+      }
+    }
+  }
+
+  .dialog-container {
+    padding: 16px 24px;
+
+    main {
+      margin-top: 40px;
+      margin-bottom: 12px;
+    }
+
+    footer {
+
+      .rect-button.white {
+        border-radius: 2px;
+      }
+
+      .rect-button:first-child {
+        margin-right: auto;
+      }
+    }
+  }
+
+  .paint-container {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 240px;
+    background-color: #F2F3F5;
+
+    canvas {
+      z-index: 1;
+    }
+
+    p {
+      position: absolute;
+      font-size: 14px;
+      line-height: 16px;
+      color: #757780;
+      pointer-events: none;
+    }
+  }
+}
+
+@media screen and (max-width: 628px) {
+  .digital-signature-dialog .dialog-container {
+    width: 100%;
+
+    footer {
+      margin-top: 0;
+    }
+  }
+}
+</style>

+ 72 - 43
packages/webview/src/components/Signatures/SignCreatePanel.vue

@@ -7,7 +7,7 @@
     </div>
     <div class="paint-container">
       <!-- 手绘签名 -->
-      <canvas v-show="activeSignWay === 'trackpad'" id="trackpadCanvas" width="578" height="240"></canvas>
+      <canvas v-show="activeSignWay === 'trackpad'" id="electronicTrackpadCanvas" width="578" height="240"></canvas>
 
       <!-- 文本签名 -->
       <div class="keyboard-paint" v-show="activeSignWay === 'keyboard'">
@@ -76,7 +76,7 @@
 </template>
 
 <script setup>
-import { ref, computed, h, watch, getCurrentInstance } from 'vue'
+import { toRaw, ref, computed, h, watch, getCurrentInstance } from 'vue'
 import { useViewerStore } from '@/stores/modules/viewer'
 import { useDocumentStore } from '@/stores/modules/document'
 import core from '@/core'
@@ -87,6 +87,8 @@ const instance = getCurrentInstance().appContext.app.config.globalProperties
 
 const useViewer = useViewerStore()
 const useDocument = useDocumentStore()
+
+const show = computed(() => useViewer.isElementOpen('signCreatePanel'))
 const activeSignWay = computed(() => useViewer.getActiveElementTab('signPanelTab'))
 const toolMode = computed(() => useViewer.getToolMode)
 const activeTool = computed(() => useDocument.getActiveTool)
@@ -99,7 +101,6 @@ const fontFamily = ref('Courier New')
 
 watch(() => toolMode.value, (newValue, oldValue) => {
   if (!(oldValue === 'sign' && newValue === 'view') && newValue !== oldValue) {
-    clearData()
     const imgUi = document.getElementById("sign-image-save")
     if (imgUi) imgUi.remove()
   }
@@ -111,20 +112,26 @@ watch(() => widthValue.value, (newValue, oldValue) => {
     widthValue.value = oldValue
   }
   if (activeSignWay.value === 'trackpad') {
-      core.handleSign('update', {
+    core.handleSign({
+      flag: 'update',
+      param: {
         width: widthValue.value,
         color: fontColor.value
-      })
-    }
+      }
+    })
+  }
 })
 
-watch(()=>fontColor.value,(newValue,oldValue)=>{
-  core.handleSign('update',{
-      width:widthValue.value,
-      color:fontColor.value
-    })
+watch(() => fontColor.value, (newValue, oldValue) => {
+  core.handleSign({
+    flag: 'update',
+    param: {
+      width: widthValue.value,
+      color: fontColor.value
+    }
+  })
 
-    if (activeSignWay.value === "keyboard") {
+  if (activeSignWay.value === "keyboard") {
     let inputElement = document.querySelector(".keyboard-paint input")
     if (inputElement) {
       inputElement.style.color = fontColor.value
@@ -132,6 +139,13 @@ watch(()=>fontColor.value,(newValue,oldValue)=>{
   }
 })
 
+watch(() => show.value, (newValue, oldValue) => {
+  if (newValue) {
+    useViewer.setActiveSignWay('keyboard')
+    core.handleSign({ flag: 'create' })
+  }
+})
+
 const changeActiveSignWay = (way) => {
   useViewer.setActiveSignWay(way)
   clearData()
@@ -141,14 +155,15 @@ const changeActiveSignWay = (way) => {
 const closePanel = (flag) => {
   useViewer.toggleElement('signCreatePanel')
   if (flag === 'cancel' && activeSignWay.value === 'trackpad') {
-    core.handleSign('reset')
+    core.handleSign({ flag: 'reset' })
   }
   if (activeSignWay.value !== 'trackpad') {
     clearData()
   }
   clearProperty()
-  useViewer.setActiceToolMode('view')
   document.body.style.overflow = 'auto'
+
+  if (!activeTool.value && flag === 'cancel') useViewer.openElement('selectSignTypeDialog')
 }
 
 const clearData = () => {
@@ -156,7 +171,7 @@ const clearData = () => {
     textSgin.value = ''
   }
   if (activeSignWay.value === "image" || activeTool.value === 'image') {
-    imageData.value='';
+    imageData.value = ''
     let imageDiv = document.querySelector('.image-paint')
     let imageInput = document.querySelector('.image-paint input')
     let imageLabel = document.querySelector('.image-paint a')
@@ -164,7 +179,7 @@ const clearData = () => {
     if (imageDiv) {
       if (imageShow) {
         imageDiv.removeChild(imageShow)
-        imageInput.value=''
+        imageInput.value = ''
       }
       if (imageLabel) {
         imageLabel.style.display = "block"
@@ -172,7 +187,7 @@ const clearData = () => {
     }
   }
   if (activeSignWay.value === 'trackpad') {
-    core.handleSign('clear')
+    core.handleSign({ flag: 'clear' })
   }
 }
 
@@ -189,9 +204,12 @@ const clearProperty = () => {
   }
   if (widthValue.value !== 5) {
     widthValue.value = 5
-    core.handleSign('update', {
-      width: widthValue.value,
-      color: fontColor.value
+    core.handleSign({
+      flag: 'update',
+      param: {
+        width: widthValue.value,
+        color: fontColor.value
+      }
     })
   }
 }
@@ -204,15 +222,16 @@ const handleFile = (evt) => {
       icon: () => h('img', { src: MessageError })
     })
     evt.target.value = ''
-    return;
+    return
   }
+
   if (toolMode.value === 'annotation') {
     useDocument.setToolState('image')
+    core.handleCreateStatus({
+      bool: true,
+      fileData: file
+    })
   }
-  core.handleCreateStatus({
-    bool: true,
-    fileData: file
-  })
 
   const fileReader = new FileReader()
   fileReader.readAsDataURL(file)
@@ -230,7 +249,7 @@ const handleFile = (evt) => {
     }
     if (activeTool.value === 'image') {
       const image = new Image()
-      image.onload = function() {
+      image.onload = function () {
         save()
       }
       image.src = imageData.value
@@ -248,6 +267,8 @@ const save = () => {
   let showData = ''
   let imgWidth = 0
   let imgHeight = 0
+  const selectedSignatureField = useViewer.getSelectedSignatureField
+
   if ((activeSignWay.value === "image" && imageData.value !== '') || (activeTool.value === 'image' && imageData.value !== '')) {
     showData = imageData.value
   }
@@ -257,27 +278,25 @@ const save = () => {
     let canvasDraw = document.createElement("canvas")
 
     let ctx = canvasItem.getContext("2d")
-    ctx.font = "46px "+fontFamily.value
+    ctx.font = "46px " + fontFamily.value
     let textWidth = ctx.measureText(textSgin.value).width
 
     canvasDraw.width = textWidth
     canvasDraw.height = 60
     ctx = canvasDraw.getContext("2d")
 
-    ctx.font = "46px "+fontFamily.value
-    ctx.fillStyle = "white"
-    ctx.fillRect(0, 0, textWidth, 60)
+    ctx.font = "46px " + fontFamily.value
     ctx.fillStyle = fontColor.value
     ctx.fillText(textSgin.value, 0, 50)
-    
-    imgWidth=textWidth
-    imgHeight=60
+
+    imgWidth = textWidth
+    imgHeight = 60
     showData = canvasDraw.toDataURL()
   }
 
   if (showData) {
     let docDiv = document.querySelector('.content')
-    if (docDiv) {
+    if (docDiv && activeTool.value === 'image') {
       let imgUi = document.getElementById("sign-image-save")
       if (!imgUi) {
         imgUi = document.createElement("img")
@@ -310,12 +329,24 @@ const save = () => {
         imgUi.style.left = (evt.clientX - offsetX) + "px"
         imgUi.style.top = (evt.clientY - offsetY) + "px"
       }
+    } else {
+      core.handleSign({
+        flag: 'save',
+        param: {
+          imageBase64: showData,
+          annotation: toRaw(selectedSignatureField)
+        }
+      })
     }
   }
   if (activeSignWay.value === 'trackpad') {
-    core.handleSign('save', {
-      width:widthValue.value,
-      color:fontColor.value
+    core.handleSign({
+      flag: 'save',
+      param: {
+        width: widthValue.value,
+        color: fontColor.value,
+        annotation: toRaw(selectedSignatureField)
+      }
     })
   }
 
@@ -336,12 +367,10 @@ const setActiveToolColor = (color) => {
   fontColor.value = color
 }
 
-const handleFontChange=(evt)=>
-{
-  let inputElement= document.querySelector(".keyboard-paint input")
-  if(inputElement)
-  {
-    inputElement.style.fontFamily=evt.target.value
+const handleFontChange = (evt) => {
+  let inputElement = document.querySelector(".keyboard-paint input")
+  if (inputElement) {
+    inputElement.style.fontFamily = evt.target.value
   }
 }
 </script>

+ 3 - 1
packages/webview/src/components/Toolbar/Toolbar.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="toolbar" :class="{ hidden: ['view', 'sign', 'document'].includes(toolMode) || (toolMode === 'compare' && compareStatus !== 'finished'), security: toolMode === 'security', editor: toolMode === 'editor'}">
+  <div class="toolbar" :class="{ hidden: ['view', 'document'].includes(toolMode) || (toolMode === 'compare' && compareStatus !== 'finished'), security: toolMode === 'security', editor: toolMode === 'editor'}">
     <template v-for="(item, index) in toolItems[toolMode]" :key="`${item.type}-${item.dataElement || index}`">
       <!-- Annotation -->
       <Annotate v-if="item.type === 'annotation'" :item="item.tools" />
@@ -20,6 +20,8 @@
       <ComparedToolbar v-else-if="item.type === 'comparedToolbar'" />
       <!-- Content Editor -->
       <ContentEditorToolBar v-else-if="item.type === 'contentEditorToolbar'" />
+      <!-- Signature -->
+      <SignatureToolBar v-else-if="item.type === 'signatureToolBar'" />
     </template>
   </div>
 </template>

+ 3 - 0
packages/webview/src/core/deleteSignature.js

@@ -0,0 +1,3 @@
+import core from '@/core'
+
+export default (signature) => core.getDocumentViewer().deleteSignature(signature)

+ 3 - 0
packages/webview/src/core/getSignature.js

@@ -0,0 +1,3 @@
+import core from '@/core'
+
+export default (data) => core.getDocumentViewer().getSignature(data)

+ 1 - 1
packages/webview/src/core/handleSign.js

@@ -1,3 +1,3 @@
 import core from '@/core'
 
-export default (flag,param) => core.getDocumentViewer().handleSign(flag,param)
+export default (data) => core.getDocumentViewer().handleSign(data)

+ 7 - 1
packages/webview/src/core/index.js

@@ -62,6 +62,9 @@ import handleReplyAnnotation from './handleReplyAnnotation'
 import setHighlightLink from './setHighlightLink'
 import setHighlightForm from './setHighlightForm'
 import setAnnotator from './setAnnotator'
+import getSignature from './getSignature'
+import deleteSignature from './deleteSignature'
+import loadCertificates from './loadCertificates'
 
 export default {
   getDocumentViewer,
@@ -130,5 +133,8 @@ export default {
   handleReplyAnnotation,
   setHighlightLink,
   setHighlightForm,
-  setAnnotator
+  setAnnotator,
+  getSignature,
+  deleteSignature,
+  loadCertificates,
 }

+ 3 - 0
packages/webview/src/core/loadCertificates.js

@@ -0,0 +1,3 @@
+import core from '@/core'
+
+export default (data) => core.getDocumentViewer().loadCertificates(data)

+ 29 - 2
packages/webview/src/stores/modules/document.js

@@ -98,6 +98,9 @@ export const useDocumentStore = defineStore({
     link: {
       color: '#DDE9FF'
     },
+    signatureFields: {
+      color: '#DDE9FF'
+    },
     propertyPanel: {
       fieldName: '',
       isHidden: '0',
@@ -143,7 +146,9 @@ export const useDocumentStore = defineStore({
     author: 'ComPDFKit', // 文档作者
     annotator: 'Guest', // 注释作者
     highlightLink: true,
-    highlightForm: true
+    highlightForm: true,
+    signatures: [],
+    selectedSignature: null, // 在侧边栏签名面板选中的签名
   }),
   getters: {
     getTotalPages () {
@@ -190,6 +195,7 @@ export const useDocumentStore = defineStore({
         annotationsCount: 0
       }
       const annotations = this.annotations
+
       for (const page in annotations) {
         const pageAnnotations = annotations[page]
         const len = pageAnnotations.length
@@ -284,6 +290,15 @@ export const useDocumentStore = defineStore({
     },
     getHighlightForm () {
       return this.highlightForm
+    },
+    getSignatures () {
+      return this.signatures
+    },
+    getSelectedSignature () {
+      return this.selectedSignature
+    },
+    getPkcs12 () {
+      return this.pkcs12
     }
   },
   actions: {
@@ -296,6 +311,7 @@ export const useDocumentStore = defineStore({
       this.activeOutlineId = null
       this.searchResults = []
       this.docEditorOperationList = []
+      this.signatures = []
     },
     setTotalPages (totalPages) {
       this.totalPages = totalPages
@@ -460,6 +476,17 @@ export const useDocumentStore = defineStore({
     setHighlightForm (bool) {
       this.highlightForm = bool
       core.setHighlightForm(bool)
-    }
+    },
+    setSelectedSignature (data) {
+      this.selectedSignature = null
+      this.selectedSignature = data
+    },
+    initSignatures (signatures) {
+      this.signatures = null
+      this.signatures = signatures.filter(signature => signature.signerList.length > 0)
+    },
+    setPkcs12 (data) {
+      this.pkcs12 = data
+    },
   }
 })

+ 55 - 13
packages/webview/src/stores/modules/viewer.js

@@ -32,6 +32,7 @@ export const useViewerStore = defineStore({
       settingsDialog: false,
       setPasswordModal: false,
       signCreatePanel: false,
+      digitalSignCreatePanel: false,
       downloadSettingDialog: false,
       printSettingDialog: false,
       contentEditorPanel: false,
@@ -40,13 +41,22 @@ export const useViewerStore = defineStore({
       insertPageSettingDialog: false,
       deletePageDialog: false,
       extractPageSettingDialog: false,
-      movePageSettingDialog: false
+      movePageSettingDialog: false,
+      selectSignTypeDialog: false,
+      signatureAppearanceDialog: false,
+      signatureDetailsDialog: false,
+      certificateViewerDialog: false,
+      deleteSignatureDialog: false,
+      addDigitalFileDialog: false,
+    },
+    digitalSignature: {
+      preview: null,
     },
     activeElementsTab: {
       leftPanelTab: 'THUMBS',
       rightPanelTab: 'GENERAL',
       stampPanelTab: 'STANDARD',
-      signPanelTab: 'trackpad'
+      signPanelTab: 'keyboard'
     },
     headers: [
       {
@@ -335,6 +345,11 @@ export const useViewerStore = defineStore({
         {
           type: 'contentEditorToolbar'
         }
+      ],
+      sign: [
+        {
+          type: 'signatureToolBar'
+        }
       ]
     },
     downloading: false,
@@ -348,7 +363,9 @@ export const useViewerStore = defineStore({
     upload: true, // 未上传文件的upload按钮
     uploadLoading: false, // 上传中的loading效果
     compareMode: 'content',
-    contentEditorType: 'text'
+    contentEditorType: 'text',
+    signatureVerify: 0, // 验证数字签名显示的横幅 0:不显示 1:有效 2:无效 3:未知
+    selectedSignatureField: null
   }),
   getters: {
     getFullMode () {
@@ -440,13 +457,25 @@ export const useViewerStore = defineStore({
     },
     getContentEditorType () {
       return this.contentEditorType
+    },
+    getSignatureVerify () {
+      return this.signatureVerify
+    },
+    getSelectedSignatureField () {
+      return this.selectedSignatureField
+    },
+    getDigitalSignaturePreview () {
+      return this.digitalSignature.preview
+    },
+    getCertSubject () {
+      return this.digitalSignature.certSubject
     }
   },
   actions: {
     resetSetting () {
       this.fullMode = false
       this.currentPage = 0
-      this.scale = '',
+      this.scale = ''
       this.themeMode = 'Light'
       this.pageMode = 0
       this.scrollMode = 'Vertical'
@@ -464,20 +493,28 @@ export const useViewerStore = defineStore({
         docEditorPreventDialog: false,
         setPasswordModal: false,
         signCreatePanel: false,
+        digitalSignCreatePanel: false,
         downloadSettingDialog: false,
         printSettingDialog: false,
         contentEditorPanel: false,
         insertPageSettingDialog: false,
         deletePageDialog: false,
         extractPageSettingDialog: false,
-        movePageSettingDialog: false
-      },
+        movePageSettingDialog: false,
+        selectSignTypeDialog: false,
+        signatureAppearanceDialog: false,
+        signatureDetailsDialog: false,
+        certificateViewerDialog: false,
+        deleteSignatureDialog: false,
+        addDigitalFileDialog: false
+      }
       this.activeElementsTab = {
         leftPanelTab: 'THUMBS',
         rightPanelTab: 'GENERAL',
         stampPanelTab: 'STANDARD',
         signPanelTab: 'trackpad'
       }
+      this.signatureVerify = 0
     },
     resetPanels () {
       this.searchStatus = false
@@ -533,10 +570,6 @@ export const useViewerStore = defineStore({
       } else {
         this.openElement(dataElement)
       }
-
-      if (dataElement === 'signCreatePanel' && this.activeElements[dataElement]) {
-        this.setActiveSignWay('trackpad')
-      }
     },
     setActiveElementTab (dataElement, tab) {
       this.activeElementsTab[dataElement] = tab
@@ -577,9 +610,6 @@ export const useViewerStore = defineStore({
     },
     setActiveSignWay (way) {
       this.activeElementsTab['signPanelTab'] = way
-      if (this.activeElementsTab['signPanelTab'] === 'trackpad') {
-        core.handleSign('create')
-      }
     },
     setPopoverChanged (bool) {
       this.popoverChanged = bool
@@ -598,6 +628,18 @@ export const useViewerStore = defineStore({
     },
     setContentEditorType (type) {
       this.contentEditorType = type
+    },
+    setSignatureVerify (bool) {
+      this.signatureVerify = bool
+    },
+    setSelectedSignatureField(annotation) {
+      this.selectedSignatureField = annotation
+    },
+    setDigitalSignaturePreview (preview) {
+      this.digitalSignature.preview = preview
+    },
+    setCertSubject (subject) {
+      this.digitalSignature.certSubject = subject
     }
   }
 })