Add initial support for signature appearance
This commit is contained in:
62
sign/appearance.go
Normal file
62
sign/appearance.go
Normal file
@@ -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
|
||||
}
|
@@ -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
|
||||
@@ -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)
|
||||
//
|
||||
@@ -100,9 +112,9 @@ 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
|
||||
|
@@ -15,17 +15,17 @@ 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",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -33,7 +33,7 @@ var testFiles = []struct {
|
||||
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")
|
||||
@@ -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{
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -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")
|
||||
@@ -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)
|
||||
}
|
||||
|
@@ -63,20 +63,11 @@ 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,
|
||||
}
|
||||
|
||||
|
@@ -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.
|
||||
@@ -72,31 +89,23 @@ 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))
|
||||
// Define the field type as a signature.
|
||||
visual_signature.WriteString(" /FT /Sig")
|
||||
// Set a unique title for the signature field.
|
||||
visual_signature.WriteString(" /T " + pdfString("Signature "+strconv.Itoa(len(context.SignData.ExistingSignatures)+1)))
|
||||
annotationFlags := AnnotationFlagPrint | AnnotationFlagLocked
|
||||
visual_signature.WriteString(fmt.Sprintf(" /F %d\n", annotationFlags))
|
||||
|
||||
// (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")
|
||||
// Define the field type as a signature.
|
||||
visual_signature.WriteString(" /FT /Sig\n")
|
||||
// Set a unique title for the signature field.
|
||||
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")
|
||||
@@ -104,8 +113,48 @@ func (context *SignContext) createVisualSignature(visible bool, pageNumber int,
|
||||
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++ {
|
||||
|
@@ -45,24 +45,15 @@ func TestVisualSignature(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 3
|
||||
sign_data.objectId = uint32(rdr.XrefInformation.ItemCount) + 3
|
||||
|
||||
context := SignContext{
|
||||
PDFReader: rdr,
|
||||
InputFile: input_file,
|
||||
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,
|
||||
}
|
||||
|
||||
expected_visual_signature := "<< /Type /Annot /Subtype /Widget /Rect [0 0 0 0] /P 4 0 R /F 132 /FT /Sig /T (Signature 1) /Ff 0 /V 13 0 R >>\n"
|
||||
expected_visual_signature := "<<\n /Type /Annot\n /Subtype /Widget\n /Rect [0 0 0 0]\n /P 4 0 R\n /F 132\n /FT /Sig\n /T (Signature 1)\n /V 13 0 R\n>>\n"
|
||||
|
||||
visual_signature, err := context.createVisualSignature(false, 1, [4]float64{0, 0, 0, 0})
|
||||
if err != nil {
|
||||
|
@@ -40,23 +40,46 @@ func (context *SignContext) addObject(object []byte) (uint32, error) {
|
||||
Offset: int64(context.OutputBuffer.Buff.Len()) + 1,
|
||||
})
|
||||
|
||||
err := context.writeObject(objectID, object)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to write object: %w", err)
|
||||
}
|
||||
|
||||
return objectID, nil
|
||||
}
|
||||
|
||||
func (context *SignContext) updateObject(id uint32, object []byte) error {
|
||||
context.updatedXrefEntries = append(context.updatedXrefEntries, xrefEntry{
|
||||
ID: id,
|
||||
Offset: int64(context.OutputBuffer.Buff.Len()) + 1,
|
||||
})
|
||||
|
||||
err := context.writeObject(id, object)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write object: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (context *SignContext) writeObject(id uint32, object []byte) error {
|
||||
// Write the object header
|
||||
if _, err := context.OutputBuffer.Write([]byte(fmt.Sprintf("\n%d 0 obj\n", objectID))); err != nil {
|
||||
return 0, fmt.Errorf("failed to write object header: %w", err)
|
||||
if _, err := context.OutputBuffer.Write([]byte(fmt.Sprintf("\n%d 0 obj\n", id))); err != nil {
|
||||
return fmt.Errorf("failed to write object header: %w", err)
|
||||
}
|
||||
|
||||
// Write the object content
|
||||
object = bytes.TrimSpace(object)
|
||||
if _, err := context.OutputBuffer.Write(object); err != nil {
|
||||
return 0, fmt.Errorf("failed to write object content: %w", err)
|
||||
return fmt.Errorf("failed to write object content: %w", err)
|
||||
}
|
||||
|
||||
// Write the object footer
|
||||
if _, err := context.OutputBuffer.Write([]byte(objectFooter)); err != nil {
|
||||
return 0, fmt.Errorf("failed to write object footer: %w", err)
|
||||
return fmt.Errorf("failed to write object footer: %w", err)
|
||||
}
|
||||
|
||||
return objectID, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeXref writes the cross-reference table or stream based on the PDF type.
|
||||
@@ -115,6 +138,19 @@ func (context *SignContext) writeIncrXrefTable() error {
|
||||
return fmt.Errorf("failed to write incremental xref header: %w", err)
|
||||
}
|
||||
|
||||
// Write updated entries
|
||||
for _, entry := range context.updatedXrefEntries {
|
||||
pageXrefObj := fmt.Sprintf("%d %d\n", entry.ID, 1)
|
||||
if _, err := context.OutputBuffer.Write([]byte(pageXrefObj)); err != nil {
|
||||
return fmt.Errorf("failed to write updated xref object: %w", err)
|
||||
}
|
||||
|
||||
xrefLine := fmt.Sprintf("%010d 00000 n\r\n", entry.Offset)
|
||||
if _, err := context.OutputBuffer.Write([]byte(xrefLine)); err != nil {
|
||||
return fmt.Errorf("failed to write updated incremental xref entry: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write xref subsection header
|
||||
startXrefObj := fmt.Sprintf("%d %d\n", context.lastXrefID+1, len(context.newXrefEntries))
|
||||
if _, err := context.OutputBuffer.Write([]byte(startXrefObj)); err != nil {
|
||||
@@ -190,26 +226,25 @@ func encodeXrefStream(data []byte, predictor int64) ([]byte, error) {
|
||||
|
||||
// writeXrefStreamHeader writes the header for the xref stream.
|
||||
func writeXrefStreamHeader(context *SignContext, streamLength int) error {
|
||||
newRoot := fmt.Sprintf("Root %d 0 R", context.CatalogData.ObjectId)
|
||||
|
||||
id := context.PDFReader.Trailer().Key("ID")
|
||||
id0 := hex.EncodeToString([]byte(id.Index(0).RawString()))
|
||||
id1 := hex.EncodeToString([]byte(id.Index(0).RawString()))
|
||||
|
||||
newXref := fmt.Sprintf("%d 0 obj\n<< /Type /XRef /Length %d /Filter /FlateDecode /DecodeParms << /Columns %d /Predictor %d >> /W [ 1 3 1 ] /Prev %d /Size %d /Index [ %d 4 ] /%s /ID [<%s><%s>] >>\n",
|
||||
context.SignData.ObjectId+1,
|
||||
streamLength,
|
||||
xrefStreamColumns,
|
||||
xrefStreamPredictor,
|
||||
context.PDFReader.XrefInformation.StartPos,
|
||||
context.PDFReader.XrefInformation.ItemCount+int64(len(context.newXrefEntries))+1,
|
||||
context.PDFReader.XrefInformation.ItemCount,
|
||||
newRoot,
|
||||
id0,
|
||||
id1,
|
||||
)
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(fmt.Sprintf("%d 0 obj\n", context.SignData.objectId))
|
||||
buffer.WriteString("<< /Type /XRef\n")
|
||||
buffer.WriteString(fmt.Sprintf(" /Length %d\n", streamLength))
|
||||
buffer.WriteString(" /Filter /FlateDecode\n")
|
||||
buffer.WriteString(fmt.Sprintf(" /DecodeParms << /Columns %d /Predictor %d >>\n", xrefStreamColumns, xrefStreamPredictor))
|
||||
buffer.WriteString(" /W [ 1 3 1 ]\n")
|
||||
buffer.WriteString(fmt.Sprintf(" /Prev %d\n", context.PDFReader.XrefInformation.StartPos))
|
||||
buffer.WriteString(fmt.Sprintf(" /Size %d\n", context.PDFReader.XrefInformation.ItemCount+int64(len(context.newXrefEntries))+1))
|
||||
buffer.WriteString(fmt.Sprintf(" /Index [ %d 4 ]\n", context.PDFReader.XrefInformation.ItemCount))
|
||||
buffer.WriteString(fmt.Sprintf(" /Root %d 0 R\n", context.CatalogData.ObjectId))
|
||||
buffer.WriteString(fmt.Sprintf(" /ID [<%s><%s>]\n", id0, id1))
|
||||
buffer.WriteString(">>\n")
|
||||
|
||||
_, err := io.WriteString(context.OutputBuffer, newXref)
|
||||
_, err := context.OutputBuffer.Write(buffer.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
|
69
sign/sign.go
69
sign/sign.go
@@ -29,7 +29,6 @@ type TSA struct {
|
||||
type RevocationFunction func(cert, issuer *x509.Certificate, i *revocation.InfoArchival) error
|
||||
|
||||
type SignData struct {
|
||||
ObjectId uint32
|
||||
Signature SignDataSignature
|
||||
Signer crypto.Signer
|
||||
DigestAlgorithm crypto.Hash
|
||||
@@ -38,12 +37,24 @@ type SignData struct {
|
||||
TSA TSA
|
||||
RevocationData revocation.InfoArchival
|
||||
RevocationFunction RevocationFunction
|
||||
ExistingSignatures []SignData
|
||||
Appearance Appearance
|
||||
|
||||
objectId uint32
|
||||
}
|
||||
|
||||
// Appearance represents the appearance of the signature
|
||||
type Appearance struct {
|
||||
Visible bool
|
||||
Page uint32
|
||||
LowerLeftX float64
|
||||
LowerLeftY float64
|
||||
UpperRightX float64
|
||||
UpperRightY float64
|
||||
}
|
||||
|
||||
type VisualSignData struct {
|
||||
PageId uint32
|
||||
ObjectId uint32
|
||||
pageObjectId uint32
|
||||
objectId uint32
|
||||
}
|
||||
|
||||
type InfoData struct {
|
||||
@@ -97,8 +108,10 @@ type SignContext struct {
|
||||
SignatureMaxLength uint32
|
||||
SignatureMaxLengthBase uint32
|
||||
|
||||
existingSignatures []SignData
|
||||
lastXrefID uint32
|
||||
newXrefEntries []xrefEntry
|
||||
updatedXrefEntries []xrefEntry
|
||||
}
|
||||
|
||||
func SignFile(input string, output string, sign_data SignData) error {
|
||||
@@ -129,21 +142,12 @@ func SignFile(input string, output string, sign_data SignData) error {
|
||||
}
|
||||
|
||||
func Sign(input io.ReadSeeker, output io.Writer, rdr *pdf.Reader, size int64, sign_data SignData) error {
|
||||
sign_data.ObjectId = uint32(rdr.XrefInformation.ItemCount) + 2
|
||||
sign_data.objectId = uint32(rdr.XrefInformation.ItemCount) + 2
|
||||
|
||||
context := SignContext{
|
||||
PDFReader: rdr,
|
||||
InputFile: input,
|
||||
OutputFile: output,
|
||||
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,
|
||||
SignatureMaxLengthBase: uint32(hex.EncodedLen(512)),
|
||||
}
|
||||
@@ -153,7 +157,7 @@ func Sign(input io.ReadSeeker, output io.Writer, rdr *pdf.Reader, size int64, si
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
context.SignData.ExistingSignatures = existingSignatures
|
||||
context.existingSignatures = existingSignatures
|
||||
|
||||
err = context.SignPDF()
|
||||
if err != nil {
|
||||
@@ -174,6 +178,9 @@ func (context *SignContext) SignPDF() error {
|
||||
if !context.SignData.DigestAlgorithm.Available() {
|
||||
context.SignData.DigestAlgorithm = crypto.SHA256
|
||||
}
|
||||
if context.SignData.Appearance.Page == 0 {
|
||||
context.SignData.Appearance.Page = 1
|
||||
}
|
||||
|
||||
context.OutputBuffer = filebuffer.New([]byte{})
|
||||
|
||||
@@ -271,25 +278,49 @@ func (context *SignContext) SignPDF() error {
|
||||
}
|
||||
|
||||
// Write the new signature object
|
||||
context.SignData.ObjectId, err = context.addObject(signature_object)
|
||||
context.SignData.objectId, err = context.addObject(signature_object)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add signature object: %w", err)
|
||||
}
|
||||
|
||||
// Create visual signature (visible or invisible based on CertType)
|
||||
// visible := context.SignData.Signature.CertType == CertificationSignature
|
||||
visible := false
|
||||
rectangle := [4]float64{0, 0, 0, 0}
|
||||
if context.SignData.Signature.CertType != ApprovalSignature && context.SignData.Appearance.Visible {
|
||||
return fmt.Errorf("visible signatures are only allowed for approval signatures")
|
||||
} else if context.SignData.Signature.CertType == ApprovalSignature && context.SignData.Appearance.Visible {
|
||||
visible = true
|
||||
rectangle = [4]float64{
|
||||
context.SignData.Appearance.LowerLeftX,
|
||||
context.SignData.Appearance.LowerLeftY,
|
||||
context.SignData.Appearance.UpperRightX,
|
||||
context.SignData.Appearance.UpperRightY,
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage: passing page number and default rect values
|
||||
visual_signature, err := context.createVisualSignature(false, 1, [4]float64{0, 0, 0, 0})
|
||||
visual_signature, err := context.createVisualSignature(visible, context.SignData.Appearance.Page, rectangle)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create visual signature: %w", err)
|
||||
}
|
||||
|
||||
// Write the new visual signature object.
|
||||
context.VisualSignData.ObjectId, err = context.addObject(visual_signature)
|
||||
context.VisualSignData.objectId, err = context.addObject(visual_signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add visual signature object: %w", err)
|
||||
}
|
||||
|
||||
if context.SignData.Appearance.Visible {
|
||||
inc_page_update, err := context.createIncPageUpdate(context.SignData.Appearance.Page, context.VisualSignData.objectId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create incremental page update: %w", err)
|
||||
}
|
||||
err = context.updateObject(context.VisualSignData.pageObjectId, inc_page_update)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add incremental page update object: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new catalog object
|
||||
catalog, err := context.createCatalog()
|
||||
if err != nil {
|
||||
|
@@ -229,6 +229,54 @@ func TestSignPDFFileUTF8(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignPDFVisible(t *testing.T) {
|
||||
cert, pkey := loadCertificateAndKey(t)
|
||||
inputFilePath := "../testfiles/minimal.pdf"
|
||||
originalFileName := filepath.Base(inputFilePath)
|
||||
|
||||
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",
|
||||
ContactInfo: "None",
|
||||
},
|
||||
CertType: ApprovalSignature,
|
||||
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
|
||||
},
|
||||
Appearance: Appearance{
|
||||
Visible: true,
|
||||
LowerLeftX: 350,
|
||||
LowerLeftY: 75,
|
||||
UpperRightX: 600,
|
||||
UpperRightY: 100,
|
||||
},
|
||||
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())
|
||||
if err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSignPDF(b *testing.B) {
|
||||
cert, pkey := loadCertificateAndKey(&testing.T{})
|
||||
certificateChains := [][]*x509.Certificate{}
|
||||
|
Reference in New Issue
Block a user