• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 25443615450

06 May 2026 03:05PM UTC coverage: 56.831%. First build
25443615450

Pull #7650

github

web-flow
Merge 76cb1db26 into d0c59f4ac
Pull Request #7650: fix: expose ValidationURL and qrcode in signature stamp templates

38 of 40 new or added lines in 2 files covered. (95.0%)

10728 of 18877 relevant lines covered (56.83%)

6.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.03
/lib/Service/SignatureTextService.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Service;
10

11
use DateTimeInterface;
12
use Exception;
13
use Imagick;
14
use ImagickDraw;
15
use ImagickPixel;
16
use OCA\Libresign\AppInfo\Application;
17
use OCA\Libresign\Exception\LibresignException;
18
use OCA\Libresign\Vendor\Endroid\QrCode\Color\Color;
19
use OCA\Libresign\Vendor\Endroid\QrCode\Encoding\Encoding;
20
use OCA\Libresign\Vendor\Endroid\QrCode\ErrorCorrectionLevel;
21
use OCA\Libresign\Vendor\Endroid\QrCode\QrCode;
22
use OCA\Libresign\Vendor\Endroid\QrCode\RoundBlockSizeMode;
23
use OCA\Libresign\Vendor\Endroid\QrCode\Writer\PngWriter;
24
use OCA\Libresign\Vendor\Twig\Environment;
25
use OCA\Libresign\Vendor\Twig\Error\SyntaxError;
26
use OCA\Libresign\Vendor\Twig\Loader\FilesystemLoader;
27
use OCP\IAppConfig;
28
use OCP\IDateTimeZone;
29
use OCP\IL10N;
30
use OCP\IRequest;
31
use OCP\IURLGenerator;
32
use OCP\IUserSession;
33
use Psr\Log\LoggerInterface;
34
use Sabre\DAV\UUIDUtil;
35

