Compare commits
17 Commits
94790aeb1f
...
0d83ec6b6c
Author | SHA1 | Date | |
---|---|---|---|
0d83ec6b6c | |||
43924743e2 | |||
![]() |
7feb03999b | ||
![]() |
0f834debb7 | ||
![]() |
bf1f861031 | ||
![]() |
e026bf1577 | ||
![]() |
25feb83db7 | ||
![]() |
4b3957a882 | ||
![]() |
6cbaf97a70 | ||
![]() |
1aafac7336 | ||
![]() |
51569c8aec | ||
![]() |
948b24a084 | ||
![]() |
abd05a5aaa | ||
![]() |
a3b23161c4 | ||
![]() |
9616146853 | ||
![]() |
ce8274cc9c | ||
![]() |
c958b32269 |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
35
.github/workflows/go.yml
vendored
35
.github/workflows/go.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Build & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.20
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
55
.github/workflows/golangci-lint.yml
vendored
55
.github/workflows/golangci-lint.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
# pull-requests: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
cache: false
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
# Require: The version of golangci-lint to use.
|
||||
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
|
||||
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
|
||||
version: v1.62
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
#
|
||||
# Note: By default, the `.golangci.yml` file should be at the root of the repository.
|
||||
# The location of the configuration file can be changed by using `--config=`
|
||||
# args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
# Optional: if set to true, then all caching functionality will be completely disabled,
|
||||
# takes precedence over all other caching options.
|
||||
# skip-cache: true
|
||||
|
||||
# Optional: if set to true, then the action won't cache or restore ~/go/pkg.
|
||||
# skip-pkg-cache: true
|
||||
|
||||
# Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
|
||||
# skip-build-cache: true
|
||||
|
||||
# Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
|
||||
# install-mode: "goinstall"
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ testfiles/*_signed.pdf
|
||||
testfiles/failed/*
|
||||
pdfsign
|
||||
certs/*
|
||||
tmp/
|
||||
|
65
README.md
65
README.md
@@ -1,15 +1,7 @@
|
||||
# Signing PDF files with Go
|
||||
|
||||
[](https://github.com/digitorus/pdfsign/actions?query=workflow%3Abuild-and-test)
|
||||
[](https://github.com/digitorus/pdfsign/actions?query=workflow%3Agolangci-lint)
|
||||
[](https://goreportcard.com/report/github.com/digitorus/pdfsign)
|
||||
[](https://codecov.io/gh/)
|
||||
[](https://pkg.go.dev/github.com/digitorus/pdfsign)
|
||||
|
||||
This PDF signing library is written in [Go](https://go.dev). The library is in development, might not work for all PDF files and the API might change, bug reports, contributions and suggestions are welcome.
|
||||
|
||||
**See also our [PDFSigner](https://github.com/digitorus/pdfsigner/), a more advanced digital signature server that is using this project.**
|
||||
|
||||
## From the command line
|
||||
|
||||
```
|
||||
@@ -98,3 +90,60 @@ if err != nil {
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Signature Appearance with Text and / or Images
|
||||
|
||||
You can add an image (JPG or PNG) to the visible signature appearance. This is useful for including a handwritten signature or a company logo in the signature field.
|
||||
|
||||
**Supported image formats:** JPG and PNG.
|
||||
|
||||
### Example: Signing a PDF with a visible signature and image
|
||||
|
||||
```go
|
||||
// Read the signature image file
|
||||
signatureImage, err := os.ReadFile("signature.jpg")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err := sign.Sign(inputFile, outputFile, rdr, size, sign.SignData{
|
||||
Signature: sign.SignDataSignature{
|
||||
Info: sign.SignDataSignatureInfo{
|
||||
Name: "John Doe",
|
||||
Location: "Somewhere",
|
||||
Reason: "Signed with image",
|
||||
ContactInfo: "None",
|
||||
Date: time.Now().Local(),
|
||||
},
|
||||
CertType: sign.ApprovalSignature,
|
||||
DocMDPPerm: sign.AllowFillingExistingFormFieldsAndSignaturesPerms,
|
||||
},
|
||||
Appearance: sign.Appearance{
|
||||
Visible: true,
|
||||
LowerLeftX: 400,
|
||||
LowerLeftY: 50,
|
||||
UpperRightX: 600,
|
||||
UpperRightY: 125,
|
||||
Image: signatureImage, // JPG or PNG image bytes
|
||||
// ImageAsWatermark: true, // Optional: set to true to draw text over the image
|
||||
},
|
||||
DigestAlgorithm: crypto.SHA512,
|
||||
Signer: privateKey,
|
||||
Certificate: certificate,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Key Features:
|
||||
|
||||
1. **Image Support**: Both JPG and PNG formats are supported
|
||||
2. **Flexible Positioning**: Control signature placement with LowerLeftX/Y and UpperRightX/Y coordinates
|
||||
3. **Watermark Mode**: Optional ImageAsWatermark setting allows drawing text over the image
|
||||
4. **Transparency Support**: PNG images with alpha channel (transparency) are properly handled
|
||||
|
||||
### Notes:
|
||||
- The image will be scaled to fit the signature rectangle while maintaining its aspect ratio
|
||||
- For optimal results, prepare your image with the desired dimensions and transparency before using it
|
||||
- Only visible approval signatures can include images
|
||||
|
10
go.mod
10
go.mod
@@ -1,12 +1,14 @@
|
||||
module github.com/digitorus/pdfsign
|
||||
module gitea.tryanks.com/Tryanks/pdfsign
|
||||
|
||||
go 1.22
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.6
|
||||
|
||||
require (
|
||||
github.com/digitorus/pdf v0.1.2
|
||||
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7
|
||||
github.com/mattetti/filebuffer v1.0.1
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/text v0.22.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/text v0.25.0
|
||||
)
|
||||
|
8
go.sum
8
go.sum
@@ -7,7 +7,7 @@ github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1G
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y=
|
||||
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
|
||||
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
|
4
sign.go
4
sign.go
@@ -12,8 +12,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/digitorus/pdfsign/sign"
|
||||
"github.com/digitorus/pdfsign/verify"
|
||||
"gitea.tryanks.com/Tryanks/pdfsign/sign"
|
||||
"gitea.tryanks.com/Tryanks/pdfsign/verify"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@@ -2,12 +2,218 @@ package sign
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg" // register JPEG format
|
||||
_ "image/png" // register PNG format
|
||||
)
|
||||
|
||||
func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) {
|
||||
text := context.SignData.Signature.Info.Name
|
||||
// Helper functions for PDF resource components
|
||||
|
||||
// writeAppearanceHeader writes the header for the appearance stream.
|
||||
//
|
||||
// Should be closed by writeFormTypeAndLength.
|
||||
func writeAppearanceHeader(buffer *bytes.Buffer, rectWidth, rectHeight float64) {
|
||||
buffer.WriteString("<<\n")
|
||||
buffer.WriteString(" /Type /XObject\n")
|
||||
buffer.WriteString(" /Subtype /Form\n")
|
||||
buffer.WriteString(fmt.Sprintf(" /BBox [0 0 %f %f]\n", rectWidth, rectHeight))
|
||||
buffer.WriteString(" /Matrix [1 0 0 1 0 0]\n") // No scaling or translation
|
||||
}
|
||||
|
||||
func createFontResource(buffer *bytes.Buffer) {
|
||||
buffer.WriteString(" /Font <<\n")
|
||||
buffer.WriteString(" /F1 <<\n")
|
||||
buffer.WriteString(" /Type /Font\n")
|
||||
buffer.WriteString(" /Subtype /Type1\n")
|
||||
buffer.WriteString(" /BaseFont /Times-Roman\n")
|
||||
buffer.WriteString(" >>\n")
|
||||
buffer.WriteString(" >>\n")
|
||||
}
|
||||
|
||||
func createImageResource(buffer *bytes.Buffer, imageObjectId uint32) {
|
||||
buffer.WriteString(" /XObject <<\n")
|
||||
buffer.WriteString(fmt.Sprintf(" /Im1 %d 0 R\n", imageObjectId))
|
||||
buffer.WriteString(" >>\n")
|
||||
}
|
||||
|
||||
func writeFormTypeAndLength(buffer *bytes.Buffer, streamLength int) {
|
||||
buffer.WriteString(" /FormType 1\n")
|
||||
buffer.WriteString(fmt.Sprintf(" /Length %d\n", streamLength))
|
||||
buffer.WriteString(">>\n")
|
||||
}
|
||||
|
||||
func writeAppearanceStreamBuffer(buffer *bytes.Buffer, stream []byte) {
|
||||
buffer.WriteString("stream\n")
|
||||
buffer.Write(stream)
|
||||
buffer.WriteString("endstream\n")
|
||||
}
|
||||
|
||||
func (context *SignContext) createImageXObject() ([]byte, []byte, error) {
|
||||
imageData := context.SignData.Appearance.Image
|
||||
|
||||
// Read image to get format and decode image data
|
||||
img, format, err := image.Decode(bytes.NewReader(imageData))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
// Get image dimensions
|
||||
bounds := img.Bounds()
|
||||
width := bounds.Max.X - bounds.Min.X
|
||||
height := bounds.Max.Y - bounds.Min.Y
|
||||
|
||||
// Create basic PDF Image XObject
|
||||
var imageObject bytes.Buffer
|
||||
var maskObjectBytes []byte
|
||||
|
||||
imageObject.WriteString("<<\n")
|
||||
imageObject.WriteString(" /Type /XObject\n")
|
||||
imageObject.WriteString(" /Subtype /Image\n")
|
||||
imageObject.WriteString(fmt.Sprintf(" /Width %d\n", width))
|
||||
imageObject.WriteString(fmt.Sprintf(" /Height %d\n", height))
|
||||
imageObject.WriteString(" /ColorSpace /DeviceRGB\n")
|
||||
imageObject.WriteString(" /BitsPerComponent 8\n")
|
||||
|
||||
var rgbData = new(bytes.Buffer)
|
||||
var alphaData = new(bytes.Buffer)
|
||||
|
||||
// Handle different formats
|
||||
switch format {
|
||||
case "jpeg":
|
||||
imageObject.WriteString(" /Filter [/FlateDecode/DCTDecode]\n")
|
||||
rgbData = bytes.NewBuffer(imageData) // JPEG data is already in the correct format
|
||||
case "png":
|
||||
imageObject.WriteString(" /Filter /FlateDecode\n")
|
||||
|
||||
// Extract RGB and alpha values from each pixel
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
// Get the color at pixel (x,y)
|
||||
originalColor := img.At(x, y)
|
||||
|
||||
// Extract RGBA values (ranges from 0-65535 in Go's color model)
|
||||
r, g, b, a := originalColor.RGBA()
|
||||
|
||||
// Convert to 8-bit (0-255)
|
||||
rgbData.WriteByte(byte(r >> 8))
|
||||
rgbData.WriteByte(byte(g >> 8))
|
||||
rgbData.WriteByte(byte(b >> 8))
|
||||
alphaData.WriteByte(byte(a >> 8))
|
||||
}
|
||||
}
|
||||
|
||||
// If image has alpha channel, create soft mask
|
||||
if hasAlpha(img) {
|
||||
compressedAlphaData := compressData(alphaData.Bytes())
|
||||
|
||||
// Create and add the soft mask object
|
||||
maskObjectBytes, err = context.createAlphaMask(width, height, compressedAlphaData)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create alpha mask: %w", err)
|
||||
}
|
||||
|
||||
imageObject.WriteString(fmt.Sprintf(" /SMask %d 0 R\n", context.getNextObjectID()+1)) // the smask will be placed after the image
|
||||
}
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unsupported image format: %s", format)
|
||||
}
|
||||
|
||||
compressedRgbData := compressData(rgbData.Bytes())
|
||||
|
||||
imageObject.WriteString(fmt.Sprintf(" /Length %d\n", len(compressedRgbData)))
|
||||
imageObject.WriteString(">>\n")
|
||||
imageObject.WriteString("stream\n")
|
||||
imageObject.Write(compressedRgbData)
|
||||
imageObject.WriteString("\nendstream\n")
|
||||
|
||||
return imageObject.Bytes(), maskObjectBytes, nil
|
||||
}
|
||||
|
||||
func compressData(data []byte) []byte {
|
||||
var compressedData bytes.Buffer
|
||||
writer := zlib.NewWriter(&compressedData)
|
||||
defer writer.Close()
|
||||
_, err := writer.Write(data)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
writer.Close()
|
||||
return compressedData.Bytes()
|
||||
}
|
||||
|
||||
func (context *SignContext) createAlphaMask(width, height int, alphaData []byte) ([]byte, error) {
|
||||
var maskObject bytes.Buffer
|
||||
|
||||
maskObject.WriteString("<<\n")
|
||||
maskObject.WriteString(" /Type /XObject\n")
|
||||
maskObject.WriteString(" /Subtype /Image\n")
|
||||
maskObject.WriteString(fmt.Sprintf(" /Width %d\n", width))
|
||||
maskObject.WriteString(fmt.Sprintf(" /Height %d\n", height))
|
||||
maskObject.WriteString(" /ColorSpace /DeviceGray\n")
|
||||
maskObject.WriteString(" /BitsPerComponent 8\n")
|
||||
maskObject.WriteString(" /Filter /FlateDecode\n")
|
||||
maskObject.WriteString(fmt.Sprintf(" /Length %d\n", len(alphaData)))
|
||||
maskObject.WriteString(">>\n")
|
||||
maskObject.WriteString("stream\n")
|
||||
maskObject.Write(alphaData)
|
||||
maskObject.WriteString("\nendstream\n")
|
||||
|
||||
return maskObject.Bytes(), nil
|
||||
}
|
||||
|
||||
// hasAlpha checks if the image has an alpha channel
|
||||
func hasAlpha(img image.Image) bool {
|
||||
switch img.(type) {
|
||||
case *image.NRGBA, *image.RGBA:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func computeTextSizeAndPosition(text string, rectWidth, rectHeight float64) (float64, float64, float64) {
|
||||
// Calculate font size
|
||||
fontSize := rectHeight * 0.8 // Use most of the height for the font
|
||||
textWidth := float64(len(text)) * fontSize * 0.5 // Approximate text width
|
||||
if textWidth > rectWidth {
|
||||
fontSize = rectWidth / (float64(len(text)) * 0.5) // Adjust font size to fit text within rect width
|
||||
}
|
||||
|
||||
// Center text horizontally and vertically
|
||||
textWidth = float64(len(text)) * fontSize * 0.5
|
||||
textX := (rectWidth - textWidth) / 2
|
||||
if textX < 0 {
|
||||
textX = 0
|
||||
}
|
||||
textY := (rectHeight-fontSize)/2 + fontSize/3 // Approximate vertical centering
|
||||
|
||||
return fontSize, textX, textY
|
||||
}
|
||||
|
||||
func drawText(buffer *bytes.Buffer, text string, fontSize float64, x, y float64) {
|
||||
buffer.WriteString("q\n") // Save graphics state
|
||||
buffer.WriteString("BT\n") // Begin text
|
||||
buffer.WriteString(fmt.Sprintf("/F1 %.2f Tf\n", fontSize)) // Set font and size
|
||||
buffer.WriteString(fmt.Sprintf("%.2f %.2f Td\n", x, y)) // Set text position
|
||||
buffer.WriteString("0.2 0.2 0.6 rg\n") // Set font color to ballpoint-like color (RGB)
|
||||
buffer.WriteString(fmt.Sprintf("%s Tj\n", pdfString(text))) // Show text
|
||||
buffer.WriteString("ET\n") // End text
|
||||
buffer.WriteString("Q\n") // Restore graphics state
|
||||
}
|
||||
|
||||
func drawImage(buffer *bytes.Buffer, rectWidth, rectHeight float64) {
|
||||
// We save state twice on purpose due to the cm operation
|
||||
buffer.WriteString("q\n") // Save graphics state
|
||||
buffer.WriteString("q\n") // Save before image transformation
|
||||
buffer.WriteString(fmt.Sprintf("%.2f 0 0 %.2f 0 0 cm\n", rectWidth, rectHeight))
|
||||
buffer.WriteString("/Im1 Do\n") // Draw image
|
||||
buffer.WriteString("Q\n") // Restore after transformation
|
||||
buffer.WriteString("Q\n") // Restore graphics state
|
||||
}
|
||||
|
||||
func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) {
|
||||
rectWidth := rect[2] - rect[0]
|
||||
rectHeight := rect[3] - rect[1]
|
||||
|
||||
@@ -15,48 +221,61 @@ func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) {
|
||||
return nil, fmt.Errorf("invalid rectangle dimensions: width %.2f and height %.2f must be greater than 0", rectWidth, rectHeight)
|
||||
}
|
||||
|
||||
// Calculate font size
|
||||
fontSize := rectHeight * 0.8 // Initial font size
|
||||
textWidth := float64(len(text)) * fontSize * 0.5 // Approximate text width
|
||||
if textWidth > rectWidth {
|
||||
fontSize = rectWidth / (float64(len(text)) * 0.5) // Adjust font size to fit text within rect width
|
||||
hasImage := len(context.SignData.Appearance.Image) > 0
|
||||
shouldDisplayText := context.SignData.Appearance.ImageAsWatermark || !hasImage
|
||||
|
||||
// Create the appearance XObject
|
||||
var appearance_buffer bytes.Buffer
|
||||
writeAppearanceHeader(&appearance_buffer, rectWidth, rectHeight)
|
||||
|
||||
// Resources dictionary with font
|
||||
appearance_buffer.WriteString(" /Resources <<\n")
|
||||
|
||||
if hasImage {
|
||||
// Create and add the image XObject
|
||||
imageBytes, maskObjectBytes, err := context.createImageXObject()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image XObject: %w", err)
|
||||
}
|
||||
|
||||
imageObjectId, err := context.addObject(imageBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add image object: %w", err)
|
||||
}
|
||||
|
||||
if maskObjectBytes != nil {
|
||||
// Create and add the mask XObject
|
||||
_, err := context.addObject(maskObjectBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add mask object: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
createImageResource(&appearance_buffer, imageObjectId)
|
||||
}
|
||||
|
||||
var appearance_stream_buffer bytes.Buffer
|
||||
appearance_stream_buffer.WriteString("q\n") // Save graphics state
|
||||
appearance_stream_buffer.WriteString("BT\n") // Begin text
|
||||
appearance_stream_buffer.WriteString(fmt.Sprintf("/F1 %.2f Tf\n", fontSize)) // Font and size
|
||||
appearance_stream_buffer.WriteString(fmt.Sprintf("0 %.2f Td\n", rectHeight-fontSize)) // Position in unit square
|
||||
appearance_stream_buffer.WriteString("0.2 0.2 0.6 rg\n") // Set font color to ballpoint-like color (RGB)
|
||||
appearance_stream_buffer.WriteString(fmt.Sprintf("%s Tj\n", pdfString(text))) // Show text
|
||||
appearance_stream_buffer.WriteString("ET\n") // End text
|
||||
appearance_stream_buffer.WriteString("Q\n") // Restore graphics state
|
||||
if shouldDisplayText {
|
||||
createFontResource(&appearance_buffer)
|
||||
}
|
||||
|
||||
var appearance_buffer bytes.Buffer
|
||||
appearance_buffer.WriteString("<<\n")
|
||||
appearance_buffer.WriteString(" /Type /XObject\n")
|
||||
appearance_buffer.WriteString(" /Subtype /Form\n")
|
||||
appearance_buffer.WriteString(fmt.Sprintf(" /BBox [0 0 %f %f]\n", rectWidth, rectHeight))
|
||||
appearance_buffer.WriteString(" /Matrix [1 0 0 1 0 0]\n") // No scaling or translation
|
||||
|
||||
// Resources dictionary
|
||||
appearance_buffer.WriteString(" /Resources <<\n")
|
||||
appearance_buffer.WriteString(" /Font <<\n")
|
||||
appearance_buffer.WriteString(" /F1 <<\n")
|
||||
appearance_buffer.WriteString(" /Type /Font\n")
|
||||
appearance_buffer.WriteString(" /Subtype /Type1\n")
|
||||
appearance_buffer.WriteString(" /BaseFont /Times-Roman\n")
|
||||
appearance_buffer.WriteString(" >>\n")
|
||||
appearance_buffer.WriteString(" >>\n")
|
||||
appearance_buffer.WriteString(" >>\n")
|
||||
|
||||
appearance_buffer.WriteString(" /FormType 1\n")
|
||||
appearance_buffer.WriteString(fmt.Sprintf(" /Length %d\n", appearance_stream_buffer.Len()))
|
||||
appearance_buffer.WriteString(">>\n")
|
||||
// Create the appearance stream
|
||||
var appearance_stream_buffer bytes.Buffer
|
||||
|
||||
appearance_buffer.WriteString("stream\n")
|
||||
appearance_buffer.Write(appearance_stream_buffer.Bytes())
|
||||
appearance_buffer.WriteString("endstream\n")
|
||||
if hasImage {
|
||||
drawImage(&appearance_stream_buffer, rectWidth, rectHeight)
|
||||
}
|
||||
|
||||
if shouldDisplayText {
|
||||
text := context.SignData.Signature.Info.Name
|
||||
fontSize, textX, textY := computeTextSizeAndPosition(text, rectWidth, rectHeight)
|
||||
drawText(&appearance_stream_buffer, text, fontSize, textX, textY)
|
||||
}
|
||||
|
||||
writeFormTypeAndLength(&appearance_buffer, appearance_stream_buffer.Len())
|
||||
|
||||
writeAppearanceStreamBuffer(&appearance_buffer, appearance_stream_buffer.Bytes())
|
||||
|
||||
return appearance_buffer.Bytes(), nil
|
||||
}
|
||||
|
@@ -28,9 +28,10 @@ func (context *SignContext) createCatalog() ([]byte, error) {
|
||||
// written in the PDF file (for example, /1.4).
|
||||
//
|
||||
// If an incremental upgrade requires a version that is higher than specified by the document.
|
||||
// if context.PDFReader.PDFVersion < "2.0" {
|
||||
// catalog_buffer.WriteString(" /Version /2.0")
|
||||
// }
|
||||
// Ensure PDF version is at least 1.5 to support SigFlags in acroFormDict (1.4) and UF in the fileSpecDict (1.5)
|
||||
if v, err := strconv.ParseFloat(context.PDFReader.PDFVersion, 64); err == nil && v < 1.5 {
|
||||
catalog_buffer.WriteString(" /Version /1.5\n")
|
||||
}
|
||||
|
||||
// Retrieve the root, its pointer and set the root string
|
||||
root := context.PDFReader.Trailer().Key("Root")
|
||||
|
@@ -21,11 +21,11 @@ var testFiles = []struct {
|
||||
},
|
||||
},
|
||||
{
|
||||
file: "../testfiles/testfile21.pdf",
|
||||
file: "../testfiles/testfile12.pdf",
|
||||
expectedCatalogs: map[CertType]string{
|
||||
CertificationSignature: "<<\n /Type /Catalog\n /Metadata 8 0 R\n /Names 6 0 R\n /Pages 9 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 3\n >>\n>>\n",
|
||||
UsageRightsSignature: "<<\n /Type /Catalog\n /Metadata 8 0 R\n /Names 6 0 R\n /Pages 9 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 1\n >>\n>>\n",
|
||||
ApprovalSignature: "<<\n /Type /Catalog\n /Metadata 8 0 R\n /Names 6 0 R\n /Pages 9 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 3\n >>\n>>\n",
|
||||
CertificationSignature: "<<\n /Type /Catalog\n /Version /1.5\n /Outlines 2 0 R\n /Pages 3 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 3\n >>\n>>\n",
|
||||
UsageRightsSignature: "<<\n /Type /Catalog\n /Version /1.5\n /Outlines 2 0 R\n /Pages 3 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 1\n >>\n>>\n",
|
||||
ApprovalSignature: "<<\n /Type /Catalog\n /Version /1.5\n /Outlines 2 0 R\n /Pages 3 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 3\n >>\n>>\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@ func (context *SignContext) writeTrailer() error {
|
||||
lines[i] = " " + strings.TrimSpace(line)
|
||||
}
|
||||
}
|
||||
trailer_string = strings.Join(lines, "\n")
|
||||
trailer_string = strings.Join(lines, "\n") + "\n"
|
||||
|
||||
// Write the new trailer.
|
||||
if _, err := context.OutputBuffer.Write([]byte(trailer_string)); err != nil {
|
||||
@@ -54,7 +54,6 @@ func (context *SignContext) writeTrailer() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Write the new xref start position.
|
||||
if _, err := context.OutputBuffer.Write([]byte(strconv.FormatInt(context.NewXrefStart, 10) + "\n")); err != nil {
|
||||
return err
|
||||
|
@@ -128,9 +128,25 @@ func (context *SignContext) createIncPageUpdate(pageNumber, annot uint32) ([]byt
|
||||
// TODO: Update digitorus/pdf to get raw values without resolving pointers
|
||||
for _, key := range page.Keys() {
|
||||
switch key {
|
||||
case "Contents", "Parent":
|
||||
case "Parent":
|
||||
ptr := page.Key(key).GetPtr()
|
||||
page_buffer.WriteString(fmt.Sprintf(" /%s %d 0 R\n", key, ptr.GetID()))
|
||||
case "Contents":
|
||||
// Special handling for Contents - must preserve stream structure
|
||||
contentsValue := page.Key(key)
|
||||
if contentsValue.Kind() == pdf.Array {
|
||||
// If Contents is an array, keep it as an array reference
|
||||
page_buffer.WriteString(" /Contents [")
|
||||
for i := 0; i < contentsValue.Len(); i++ {
|
||||
ptr := contentsValue.Index(i).GetPtr()
|
||||
page_buffer.WriteString(fmt.Sprintf(" %d 0 R", ptr.GetID()))
|
||||
}
|
||||
page_buffer.WriteString(" ]\n")
|
||||
} else {
|
||||
// If Contents is a single reference, keep it as a single reference
|
||||
ptr := contentsValue.GetPtr()
|
||||
page_buffer.WriteString(fmt.Sprintf(" /%s %d 0 R\n", key, ptr.GetID()))
|
||||
}
|
||||
case "Annots":
|
||||
page_buffer.WriteString(" /Annots [\n")
|
||||
for i := 0; i < page.Key("Annots").Len(); i++ {
|
||||
|
@@ -16,6 +16,19 @@ const (
|
||||
objectFooter = "\nendobj\n"
|
||||
)
|
||||
|
||||
func (context *SignContext) getNextObjectID() uint32 {
|
||||
if context.lastXrefID == 0 {
|
||||
lastXrefID, err := context.getLastObjectIDFromXref()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
context.lastXrefID = lastXrefID
|
||||
}
|
||||
|
||||
objectID := context.lastXrefID + uint32(len(context.newXrefEntries)) + 1
|
||||
return objectID
|
||||
}
|
||||
|
||||
func (context *SignContext) addObject(object []byte) (uint32, error) {
|
||||
if context.lastXrefID == 0 {
|
||||
lastXrefID, err := context.getLastObjectIDFromXref()
|
||||
|
@@ -20,7 +20,6 @@ func TestGetLastObjectIDFromXref(t *testing.T) {
|
||||
{"testfile16.pdf", 567},
|
||||
{"testfile17.pdf", 20},
|
||||
{"testfile20.pdf", 10},
|
||||
{"testfile21.pdf", 16},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
@@ -8,7 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/digitorus/pdfsign/revocation"
|
||||
"gitea.tryanks.com/Tryanks/pdfsign/revocation"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import (
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
"github.com/digitorus/pdfsign/revocation"
|
||||
"gitea.tryanks.com/Tryanks/pdfsign/revocation"
|
||||
)
|
||||
|
||||
const certPem = `-----BEGIN CERTIFICATE-----
|
||||
|
@@ -9,8 +9,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gitea.tryanks.com/Tryanks/pdfsign/revocation"
|
||||
"github.com/digitorus/pdf"
|
||||
"github.com/digitorus/pdfsign/revocation"
|
||||
"github.com/digitorus/pkcs7"
|
||||
|
||||
"github.com/mattetti/filebuffer"
|
||||
@@ -45,12 +45,16 @@ type SignData struct {
|
||||
|
||||
// Appearance represents the appearance of the signature
|
||||
type Appearance struct {
|
||||
Visible bool
|
||||
Visible bool
|
||||
|
||||
Page uint32
|
||||
LowerLeftX float64
|
||||
LowerLeftY float64
|
||||
UpperRightX float64
|
||||
UpperRightY float64
|
||||
|
||||
Image []byte // Image data to use as signature appearance
|
||||
ImageAsWatermark bool // If true, the text will be drawn over the image
|
||||
}
|
||||
|
||||
type VisualSignData struct {
|
||||
|
@@ -12,9 +12,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.tryanks.com/Tryanks/pdfsign/revocation"
|
||||
"gitea.tryanks.com/Tryanks/pdfsign/verify"
|
||||
"github.com/digitorus/pdf"
|
||||
"github.com/digitorus/pdfsign/revocation"
|
||||
"github.com/digitorus/pdfsign/verify"
|
||||
"github.com/mattetti/filebuffer"
|
||||
)
|
||||
|
||||
@@ -231,7 +231,7 @@ func TestSignPDFFileUTF8(t *testing.T) {
|
||||
|
||||
func TestSignPDFVisible(t *testing.T) {
|
||||
cert, pkey := loadCertificateAndKey(t)
|
||||
inputFilePath := "../testfiles/testfile20.pdf"
|
||||
inputFilePath := "../testfiles/testfile12.pdf"
|
||||
originalFileName := filepath.Base(inputFilePath)
|
||||
|
||||
tmpfile, err := os.CreateTemp("", t.Name())
|
||||
@@ -475,3 +475,248 @@ func TestTimestampPDFFile(t *testing.T) {
|
||||
|
||||
verifySignedFile(t, tmpfile, "testfile20.pdf")
|
||||
}
|
||||
|
||||
// TestSignPDFWithImage tests signing a PDF with an image in the signature
|
||||
func TestSignPDFWithImage(t *testing.T) {
|
||||
cert, pkey := loadCertificateAndKey(t)
|
||||
inputFilePath := "../testfiles/testfile12.pdf"
|
||||
originalFileName := filepath.Base(inputFilePath)
|
||||
|
||||
// Read the signature image file
|
||||
signatureImage, err := os.ReadFile("../testfiles/pdfsign-signature.jpg")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read signature image: %s", err.Error())
|
||||
}
|
||||
|
||||
tmpfile, err := os.CreateTemp("", t.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
if !testing.Verbose() {
|
||||
defer os.Remove(tmpfile.Name())
|
||||
}
|
||||
|
||||
err = SignFile(inputFilePath, tmpfile.Name(), SignData{
|
||||
Signature: SignDataSignature{
|
||||
Info: SignDataSignatureInfo{
|
||||
Name: "John Doe",
|
||||
Location: "Somewhere",
|
||||
Reason: "Test with visible signature and image",
|
||||
ContactInfo: "None",
|
||||
Date: time.Now().Local(),
|
||||
},
|
||||
CertType: ApprovalSignature,
|
||||
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
|
||||
},
|
||||
Appearance: Appearance{
|
||||
Visible: true,
|
||||
LowerLeftX: 400,
|
||||
LowerLeftY: 50,
|
||||
UpperRightX: 600,
|
||||
UpperRightY: 125,
|
||||
Image: signatureImage, // Use the signature image
|
||||
},
|
||||
DigestAlgorithm: crypto.SHA512,
|
||||
Signer: pkey,
|
||||
Certificate: cert,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s", originalFileName, err.Error())
|
||||
}
|
||||
|
||||
verifySignedFile(t, tmpfile, originalFileName)
|
||||
}
|
||||
|
||||
// TestSignPDFWithTwoImages tests signing a PDF with two different signatures with images
|
||||
func TestSignPDFWithTwoImages(t *testing.T) {
|
||||
cert, pkey := loadCertificateAndKey(t)
|
||||
tbsFile := "../testfiles/testfile12.pdf"
|
||||
|
||||
// Read the signature image file
|
||||
signatureImage, err := os.ReadFile("../testfiles/pdfsign-signature.jpg")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read signature image: %s", err.Error())
|
||||
}
|
||||
|
||||
// First signature
|
||||
firstSignature, err := os.CreateTemp("", fmt.Sprintf("%s_first_", t.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
if !testing.Verbose() {
|
||||
defer os.Remove(firstSignature.Name())
|
||||
}
|
||||
|
||||
err = SignFile(tbsFile, firstSignature.Name(), SignData{
|
||||
Signature: SignDataSignature{
|
||||
Info: SignDataSignatureInfo{
|
||||
Name: "John Doe",
|
||||
Location: "Somewhere",
|
||||
Reason: "First signature with image",
|
||||
ContactInfo: "None",
|
||||
Date: time.Now().Local(),
|
||||
},
|
||||
CertType: ApprovalSignature,
|
||||
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
|
||||
},
|
||||
Appearance: Appearance{
|
||||
Visible: true,
|
||||
LowerLeftX: 50,
|
||||
LowerLeftY: 50,
|
||||
UpperRightX: 250,
|
||||
UpperRightY: 125,
|
||||
Image: signatureImage,
|
||||
},
|
||||
DigestAlgorithm: crypto.SHA512,
|
||||
Signer: pkey,
|
||||
Certificate: cert,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("First signature failed: %s", err.Error())
|
||||
}
|
||||
|
||||
verifySignedFile(t, firstSignature, filepath.Base(tbsFile))
|
||||
|
||||
// Second signature
|
||||
secondSignature, err := os.CreateTemp("", fmt.Sprintf("%s_second_", t.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
if !testing.Verbose() {
|
||||
defer os.Remove(secondSignature.Name())
|
||||
}
|
||||
|
||||
err = SignFile(firstSignature.Name(), secondSignature.Name(), SignData{
|
||||
Signature: SignDataSignature{
|
||||
Info: SignDataSignatureInfo{
|
||||
Name: "Jane Doe",
|
||||
Location: "Elsewhere",
|
||||
Reason: "Second signature with image",
|
||||
ContactInfo: "None",
|
||||
Date: time.Now().Local(),
|
||||
},
|
||||
CertType: ApprovalSignature,
|
||||
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
|
||||
},
|
||||
Appearance: Appearance{
|
||||
Visible: true,
|
||||
LowerLeftX: 300,
|
||||
LowerLeftY: 50,
|
||||
UpperRightX: 500,
|
||||
UpperRightY: 125,
|
||||
Image: signatureImage,
|
||||
},
|
||||
DigestAlgorithm: crypto.SHA512,
|
||||
Signer: pkey,
|
||||
Certificate: cert,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Second signature failed: %s", err.Error())
|
||||
}
|
||||
|
||||
verifySignedFile(t, secondSignature, filepath.Base(tbsFile))
|
||||
}
|
||||
|
||||
// TestSignPDFWithWatermarkImage tests signing a PDF with a JPG image and text above
|
||||
func TestSignPDFWithWatermarkImageJPG(t *testing.T) {
|
||||
cert, pkey := loadCertificateAndKey(t)
|
||||
inputFilePath := "../testfiles/testfile12.pdf"
|
||||
originalFileName := filepath.Base(inputFilePath)
|
||||
|
||||
// Read the signature image file
|
||||
signatureImage, err := os.ReadFile("../testfiles/pdfsign-signature-watermark.jpg")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read signature image: %s", err.Error())
|
||||
}
|
||||
|
||||
tmpfile, err := os.CreateTemp("", t.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
if !testing.Verbose() {
|
||||
defer os.Remove(tmpfile.Name())
|
||||
}
|
||||
|
||||
err = SignFile(inputFilePath, tmpfile.Name(), SignData{
|
||||
Signature: SignDataSignature{
|
||||
Info: SignDataSignatureInfo{
|
||||
Name: "James SuperSmith",
|
||||
Location: "Somewhere",
|
||||
Reason: "Test with visible signature and watermark image",
|
||||
ContactInfo: "None",
|
||||
Date: time.Now().Local(),
|
||||
},
|
||||
CertType: ApprovalSignature,
|
||||
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
|
||||
},
|
||||
Appearance: Appearance{
|
||||
Visible: true,
|
||||
LowerLeftX: 400,
|
||||
LowerLeftY: 50,
|
||||
UpperRightX: 600,
|
||||
UpperRightY: 125,
|
||||
Image: signatureImage, // Use the signature image
|
||||
ImageAsWatermark: true, // Set the image as a watermark
|
||||
},
|
||||
DigestAlgorithm: crypto.SHA512,
|
||||
Signer: pkey,
|
||||
Certificate: cert,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s", originalFileName, err.Error())
|
||||
}
|
||||
|
||||
verifySignedFile(t, tmpfile, originalFileName)
|
||||
}
|
||||
|
||||
// TestSignPDFWithWatermarkImage tests signing a PDF with a PNG image and text above
|
||||
func TestSignPDFWithWatermarkImagePNG(t *testing.T) {
|
||||
cert, pkey := loadCertificateAndKey(t)
|
||||
inputFilePath := "../testfiles/testfile12.pdf"
|
||||
originalFileName := filepath.Base(inputFilePath)
|
||||
|
||||
// Read the signature image file
|
||||
signatureImage, err := os.ReadFile("../testfiles/pdfsign-signature-watermark.png")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read signature image: %s", err.Error())
|
||||
}
|
||||
|
||||
tmpfile, err := os.CreateTemp("", t.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
if !testing.Verbose() {
|
||||
defer os.Remove(tmpfile.Name())
|
||||
}
|
||||
|
||||
err = SignFile(inputFilePath, tmpfile.Name(), SignData{
|
||||
Signature: SignDataSignature{
|
||||
Info: SignDataSignatureInfo{
|
||||
Name: "James SuperSmith",
|
||||
Location: "Somewhere",
|
||||
Reason: "Test with visible signature and watermark image",
|
||||
ContactInfo: "None",
|
||||
Date: time.Now().Local(),
|
||||
},
|
||||
CertType: ApprovalSignature,
|
||||
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
|
||||
},
|
||||
Appearance: Appearance{
|
||||
Visible: true,
|
||||
LowerLeftX: 400,
|
||||
LowerLeftY: 50,
|
||||
UpperRightX: 600,
|
||||
UpperRightY: 125,
|
||||
Image: signatureImage, // Use the signature image
|
||||
ImageAsWatermark: true, // Set the image as a watermark
|
||||
},
|
||||
DigestAlgorithm: crypto.SHA512,
|
||||
Signer: pkey,
|
||||
Certificate: cert,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s", originalFileName, err.Error())
|
||||
}
|
||||
|
||||
verifySignedFile(t, tmpfile, originalFileName)
|
||||
}
|
||||
|
BIN
testfiles/pdfsign-signature-watermark.jpg
Normal file
BIN
testfiles/pdfsign-signature-watermark.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
testfiles/pdfsign-signature-watermark.png
Normal file
BIN
testfiles/pdfsign-signature-watermark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
BIN
testfiles/pdfsign-signature.jpg
Normal file
BIN
testfiles/pdfsign-signature.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
@@ -12,8 +12,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.tryanks.com/Tryanks/pdfsign/revocation"
|
||||
"github.com/digitorus/pdf"
|
||||
"github.com/digitorus/pdfsign/revocation"
|
||||
|
||||
"github.com/digitorus/pkcs7"
|
||||
"github.com/digitorus/timestamp"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
Reference in New Issue
Block a user