Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,44 @@ reading the current ones first:
$param->addJSignParameters(['-ha' => 'SHA512']);
```

## Existing signature fields

JSignPdf 3.2 can inspect existing signature fields in a PDF. Only the PDF is
required for inspection; a certificate and signing password are not needed.

```php
$param = JSignParam::instance();
$param->setPdf(file_get_contents('/path/to/file/pdf_to_sign.pdf'));

$jSignPdf = new JSignPDF($param);
$fields = $jSignPdf->getSignatureFields();

foreach ($fields as $field) {
echo $field->getName();
echo $field->getPage();
echo $field->isSigned() ? 'signed' : 'blank';
}
```

Each signature field exposes its name, page, rectangle coordinates and whether
it is signed or hidden. `isBlank()` is the opposite of `isSigned()`, and
`hasVisibleRectangle()` reports whether the field has a non-zero rectangle.

To sign an existing blank signature field, select it by name before signing:

```php
$param->setCertificate(file_get_contents('/path/to/file/certificate.pfx'));
$param->setPassword('certificate_password');
$param->setSignatureField('Customer Signature');

$fileSigned = JSignPDF::instance($param)->sign();
```

The value passed to `setSignatureField()` is passed directly to JSignPdf.
Names containing spaces or Unicode characters are supported. JSignPdf also
supports its own field selectors such as `auto` and `#1`; when a field has one
of those literal names, JSignPdf gives the field name precedence.

## Passwords

Besides the certificate password of `setPassword()`, JSignPdf takes a password
Expand Down
14 changes: 13 additions & 1 deletion src/JSignPDF.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Exception;
use Jeidison\JSignPDF\Sign\JSignParam;
use Jeidison\JSignPDF\Sign\JSignService;
use Jeidison\JSignPDF\Sign\SignatureField;

/**
* @author Jeidison Farias <jeidison.farias@gmail.com>
Expand Down Expand Up @@ -41,9 +42,20 @@ public function getVersion(): string
return $this->service->getVersion($this->param);
}

/**
* @return list<SignatureField>
*/
public function getSignatureFields(): array
{
if (!$this->param instanceof JSignParam) {
throw new Exception('Invalid JSignParam instance');
}

return $this->service->getSignatureFields($this->param);
}

public function setParam(JSignParam $param): void
{
$this->param = $param;
}

}
13 changes: 13 additions & 0 deletions src/Sign/JSignParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class JSignParam
/** @var array<string, string> */
private array $parameterPasswords = [];

private ?string $signatureField = null;

public function __construct()
{
$this->tempName = md5(time() . uniqid() . mt_rand());
Expand Down Expand Up @@ -397,4 +399,15 @@ public function getJSignPdfDownloadUrl(): string
{
return $this->jSignPdfDownloadUrl;
}

public function setSignatureField(?string $fieldName): self
{
$this->signatureField = $fieldName;
return $this;
}

public function getSignatureField(): ?string
{
return $this->signatureField;
}
}
157 changes: 156 additions & 1 deletion src/Sign/JSignService.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,156 @@ public function getVersion(JSignParam $params): string
return explode('version ', $lastRow)[1];
}

/**
* @return list<SignatureField>
*/
public function getSignatureFields(JSignParam $params): array
{
$this->validateSignatureFieldInspection($params);

$pdf = $this->fileService->storeFile(
$params->getTempPath(),
$params->getTempName('.pdf'),
$params->getPdf()
);

try {
$command = $this->commandListSignatureFields($params, $pdf);
[$output, $exitCode] = $this->run($command, $params);

if ($exitCode !== 0) {
$diagnostic = trim(implode(PHP_EOL, $output));

if ($diagnostic === '') {
$diagnostic = 'Can not read the signature fields.';
}

throw new Exception($diagnostic);
}

return $this->parseSignatureFields($output);
} finally {
$this->fileService->deleteFile($pdf);
}
}

private function validateSignatureFieldInspection(JSignParam $params): void
{
$this->throwIf(
empty($params->getTempPath()) || !is_writable($params->getTempPath()),
'Temp Path is invalid or has not permission to writable.'
);

$this->throwIf(
empty($params->getPdf()),
'PDF is Empty or Invalid.'
);
}

private function commandListSignatureFields(JSignParam $params, string $pdf): string
{
$java = escapeshellarg($this->javaCommand($params));
$jSignPdf = $this->jSignPdfInvocation($params);
$pdf = escapeshellarg($pdf);

$javaOptions = implode(
' ',
array_merge(['-Duser.language=en'], $this->javaOptions($params))
);

return "$java $javaOptions $jSignPdf --quiet --list-sig-fields $pdf 2>&1";
}

/**
* @param list<string> $output
* @return list<SignatureField>
*/
private function parseSignatureFields(array $output): array
{
$fields = [];
$sawHeader = false;
$sawNoFields = false;

foreach ($output as $line) {
if (preg_match('/^Signature fields of .+:$/u', $line) === 1) {
if ($sawHeader || $sawNoFields || $fields !== []) {
throw new Exception(
"Unexpected signature field output: $line"
);
}

$sawHeader = true;
continue;
}

if (preg_match('/:\s*no signature fields\s*$/', $line) === 1) {
if ($sawHeader || $sawNoFields || $fields !== []) {
throw new Exception(
"Unexpected signature field output: $line"
);
}

$sawNoFields = true;
continue;
}

if ($sawNoFields) {
throw new Exception(
"Unexpected signature field output: $line"
);
}

$line = preg_replace(
'/\s+- this field name shadows the selector of the same name, the field name wins\s*$/',
'',
$line
);

if ($line === null) {
throw new Exception(
'Unexpected signature field output.'
);
}

$matches = [];

$matched = preg_match(
'/^#\d+\s+(.+?)\s+page\s+(\d+)\s+\[(-?(?:\d+(?:\.\d*)?|\.\d+))\s+(-?(?:\d+(?:\.\d*)?|\.\d+))\s+(-?(?:\d+(?:\.\d*)?|\.\d+))\s+(-?(?:\d+(?:\.\d*)?|\.\d+))\]\s+(blank|signed)(?:,\s*hidden|\s+hidden)?\s*$/u',
$line,
$matches
);

if ($matched !== 1) {
throw new Exception(
"Unexpected signature field output: $line"
);
}

$fields[] = new SignatureField(
rtrim($matches[1]),
(int) $matches[2],
(float) $matches[3],
(float) $matches[4],
(float) $matches[5],
(float) $matches[6],
$matches[7] === 'signed',
preg_match('/(?:,\s*|\s+)hidden\s*$/', $line) === 1,
);
}

if ($sawNoFields) {
return [];
}

if ($fields === []) {
throw new Exception(
'Unexpected signature field output: empty output'
);
}

return $fields;
}

private function validation(JSignParam $params): void
{
$this->throwIf(empty($params->getTempPath()) || !is_writable($params->getTempPath()), 'Temp Path is invalid or has not permission to writable.');
Expand Down Expand Up @@ -140,11 +290,16 @@ private function commandSign(JSignParam $params): string
$javaOptions = implode(' ', array_merge(['-Duser.language=en'], $this->javaOptions($params)));

$passwords = '';
$signatureField = '';
if ($params->getSignatureField() !== null) {
$signatureField = '--sig-field ' . escapeshellarg($params->getSignatureField()) . ' ';
}

foreach (array_keys($params->getPasswords()) as $option) {
$passwords .= "$option - ";
}

return "$java $javaOptions $jSignPdf $pdf -ksf $certificate --enable-stdin-passwords -ksp - {$passwords}{$params->getJSignParameters()} -d $pathPdfSigned 2>&1";
return "$java $javaOptions $jSignPdf $pdf -ksf $certificate --enable-stdin-passwords -ksp - {$passwords}{$signatureField}{$params->getJSignParameters()} -d $pathPdfSigned 2>&1";
}

/**
Expand Down
69 changes: 69 additions & 0 deletions src/Sign/SignatureField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Jeidison\JSignPDF\Sign;

final class SignatureField
{
public function __construct(
private string $name,
private int $page,
private float $llx,
private float $lly,
private float $urx,
private float $ury,
private bool $signed,
private bool $hidden,
) {
}

public function getName(): string
{
return $this->name;
}

public function getPage(): int
{
return $this->page;
}

public function getLlx(): float
{
return $this->llx;
}

public function getLly(): float
{
return $this->lly;
}

public function getUrx(): float
{
return $this->urx;
}

public function getUry(): float
{
return $this->ury;
}

public function isSigned(): bool
{
return $this->signed;
}

public function isBlank(): bool
{
return !$this->signed;
}

public function isHidden(): bool
{
return $this->hidden;
}

public function hasVisibleRectangle(): bool
{
return $this->urx > $this->llx
&& $this->ury > $this->lly;
}
}
Loading