36
class SignatureTextService {
37
        public const TEMPLATE_DEFAULT_FONT_SIZE = 10;
38
        public const SIGNATURE_DEFAULT_FONT_SIZE = 20;
39
        public const SIGNATURE_DIMENSION_MINIMUM = 1;
40
        public const FONT_SIZE_MINIMUM = 0.1;
41
        public const FRONT_SIZE_MAX = 30;
42
        public const DEFAULT_SIGNATURE_WIDTH = 350;
43
        public const DEFAULT_SIGNATURE_HEIGHT = 100;
44
        private const QRCODE_SIZE = 100;
45
        public function __construct(
46
                private IAppConfig $appConfig,
47
                private IL10N $l10n,
48
                private IDateTimeZone $dateTimeZone,
49
                private IRequest $request,
50
                private IUserSession $userSession,
51
                private IURLGenerator $urlGenerator,
52
                protected LoggerInterface $logger,
53
        ) {
54
        }
148✔
55

56
        /**
57
         * @return array{template: string, parsed: string, templateFontSize: float, signatureFontSize: float, signatureWidth: float, signatureHeight: float, renderMode: string}
58
         * @throws LibresignException
59
         */
60
        public function save(
61
                string $template,
62
                float $templateFontSize = self::TEMPLATE_DEFAULT_FONT_SIZE,
63
                float $signatureFontSize = self::SIGNATURE_DEFAULT_FONT_SIZE,
64
                float $signatureWidth = self::DEFAULT_SIGNATURE_WIDTH,
65
                float $signatureHeight = self::DEFAULT_SIGNATURE_HEIGHT,
66
                string $renderMode = SignerElementsService::RENDER_MODE_DEFAULT,
67
        ): array {
68
                if ($templateFontSize > self::FRONT_SIZE_MAX || $templateFontSize < self::FONT_SIZE_MINIMUM) {
22✔
69
                        // TRANSLATORS This message refers to the font size used in the text
70
                        // that is used together or to replace a person's handwritten
71
                        // signature in the signed PDF. The user must enter a numeric value
72
                        // within the accepted range.
73
                        throw new LibresignException($this->l10n->t('Invalid template font size. The value must be between %.1f and %.0f.', [self::FONT_SIZE_MINIMUM, self::FRONT_SIZE_MAX]));
×
74
                }
75
                if ($signatureFontSize > self::FRONT_SIZE_MAX || $signatureFontSize < self::FONT_SIZE_MINIMUM) {
22✔
76
                        // TRANSLATORS This message refers to the font size used in the text
77
                        // that is used together or to replace a person's handwritten
78
                        // signature in the signed PDF. The user must enter a numeric value
79
                        // within the accepted range.
80
                        throw new LibresignException($this->l10n->t('Invalid signature font size. The value must be between %.1f and %.0f.', [self::FONT_SIZE_MINIMUM, self::FRONT_SIZE_MAX]));
×
81
                }
82
                if (
83
                        !is_finite($signatureWidth)
22✔
84
                        || !is_finite($signatureHeight)
22✔
85
                        || $signatureWidth < self::SIGNATURE_DIMENSION_MINIMUM
22✔
86
                        || $signatureHeight < self::SIGNATURE_DIMENSION_MINIMUM
22✔
87
                ) {
88
                        // TRANSLATORS This message is shown when the visible signature box size
89
                        // configured by the admin is invalid. "Signature box" is the rectangular
90
                        // area reserved for the handwritten-style signature image in the signed
91
                        // PDF. "Width" and "height" are its pixel dimensions. %.0f is the
92
                        // minimum allowed value for each dimension.
93
                        throw new LibresignException($this->l10n->t('Invalid signature box size. Width and height must be at least %.0f.', [self::SIGNATURE_DIMENSION_MINIMUM]));
15✔
94
                }
95
                $template = trim($template);
7✔
96
                $template = preg_replace(
7✔
97
                        [
7✔
98
                                '/>\s+</',
7✔
99
                                '/<br\s*\/?>/i',
7✔
100
                                '/<p[^>]*>/i',
7✔
101
                                '/<\/p>/i',
7✔
102
                        ],
7✔
103
                        [
7✔
104
                                '><',
7✔
105
                                "\n",
7✔
106
                                '',
7✔
107
                                "\n"
7✔
108
                        ],
7✔
109
                        $template
7✔
110
                );
7✔
111
                $template = strip_tags((string)$template);
7✔
112
                $template = trim($template);
7✔
113
                $template = html_entity_decode($template);
7✔
114
                $this->appConfig->setValueString(Application::APP_ID, 'signature_text_template', $template);
7✔
115
                $this->appConfig->setValueFloat(Application::APP_ID, 'signature_width', $signatureWidth);
7✔
116
                $this->appConfig->setValueFloat(Application::APP_ID, 'signature_height', $signatureHeight);
7✔
117
                $this->appConfig->setValueFloat(Application::APP_ID, 'template_font_size', $templateFontSize);
7✔
118
                $this->appConfig->setValueFloat(Application::APP_ID, 'signature_font_size', $signatureFontSize);
7✔
119
                $this->appConfig->setValueString(Application::APP_ID, 'signature_render_mode', $renderMode);
7✔
120
                return $this->parse($template);
7✔
121
        }
122

123
        /**
124
         * @return array{template: string, parsed: string, templateFontSize: float, signatureFontSize: float, signatureWidth: float, signatureHeight: float, renderMode: string}
125
         * @throws LibresignException
126
         */
127
        public function parse(string $template = '', array $context = []): array {
128
                $templateFontSize = $this->getTemplateFontSize();
17✔
129
                $signatureFontSize = $this->getSignatureFontSize();
17✔
130
                $signatureWidth = $this->getFullSignatureWidth();
17✔
131
                $signatureHeight = $this->getFullSignatureHeight();
17✔
132
                $renderMode = $this->getRenderMode();
17✔
133
                if (empty($template)) {
17✔
134
                        $template = $this->getTemplate();
10✔
135
                }
136
                if (empty($template)) {
17✔
137
                        return [
2✔
138
                                'parsed' => '',
2✔
139
                                'template' => $template,
2✔
140
                                'templateFontSize' => $templateFontSize,
2✔
141
                                'signatureFontSize' => $signatureFontSize,
2✔
142
                                'signatureWidth' => $signatureWidth,
2✔
143
                                'signatureHeight' => $signatureHeight,
2✔
144
                                'renderMode' => $renderMode,
2✔
145
                        ];
2✔
146
                }
147
                if (empty($context)) {
15✔
148
                        $date = new \DateTime('now', new \DateTimeZone('UTC'));
6✔
149
                        $documentUuid = UUIDUtil::getUUID();
6✔
150
                        $validationUrl = $this->buildValidationUrl($documentUuid);
6✔
151
                        $context = [
6✔
152
                                'DocumentUUID' => $documentUuid,
6✔
153
                                'IssuerCommonName' => 'Acme Cooperative',
6✔
154
                                'LocalSignerSignatureDateOnly' => ($date)->format('Y-m-d'),
6✔
155
                                'LocalSignerSignatureDateTime' => ($date)->format(DateTimeInterface::ATOM),
6✔
156
                                'LocalSignerTimezone' => $this->dateTimeZone->getTimeZone()->getName(),
6✔
157
                                'ServerSignatureDate' => ($date)->format(DateTimeInterface::ATOM),
6✔
158
                                'SignerIP' => $this->request->getRemoteAddress(),
6✔
159
                                'SignerCommonName' => $this->userSession?->getUser()?->getDisplayName() ?? 'John Doe',
6✔
160
                                'SignerEmail' => $this->userSession?->getUser()?->getEMailAddress() ?? 'john.doe@libresign.coop',
6✔
161
                                'SignerUserAgent' => $this->request->getHeader('User-Agent'),
6✔
162
                                'ValidationURL' => $validationUrl,
6✔
163
                                'qrcode' => $this->getQrCodeImageBase64($validationUrl),
6✔
164
                        ];
6✔
165
                }
166

167
                if (!isset($context['ValidationURL']) && isset($context['DocumentUUID']) && is_string($context['DocumentUUID']) && $context['DocumentUUID'] !== '') {
15✔
NEW
168
                        $context['ValidationURL'] = $this->buildValidationUrl($context['DocumentUUID']);
×
169
                }
170
                if (!isset($context['qrcode']) && isset($context['ValidationURL']) && is_string($context['ValidationURL'])) {
15✔
171
                        $context['qrcode'] = $this->getQrCodeImageBase64($context['ValidationURL']);
1✔
172
                }
173
                try {
174
                        $twigEnvironment = new Environment(
15✔
175
                                new FilesystemLoader(),
15✔
176
                        );
15✔
177
                        $parsed = $twigEnvironment
15✔
178
                                ->createTemplate($template)
15✔
179
                                ->render($context);
15✔
180
                        return [
15✔
181
                                'parsed' => $parsed,
15✔
182
                                'template' => $template,
15✔
183
                                'templateFontSize' => $templateFontSize,
15✔
184
                                'signatureFontSize' => $signatureFontSize,
15✔
185
                                'signatureWidth' => $signatureWidth,
15✔
186
                                'signatureHeight' => $signatureHeight,
15✔
187
                                'renderMode' => $renderMode,
15✔
188
                        ];
15✔
189
                } catch (SyntaxError $e) {
×
190
                        throw new LibresignException((string)preg_replace('/in "[^"]+" at line \d+/', '', $e->getMessage()));
×
191
                }
192
        }
193

194
        public function getTemplate(): string {
195
                if ($this->appConfig->hasKey(Application::APP_ID, 'signature_text_template')) {
10✔
196
                        return $this->appConfig->getValueString(Application::APP_ID, 'signature_text_template');
10✔
197
                }
198
                return $this->getDefaultTemplate();
×
199
        }
200

201
        public function getAvailableVariables(): array {
202
                $list = [
2✔
203
                        '{{DocumentUUID}}' => $this->l10n->t('Unique identifier of the signed document'),
2✔
204
                        '{{IssuerCommonName}}' => $this->l10n->t('Name of the certificate issuer used for the signature.'),
2✔
205
                        '{{LocalSignerSignatureDateOnly}}' => $this->l10n->t('Date when the signer sent the request to sign (without time, in their local time zone).'),
2✔
206
                        '{{LocalSignerSignatureDateTime}}' => $this->l10n->t('Date and time when the signer sent the request to sign (in their local time zone).'),
2✔
207
                        '{{LocalSignerTimezone}}' => $this->l10n->t('Time zone of signer when sent the request to sign (in their local time zone).'),
2✔
208
                        '{{ServerSignatureDate}}' => $this->l10n->t('Date and time when the signature was applied on the server (ISO 8601 format). Can be formatted using the Twig date filter.'),
2✔
209
                        '{{SignerCommonName}}' => $this->l10n->t('Common Name (CN) used to identify the document signer.'),
2✔
210
                        '{{SignerEmail}}' => $this->l10n->t('The signer\'s email is optional and can be left blank.'),
2✔
211
                        '{{SignerIdentifier}}' => $this->l10n->t('Unique information used to identify the signer (such as email, phone number, or username).'),
2✔
212
                        '{{ValidationURL}}' => $this->l10n->t('Validation URL of the signed document.'),
2✔
213
                        // TRANSLATORS This sentence is a description shown in the list of
214
                        // available template variables.
215
                        // Keep placeholder names unchanged: {{ qrcode }} and {{ValidationURL}}.
216
                        // Keep this HTML snippet unchanged:
217
                        // <img src="data:image/png;base64,{{ qrcode }}">
218
                        '{{qrcode}}' => $this->l10n->t('Base64-encoded PNG QR code for the validation URL. In HTML/Twig, use <img src="data:image/png;base64,{{ qrcode }}">. In plain-text templates, use {{ValidationURL}}.'),
2✔
219
                ];
2✔
220
                $collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false);
2✔
221
                if ($collectMetadata) {
2✔
222
                        $list['{{SignerIP}}'] = $this->l10n->t('IP address of the person who signed the document.');
1✔
223
                        $list['{{SignerUserAgent}}'] = $this->l10n->t('Browser and device information of the person who signed the document.');
1✔
224
                }
225
                return $list;
2✔
226
        }
