Signature Appearance Modules

Introduction

A digital signature in a PDF document can be created with an invisible or visible signature field.

The SetaPDF-Signer component comes with simple modules that may create the signature appearance for a visible signature field for you or allow you to define your own appearance.

A module instance has to be passed to the \setasign\SetaPDF2\Signer\Signer::setAppearance() method. 

In version 2.0 an appearance module was a proxy module for a signature module and also implements the \setasign\SetaPDF2\Signer\Signature\Module\ModuleInterface interface.
Since version 2.1 the modules are separated and a refactor is needed! 

Dynamic Appearance

The SetaPDF-Signer comes with an appearance module that will create a signature appearance simliar to the appearance that can be created with e.g. Acrobat.

The module allows you to define a background, a graphic and it will automatically extract information from the signature certificate. Its simplest output will look like: 

PHP
<?php

use setasign\SetaPDF2\Core\Document;
use setasign\SetaPDF2\Core\Font\Type0\Subset as Type0Subset;
use setasign\SetaPDF2\Core\Writer\HttpWriter;
use setasign\SetaPDF2\Signer\Signature\Appearance\Dynamic as DynamicAppearance;
use setasign\SetaPDF2\Signer\Signature\Module\Cms as CmsModule;
use setasign\SetaPDF2\Signer\SignatureField;
use setasign\SetaPDF2\Signer\Signer;

require_once('library/SetaPDF/Autoload.php');

// create a writer
$writer = new HttpWriter('simple.pdf', true);
// create a new document instance
$document = Document::loadByFilename(
    'files/pdfs/tektown/Laboratory-Report.pdf', $writer
);

// create a signer instance
$signer = new Signer($document);

// set some signature properties
$signer->setReason('Testing');
$signer->setLocation('SetaPDF-Signer Manual');

// create a signature module
$module = new CmsModule();
// load the certificate
$certificate = 'file://files/certificates/setapdf-no-pw.pem';
$module->setCertificate($certificate);
$module->setPrivateKey([$certificate, '' /* no password */]);

// create a visible signature field
$signer->addSignatureField(
    SignatureField::DEFAULT_FIELD_NAME,
    1,
    SignatureField::POSITION_RIGHT_TOP,
    array('x' => -160, 'y' => -100),
    180,
    60
);

// create the appearance
$appearance = new DynamicAppearance($certificate);

// create a subset font
$font = new Type0Subset($document, 'files/fonts/DejaVuSans.ttf');
$appearance->setFont($font);

// define the appearance
$signer->setAppearance($appearance);

// sign the document and send the final document to the initial writer
$signer->sign($module);

Configure the Background

An appearance background can be defined by passing a XObject (Image or Form XObject) to the setBackgroundLogo() method. The second parameter allows you to define an opacity value (between 0 and 1): 

PHP
<?php

use setasign\SetaPDF2\Core\Document;
use setasign\SetaPDF2\Core\Font\Type0\Subset as Type0Subset;
use setasign\SetaPDF2\Core\PageBoundaries;
use setasign\SetaPDF2\Core\Writer\HttpWriter;
use setasign\SetaPDF2\Signer\Signature\Appearance\Dynamic as DynamicAppearance;
use setasign\SetaPDF2\Signer\Signature\Module\Cms as CmsModule;
use setasign\SetaPDF2\Signer\SignatureField;
use setasign\SetaPDF2\Signer\Signer;

require_once('library/SetaPDF/Autoload.php');

// create a writer
$writer = new HttpWriter('simple.pdf', true);
// create a new document instance
$document = Document::loadByFilename(
    'files/pdfs/tektown/Laboratory-Report.pdf', $writer
);

// create a signer instance
$signer = new Signer($document);

// set some signature properties
$signer->setReason('Testing');
$signer->setLocation('SetaPDF-Signer Manual');

// create a signature module
$module = new CmsModule();
// load the certificate
$certificate = 'file://files/certificates/setapdf-no-pw.pem';
$module->setCertificate($certificate);
$module->setPrivateKey([$certificate, '' /* no password */]);

// create a visible signature field
$signer->addSignatureField(
    SignatureField::DEFAULT_FIELD_NAME,
    1,
    SignatureField::POSITION_RIGHT_TOP,
    ['x' => -160, 'y' => -100],
    180,
    60
);

// load a PDF for the background appearance
$bgDocument = Document::loadByFilename('files/pdfs/tektown/Logo.pdf');
// convert the first page to a XObject
$xObject = $bgDocument
    ->getCatalog()
    ->getPages()
    ->getPage(1)
    ->toXObject($document, PageBoundaries::ART_BOX);

