282 lines
9.2 KiB
Go
282 lines
9.2 KiB
Go
package sign
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"fmt"
|
|
"image"
|
|
_ "image/jpeg" // register JPEG format
|
|
_ "image/png" // register PNG format
|
|
)
|
|
|
|
// 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]
|
|
|
|
if rectWidth < 1 || rectHeight < 1 {
|
|
return nil, fmt.Errorf("invalid rectangle dimensions: width %.2f and height %.2f must be greater than 0", rectWidth, rectHeight)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if shouldDisplayText {
|
|
createFontResource(&appearance_buffer)
|
|
}
|
|
|
|
appearance_buffer.WriteString(" >>\n")
|
|
|
|
// Create the appearance stream
|
|
var appearance_stream_buffer bytes.Buffer
|
|
|
|
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
|
|
}
|