227

228
        private function getQrCodeImageBase64(string $text): string {
229
                $qrCode = new QrCode(
7✔
230
                        data: $text,
7✔
231
                        encoding: new Encoding('UTF-8'),
7✔
232
                        errorCorrectionLevel: ErrorCorrectionLevel::Low,
7✔
233
                        size: self::QRCODE_SIZE,
7✔
234
                        margin: 4,
7✔
235
                        roundBlockSizeMode: RoundBlockSizeMode::Margin,
7✔
236
                        foregroundColor: new Color(0, 0, 0),
7✔
237
                        backgroundColor: new Color(255, 255, 255)
7✔
238
                );
7✔
239

240
                $writer = new PngWriter();
7✔
241
                $result = $writer->write($qrCode);
7✔
242

243
                return base64_encode($result->getString());
7✔
244
        }
245

246
        public function signerNameImage(
247
                string $text,
248
                int $width,
249
                int $height,
250
                string $align = 'center',
251
                float $fontSize = 0,
252
                bool $isDarkTheme = false,
253
                float $scale = 5,
254
        ): string {
255
                if (!extension_loaded('imagick')) {
12✔
256
                        throw new Exception('Extension imagick is not loaded.');
×
257
                }
258
                $width *= $scale;
12✔
259
                $height *= $scale;
12✔
260

261
                $image = new Imagick();
12✔
262
                $image->setResolution(600, 600);
12✔
263
                $image->newImage((int)$width, (int)$height, new ImagickPixel('transparent'));
12✔
264
                $image->setImageFormat('png');
12✔
265

266
                $draw = new ImagickDraw();
12✔
267
                $fonts = Imagick::queryFonts();
12✔
268
                if ($fonts) {
12✔
269
                        $draw->setFont($fonts[0]);
12✔
270
                } else {
271
                        $fallbackFond = __DIR__ . '/../../3rdparty/composer/mpdf/mpdf/ttfonts/DejaVuSerifCondensed.ttf';
×
272
                        if (!file_exists($fallbackFond)) {
×
273
                                $this->logger->error('No fonts available at system, and fallback font not found: ' . $fallbackFond);
×
274
                                throw new LibresignException('No fonts available at system, and fallback font not found: ' . $fallbackFond);
×
275
                        }
276
                        $draw->setFont(__DIR__ . '/../../3rdparty/composer/mpdf/mpdf/ttfonts/DejaVuSerifCondensed.ttf');
×
277
                }
278
                if (!$fontSize) {
12✔
279
                        $fontSize = $this->getSignatureFontSize();
12✔
280
                }
281
                $fontSize *= $scale;
12✔
282
                $draw->setFontSize($fontSize);
12✔
283
                $draw->setFillColor(new ImagickPixel($isDarkTheme ? 'white' : 'black'));
12✔
284
                $align = match ($align) {
12✔
285
                        'left' => Imagick::ALIGN_LEFT,
4✔
286
                        'center' => Imagick::ALIGN_CENTER,
4✔
287
                        'right' => Imagick::ALIGN_RIGHT,
4✔
288
                };
12✔
289
                $draw->setTextAlignment($align);
12✔
290

291
                $maxCharsPerLine = $this->splitAndGetLongestHalfLength($text);
12✔
292
                $wrappedText = $this->mbWordwrap($text, $maxCharsPerLine, "\n", true);
12✔
293

294
                $textMetrics = $image->queryFontMetrics($draw, $wrappedText);
12✔
295
                $lineCount = substr_count($wrappedText, "\n") + 1;
12✔
296
                $y = $this->getCenteredBaselineY($height, $lineCount, $textMetrics['textHeight'], $textMetrics['ascender'], $textMetrics['descender']);
12✔
297

298
                $x = match ($align) {
12✔
299
                        Imagick::ALIGN_LEFT => 0,
4✔
300
                        Imagick::ALIGN_CENTER => $width / 2,
4✔
301
                        Imagick::ALIGN_RIGHT => $width,
4✔
302
                };
12✔
303

304
                $image->annotateImage($draw, $x, $y, 0, $wrappedText);
12✔
305

306
                $blob = $image->getImagesBlob();
12✔
307
                $image->destroy();
12✔
308

309
                return $blob;
12✔
310
        }
