Image visual appearance
This commit is contained in:
@@ -3,11 +3,18 @@ package sign
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg" // register JPEG format
|
||||
)
|
||||
|
||||
func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) {
|
||||
text := context.SignData.Signature.Info.Name
|
||||
if len(context.SignData.Signature.Info.Image) > 0 {
|
||||
return context.createImageAppearance(rect)
|
||||
}
|
||||
return context.createTextAppearance(rect)
|
||||
}
|
||||
|
||||
func (context *SignContext) createTextAppearance(rect [4]float64) ([]byte, error) {
|
||||
rectWidth := rect[2] - rect[0]
|
||||
rectHeight := rect[3] - rect[1]
|
||||
|
||||
@@ -15,23 +22,36 @@ 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)
|
||||
}
|
||||
|
||||
var appearance_stream_buffer bytes.Buffer
|
||||
|
||||
text := context.SignData.Signature.Info.Name
|
||||
|
||||
// Calculate font size
|
||||
fontSize := rectHeight * 0.8 // Initial 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
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
// Create the appearance XObject
|
||||
var appearance_buffer bytes.Buffer
|
||||
appearance_buffer.WriteString("<<\n")
|
||||
appearance_buffer.WriteString(" /Type /XObject\n")
|
||||
@@ -39,7 +59,7 @@ func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) {
|
||||
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
|
||||
// Resources dictionary with font
|
||||
appearance_buffer.WriteString(" /Resources <<\n")
|
||||
appearance_buffer.WriteString(" /Font <<\n")
|
||||
appearance_buffer.WriteString(" /F1 <<\n")
|
||||
@@ -60,3 +80,93 @@ func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) {
|
||||
|
||||
return appearance_buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (context *SignContext) createImageAppearance(rect [4]float64) ([]byte, error) {
|
||||
rectWidth := rect[2] - rect[0]
|
||||
rectHeight := rect[3] - rect[1]
|
||||
|
||||
if rectWidth < 1 || rectHeight < 1 {
|
||||
return nil, fmt.Errorf("invalid rectangle dimensions: width %.2f and height %.2f must be greater than 0", rectWidth, rectHeight)
|
||||
}
|
||||
|
||||
// Create and add the image XObject
|
||||
imageStream, err := context.createImageXObject()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image XObject: %w", err)
|
||||
}
|
||||
|
||||
imageObjectId, err := context.addObject(imageStream)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add image object: %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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))
|
||||
|
||||
// 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")
|
||||
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")
|
||||
|
||||
appearance_buffer.WriteString("stream\n")
|
||||
appearance_buffer.Write(appearance_stream_buffer.Bytes())
|
||||
appearance_buffer.WriteString("endstream\n")
|
||||
|
||||
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
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -195,3 +196,35 @@ 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
|
||||
}
|
||||
|
@@ -93,6 +93,7 @@ type SignDataSignatureInfo struct {
|
||||
Reason string
|
||||
ContactInfo string
|
||||
Date time.Time
|
||||
Image []byte // Image data to use in signature appearance
|
||||
}
|
||||
|
||||
type SignContext struct {
|
||||
|
@@ -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())
|
||||
|
||||
err2 := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName)
|
||||
if err2 != nil {
|
||||
t.Error(err2)
|
||||
destinationPath := "../testfiles/failed/" + originalFileName
|
||||
if copyErr := CopyFile(tmpfile.Name(), destinationPath); copyErr != nil {
|
||||
t.Errorf("Failed to copy failed test file: %s", copyErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +214,7 @@ func TestSignPDFFileUTF8(t *testing.T) {
|
||||
info, err := verify.File(tmpfile)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s", tmpfile.Name(), err.Error())
|
||||
if err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName); err != nil {
|
||||
if err := CopyFile(tmpfile.Name(), "../testfiles/failed/"+originalFileName); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
} else if len(info.Signers) == 0 {
|
||||
@@ -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())
|
||||
@@ -271,7 +271,7 @@ func TestSignPDFVisible(t *testing.T) {
|
||||
_, err = verify.File(tmpfile)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s", tmpfile.Name(), err.Error())
|
||||
if err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName); err != nil {
|
||||
if err := CopyFile(tmpfile.Name(), "../testfiles/failed/"+originalFileName); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -475,3 +475,61 @@ 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(),
|
||||
Image: signatureImage, // Use the signature image
|
||||
},
|
||||
CertType: ApprovalSignature,
|
||||
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
|
||||
},
|
||||
Appearance: Appearance{
|
||||
Visible: true,
|
||||
LowerLeftX: 400,
|
||||
LowerLeftY: 50,
|
||||
UpperRightX: 600,
|
||||
UpperRightY: 125,
|
||||
},
|
||||
DigestAlgorithm: crypto.SHA512,
|
||||
Signer: pkey,
|
||||
Certificate: cert,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s", originalFileName, err.Error())
|
||||
}
|
||||
|
||||
_, err = verify.File(tmpfile)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
testfiles/pdfsign-signature.jpg
Normal file
BIN
testfiles/pdfsign-signature.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
Reference in New Issue
Block a user