From a3b23161c41648de07322c34d1b4ba2fd12337c9 Mon Sep 17 00:00:00 2001 From: Corentin Mors Date: Thu, 1 May 2025 10:13:39 +0200 Subject: [PATCH] Split down into multiple functions --- .devcontainer/devcontainer.json | 3 + .gitignore | 1 + sign/appearance.go | 227 ++++++++++++++++++-------------- sign/helpers.go | 33 ----- sign/sign.go | 7 +- sign/sign_test.go | 18 +-- 6 files changed, 147 insertions(+), 142 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f81abe3..9d2a0e4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,6 +13,9 @@ "extensions": ["golang.go"] } }, + "containerEnv": { + "TMPDIR": "/workspaces/pdfsign/tmp" + }, "forwardPorts": [], "postCreateCommand": "go mod download", "remoteUser": "root" diff --git a/.gitignore b/.gitignore index d350d7f..0c21b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ testfiles/*_signed.pdf testfiles/failed/* pdfsign certs/* +tmp/ diff --git a/sign/appearance.go b/sign/appearance.go index 50bba51..43010cb 100644 --- a/sign/appearance.go +++ b/sign/appearance.go @@ -8,12 +8,127 @@ import ( ) func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) { - if len(context.SignData.Signature.Info.Image) > 0 { + if len(context.SignData.Appearance.Image) > 0 { return context.createImageAppearance(rect) } return context.createTextAppearance(rect) } +// 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 writeBufferStream(buffer *bytes.Buffer, stream []byte) { + buffer.WriteString("stream\n") + buffer.Write(stream) + buffer.WriteString("endstream\n") +} + +func (context *SignContext) createImageXObject() ([]byte, error) { + imageData := context.SignData.Appearance.Image + + // Read image configuration to get original dimensions + img, _, err := image.DecodeConfig(bytes.NewReader(imageData)) + if err != nil { + return nil, fmt.Errorf("failed to decode image configuration: %w", err) + } + + // Use original image dimensions + width := float64(img.Width) + height := float64(img.Height) + + // Create basic PDF Image XObject + var imageObject bytes.Buffer + + imageObject.WriteString("<<\n") + imageObject.WriteString(" /Type /XObject\n") + imageObject.WriteString(" /Subtype /Image\n") + imageObject.WriteString(fmt.Sprintf(" /Width %.0f\n", width)) + imageObject.WriteString(fmt.Sprintf(" /Height %.0f\n", height)) + imageObject.WriteString(" /ColorSpace /DeviceRGB\n") + imageObject.WriteString(" /BitsPerComponent 8\n") + imageObject.WriteString(" /Filter /DCTDecode\n") + imageObject.WriteString(fmt.Sprintf(" /Length %d\n", len(imageData))) + imageObject.WriteString(">>\n") + + imageObject.WriteString("stream\n") + imageObject.Write(imageData) + imageObject.WriteString("\nendstream\n") + + return imageObject.Bytes(), nil +} + +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) createTextAppearance(rect [4]float64) ([]byte, error) { rectWidth := rect[2] - rect[0] rectHeight := rect[3] - rect[1] @@ -22,61 +137,26 @@ func (context *SignContext) createTextAppearance(rect [4]float64) ([]byte, error return nil, fmt.Errorf("invalid rectangle dimensions: width %.2f and height %.2f must be greater than 0", rectWidth, rectHeight) } - var appearance_stream_buffer bytes.Buffer - text := context.SignData.Signature.Info.Name - // 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 - } + fontSize, textX, textY := computeTextSizeAndPosition(text, rectWidth, rectHeight) - 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 + var appearance_stream_buffer bytes.Buffer - // 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 - - appearance_stream_buffer.WriteString(fmt.Sprintf("%.2f %.2f Td\n", textX, textY)) // Position text - 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 + drawText(&appearance_stream_buffer, text, fontSize, textX, textY) // Create the appearance XObject 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 + writeAppearanceHeader(&appearance_buffer, rectWidth, rectHeight) // Resources dictionary with font 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") + createFontResource(&appearance_buffer) 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") + writeFormTypeAndLength(&appearance_buffer, appearance_stream_buffer.Len()) - appearance_buffer.WriteString("stream\n") - appearance_buffer.Write(appearance_stream_buffer.Bytes()) - appearance_buffer.WriteString("endstream\n") + writeBufferStream(&appearance_buffer, appearance_stream_buffer.Bytes()) return appearance_buffer.Bytes(), nil } @@ -102,71 +182,20 @@ func (context *SignContext) createImageAppearance(rect [4]float64) ([]byte, erro var appearance_stream_buffer bytes.Buffer - // We save state twice on purpose due to the cm operation - appearance_stream_buffer.WriteString("q\n") // Save graphics state - appearance_stream_buffer.WriteString("q\n") // Save before image transformation - appearance_stream_buffer.WriteString(fmt.Sprintf("%.2f 0 0 %.2f 0 0 cm\n", rectWidth, rectHeight)) - appearance_stream_buffer.WriteString("/Im1 Do\n") - appearance_stream_buffer.WriteString("Q\n") // Restore after transformation - appearance_stream_buffer.WriteString("Q\n") // Restore graphics state + drawImage(&appearance_stream_buffer, rectWidth, rectHeight) // Create the appearance XObject 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)) + writeAppearanceHeader(&appearance_buffer, rectWidth, rectHeight) // Resources dictionary with XObject appearance_buffer.WriteString(" /Resources <<\n") - - appearance_buffer.WriteString(" /XObject <<\n") - appearance_buffer.WriteString(fmt.Sprintf(" /Im1 %d 0 R\n", imageObjectId)) - appearance_buffer.WriteString(" >>\n") + createImageResource(&appearance_buffer, imageObjectId) 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") + writeFormTypeAndLength(&appearance_buffer, appearance_stream_buffer.Len()) - appearance_buffer.WriteString("stream\n") - appearance_buffer.Write(appearance_stream_buffer.Bytes()) - appearance_buffer.WriteString("endstream\n") + writeBufferStream(&appearance_buffer, appearance_stream_buffer.Bytes()) return appearance_buffer.Bytes(), nil } - -func (context *SignContext) createImageXObject() ([]byte, error) { - imageData := context.SignData.Signature.Info.Image - - // Read image configuration to get original dimensions - img, _, err := image.DecodeConfig(bytes.NewReader(imageData)) - if err != nil { - return nil, fmt.Errorf("failed to decode image configuration: %w", err) - } - - // Use original image dimensions - width := float64(img.Width) - height := float64(img.Height) - - // Create basic PDF Image XObject - var imageObject bytes.Buffer - - imageObject.WriteString("<<\n") - imageObject.WriteString(" /Type /XObject\n") - imageObject.WriteString(" /Subtype /Image\n") - imageObject.WriteString(fmt.Sprintf(" /Width %.0f\n", width)) - imageObject.WriteString(fmt.Sprintf(" /Height %.0f\n", height)) - imageObject.WriteString(" /ColorSpace /DeviceRGB\n") - imageObject.WriteString(" /BitsPerComponent 8\n") - imageObject.WriteString(" /Filter /DCTDecode\n") // JPEG compression - imageObject.WriteString(fmt.Sprintf(" /Length %d\n", len(imageData))) - imageObject.WriteString(">>\n") - - imageObject.WriteString("stream\n") - imageObject.Write(imageData) - imageObject.WriteString("\nendstream\n") - - return imageObject.Bytes(), nil -} diff --git a/sign/helpers.go b/sign/helpers.go index f0cb971..32a44f5 100644 --- a/sign/helpers.go +++ b/sign/helpers.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "math" - "os" "strings" "time" @@ -196,35 +195,3 @@ func isASCII(s string) bool { } return true } - -// CopyFile copies a file from src to dst. -// It's a replacement for os.Rename that works across filesystems. -func CopyFile(src, dst string) error { - // Open the source file - srcFile, err := os.Open(src) - if err != nil { - return fmt.Errorf("failed to open source file: %w", err) - } - defer srcFile.Close() - - // Create the destination directory if it doesn't exist - dstDir := dst[:strings.LastIndex(dst, "/")] - if err := os.MkdirAll(dstDir, 0o755); err != nil { - return fmt.Errorf("failed to create destination directory: %w", err) - } - - // Create the destination file - dstFile, err := os.Create(dst) - if err != nil { - return fmt.Errorf("failed to create destination file: %w", err) - } - defer dstFile.Close() - - // Copy the contents - _, err = io.Copy(dstFile, srcFile) - if err != nil { - return fmt.Errorf("failed to copy file contents: %w", err) - } - - return nil -} diff --git a/sign/sign.go b/sign/sign.go index 27935cf..371f74c 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -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 { @@ -93,7 +97,6 @@ type SignDataSignatureInfo struct { Reason string ContactInfo string Date time.Time - Image []byte // Image data to use in signature appearance } type SignContext struct { diff --git a/sign/sign_test.go b/sign/sign_test.go index b59fa4c..37d8313 100644 --- a/sign/sign_test.go +++ b/sign/sign_test.go @@ -80,9 +80,9 @@ func verifySignedFile(t *testing.T, tmpfile *os.File, originalFileName string) { if err != nil { t.Fatalf("%s: %s", tmpfile.Name(), err.Error()) - destinationPath := "../testfiles/failed/" + originalFileName - if copyErr := CopyFile(tmpfile.Name(), destinationPath); copyErr != nil { - t.Errorf("Failed to copy failed test file: %s", copyErr) + err2 := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName) + if err2 != nil { + t.Error(err2) } } } @@ -214,7 +214,8 @@ func TestSignPDFFileUTF8(t *testing.T) { info, err := verify.File(tmpfile) if err != nil { t.Fatalf("%s: %s", tmpfile.Name(), err.Error()) - if err := CopyFile(tmpfile.Name(), "../testfiles/failed/"+originalFileName); err != nil { + err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName) + if err != nil { t.Error(err) } } else if len(info.Signers) == 0 { @@ -271,7 +272,8 @@ func TestSignPDFVisible(t *testing.T) { _, err = verify.File(tmpfile) if err != nil { t.Fatalf("%s: %s", tmpfile.Name(), err.Error()) - if err := CopyFile(tmpfile.Name(), "../testfiles/failed/"+originalFileName); err != nil { + err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName) + if err != nil { t.Error(err) } } @@ -504,7 +506,6 @@ func TestSignPDFWithImage(t *testing.T) { Reason: "Test with visible signature and image", ContactInfo: "None", Date: time.Now().Local(), - Image: signatureImage, // Use the signature image }, CertType: ApprovalSignature, DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms, @@ -515,6 +516,7 @@ func TestSignPDFWithImage(t *testing.T) { LowerLeftY: 50, UpperRightX: 600, UpperRightY: 125, + Image: signatureImage, // Use the signature image }, DigestAlgorithm: crypto.SHA512, Signer: pkey, @@ -555,7 +557,6 @@ func TestSignPDFWithTwoImages(t *testing.T) { Reason: "First signature with image", ContactInfo: "None", Date: time.Now().Local(), - Image: signatureImage, }, CertType: ApprovalSignature, DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms, @@ -566,6 +567,7 @@ func TestSignPDFWithTwoImages(t *testing.T) { LowerLeftY: 50, UpperRightX: 250, UpperRightY: 125, + Image: signatureImage, }, DigestAlgorithm: crypto.SHA512, Signer: pkey, @@ -594,7 +596,6 @@ func TestSignPDFWithTwoImages(t *testing.T) { Reason: "Second signature with image", ContactInfo: "None", Date: time.Now().Local(), - Image: signatureImage, }, CertType: ApprovalSignature, DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms, @@ -605,6 +606,7 @@ func TestSignPDFWithTwoImages(t *testing.T) { LowerLeftY: 50, UpperRightX: 500, UpperRightY: 125, + Image: signatureImage, }, DigestAlgorithm: crypto.SHA512, Signer: pkey,