311

312
        private function getCenteredBaselineY(
313
                float $canvasHeight,
314
                int $lineCount,
315
                float $lineHeight,
316
                float $ascender,
317
                float $descender,
318
        ): float {
319
                $centerY = $canvasHeight / 2;
12✔
320
                $textBlockHeight = $lineHeight * $lineCount;
12✔
321
                $visualCenterOffset = ($ascender + $descender) / 2;
12✔
322

323
                return $centerY - ($textBlockHeight / 2) + $lineHeight - $visualCenterOffset;
12✔
324
        }
325

326
        private function splitAndGetLongestHalfLength(string $text): int {
327
                $text = trim($text);
38✔
328
                $length = mb_strlen($text);
38✔
329

330
                if ($length === 0) {
38✔
331
                        return 0;
2✔
332
                }
333

334
                $middle = (int)($length / 2);
36✔
335
                $results = [];
36✔
336

337
                foreach (['backward' => -1, 'forward' => 1] as $directionName => $direction) {
36✔
338
                        $index = $middle;
36✔
339

340
                        while (
341
                                $index >= 0
36✔
342
                                && $index < $length
36✔
343
                                && mb_substr($text, $index, 1) !== ' '
36✔
344
                        ) {
345
                                $index += $direction;
27✔
346
                        }
347

348
                        if (
349
                                $index > 0
36✔
350
                                && $index < $length
36✔
351
                                && mb_substr($text, $index, 1) === ' '
36✔
352
                        ) {
353
                                $first = mb_substr($text, 0, $index);
29✔
354
                                $second = mb_substr($text, $index + 1);
29✔
355
                                $results[] = max(mb_strlen($first), mb_strlen($second));
29✔
356
                        }
357
                }
358

359
                return !empty($results) ? max($results) : $length;
36✔
360
        }