// create the appearance
$appearance = new DynamicAppearance($certificate);
$appearance->setBackgroundLogo($xObject, .3);

// create a subset font
$font = new Type0Subset($document, 'files/fonts/DejaVuSans.ttf');
$appearance->setFont($font);

// define the appearance
$signer->setAppearance($appearance);

// sign the document and send the final document to the initial writer
$signer->sign($module);

Configure the Graphic

The graphic is the text or graphic that should be displayed in the left row of the appearance. A boolean value defines if the graphic will be shown or not. If you pass an XObject to its setter method the XObject will be displayed: 

PHP
<?php

use setasign\SetaPDF2\Core\Document;
use setasign\SetaPDF2\Core\Font\Type0\Subset as Type0Subset;
use setasign\SetaPDF2\Core\PageBoundaries;
use setasign\SetaPDF2\Core\Writer\HttpWriter;
use setasign\SetaPDF2\Signer\Signature\Appearance\Dynamic as DynamicAppearance;
use setasign\SetaPDF2\Signer\Signature\Module\Cms as CmsModule;
use setasign\SetaPDF2\Signer\SignatureField;
use setasign\SetaPDF2\Signer\Signer;

require_once('library/SetaPDF/Autoload.php');

// create a writer
$writer = new HttpWriter('simple.pdf', true);
// create a new document instance
$document = Document::loadByFilename(
    'files/pdfs/tektown/Laboratory-Report.pdf', $writer
);

// create a signer instance
$signer = new Signer($document);

// set some signature properties
$signer->setReason('Testing');
$signer->setLocation('SetaPDF-Signer Manual');

// create a signature module
$module = new CmsModule();
// load the certificate
$certificate = 'file://files/certificates/setapdf-no-pw.pem';
$module->setCertificate($certificate);
$module->setPrivateKey([$certificate, '' /* no password */]);

// create a visible signature field
$signer->addSignatureField(
    SignatureField::DEFAULT_FIELD_NAME,
    1,
    SignatureField::POSITION_RIGHT_TOP,
    ['x' => -160, 'y' => -100],
    180,
    60
);

// load a PDF for the graphic appearance
$graphicDocument = Document::loadByFilename('files/pdfs/misc/Handwritten-Signature.pdf');
// convert the first page to a XObject
$xObject = $graphicDocument
    ->getCatalog()
    ->getPages()
    ->getPage(1)
    ->toXObject($document, PageBoundaries::ART_BOX);

// create the appearance
$appearance = new DynamicAppearance($certificate);
$appearance->setGraphic($xObject);

// create a subset font
$font = new Type0Subset($document, 'files/fonts/DejaVuSans.ttf');
$appearance->setFont($font);

// define the appearance
$signer->setAppearance($appearance);

// sign the document and send the final document to the initial writer
$signer->sign($module);

Configure Labels and Certificate Data

The right column displays data that was extracted from the certificate that was passed to the signature module. The configuration of this text pieces can be controlled by the setShow() and setShowTpl() methods. Their names are represented via class constants (CONFIG_XXX).

By default the appearance class uses the standard font \setasign\SetaPDF2\Core\Font\Standard\Helvetica. It is also possible to use an embedded TrueType subset font which allows you to use characters which are not available in the standard encoding (WinAnsi) used by \setasign\SetaPDF2\Core\Font\Standard\Helvetica.

Following example shows how to format the labels, hide a value and format the date value individually while using a TrueType font: 

PHP
<?php

use setasign\SetaPDF2\Core\Document;
use setasign\SetaPDF2\Core\Font\TrueType\Subset as TrueTypeSubset;
use setasign\SetaPDF2\Core\Writer\HttpWriter;
use setasign\SetaPDF2\Signer\Signature\Appearance\Dynamic as DynamicAppearance;
use setasign\SetaPDF2\Signer\Signature\Module\Cms as CmsModule;
use setasign\SetaPDF2\Signer\SignatureField;
use setasign\SetaPDF2\Signer\Signer;

require_once('library/SetaPDF/Autoload.php');

// create a writer
$writer = new HttpWriter('simple.pdf', true);
// create a new document instance
$document = Document::loadByFilename(
    'files/pdfs/tektown/Laboratory-Report.pdf', $writer
);

// create a signer instance
$signer = new Signer($document);

// set some signature properties
$signer->setReason('Testing');
$signer->setLocation('SetaPDF-Signer Manual');

