From 51569c8aecc4e8f2b89572c039562576ebafbc1a Mon Sep 17 00:00:00 2001 From: Corentin Mors Date: Thu, 1 May 2025 14:57:29 +0200 Subject: [PATCH] PNG support --- sign/appearance.go | 131 +++++++++++++++++++--- sign/pdfxref.go | 13 +++ sign/sign_test.go | 56 ++++++++- testfiles/pdfsign-signature-watermark.png | Bin 0 -> 10154 bytes 4 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 testfiles/pdfsign-signature-watermark.png diff --git a/sign/appearance.go b/sign/appearance.go index 7b3b5d5..4ad4db0 100644 --- a/sign/appearance.go +++ b/sign/appearance.go @@ -2,9 +2,11 @@ package sign import ( "bytes" + "compress/zlib" "fmt" "image" _ "image/jpeg" // register JPEG format + _ "image/png" // register PNG format ) // Helper functions for PDF resource components @@ -48,38 +50,127 @@ func writeAppearanceStreamBuffer(buffer *bytes.Buffer, stream []byte) { buffer.WriteString("endstream\n") } -func (context *SignContext) createImageXObject() ([]byte, error) { +func (context *SignContext) createImageXObject() ([]byte, []byte, error) { imageData := context.SignData.Appearance.Image - // Read image configuration to get original dimensions - img, _, err := image.DecodeConfig(bytes.NewReader(imageData)) + // Read image to get format and decode image data + img, format, err := image.Decode(bytes.NewReader(imageData)) if err != nil { - return nil, fmt.Errorf("failed to decode image configuration: %w", err) + return nil, nil, fmt.Errorf("failed to decode image: %w", err) } - // Use original image dimensions - width := float64(img.Width) - height := float64(img.Height) + // 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 %.0f\n", width)) - imageObject.WriteString(fmt.Sprintf(" /Height %.0f\n", height)) + 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") - imageObject.WriteString(" /Filter /DCTDecode\n") - imageObject.WriteString(fmt.Sprintf(" /Length %d\n", len(imageData))) - imageObject.WriteString(">>\n") + var rgbData = new(bytes.Buffer) + var alphaData = new(bytes.Buffer) + + // Handle different formats + switch format { + case "jpeg": + imageObject.WriteString(" /Filter /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(imageData) + imageObject.Write(compressedRgbData) imageObject.WriteString("\nendstream\n") - return imageObject.Bytes(), nil + 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) { @@ -142,16 +233,24 @@ func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) { if hasImage { // Create and add the image XObject - imageStream, err := context.createImageXObject() + imageBytes, maskObjectBytes, err := context.createImageXObject() if err != nil { return nil, fmt.Errorf("failed to create image XObject: %w", err) } - imageObjectId, err := context.addObject(imageStream) + 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) } diff --git a/sign/pdfxref.go b/sign/pdfxref.go index 6776390..4647843 100644 --- a/sign/pdfxref.go +++ b/sign/pdfxref.go @@ -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() diff --git a/sign/sign_test.go b/sign/sign_test.go index e1160f6..d714374 100644 --- a/sign/sign_test.go +++ b/sign/sign_test.go @@ -617,8 +617,8 @@ func TestSignPDFWithTwoImages(t *testing.T) { verifySignedFile(t, secondSignature, filepath.Base(tbsFile)) } -// TestSignPDFWithWatermarkImage tests signing a PDF with an image and text above -func TestSignPDFWithWatermarkImage(t *testing.T) { +// 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) @@ -668,3 +668,55 @@ func TestSignPDFWithWatermarkImage(t *testing.T) { 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) +} diff --git a/testfiles/pdfsign-signature-watermark.png b/testfiles/pdfsign-signature-watermark.png new file mode 100644 index 0000000000000000000000000000000000000000..752ef68f15751bdc1c5a6f0cc471c0758067a29a GIT binary patch literal 10154 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 literal 0 HcmV?d00001