361

362
        /**
363
         * Multibyte-safe version of wordwrap
364
         *
365
         * @param string $text The text to wrap
366
         * @param int $width The number of characters at which the string will be wrapped
367
         * @param string $break The line break character
368
         * @param bool $cut If true, words longer than $width will be broken
369
         * @return string The wrapped text
370
         */
371
        private function mbWordwrap(string $text, int $width, string $break = "\n", bool $cut = false): string {
372
                if ($width <= 0) {
12✔
373
                        return $text;
×
374
                }
375

376
                $lines = [];
12✔
377
                $currentLine = '';
12✔
378
                $currentLength = 0;
12✔
379

380
                $paragraphs = explode("\n", $text);
12✔
381

382
                foreach ($paragraphs as $paragraphIndex => $paragraph) {
12✔
383
                        if ($paragraph === '') {
12✔
384
                                if ($currentLength > 0) {
×
385
                                        $lines[] = $currentLine;
×
386
                                        $currentLine = '';
×
387
                                        $currentLength = 0;
×
388
                                }
389
                                $lines[] = '';
×
390
                                continue;
×
391
                        }
392

393
                        $words = explode(' ', $paragraph);
12✔
394

395
                        foreach ($words as $word) {
12✔
396
                                $wordLength = mb_strlen($word);
12✔
397

398
                                if ($cut && $wordLength > $width) {
12✔
399
                                        if ($currentLength > 0) {
×
400
                                                $lines[] = $currentLine;
×
401
                                                $currentLine = '';
×
402
                                                $currentLength = 0;
×
403
                                        }
404

405
                                        while ($wordLength > $width) {
×
406
                                                $lines[] = mb_substr($word, 0, $width);
×
407
                                                $word = mb_substr($word, $width);
×
408
                                                $wordLength = mb_strlen($word);
×
409
                                        }
410

411
                                        if ($wordLength > 0) {
×
412
                                                $currentLine = $word;
×
413
                                                $currentLength = $wordLength;
×
414
                                        }
415
                                        continue;
×
416
                                }
417

418
                                $spaceLength = ($currentLength > 0) ? 1 : 0;
12✔
419
                                if ($currentLength + $spaceLength + $wordLength > $width && $currentLength > 0) {
12✔
420
                                        $lines[] = $currentLine;
11✔
421
                                        $currentLine = $word;
11✔
422
                                        $currentLength = $wordLength;
11✔
423
                                } else {
424
                                        if ($currentLength > 0) {
12✔
425
                                                $currentLine .= ' ';
5✔
426
                                                $currentLength++;
5✔
427
                                        }
428
                                        $currentLine .= $word;
12✔
429
                                        $currentLength += $wordLength;
12✔
430
                                }
431
                        }
432

433
                        if ($currentLength > 0 && $paragraphIndex < count($paragraphs) - 1) {
12✔
434
                                $lines[] = $currentLine;
×
435
                                $currentLine = '';
×
436
                                $currentLength = 0;
×
437
                        }
438
                }
439

440
                if ($currentLength > 0) {
12✔
441
                        $lines[] = $currentLine;
12✔
442
                }
443

444
                return implode($break, $lines);
12✔
445
        }
