Skip to content

Commit

Permalink
Add initial support for signature appearance
Browse files Browse the repository at this point in the history
  • Loading branch information
vanbroup committed Dec 16, 2024
1 parent e5cdb61 commit b9112bb
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 122 deletions.
62 changes: 62 additions & 0 deletions sign/appearance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package sign

import (
"bytes"
"fmt"
)

func (context *SignContext) createAppearance(rect [4]float64) ([]byte, error) {
text := context.SignData.Signature.Info.Name

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)
}

// Calculate font size
fontSize := rectHeight * 0.8 // Initial font size
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

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

// Resources dictionary
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")
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
}
40 changes: 26 additions & 14 deletions sign/pdfcatalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func (context *SignContext) createCatalog() ([]byte, error) {

// Start the catalog object
catalog_buffer.WriteString("<<\n")
catalog_buffer.WriteString(" /Type /Catalog")
catalog_buffer.WriteString(" /Type /Catalog\n")

// (Optional; PDF 1.4) The version of the PDF specification to which
// the document conforms (for example, 1.4) if later than the version
Expand Down Expand Up @@ -49,33 +49,45 @@ func (context *SignContext) createCatalog() ([]byte, error) {
// Add Pages and Names references if they exist
if foundPages {
pages := root.Key("Pages").GetPtr()
catalog_buffer.WriteString(" /Pages " + strconv.Itoa(int(pages.GetID())) + " " + strconv.Itoa(int(pages.GetGen())) + " R")
catalog_buffer.WriteString(" /Pages " + strconv.Itoa(int(pages.GetID())) + " " + strconv.Itoa(int(pages.GetGen())) + " R\n")
}
if foundNames {
names := root.Key("Names").GetPtr()
catalog_buffer.WriteString(" /Names " + strconv.Itoa(int(names.GetID())) + " " + strconv.Itoa(int(names.GetGen())) + " R")
catalog_buffer.WriteString(" /Names " + strconv.Itoa(int(names.GetID())) + " " + strconv.Itoa(int(names.GetGen())) + " R\n")
}

// Start the AcroForm dictionary with /NeedAppearances
catalog_buffer.WriteString(" /AcroForm << /Fields [")
catalog_buffer.WriteString(" /AcroForm <<\n")
catalog_buffer.WriteString(" /Fields [")

// Add existing signatures to the AcroForm dictionary
for i, sig := range context.SignData.ExistingSignatures {
for i, sig := range context.existingSignatures {
if i > 0 {
catalog_buffer.WriteString(" ")
}
catalog_buffer.WriteString(strconv.Itoa(int(sig.ObjectId)) + " 0 R")
catalog_buffer.WriteString(strconv.Itoa(int(sig.objectId)) + " 0 R")
}

// Add the visual signature field to the AcroForm dictionary
if len(context.SignData.ExistingSignatures) > 0 {
if len(context.existingSignatures) > 0 {
catalog_buffer.WriteString(" ")
}
catalog_buffer.WriteString(strconv.Itoa(int(context.VisualSignData.ObjectId)) + " 0 R")
catalog_buffer.WriteString(strconv.Itoa(int(context.VisualSignData.objectId)) + " 0 R")

catalog_buffer.WriteString("]") // close Fields array
catalog_buffer.WriteString("]\n") // close Fields array

catalog_buffer.WriteString(" /NeedAppearances false")
// (Optional; deprecated in PDF 2.0) A flag specifying whether
// to construct appearance streams and appearance
// dictionaries for all widget annotations in the document (see
// 12.7.4.3, "Variable text"). Default value: false. A PDF writer
// shall include this key, with a value of true, if it has not
// provided appearance streams for all visible widget
// annotations present in the document.
// if context.SignData.Visible {
// catalog_buffer.WriteString(" /NeedAppearances true")
// } else {
// catalog_buffer.WriteString(" /NeedAppearances false")
// }

// Signature flags (Table 225)
//
Expand All @@ -100,14 +112,14 @@ func (context *SignContext) createCatalog() ([]byte, error) {
// Set SigFlags and Permissions based on Signature Type
switch context.SignData.Signature.CertType {
case CertificationSignature, ApprovalSignature, TimeStampSignature:
catalog_buffer.WriteString(" /SigFlags 3")
catalog_buffer.WriteString(" /SigFlags 3\n")
case UsageRightsSignature:
catalog_buffer.WriteString(" /SigFlags 1")
catalog_buffer.WriteString(" /SigFlags 1\n")
}

// Finalize the AcroForm and Catalog object
catalog_buffer.WriteString(" >>\n") // Close AcroForm
catalog_buffer.WriteString(">>\n") // Close Catalog
catalog_buffer.WriteString(" >>\n") // Close AcroForm
catalog_buffer.WriteString(">>\n") // Close Catalog

return catalog_buffer.Bytes(), nil
}
24 changes: 9 additions & 15 deletions sign/pdfcatalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@ var testFiles = []struct {
{
file: "../testfiles/testfile20.pdf",
expectedCatalogs: map[CertType]string{
CertificationSignature: "<<\n /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 3 >>\n>>\n",
UsageRightsSignature: "<<\n /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 1 >>\n>>\n",
ApprovalSignature: "<<\n /Type /Catalog /Pages 3 0 R /AcroForm << /Fields [10 0 R] /NeedAppearances false /SigFlags 3 >>\n>>\n",
CertificationSignature: "<<\n /Type /Catalog\n /Pages 3 0 R\n /AcroForm <<\n /Fields [10 0 R]\n /SigFlags 3\n >>\n>>\n",
UsageRightsSignature: "<<\n /Type /Catalog\n /Pages 3 0 R\n /AcroForm <<\n /Fields [10 0 R]\n /SigFlags 1\n >>\n>>\n",
ApprovalSignature: "<<\n /Type /Catalog\n /Pages 3 0 R\n /AcroForm <<\n /Fields [10 0 R]\n /SigFlags 3\n >>\n>>\n",
},
},
{
file: "../testfiles/testfile21.pdf",
expectedCatalogs: map[CertType]string{
CertificationSignature: "<<\n /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 3 >>\n>>\n",
UsageRightsSignature: "<<\n /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 1 >>\n>>\n",
ApprovalSignature: "<<\n /Type /Catalog /Pages 9 0 R /Names 6 0 R /AcroForm << /Fields [16 0 R] /NeedAppearances false /SigFlags 3 >>\n>>\n",
CertificationSignature: "<<\n /Type /Catalog\n /Pages 9 0 R\n /Names 6 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 3\n >>\n>>\n",
UsageRightsSignature: "<<\n /Type /Catalog\n /Pages 9 0 R\n /Names 6 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 1\n >>\n>>\n",
ApprovalSignature: "<<\n /Type /Catalog\n /Pages 9 0 R\n /Names 6 0 R\n /AcroForm <<\n /Fields [16 0 R]\n /SigFlags 3\n >>\n>>\n",
},
},
}

func TestCreateCatalog(t *testing.T) {
for _, testFile := range testFiles {
for certType, expectedCatalog := range testFile.expectedCatalogs {
t.Run(fmt.Sprintf("%s_certType-%d", testFile.file, certType), func(st *testing.T) {
t.Run(fmt.Sprintf("%s_%s", testFile.file, certType.String()), func(st *testing.T) {
inputFile, err := os.Open(testFile.file)
if err != nil {
st.Errorf("Failed to load test PDF")
Expand All @@ -57,13 +57,7 @@ func TestCreateCatalog(t *testing.T) {
PDFReader: rdr,
InputFile: inputFile,
VisualSignData: VisualSignData{
ObjectId: uint32(rdr.XrefInformation.ItemCount),
},
CatalogData: CatalogData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
},
InfoData: InfoData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
objectId: uint32(rdr.XrefInformation.ItemCount),
},
SignData: SignData{
Signature: SignDataSignature{
Expand All @@ -80,7 +74,7 @@ func TestCreateCatalog(t *testing.T) {
}

if string(catalog) != expectedCatalog {
st.Errorf("Catalog mismatch, expected\n%s\nbut got\n%s", expectedCatalog, catalog)
st.Errorf("Catalog mismatch, expected\n%q\nbut got\n%q", expectedCatalog, catalog)
}
})
}
Expand Down
4 changes: 2 additions & 2 deletions sign/pdfsignature.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (context *SignContext) createSignaturePlaceholder() []byte {
//
// A timestamp can be embedded in a CMS binary data object (see 12.8.3.3, "CMS
// (PKCS #7) signatures").
if context.SignData.TSA.URL == "" {
if context.SignData.TSA.URL == "" && !context.SignData.Signature.Info.Date.IsZero() {
signature_buffer.WriteString(" /M ")
signature_buffer.WriteString(pdfDateTime(context.SignData.Signature.Info.Date))
signature_buffer.WriteString("\n")
Expand Down Expand Up @@ -499,7 +499,7 @@ func (context *SignContext) fetchExistingSignatures() ([]SignData, error) {
if field.Key("FT").Name() == "Sig" {
ptr := field.GetPtr()
sig := SignData{
ObjectId: uint32(ptr.GetID()),
objectId: uint32(ptr.GetID()),
}
signatures = append(signatures, sig)
}
Expand Down
13 changes: 2 additions & 11 deletions sign/pdfsignature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,12 @@ func TestCreateSignaturePlaceholder(t *testing.T) {
},
}

sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3
sign_data.objectId = uint32(rdr.XrefInformation.ItemCount) + 3

context := SignContext{
PDFReader: rdr,
InputFile: inputFile,
VisualSignData: VisualSignData{
ObjectId: uint32(rdr.XrefInformation.ItemCount),
},
CatalogData: CatalogData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 1,
},
InfoData: InfoData{
ObjectId: uint32(rdr.XrefInformation.ItemCount) + 2,
},
SignData: sign_data,
SignData: sign_data,
}

signature := context.createSignaturePlaceholder()
Expand Down
95 changes: 72 additions & 23 deletions sign/pdfvisualsignature.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,37 @@ const (
// pageNumber: the page number where the signature should be placed.
// rect: the rectangle defining the position and size of the signature field.
// Returns the visual signature string and an error if any.
func (context *SignContext) createVisualSignature(visible bool, pageNumber int, rect [4]float64) ([]byte, error) {
func (context *SignContext) createVisualSignature(visible bool, pageNumber uint32, rect [4]float64) ([]byte, error) {
var visual_signature bytes.Buffer

visual_signature.WriteString("<<\n")

// Define the object as an annotation.
visual_signature.WriteString("<< /Type /Annot")
visual_signature.WriteString(" /Type /Annot\n")
// Specify the annotation subtype as a widget.
visual_signature.WriteString(" /Subtype /Widget")
visual_signature.WriteString(" /Subtype /Widget\n")

if visible {
// Set the position and size of the signature field if visible.
visual_signature.WriteString(fmt.Sprintf(" /Rect [%f %f %f %f]", rect[0], rect[1], rect[2], rect[3]))
visual_signature.WriteString(fmt.Sprintf(" /Rect [%f %f %f %f]\n", rect[0], rect[1], rect[2], rect[3]))

appearance, err := context.createAppearance(rect)
if err != nil {
return nil, fmt.Errorf("failed to create appearance: %w", err)
}

appearanceObjectId, err := context.addObject(appearance)
if err != nil {
return nil, fmt.Errorf("failed to add appearance object: %w", err)
}

// An appearance dictionary specifying how the annotation
// shall be presented visually on the page (see 12.5.5, "Appearance streams").
visual_signature.WriteString(fmt.Sprintf(" /AP << /N %d 0 R >>\n", appearanceObjectId))

} else {
// Set the rectangle to zero if the signature is invisible.
visual_signature.WriteString(" /Rect [0 0 0 0]")
visual_signature.WriteString(" /Rect [0 0 0 0]\n")
}

// Retrieve the root object from the PDF trailer.
Expand Down Expand Up @@ -72,40 +89,72 @@ func (context *SignContext) createVisualSignature(visible bool, pageNumber int,
page_ptr := page.GetPtr()

// Store the page ID in the visual signature context so that we can add it to xref table later.
context.VisualSignData.PageId = page_ptr.GetID()
context.VisualSignData.pageObjectId = page_ptr.GetID()

// Add the page reference to the visual signature.
visual_signature.WriteString(" /P " + strconv.Itoa(int(page_ptr.GetID())) + " " + strconv.Itoa(int(page_ptr.GetGen())) + " R")
visual_signature.WriteString(" /P " + strconv.Itoa(int(page_ptr.GetID())) + " " + strconv.Itoa(int(page_ptr.GetGen())) + " R\n")
}

// Define the annotation flags for the signature field (132)
// annotationFlags := AnnotationFlagPrint | AnnotationFlagNoZoom | AnnotationFlagNoRotate | AnnotationFlagReadOnly | AnnotationFlagLockedContents
visual_signature.WriteString(fmt.Sprintf(" /F %d", 132))
annotationFlags := AnnotationFlagPrint | AnnotationFlagLocked
visual_signature.WriteString(fmt.Sprintf(" /F %d\n", annotationFlags))

// Define the field type as a signature.
visual_signature.WriteString(" /FT /Sig")
visual_signature.WriteString(" /FT /Sig\n")
// Set a unique title for the signature field.
visual_signature.WriteString(" /T " + pdfString("Signature "+strconv.Itoa(len(context.SignData.ExistingSignatures)+1)))

// (Optional) A set of bit flags specifying the interpretation of specific entries
// in this dictionary. A value of 1 for the flag indicates that the associated entry
// is a required constraint. A value of 0 indicates that the associated entry is
// an optional constraint. Bit positions are 1 (Filter); 2 (SubFilter); 3 (V); 4
// (Reasons); 5 (LegalAttestation); 6 (AddRevInfo); and 7 (DigestMethod).
// For PDF 2.0 the following bit flags are added: 8 (Lockdocument); and 9
// (AppearanceFilter). Default value: 0.
visual_signature.WriteString(" /Ff 0")
visual_signature.WriteString(fmt.Sprintf(" /T %s\n", pdfString("Signature "+strconv.Itoa(len(context.existingSignatures)+1))))

// Reference the signature dictionary.
visual_signature.WriteString(" /V " + strconv.Itoa(int(context.SignData.ObjectId)) + " 0 R")
visual_signature.WriteString(fmt.Sprintf(" /V %d 0 R\n", context.SignData.objectId))

// Close the dictionary and end the object.
visual_signature.WriteString(" >>\n")
visual_signature.WriteString(">>\n")

return visual_signature.Bytes(), nil
}

func (context *SignContext) createIncPageUpdate(pageNumber, annot uint32) ([]byte, error) {
var page_buffer bytes.Buffer

// Retrieve the root object from the PDF trailer.
root := context.PDFReader.Trailer().Key("Root")
page, err := findPageByNumber(root.Key("Pages"), pageNumber)
if err != nil {
return nil, err
}

page_buffer.WriteString("<<\n")

// TODO: Update digitorus/pdf to get raw values without resolving pointers
for _, key := range page.Keys() {
switch key {
case "Contents", "Parent":
ptr := page.Key(key).GetPtr()
page_buffer.WriteString(fmt.Sprintf(" /%s %d 0 R\n", key, ptr.GetID()))
case "Annots":
page_buffer.WriteString(" /Annots [\n")
for i := 0; i < page.Key("Annots").Len(); i++ {
ptr := page.Key(key).Index(i).GetPtr()
page_buffer.WriteString(fmt.Sprintf(" %d 0 R\n", ptr.GetID()))
}
page_buffer.WriteString(fmt.Sprintf(" %d 0 R\n", annot))
page_buffer.WriteString(" ]\n")
default:
page_buffer.WriteString(fmt.Sprintf(" /%s %s\n", key, page.Key(key).String()))
}
}

if page.Key("Annots").IsNull() {
page_buffer.WriteString(fmt.Sprintf(" /Annots [%d 0 R]\n", annot))
}

page_buffer.WriteString(">>\n")

return page_buffer.Bytes(), nil
}

// Helper function to find a page by its number.
func findPageByNumber(pages pdf.Value, pageNumber int) (pdf.Value, error) {
func findPageByNumber(pages pdf.Value, pageNumber uint32) (pdf.Value, error) {
if pages.Key("Type").Name() == "Pages" {
kids := pages.Key("Kids")
for i := 0; i < kids.Len(); i++ {
Expand Down
Loading

0 comments on commit b9112bb

Please sign in to comment.