// create a signature module
$module = new CmsModule();
// load the certificate
$certificate = 'file://files/certificates/setapdf-no-pw.pem';
$module->setCertificate($certificate);
$module->setPrivateKey([$certificate, '' /* no password */]);

// create a visible signature field
$signer->addSignatureField(
    SignatureField::DEFAULT_FIELD_NAME,
    1,
    SignatureField::POSITION_RIGHT_TOP,
    ['x' => -160, 'y' => -100],
    180,
    60
);

// create the appearance
$appearance = new DynamicAppearance($certificate);

// let's use a TrueType font subset
$font = new TrueTypeSubset(
    $document,
    'files/fonts/DejaVuSans.ttf'
);
// and add it
$appearance->setFont($font);

// disable the distinguished name
$appearance->setShow(
    DynamicAppearance::CONFIG_DISTINGUISHED_NAME, false
);

// Translate the labels to german:
$appearance->setShowTpl(
    DynamicAppearance::CONFIG_REASON, 'Grund: %s'
);

$appearance->setShowTpl(
    DynamicAppearance::CONFIG_LOCATION, 'Ort: %s'
);

$appearance->setShowTpl(
    DynamicAppearance::CONFIG_DATE, 'Datum: %s'
);

$appearance->setShowTpl(
    DynamicAppearance::CONFIG_NAME,
    'Digital signiert durch: %s'
);

// format the date
$appearance->setShowFormat(
    DynamicAppearance::CONFIG_DATE, 'd.m.Y H:i:s'
);

// define the appearance
$signer->setAppearance($appearance);

// sign the document and send the final document to the initial writer
$signer->sign($module);

XObject Appearance

The class \setasign\SetaPDF2\Signer\Signature\Appearance\XObject representing a signature appearance based on an existing XObject.

An XObject could be an image, an extracted page or a fresh unique instance of a Form XObject.

To create an appearance of an existing image, you just have to pass the image instance to the constructor of the module: 

PHP
use setasign\SetaPDF2\Core\Image\Image;
use setasign\SetaPDF2\Signer\Signature\Appearance\XObject as XObjectAppearance;
...

// load the image
$image = Image::getByPath('files/pdfs/tektown/Logo.png');
// get an XObject instance of it
$xObject = $image->toXObject($document);
// pass it to the appearance modules constructor
$appearance = new XObjectAppearance($xObject);

// pass the appearance
$signer->setAppearance($appearance);

// pass the module to the sign method
$signer->sign($module);

An XObject can also be created from a page of an existing document: 

PHP
use setasign\SetaPDF2\Core\Document;
use setasign\SetaPDF2\Core\PageBoundaries;
use setasign\SetaPDF2\Signer\Signature\Appearance\XObject as XObjectAppearance;

...

// load a document
$appDocument = Document::loadByFilename('files/pdfs/handwritten-signature.pdf');

$pageXObject = $appDocument
    ->getCatalog()
    ->getPages()
    ->getPage(1)->toXObject($document, PageBoundaries::ART_BOX);

// create a static visible appearance from the xObject
$appearance = new XObjectAppearance($pageXObject);

// pass the appearance
$signer->setAppearance($appearance);

// pass the module to the sign method
$signer->sign($module);

At the end it is also possible to create an XObject instance manually and create the appearance by its Canvas instance.

The XObject will placed centered with the maximum height or width possible into the available annotation rectangle while aspect ratio is kept.

For an additional example please see our demo environment.

Example with a Rich-Text Block

Following example uses a rich-text block for the signature appearance. We extract the subject from the certificate and show it with some rich-text styling. For sure this can be combined with images or other individual drawing operations. At the end you can create the appearance completely individual.

PHP
<?php

use setasign\SetaPDF2\Core\Document;
use setasign\SetaPDF2\Core\Font\Type0\Subset as Type0Subset;
use setasign\SetaPDF2\Core\Text\RichTextBlock;
use setasign\SetaPDF2\Core\Text\Text;
use setasign\SetaPDF2\Core\Writer\HttpWriter;
use setasign\SetaPDF2\Core\XObject\Form as FormXObject;
use setasign\SetaPDF2\Signer\Signature\Appearance\XObject as XObjectAppearance;
use setasign\SetaPDF2\Signer\Signature\Module\Cms as CmsModule;
use setasign\SetaPDF2\Signer\SignatureField;
use setasign\SetaPDF2\Signer\Signer;
use setasign\SetaPDF2\Signer\X509\Certificate as X509Certificate;