446

447
        public function getDefaultTemplate(): string {
448
                $collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false);
2✔
449
                if ($collectMetadata) {
2✔
450
                        // TRANSLATORS Variables enclosed in double curly braces {{variableName}} are template placeholders.
451
                        //
452
                        // DO NOT translate or remove these variables:
453
                        // - {{SignerCommonName}}
454
                        // - {{IssuerCommonName}}
455
                        // - {{ServerSignatureDate}}
456
                        // - {{SignerIP}}
457
                        // - {{SignerUserAgent}}
458
                        //
459
                        // Only translate the text outside the curly braces, such as:
460
                        // - "Signed with LibreSign"
461
                        // - "Issuer:"
462
                        // - "Date:"
463
                        // - "IP:"
464
                        // - "User agent:"
465
                        return $this->l10n->t(
1✔
466
                                "Signed with LibreSign\n"
1✔
467
                                . "{{SignerCommonName}}\n"
1✔
468
                                . "Issuer: {{IssuerCommonName}}\n"
1✔
469
                                . "Date: {{ServerSignatureDate}}\n"
1✔
470
                                . "IP: {{SignerIP}}\n"
1✔
471
                                . 'User agent: {{SignerUserAgent}}'
1✔
472
                        );
1✔
473
                }
474
                // TRANSLATORS Variables enclosed in double curly braces {{variableName}} are template placeholders.
475
                //
476
                // DO NOT translate or remove these variables:
477
                // - {{SignerCommonName}}
478
                // - {{IssuerCommonName}}
479
                // - {{ServerSignatureDate}}
480
                //
481
                // Only translate the text outside the curly braces, such as:
