Compare commits

...

17 Commits

Author SHA1 Message Date
0d83ec6b6c Migrate repository to new hosting and cleanup CI/CD files
Switched module path and imports to gitea.tryanks.com. Removed GitHub-specific files like workflows, dependabot, and devcontainer as part of migration. This streamlines the codebase for the new hosting environment.
2025-05-12 22:33:48 +08:00
43924743e2 Merge branch 'main' into feature/image-appearance 2025-05-12 22:14:06 +08:00
Paul van Brouwershaven
7feb03999b Enhance handling of PDF Contents in createIncPageUpdate to preserve stream structure 2025-05-12 11:12:20 +02:00
Paul van Brouwershaven
0f834debb7 Fix test removed invalid source test file 2025-05-12 11:00:34 +02:00
Paul van Brouwershaven
bf1f861031 Ensure PDF version is at least 1.5 for SigFlags and UF support 2025-05-12 11:00:34 +02:00
Paul van Brouwershaven
e026bf1577 Add newline, fix #71 2025-05-12 09:30:51 +02:00
Corentin Mors
25feb83db7 Update readme with image appearance info 2025-05-10 18:28:20 +02:00
Corentin Mors
4b3957a882 Fix JPG decode 2025-05-10 18:24:05 +02:00
dependabot[bot]
6cbaf97a70 Bump golang.org/x/text from 0.22.0 to 0.25.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.22.0 to 0.25.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.22.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-10 11:46:50 +02:00
dependabot[bot]
1aafac7336 Bump golang.org/x/crypto from 0.33.0 to 0.35.0 (#68) 2025-05-10 11:40:19 +02:00
Corentin Mors
51569c8aec PNG support 2025-05-01 15:10:54 +02:00
Corentin Mors
948b24a084 Revert some unecessary changes 2025-05-01 13:22:42 +02:00
Corentin Mors
abd05a5aaa Implement image watermark 2025-05-01 11:31:06 +02:00
Corentin Mors
a3b23161c4 Split down into multiple functions 2025-05-01 11:01:57 +02:00
Corentin Mors
9616146853 Test with two consecutive signatures 2025-04-29 11:47:37 +02:00
Corentin Mors
ce8274cc9c Image visual appearance 2025-04-29 11:35:42 +02:00
Corentin Mors
c958b32269 Add a devcontainer for development 2025-04-28 16:34:17 +02:00
24 changed files with 624 additions and 176 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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
View File

@@ -6,3 +6,4 @@ testfiles/*_signed.pdf
testfiles/failed/*
pdfsign
certs/*
tmp/

View File

@@ -1,15 +1,7 @@
# Signing PDF files with Go
[![Build & Test](https://github.com/digitorus/pdfsign/workflows/Build%20&%20Test/badge.svg)](https://github.com/digitorus/pdfsign/actions?query=workflow%3Abuild-and-test)
[![golangci-lint](https://github.com/digitorus/pdfsign/workflows/golangci-lint/badge.svg)](https://github.com/digitorus/pdfsign/actions?query=workflow%3Agolangci-lint)
[![Go Report Card](https://goreportcard.com/badge/github.com/digitorus/pdfsign)](https://goreportcard.com/report/github.com/digitorus/pdfsign)
[![Coverage Status](https://codecov.io/gh/digitorus/pdfsign/branch/main/graph/badge.svg)](https://codecov.io/gh/)
[![Go Reference](https://pkg.go.dev/badge/github.com/digitorus/pdfsign.svg)](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
View File

@@ -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
View File

@@ -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=

View File

@@ -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 (

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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",
},
},
}

View File

@@ -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

View File

@@ -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++ {

View File

@@ -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()

View File

@@ -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 {

View File

@@ -8,7 +8,7 @@ import (
"net/http"
"strings"
"github.com/digitorus/pdfsign/revocation"
"gitea.tryanks.com/Tryanks/pdfsign/revocation"
"golang.org/x/crypto/ocsp"
)

View File

@@ -9,7 +9,7 @@ import (
"encoding/pem"
"testing"
"github.com/digitorus/pdfsign/revocation"
"gitea.tryanks.com/Tryanks/pdfsign/revocation"
)
const certPem = `-----BEGIN CERTIFICATE-----

View File

@@ -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 {

View File

@@ -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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

View File

@@ -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"