From 5c2252e3d5d6c98910bc9c19c08e9a021cdd240f Mon Sep 17 00:00:00 2001 From: vikas-kushwaha-dev Date: Thu, 17 Sep 2026 19:44:24 +0100 Subject: [PATCH] feat: support existing PDF signature fields Signed-off-by: vikas-kushwaha-dev --- README.md | 38 ++++ src/JSignPDF.php | 14 +- src/Sign/JSignParam.php | 13 ++ src/Sign/JSignService.php | 157 ++++++++++++++- src/Sign/SignatureField.php | 69 +++++++ tests/Integration/SignPdfTest.php | 263 +++++++++++++++++++++++++ tests/JSignPDFTest.php | 275 +++++++++++++++++++++++++++ tests/Sign/JSignParamTest.php | 37 +++- tests/Sign/SignatureFieldTest.php | 69 +++++++ tests/resources/signature-fields.pdf | Bin 0 -> 1370 bytes 10 files changed, 932 insertions(+), 3 deletions(-) create mode 100644 src/Sign/SignatureField.php create mode 100644 tests/Sign/SignatureFieldTest.php create mode 100644 tests/resources/signature-fields.pdf diff --git a/README.md b/README.md index 1958831..a109c00 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/JSignPDF.php b/src/JSignPDF.php index 1768b5e..9f8285a 100644 --- a/src/JSignPDF.php +++ b/src/JSignPDF.php @@ -5,6 +5,7 @@ use Exception; use Jeidison\JSignPDF\Sign\JSignParam; use Jeidison\JSignPDF\Sign\JSignService; +use Jeidison\JSignPDF\Sign\SignatureField; /** * @author Jeidison Farias @@ -41,9 +42,20 @@ public function getVersion(): string return $this->service->getVersion($this->param); } + /** + * @return list + */ + 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; } - } diff --git a/src/Sign/JSignParam.php b/src/Sign/JSignParam.php index 0df56cb..84469d0 100644 --- a/src/Sign/JSignParam.php +++ b/src/Sign/JSignParam.php @@ -54,6 +54,8 @@ class JSignParam /** @var array */ private array $parameterPasswords = []; + private ?string $signatureField = null; + public function __construct() { $this->tempName = md5(time() . uniqid() . mt_rand()); @@ -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; + } } diff --git a/src/Sign/JSignService.php b/src/Sign/JSignService.php index d87e40f..5c37002 100644 --- a/src/Sign/JSignService.php +++ b/src/Sign/JSignService.php @@ -91,6 +91,156 @@ public function getVersion(JSignParam $params): string return explode('version ', $lastRow)[1]; } + /** + * @return list + */ + 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 $output + * @return list + */ + 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.'); @@ -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"; } /** diff --git a/src/Sign/SignatureField.php b/src/Sign/SignatureField.php new file mode 100644 index 0000000..93a0d1e --- /dev/null +++ b/src/Sign/SignatureField.php @@ -0,0 +1,69 @@ +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; + } +} diff --git a/tests/Integration/SignPdfTest.php b/tests/Integration/SignPdfTest.php index 1e0a910..f3f6851 100644 --- a/tests/Integration/SignPdfTest.php +++ b/tests/Integration/SignPdfTest.php @@ -94,4 +94,267 @@ public function testSignAPdfOlderThan16WithTheDefaultParameters(): void $this->assertStringStartsWith('%PDF-', $signed); $this->assertStringContainsString('/ByteRange', $signed); } + + public function testGetSignatureFieldsReturnsEmptyListWhenPdfHasNoSignatureFields(): void + { + $fields = JSignPDF::instance( + $this->inspectionParams() + )->getSignatureFields(); + + $this->assertSame([], $fields); + } + + private function inspectionParams(string $file = 'pdf-test.pdf'): JSignParam + { + $params = JSignParam::instance(); + $params->setPdf(file_get_contents(__DIR__ . '/../resources/' . $file)); + $params->setEnvironmentVariables([ + 'HOME' => '/tmp', + 'XDG_CONFIG_HOME' => '/tmp/.config', + 'LANG' => 'C.UTF-8', + 'LC_ALL' => 'C.UTF-8', + ]); + + return $params; + } + + public function testGetSignatureFieldsReturnsFieldsReportedByJSignPdf(): void + { + $fields = JSignPDF::instance( + $this->inspectionParams('signature-fields.pdf') + )->getSignatureFields(); + + $this->assertCount(8, $fields); + + $this->assertSame('Customer Signature', $fields[0]->getName()); + $this->assertSame(1, $fields[0]->getPage()); + $this->assertSame(70.0, $fields[0]->getLlx()); + $this->assertSame(700.0, $fields[0]->getLly()); + $this->assertSame(300.0, $fields[0]->getUrx()); + $this->assertSame(760.0, $fields[0]->getUry()); + $this->assertTrue($fields[0]->isBlank()); + $this->assertFalse($fields[0]->isHidden()); + $this->assertTrue($fields[0]->hasVisibleRectangle()); + + $this->assertSame('Invisible', $fields[1]->getName()); + $this->assertSame(1, $fields[1]->getPage()); + $this->assertSame(0.0, $fields[1]->getLlx()); + $this->assertSame(0.0, $fields[1]->getLly()); + $this->assertSame(0.0, $fields[1]->getUrx()); + $this->assertSame(0.0, $fields[1]->getUry()); + $this->assertTrue($fields[1]->isBlank()); + $this->assertFalse($fields[1]->hasVisibleRectangle()); + + $this->assertSame('Hidden', $fields[2]->getName()); + $this->assertTrue($fields[2]->isBlank()); + $this->assertTrue($fields[2]->isHidden()); + + $this->assertSame('auto', $fields[3]->getName()); + $this->assertTrue($fields[3]->isBlank()); + + $this->assertSame('#1', $fields[4]->getName()); + $this->assertTrue($fields[4]->isBlank()); + + $this->assertSame( + 'This signature field name is much longer than thirty characters', + $fields[5]->getName() + ); + $this->assertTrue($fields[5]->isBlank()); + + $this->assertSame('Podpis zákazníka', $fields[6]->getName()); + $this->assertSame(2, $fields[6]->getPage()); + $this->assertTrue($fields[6]->isBlank()); + $this->assertFalse($fields[6]->isSigned()); + + $this->assertSame('Already Signed', $fields[7]->getName()); + $this->assertSame(2, $fields[7]->getPage()); + $this->assertFalse($fields[7]->isBlank()); + $this->assertTrue($fields[7]->isSigned()); + } + + public function testSignsIntoExistingSignatureFieldByName(): void + { + $params = $this->params(); + $params->setPdf( + file_get_contents(__DIR__ . '/../resources/signature-fields.pdf') + ); + $params->setSignatureField('Customer Signature'); + $params->setEnvironmentVariables([ + 'HOME' => '/tmp', + 'XDG_CONFIG_HOME' => '/tmp/.config', + 'LANG' => 'C.UTF-8', + 'LC_ALL' => 'C.UTF-8', + ]); + + $signedPdf = JSignPDF::instance($params)->sign(); + + $inspectionParams = $this->inspectionParams(); + $inspectionParams->setPdf($signedPdf); + + $fields = JSignPDF::instance($inspectionParams)->getSignatureFields(); + + $customerSignature = array_values( + array_filter( + $fields, + static fn ($field): bool => + $field->getName() === 'Customer Signature' + ) + ); + + $this->assertCount(1, $customerSignature); + $this->assertTrue($customerSignature[0]->isSigned()); + $this->assertFalse($customerSignature[0]->isBlank()); + } + + public function testSignsIntoExistingUnicodeSignatureField(): void + { + $params = $this->params(); + $params->setPdf( + file_get_contents(__DIR__ . '/../resources/signature-fields.pdf') + ); + $params->setSignatureField('Podpis zákazníka'); + $params->setEnvironmentVariables([ + 'HOME' => '/tmp', + 'XDG_CONFIG_HOME' => '/tmp/.config', + 'LANG' => 'C.UTF-8', + 'LC_ALL' => 'C.UTF-8', + ]); + + $signedPdf = JSignPDF::instance($params)->sign(); + + $inspectionParams = $this->inspectionParams(); + $inspectionParams->setPdf($signedPdf); + + $fields = JSignPDF::instance($inspectionParams)->getSignatureFields(); + + $unicodeField = array_values( + array_filter( + $fields, + static fn ($field): bool => + $field->getName() === 'Podpis zákazníka' + ) + ); + + $this->assertCount(1, $unicodeField); + $this->assertTrue($unicodeField[0]->isSigned()); + } + + public function testSignsIntoFieldLiterallyNamedAuto(): void + { + $params = $this->params(); + $params->setPdf( + file_get_contents(__DIR__ . '/../resources/signature-fields.pdf') + ); + $params->setSignatureField('auto'); + $params->setEnvironmentVariables([ + 'HOME' => '/tmp', + 'XDG_CONFIG_HOME' => '/tmp/.config', + 'LANG' => 'C.UTF-8', + 'LC_ALL' => 'C.UTF-8', + ]); + + $signedPdf = JSignPDF::instance($params)->sign(); + + $inspectionParams = $this->inspectionParams(); + $inspectionParams->setPdf($signedPdf); + + $fields = JSignPDF::instance($inspectionParams)->getSignatureFields(); + + $field = array_values( + array_filter( + $fields, + static fn ($field): bool => $field->getName() === 'auto' + ) + ); + + $this->assertCount(1, $field); + $this->assertTrue($field[0]->isSigned()); + } + + public function testSignsIntoFieldLiterallyNamedNumberSelector(): void + { + $params = $this->params(); + $params->setPdf( + file_get_contents(__DIR__ . '/../resources/signature-fields.pdf') + ); + $params->setSignatureField('#1'); + $params->setEnvironmentVariables([ + 'HOME' => '/tmp', + 'XDG_CONFIG_HOME' => '/tmp/.config', + 'LANG' => 'C.UTF-8', + 'LC_ALL' => 'C.UTF-8', + ]); + + $signedPdf = JSignPDF::instance($params)->sign(); + + $inspectionParams = $this->inspectionParams(); + $inspectionParams->setPdf($signedPdf); + + $fields = JSignPDF::instance($inspectionParams)->getSignatureFields(); + + $field = array_values( + array_filter( + $fields, + static fn ($field): bool => $field->getName() === '#1' + ) + ); + + $this->assertCount(1, $field); + $this->assertTrue($field[0]->isSigned()); + } + + public function testSigningAlreadySignedFieldPreservesJSignPdfFailure(): void + { + $params = $this->params(); + $params->setPdf( + file_get_contents(__DIR__ . '/../resources/signature-fields.pdf') + ); + $params->setSignatureField('Already Signed'); + $params->setEnvironmentVariables([ + 'HOME' => '/tmp', + 'XDG_CONFIG_HOME' => '/tmp/.config', + 'LANG' => 'C.UTF-8', + 'LC_ALL' => 'C.UTF-8', + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + "The signature field 'Already Signed' is already signed, choose a blank one." + ); + + JSignPDF::instance($params)->sign(); + } + + public function testSigningMissingFieldPreservesJSignPdfFailure(): void + { + $params = $this->params(); + $params->setPdf( + file_get_contents(__DIR__ . '/../resources/signature-fields.pdf') + ); + $params->setSignatureField('Field That Does Not Exist'); + $params->setEnvironmentVariables([ + 'HOME' => '/tmp', + 'XDG_CONFIG_HOME' => '/tmp/.config', + 'LANG' => 'C.UTF-8', + 'LC_ALL' => 'C.UTF-8', + ]); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + "No signature field matches 'Field That Does Not Exist'." + ); + + JSignPDF::instance($params)->sign(); + } + + public function testGetSignatureFieldsRejectsInvalidPdf(): void + { + $params = $this->inspectionParams(); + $params->setPdf('not a pdf'); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Can not read the signature fields'); + + JSignPDF::instance($params)->getSignatureFields(); + } } diff --git a/tests/JSignPDFTest.php b/tests/JSignPDFTest.php index f733483..6530456 100644 --- a/tests/JSignPDFTest.php +++ b/tests/JSignPDFTest.php @@ -38,6 +38,7 @@ function proc_close($process) use org\bovigo\vfs\vfsStream; use Exception; +use Jeidison\JSignPDF\JSignPDF; use Jeidison\JSignPDF\Sign\JSignParam; use Jeidison\JSignPDF\Sign\JSignService; use Jeidison\JSignPDF\Tests\Builder\JSignParamBuilder; @@ -724,4 +725,278 @@ public static function providerDashAsPasswordSpellings(): array 'short option with assignment' => [['-tsp=-']], ]; } + + public function testSignPassesTheConfiguredSignatureField(): void + { + global $mockExec, $mockProcCommand; + $mockExec = ['Finished: Signature succesfully created.']; + + $params = $this->withFakeRuntime(); + $params->setSignatureField("Customer Signature 'Main'"); + $params->setCertificate($this->getNewCert($params->getPassword())); + $params->setPathPdfSigned('vfs://download/temp'); + file_put_contents($params->getTempPdfSignedPath(), 'signed file content'); + + $this->service->sign($params); + + $this->assertStringContainsString( + '--sig-field ' . escapeshellarg("Customer Signature 'Main'"), + $mockProcCommand + ); + } + + public function testSignDoesNotPassSignatureFieldWhenItIsNotConfigured(): void + { + global $mockExec, $mockProcCommand; + $mockExec = ['Finished: Signature succesfully created.']; + + $params = $this->withFakeRuntime(); + $params->setCertificate($this->getNewCert($params->getPassword())); + $params->setPathPdfSigned('vfs://download/temp'); + file_put_contents($params->getTempPdfSignedPath(), 'signed file content'); + + $this->service->sign($params); + + $this->assertStringNotContainsString('--sig-field', $mockProcCommand); + } + + public function testGetSignatureFieldsUsesTheInspectionCommandWithoutSigningCredentials(): void + { + global $mockExec, $mockProcCommand; + + $mockExec = ['document.pdf: no signature fields']; + + $params = $this->withFakeRuntime(); + $params->setCertificate(''); + $params->setPassword(''); + + $fields = $this->service->getSignatureFields($params); + + $this->assertSame([], $fields); + $this->assertStringContainsString('--quiet --list-sig-fields', $mockProcCommand); + $this->assertStringContainsString('-Duser.language=en', $mockProcCommand); + $this->assertStringNotContainsString('-ksf', $mockProcCommand); + $this->assertStringNotContainsString('--enable-stdin-passwords', $mockProcCommand); + } + + public function testGetSignatureFieldsDeletesTheTemporaryPdfAfterInspection(): void + { + global $mockExec; + + $mockExec = ['document.pdf: no signature fields']; + + $params = $this->withFakeRuntime(); + $tempPdf = $params->getTempPdfPath(); + + $this->service->getSignatureFields($params); + + $this->assertFileDoesNotExist($tempPdf); + } + + public function testGetSignatureFieldsParsesJSignPdfOutput(): void + { + global $mockExec; + + $mockExec = [ + '#1 Customer Signature page 1 [70.0 700.0 300.0 760.0] blank', + '#2 Manager Signature page 2 [70.5 600.25 300.75 660.0] signed, hidden', + '#3 Podpis zákazníka page 3 [-10.5 -20.25 0.0 0.0] blank hidden', + ]; + + $params = $this->withFakeRuntime(); + + $fields = $this->service->getSignatureFields($params); + + $this->assertCount(3, $fields); + + $this->assertSame('Customer Signature', $fields[0]->getName()); + $this->assertSame(1, $fields[0]->getPage()); + $this->assertSame(70.0, $fields[0]->getLlx()); + $this->assertSame(700.0, $fields[0]->getLly()); + $this->assertSame(300.0, $fields[0]->getUrx()); + $this->assertSame(760.0, $fields[0]->getUry()); + $this->assertTrue($fields[0]->isBlank()); + $this->assertFalse($fields[0]->isHidden()); + + $this->assertSame('Manager Signature', $fields[1]->getName()); + $this->assertSame(2, $fields[1]->getPage()); + $this->assertTrue($fields[1]->isSigned()); + $this->assertTrue($fields[1]->isHidden()); + + $this->assertSame('Podpis zákazníka', $fields[2]->getName()); + $this->assertSame(-10.5, $fields[2]->getLlx()); + $this->assertSame(-20.25, $fields[2]->getLly()); + $this->assertTrue($fields[2]->isBlank()); + $this->assertTrue($fields[2]->isHidden()); + } + + public function testGetSignatureFieldsParsesNamesLongerThanTheDisplayWidth(): void + { + global $mockExec; + + $name = 'This signature field name is much longer than thirty characters'; + + $mockExec = [ + "#1 $name page 1 [10.0 20.0 30.0 40.0] blank", + ]; + + $fields = $this->service->getSignatureFields($this->withFakeRuntime()); + + $this->assertSame($name, $fields[0]->getName()); + } + + public function testGetSignatureFieldsIgnoresSelectorShadowSuffix(): void + { + global $mockExec; + + $suffix = ' - this field name shadows the selector of the same name, the field name wins'; + + $mockExec = [ + '#1 auto page 1 [10.0 20.0 30.0 40.0] blank' . $suffix, + '#2 #1 page 2 [50.0 60.0 70.0 80.0] signed' . $suffix, + ]; + + $fields = $this->service->getSignatureFields($this->withFakeRuntime()); + + $this->assertSame('auto', $fields[0]->getName()); + $this->assertSame('#1', $fields[1]->getName()); + } + + public function testGetSignatureFieldsRejectsMalformedFieldOutput(): void + { + global $mockExec; + + $mockExec = [ + '#1 broken signature field output', + ]; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('#1 broken signature field output'); + + $this->service->getSignatureFields($this->withFakeRuntime()); + } + + public function testGetSignatureFieldsPreservesJSignPdfFailureDiagnostic(): void + { + global $mockExec, $mockProcExitCode; + + $mockExec = [ + "Can not read the signature fields of '/tmp/document.pdf': Invalid PDF", + ]; + $mockProcExitCode = 5; + + $this->expectException(Exception::class); + $this->expectExceptionMessage( + "Can not read the signature fields of '/tmp/document.pdf': Invalid PDF" + ); + + $this->service->getSignatureFields($this->withFakeRuntime()); + } + + public function testGetSignatureFieldsDeletesTemporaryPdfWhenExecutionFails(): void + { + global $mockExec, $mockProcExitCode; + + $mockExec = [ + "Can not read the signature fields of '/tmp/document.pdf': Invalid PDF", + ]; + $mockProcExitCode = 5; + + $params = $this->withFakeRuntime(); + $tempPdf = $params->getTempPdfPath(); + + try { + $this->service->getSignatureFields($params); + $this->fail('Expected signature field inspection to fail.'); + } catch (Exception $e) { + $this->assertStringContainsString( + 'Can not read the signature fields', + $e->getMessage() + ); + } + + $this->assertFileDoesNotExist($tempPdf); + } + + public function testGetSignatureFieldsDeletesTemporaryPdfWhenParsingFails(): void + { + global $mockExec; + + $mockExec = [ + '#1 malformed output', + ]; + + $params = $this->withFakeRuntime(); + $tempPdf = $params->getTempPdfPath(); + + try { + $this->service->getSignatureFields($params); + $this->fail('Expected signature field parsing to fail.'); + } catch (Exception $e) { + $this->assertStringContainsString( + '#1 malformed output', + $e->getMessage() + ); + } + + $this->assertFileDoesNotExist($tempPdf); + } + + public function testGetSignatureFieldsThroughFacadeRequiresParams(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid JSignParam instance'); + + JSignPDF::instance()->getSignatureFields(); + } + + public function testGetSignatureFieldsParsesHeaderBeforeFields(): void + { + global $mockExec; + + $mockExec = [ + 'Signature fields of /tmp/example.pdf:', + '#1 Customer Signature page 1 [70.0 700.0 300.0 760.0] blank', + ]; + + $fields = $this->service->getSignatureFields( + $this->withFakeRuntime() + ); + + $this->assertCount(1, $fields); + $this->assertSame('Customer Signature', $fields[0]->getName()); + } + + public function testGetSignatureFieldsRejectsEmptySuccessfulOutput(): void + { + global $mockExec; + + $mockExec = ['']; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unexpected signature field output'); + + $this->service->getSignatureFields( + $this->withFakeRuntime() + ); + } + + public function testGetSignatureFieldsRejectsFieldAfterNoFieldsOutput(): void + { + global $mockExec; + + $mockExec = [ + '/tmp/example.pdf: no signature fields', + '#1 Customer Signature page 1 [70.0 700.0 300.0 760.0] blank', + ]; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'Unexpected signature field output' + ); + + $this->service->getSignatureFields( + $this->withFakeRuntime() + ); + } } diff --git a/tests/Sign/JSignParamTest.php b/tests/Sign/JSignParamTest.php index cf1b55f..2c773e9 100644 --- a/tests/Sign/JSignParamTest.php +++ b/tests/Sign/JSignParamTest.php @@ -1,6 +1,6 @@ ["pass\r\nword"], ]; } + + public function testSignatureFieldDefaultsToNull(): void + { + $params = JSignParam::instance(); + + $this->assertNull($params->getSignatureField()); + } + + public function testCanSetSignatureField(): void + { + $params = JSignParam::instance(); + + $result = $params->setSignatureField('Customer Signature'); + + $this->assertSame($params, $result); + $this->assertSame('Customer Signature', $params->getSignatureField()); + } + + public function testSignatureFieldIsNotReinterpreted(): void + { + $params = JSignParam::instance(); + + $params->setSignatureField(' #1 '); + + $this->assertSame(' #1 ', $params->getSignatureField()); + } + + public function testSignatureFieldCanBeResetToNull(): void + { + $params = JSignParam::instance() + ->setSignatureField('CustomerSignature') + ->setSignatureField(null); + + $this->assertNull($params->getSignatureField()); + } } diff --git a/tests/Sign/SignatureFieldTest.php b/tests/Sign/SignatureFieldTest.php new file mode 100644 index 0000000..0ed1e1e --- /dev/null +++ b/tests/Sign/SignatureFieldTest.php @@ -0,0 +1,69 @@ +assertSame('Customer Signature', $field->getName()); + $this->assertSame(2, $field->getPage()); + $this->assertSame(70.0, $field->getLlx()); + $this->assertSame(600.0, $field->getLly()); + $this->assertSame(300.0, $field->getUrx()); + $this->assertSame(660.0, $field->getUry()); + $this->assertTrue($field->isSigned()); + $this->assertFalse($field->isBlank()); + $this->assertTrue($field->isHidden()); + $this->assertTrue($field->hasVisibleRectangle()); + } + + public function testBlankField(): void + { + $field = new SignatureField( + 'CustomerSignature', + 1, + 70.0, + 700.0, + 300.0, + 760.0, + false, + false, + ); + + $this->assertFalse($field->isSigned()); + $this->assertTrue($field->isBlank()); + $this->assertFalse($field->isHidden()); + $this->assertTrue($field->hasVisibleRectangle()); + } + + public function testZeroSizeRectangleIsNotVisible(): void + { + $field = new SignatureField( + 'InvisibleSignature', + 1, + 0.0, + 0.0, + 0.0, + 0.0, + false, + false, + ); + + $this->assertFalse($field->hasVisibleRectangle()); + } +} diff --git a/tests/resources/signature-fields.pdf b/tests/resources/signature-fields.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8d69742674fa3b137bffda5e8f0047cf2a4a4b6c GIT binary patch literal 1370 zcmY!laBZ^4=fsl4ocwey{jk)c;>`R! z1${$36E6LL#Prl+1tXyHATE8!}U1!Rn&DUx|Usd?!o844!m zT>5UAIVGt@3i@t2i6yBnsmb{%sa!x^#U(|liMhO76?2NY9$qRp4lytZ;_!TIWMU9t zu-zbl!}H-K4i6vyP=hNW948+Y@jhf?So)Zwix*@aL@(T8bJ|#Z31RUYj&3X#8-aoV z7J^V8o0?g`LeoDfE4U<=OW#ky&=Pwv0K=xJxJ1Fw*bp^DwoW(FDG<5@o+AY zT;jmN&K4r@V8SsG7K2HSY%;lgO+gBVDObdu-p%Dt;qho%&tls)$%SE?BIn`vJbMqM z^sO{um0JAVFrX=}-~+Fuu)$on*_S7`W$1q2)%fDz3bt*UO}0w?+wB#0t&V!#a46^5 z>u|T}`#RdUaF}tM@ryY&E6-g#*ZbVrU2@{S9s4p;pDg;td%L2^`(vWz?0|z6Op#t{ zk<-%WCFy3%8q_R)5ojJ7Eq=nsDr(6VK5l~xjqb(S+TW8dm^(hco{`k^?fuI&W&*XQ z)fSsul6tmSi|^Ynz4f|8#6znt*AM^J<;Ij=F@2z=9UJ|cUv6>12GMy@NA9T|F79R0fv1;zTHP`v)_g-$?a^9o0b9ub0vYMB5 zq|BCA(!%w{{4E{(53Wyb`jh(F&O;&Ugj@N{>R7w%bS}<|m&B$TmAz+AdA`lCo;m;G z2Y&0_=eJ@@p(dCq)C`y>gYxr%$qpz7%+Q`L3eh&sE-seN&c<%$CguhfCI+S!=0;}D zmM*SF<_1R2E@lR1W|l@yCI(K9j+TzD#%^ZDE^a`+tBbRxxvP^fze=T6V!1iBr(vtI9URKQ$CtO_M+AhcJ<}+hsM7hoCuP){yA#W_+c%NHe$7RnZ ciX}*kOA?DpDvE$EG&C?Y;8Im}^>^a}0N*+P4FCWD literal 0 HcmV?d00001