482
                // - "Signed with LibreSign"
483
                // - "Issuer:"
484
                // - "Date:"
485
                return $this->l10n->t(
1✔
486
                        "Signed with LibreSign\n"
1✔
487
                        . "{{SignerCommonName}}\n"
1✔
488
                        . "Issuer: {{IssuerCommonName}}\n"
1✔
489
                        . 'Date: {{ServerSignatureDate}}'
1✔
490
                );
1✔
491
        }
492

493
        public function getFullSignatureWidth(): float {
494
                return $this->getSanitizedDimension('signature_width', self::DEFAULT_SIGNATURE_WIDTH);
18✔
495
        }
496

497
        public function getFullSignatureHeight(): float {
498
                return $this->getSanitizedDimension('signature_height', self::DEFAULT_SIGNATURE_HEIGHT);
18✔
499
        }
500

501
        public function getSignatureWidth(): float {
502
                $current = $this->appConfig->getValueFloat(Application::APP_ID, 'signature_width', self::DEFAULT_SIGNATURE_WIDTH);
×
503
                if ($this->getRenderMode() === SignerElementsService::RENDER_MODE_GRAPHIC_ONLY || !$this->getTemplate()) {
×
504
                        return $current;
×
505
                }
506
                return $current / 2;
×
507
        }
508

509
        public function getSignatureHeight(): float {
510
                return $this->getFullSignatureHeight();
×
511
        }
512

513
        private function getSanitizedDimension(string $key, float $default): float {
514
                $value = $this->appConfig->getValueFloat(Application::APP_ID, $key, $default);
18✔
515
                if (!is_finite($value) || $value < self::SIGNATURE_DIMENSION_MINIMUM) {
18✔
516
                        $this->appConfig->setValueFloat(Application::APP_ID, $key, $default);
1✔
517
                        $this->logger->warning('Invalid signature dimension found in app config. Falling back to default.', [
1✔
518
                                'key' => $key,
1✔
519
                                'value' => $value,
1✔
520
                                'default' => $default,
1✔
521
                        ]);
1✔
522
                        return $default;
1✔
523
                }
524
                return $value;
17✔
525
        }
526

527
        public function getTemplateFontSize(): float {
528
                $collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false);
17✔
529
                if ($collectMetadata) {
17✔
530
                        return $this->appConfig->getValueFloat(Application::APP_ID, 'template_font_size', self::TEMPLATE_DEFAULT_FONT_SIZE - 1);
×
531
                }
532
                return $this->appConfig->getValueFloat(Application::APP_ID, 'template_font_size', self::TEMPLATE_DEFAULT_FONT_SIZE);
17✔
533
        }
534

535
        public function getDefaultTemplateFontSize(): float {
536
                $collectMetadata = $this->appConfig->getValueBool(Application::APP_ID, 'collect_metadata', false);
×
537
                if ($collectMetadata) {
×
538
                        return self::TEMPLATE_DEFAULT_FONT_SIZE - 0.2;
×
539
                }
540
                return self::TEMPLATE_DEFAULT_FONT_SIZE;
×
541
        }
542

543
        public function getSignatureFontSize(): float {
544
                return $this->appConfig->getValueFloat(Application::APP_ID, 'signature_font_size', self::SIGNATURE_DEFAULT_FONT_SIZE);
29✔
545
        }
546

547
        public function getRenderMode(): string {
548
                return $this->appConfig->getValueString(Application::APP_ID, 'signature_render_mode', SignerElementsService::RENDER_MODE_DEFAULT);
25✔
549
        }
550

551
        public function isEnabled(): bool {
552
                return !empty($this->getTemplate());
×
553
        }
554

555
        private function buildValidationUrl(string $uuid): string {
556
                $validationSite = trim($this->appConfig->getValueString(Application::APP_ID, 'validation_site', ''));
6✔
557
                if ($validationSite !== '') {
6✔
NEW
558
                        return rtrim($validationSite, '/') . '/' . $uuid;
×
559
                }
560

561
                return $this->urlGenerator->linkToRouteAbsolute('libresign.page.validationFileWithShortUrl', [
6✔
562
                        'uuid' => $uuid,
6✔
563
                ]);
6✔
564
        }
565
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc