Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.92% |
94 / 96 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
| SignatureStampAppearanceBuilder | |
97.92% |
94 / 96 |
|
50.00% |
2 / 4 |
24 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| buildXObject | |
98.46% |
64 / 65 |
|
0.00% |
0 / 1 |
11 | |||
| wrapTextForPdf | |
96.15% |
25 / 26 |
|
0.00% |
0 / 1 |
11 | |||
| escapePdfText | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | /** |
| 5 | * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors |
| 6 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 7 | */ |
| 8 | |
| 9 | namespace OCA\Libresign\Service\SignatureStampPreview; |
| 10 | |
| 11 | use OCA\Libresign\Service\SignatureTextService; |
| 12 | use OCA\Libresign\Service\SignerElementsService; |
| 13 | use SignerPHP\PdfSigner\Application\DTO\SignatureAppearanceXObjectDto; |
| 14 | |
| 15 | class SignatureStampAppearanceBuilder { |
| 16 | public function __construct( |
| 17 | private SignatureTextService $signatureTextService, |
| 18 | ) { |
| 19 | } |
| 20 | |
| 21 | public function buildXObject( |
| 22 | int $width, |
| 23 | int $height, |
| 24 | string $renderMode, |
| 25 | array $context = [], |
| 26 | string $template = '', |
| 27 | ?float $templateFontSize = null, |
| 28 | ?float $signatureFontSize = null, |
| 29 | ?string $fallbackCommonName = null, |
| 30 | ): SignatureAppearanceXObjectDto { |
| 31 | if ($renderMode === SignerElementsService::RENDER_MODE_GRAPHIC_ONLY) { |
| 32 | return new SignatureAppearanceXObjectDto(stream: '', resources: []); |
| 33 | } |
| 34 | |
| 35 | $context['ServerSignatureDate'] = (new \DateTimeImmutable('now', new \DateTimeZone('UTC'))) |
| 36 | ->format(\DateTimeInterface::ATOM); |
| 37 | |
| 38 | $textData = $this->signatureTextService->parse($template, $context); |
| 39 | $parsed = trim((string)($textData['parsed'] ?? '')); |
| 40 | |
| 41 | $descFontSize = $templateFontSize |
| 42 | ?? (float)($textData['templateFontSize'] ?? $this->signatureTextService->getTemplateFontSize()); |
| 43 | $descLineHeight = $descFontSize * 1.0; |
| 44 | $leftPadding = max(2.0, $descFontSize * 0.15); |
| 45 | |
| 46 | $isDescriptionOnly = $renderMode === SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY; |
| 47 | $textStartX = $isDescriptionOnly ? $leftPadding : ((float)$width / 2.0) + $leftPadding; |
| 48 | $availableWidth = $isDescriptionOnly ? (float)$width : (float)$width / 2.0; |
| 49 | |
| 50 | $stream = ''; |
| 51 | |
| 52 | if ($renderMode === SignerElementsService::RENDER_MODE_SIGNAME_AND_DESCRIPTION) { |
| 53 | $commonName = !empty($context['SignerCommonName']) |
| 54 | ? (string)$context['SignerCommonName'] |
| 55 | : ($fallbackCommonName ?? ''); |
| 56 | |
| 57 | if ($commonName !== '') { |
| 58 | $nameFontSize = $signatureFontSize ?? $this->signatureTextService->getSignatureFontSize(); |
| 59 | $leftHalfW = (float)$width / 2.0; |
| 60 | $nameLines = $this->wrapTextForPdf($commonName, $leftHalfW - $leftPadding * 2, $nameFontSize); |
| 61 | $nameLineCount = count($nameLines); |
| 62 | $totalNameHeight = $nameLineCount * $nameFontSize * 1.0; |
| 63 | $nameStartY = ((float)$height + $totalNameHeight) / 2.0 - $nameFontSize; |
| 64 | $nameStartY = max(0.0, $nameStartY); |
| 65 | $nameY = $nameStartY; |
| 66 | $estimatedCharWidth = $nameFontSize * 0.52; |
| 67 | foreach ($nameLines as $nameLine) { |
| 68 | $lineWidth = strlen($nameLine) * $estimatedCharWidth; |
| 69 | $nameX = max($leftPadding, ($leftHalfW - $lineWidth) / 2.0); |
| 70 | $escaped = $this->escapePdfText($nameLine); |
| 71 | $stream .= "BT\n"; |
| 72 | $stream .= sprintf("/F1 %.2F Tf\n", $nameFontSize); |
| 73 | $stream .= "0 0 0 rg\n"; |
| 74 | $stream .= sprintf("%.2F %.2F Td\n", $nameX, $nameY); |
| 75 | $stream .= sprintf("(%s) Tj\n", $escaped); |
| 76 | $stream .= "ET\n"; |
| 77 | $nameY -= $nameFontSize * 1.0; |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | $currentY = (float)$height - $descFontSize - 2.0; |
| 83 | foreach (explode(PHP_EOL, $parsed) as $line) { |
| 84 | $wrappedLines = $this->wrapTextForPdf($line, $availableWidth, $descFontSize); |
| 85 | foreach ($wrappedLines as $wrappedLine) { |
| 86 | if ($currentY < 0) { |
| 87 | break 2; |
| 88 | } |
| 89 | $escaped = $this->escapePdfText($wrappedLine); |
| 90 | $stream .= "BT\n"; |
| 91 | $stream .= sprintf("/F1 %.2F Tf\n", $descFontSize); |
| 92 | $stream .= "0 0 0 rg\n"; |
| 93 | $stream .= sprintf("%.2F %.2F Td\n", $textStartX, $currentY); |
| 94 | $stream .= sprintf("(%s) Tj\n", $escaped); |
| 95 | $stream .= "ET\n"; |
| 96 | $currentY -= $descLineHeight; |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | return new SignatureAppearanceXObjectDto( |
| 101 | stream: $stream, |
| 102 | resources: [ |
| 103 | 'Font' => [ |
| 104 | 'F1' => [ |
| 105 | 'Type' => '/Font', |
| 106 | 'Subtype' => '/Type1', |
| 107 | 'BaseFont' => '/Helvetica', |
| 108 | ], |
| 109 | ], |
| 110 | ], |
| 111 | ); |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * @return string[] |
| 116 | */ |
| 117 | public function wrapTextForPdf(string $line, float $availableWidth, float $fontSize): array { |
| 118 | $trimmed = trim($line); |
| 119 | if ($trimmed === '') { |
| 120 | return ['']; |
| 121 | } |
| 122 | |
| 123 | $estimatedCharWidth = max(1.0, $fontSize * 0.52); |
| 124 | $maxChars = max(1, (int)floor($availableWidth / $estimatedCharWidth)); |
| 125 | if (strlen($trimmed) <= $maxChars) { |
| 126 | return [$trimmed]; |
| 127 | } |
| 128 | |
| 129 | $result = []; |
| 130 | $current = ''; |
| 131 | foreach (preg_split('/\s+/', $trimmed) ?: [] as $word) { |
| 132 | if ($word === '') { |
| 133 | continue; |
| 134 | } |
| 135 | |
| 136 | $candidate = $current === '' ? $word : $current . ' ' . $word; |
| 137 | if (strlen($candidate) <= $maxChars) { |
| 138 | $current = $candidate; |
| 139 | continue; |
| 140 | } |
| 141 | |
| 142 | if ($current !== '') { |
| 143 | $result[] = $current; |
| 144 | $current = ''; |
| 145 | } |
| 146 | |
| 147 | while (strlen($word) > $maxChars) { |
| 148 | $result[] = substr($word, 0, $maxChars); |
| 149 | $word = substr($word, $maxChars); |
| 150 | } |
| 151 | |
| 152 | $current = $word; |
| 153 | } |
| 154 | |
| 155 | if ($current !== '') { |
| 156 | $result[] = $current; |
| 157 | } |
| 158 | |
| 159 | return $result; |
| 160 | } |
| 161 | |
| 162 | public function escapePdfText(string $value): string { |
| 163 | $value = str_replace('\\', '\\\\', $value); |
| 164 | $value = str_replace('(', '\\(', $value); |
| 165 | $value = str_replace(')', '\\)', $value); |
| 166 | |
| 167 | return $value; |
| 168 | } |
| 169 | } |