require_once('library/SetaPDF/Autoload.php');

// create a writer
$writer = new HttpWriter('rich-text-in-xobject.pdf', true);
// create a new document instance
$document = Document::loadByFilename(
    'files/pdfs/tektown/Laboratory-Report.pdf', $writer
);

// create a signer instance
$signer = new Signer($document);

// set some signature properties
$signer->setReason('Testing');
$signer->setLocation('SetaPDF-Signer Manual');

// create a signature module
$module = new CmsModule();
// load the certificate
$certificatePath = 'file://files/certificates/setapdf-no-pw.pem';
$module->setCertificate($certificatePath);
$module->setPrivateKey([$certificatePath, '' /* no password */]);

// create a visible signature field
$field = $signer->addSignatureField(
    SignatureField::DEFAULT_FIELD_NAME,
    1,
    SignatureField::POSITION_CENTER_TOP,
    ['y' => -100],
    180,
    60
);

// creat a X509 certificate instance
$certificate = X509Certificate::fromFile($certificatePath);

// let's create a rich-text block
$richText = new RichTextBlock($document);

// define a font loader callback
$loadedFonts = [];
$fontLoader = static function (Document $document, $fontFamily, $fontStyle) use (&$loadedFonts) {
    $cacheKey = $document->getInstanceIdent() . '_' . $fontFamily . '_' . $fontStyle;
    if (!array_key_exists($cacheKey, $loadedFonts)) {
        $font = null;
        if ($fontFamily === 'DejaVuSans' && $fontStyle === '') {
            $font = new Type0Subset($document, 'files/fonts/DejaVuSans.ttf');
        } elseif ($fontFamily === 'DejaVuSans' && $fontStyle === 'B') {
            $font = new Type0Subset($document, 'files/fonts/DejaVuSans-Bold.ttf');
        } elseif ($fontFamily === 'DejaVuSans' && $fontStyle === 'I') {
            $font = new Type0Subset($document, 'files/fonts/DejaVuSans-Oblique.ttf');
        } elseif ($fontFamily === 'DejaVuSans' && $fontStyle === 'BI') {
            $font = new Type0Subset($document, 'files/fonts/DejaVuSans-BoldOblique.ttf');
        }
        $loadedFonts[$cacheKey] = $font;
    }
    return $loadedFonts[$cacheKey];
};

$richText->registerFontLoader($fontLoader);
$richText->setDefaultFontFamily('DejaVuSans');
$richText->setDefaultFontSize(8);
$richText->setAlign(Text::ALIGN_CENTER);
$richText->setPadding(3);
$richText->setBorderColor('#3b65b0');
$richText->setBorderWidth(1);
$richText->setBackgroundColor('#ffffff');
$richText->setTextWidth($field->getWidth() - 6);
$subject = [];
foreach ($certificate->getSubjectName(true) as $subjectItem) {
    $item = $subjectItem['name'] . ': ';
    if ($subjectItem['name'] === 'emailAddress') {
        $item .= '<u><b>' . $subjectItem['value'] . '</b></u>';
    } else {
        $item .= '<b>' . $subjectItem['value'] . '</b>';
    }

    $subject[] = $item;
}
$subjectString = implode(', ', $subject);
$richText->setText(<<<HTML
    Signature by {$subjectString}<br/>
    <span style="font-size:4pt;color: #b1b1b1;">Captured by 
    <span style="color:#000000;">tek</span><span style="color:#3b65b0;">town</span>-signer</span>
HTML
);

// create an XObject
$xObject = FormXObject::create($document, [0, 0, $richText->getWidth(), $richText->getHeight()]);
// draw the rich-text block onto the canvas
$richText->draw($xObject->getCanvas(), 0, 0);

// create the appearance instance
$appearance = new XObjectAppearance($xObject);

// define the appearance
$signer->setAppearance($appearance);

// sign the document and send the final document to the initial writer
$signer->sign($module);

Appearances on Several Pages

The SetaPDF-Signer component does not support multiple appearances of a single signature field.

A simple quote from the "Digital Signature Appearances specifications" (not publicity available anymore) of Adobe (which also landed with a simliar wording in PDF 2.0) will confirm this limitation:

The location of a signature within a document can have a bearing on its legal meaning. For this reason, signature fields never refer to more than one annotation. If more than one location is associated with a signature, the meaning may become ambiguous.[...]

To emulate such behavior it is possible to add stamp annotations to e.g. all pages which share the appearance of the signature field. We prepared a demo for this idea here.