PNG support
This commit is contained in:
@@ -2,9 +2,11 @@ package sign
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
_ "image/jpeg" // register JPEG format
|
_ "image/jpeg" // register JPEG format
|
||||||
|
_ "image/png" // register PNG format
|
||||||
)
|
)
|
||||||
|
|
||||||
// Helper functions for PDF resource components
|
// Helper functions for PDF resource components
|
||||||
@@ -48,38 +50,127 @@ func writeAppearanceStreamBuffer(buffer *bytes.Buffer, stream []byte) {
|
|||||||
buffer.WriteString("endstream\n")
|
buffer.WriteString("endstream\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (context *SignContext) createImageXObject() ([]byte, error) {
|
func (context *SignContext) createImageXObject() ([]byte, []byte, error) {
|
||||||
imageData := context.SignData.Appearance.Image
|
imageData := context.SignData.Appearance.Image
|
||||||
|
|
||||||
// Read image configuration to get original dimensions
|
// Read image to get format and decode image data
|
||||||
img, _, err := image.DecodeConfig(bytes.NewReader(imageData))
|
img, format, err := image.Decode(bytes.NewReader(imageData))
|
||||||
if err != nil {
|
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
|
// Get image dimensions
|
||||||
width := float64(img.Width)
|
bounds := img.Bounds()
|
||||||
height := float64(img.Height)
|
width := bounds.Max.X - bounds.Min.X
|
||||||
|
height := bounds.Max.Y - bounds.Min.Y
|
||||||
|
|
||||||
// Create basic PDF Image XObject
|
// Create basic PDF Image XObject
|
||||||
var imageObject bytes.Buffer
|
var imageObject bytes.Buffer
|
||||||
|
var maskObjectBytes []byte
|
||||||
|
|
||||||
imageObject.WriteString("<<\n")
|
imageObject.WriteString("<<\n")
|
||||||
imageObject.WriteString(" /Type /XObject\n")
|
imageObject.WriteString(" /Type /XObject\n")
|
||||||
imageObject.WriteString(" /Subtype /Image\n")
|
imageObject.WriteString(" /Subtype /Image\n")
|
||||||
imageObject.WriteString(fmt.Sprintf(" /Width %.0f\n", width))
|
imageObject.WriteString(fmt.Sprintf(" /Width %d\n", width))
|
||||||
imageObject.WriteString(fmt.Sprintf(" /Height %.0f\n", height))
|
imageObject.WriteString(fmt.Sprintf(" /Height %d\n", height))
|
||||||
imageObject.WriteString(" /ColorSpace /DeviceRGB\n")
|
imageObject.WriteString(" /ColorSpace /DeviceRGB\n")
|
||||||
imageObject.WriteString(" /BitsPerComponent 8\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.WriteString("stream\n")
|
||||||
imageObject.Write(imageData)
|
imageObject.Write(compressedRgbData)
|
||||||
imageObject.WriteString("\nendstream\n")
|
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) {
|
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 {
|
if hasImage {
|
||||||
// Create and add the image XObject
|
// Create and add the image XObject
|
||||||
imageStream, err := context.createImageXObject()
|
imageBytes, maskObjectBytes, err := context.createImageXObject()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create image XObject: %w", err)
|
return nil, fmt.Errorf("failed to create image XObject: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
imageObjectId, err := context.addObject(imageStream)
|
imageObjectId, err := context.addObject(imageBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add image object: %w", err)
|
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)
|
createImageResource(&appearance_buffer, imageObjectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,6 +16,19 @@ const (
|
|||||||
objectFooter = "\nendobj\n"
|
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) {
|
func (context *SignContext) addObject(object []byte) (uint32, error) {
|
||||||
if context.lastXrefID == 0 {
|
if context.lastXrefID == 0 {
|
||||||
lastXrefID, err := context.getLastObjectIDFromXref()
|
lastXrefID, err := context.getLastObjectIDFromXref()
|
||||||
|
@@ -617,8 +617,8 @@ func TestSignPDFWithTwoImages(t *testing.T) {
|
|||||||
verifySignedFile(t, secondSignature, filepath.Base(tbsFile))
|
verifySignedFile(t, secondSignature, filepath.Base(tbsFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSignPDFWithWatermarkImage tests signing a PDF with an image and text above
|
// TestSignPDFWithWatermarkImage tests signing a PDF with a JPG image and text above
|
||||||
func TestSignPDFWithWatermarkImage(t *testing.T) {
|
func TestSignPDFWithWatermarkImageJPG(t *testing.T) {
|
||||||
cert, pkey := loadCertificateAndKey(t)
|
cert, pkey := loadCertificateAndKey(t)
|
||||||
inputFilePath := "../testfiles/testfile12.pdf"
|
inputFilePath := "../testfiles/testfile12.pdf"
|
||||||
originalFileName := filepath.Base(inputFilePath)
|
originalFileName := filepath.Base(inputFilePath)
|
||||||
@@ -668,3 +668,55 @@ func TestSignPDFWithWatermarkImage(t *testing.T) {
|
|||||||
|
|
||||||
verifySignedFile(t, tmpfile, originalFileName)
|
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.png
Normal file
BIN
testfiles/pdfsign-signature-watermark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
Reference in New Issue
Block a user