Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
32.00% |
32 / 100 |
|
20.00% |
3 / 15 |
CRAP | |
0.00% |
0 / 1 |
| SignatureBackgroundService | |
32.00% |
32 / 100 |
|
20.00% |
3 / 15 |
420.18 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getRootFolder | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| updateImage | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| getSignatureBackgroundType | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
3.07 | |||
| isEnabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| optmizeImage | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
| scaleDimensions | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
| fitWithinBounds | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
5.02 | |||
| delete | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| reset | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| saveSystemBackgroundType | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| getImage | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
| getDefaultImageBlob | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getCustomImageBlob | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| getImagePath | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
| 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 Exception; |
| 12 | use Imagick; |
| 13 | use ImagickPixel; |
| 14 | use OCA\Libresign\Files\TSimpleFile; |
| 15 | use OCA\Libresign\Service\Policy\PolicyService; |
| 16 | use OCA\Libresign\Service\Policy\Provider\SignatureText\SignatureTextPolicy; |
| 17 | use OCA\Libresign\Service\Policy\Provider\SignatureText\SignatureTextPolicyValue; |
| 18 | use OCP\Files\IAppData; |
| 19 | use OCP\Files\NotFoundException; |
| 20 | use OCP\Files\NotPermittedException; |
| 21 | use OCP\Files\SimpleFS\InMemoryFile; |
| 22 | use OCP\Files\SimpleFS\ISimpleFile; |
| 23 | use OCP\Files\SimpleFS\ISimpleFolder; |
| 24 | use OCP\IAppConfig; |
| 25 | use OCP\IConfig; |
| 26 | use OCP\ITempManager; |
| 27 | |
| 28 | class SignatureBackgroundService { |
| 29 | use TSimpleFile; |
| 30 | public const SCALE_FACTOR = 3; |
| 31 | |
| 32 | public function __construct( |
| 33 | private IAppData $appData, |
| 34 | private IAppConfig $appConfig, |
| 35 | private IConfig $config, |
| 36 | private ITempManager $tempManager, |
| 37 | private SignatureTextService $signatureTextService, |
| 38 | private PolicyService $policyService, |
| 39 | ) { |
| 40 | } |
| 41 | |
| 42 | private function getRootFolder(): ISimpleFolder { |
| 43 | try { |
| 44 | return $this->appData->getFolder('signature'); |
| 45 | } catch (NotFoundException) { |
| 46 | return $this->appData->newFolder('signature'); |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | public function updateImage(string $tmpFile): void { |
| 51 | $folder = $this->getRootFolder(); |
| 52 | $detectedMimeType = mime_content_type($tmpFile); |
| 53 | if (!in_array($detectedMimeType, ['image/png'], true)) { |
| 54 | throw new \Exception('Unsupported image type: ' . $detectedMimeType); |
| 55 | } |
| 56 | |
| 57 | $content = $this->optmizeImage(file_get_contents($tmpFile)); |
| 58 | |
| 59 | $this->saveSystemBackgroundType('custom'); |
| 60 | $target = $folder->newFile('background.png'); |
| 61 | $target->putContent($content); |
| 62 | } |
| 63 | |
| 64 | public function getSignatureBackgroundType(): string { |
| 65 | $stampValue = SignatureTextPolicyValue::normalize( |
| 66 | $this->policyService->resolve(SignatureTextPolicy::KEY)->getEffectiveValue(), |
| 67 | ); |
| 68 | $value = $stampValue['background_type'] ?? 'default'; |
| 69 | if (!is_string($value)) { |
| 70 | return 'default'; |
| 71 | } |
| 72 | |
| 73 | $normalized = trim(strtolower($value)); |
| 74 | if (!in_array($normalized, ['default', 'custom', 'deleted'], true)) { |
| 75 | return 'default'; |
| 76 | } |
| 77 | |
| 78 | return $normalized; |
| 79 | } |
| 80 | |
| 81 | public function isEnabled(): bool { |
| 82 | return $this->getSignatureBackgroundType() !== 'deleted'; |
| 83 | } |
| 84 | |
| 85 | private function optmizeImage(string $content, float $opacity = 1): string { |
| 86 | if (!extension_loaded('imagick')) { |
| 87 | throw new Exception('Extension imagick is not loaded.'); |
| 88 | } |
| 89 | $image = new Imagick(); |
| 90 | $image->setBackgroundColor(new ImagickPixel('transparent')); |
| 91 | $image->readImageBlob($content); |
| 92 | $width = $image->getImageWidth(); |
| 93 | $height = $image->getImageHeight(); |
| 94 | $dimensions = $this->scaleDimensions($width, $height); |
| 95 | if ($dimensions['width'] === $width && $dimensions['height'] === $height && $image->readImageBlob($content) === 'PNG') { |
| 96 | return $content; |
| 97 | } |
| 98 | $image->setImageResolution(300, 300); |
| 99 | $image->resampleImage(300, 300, Imagick::FILTER_LANCZOS, 1); |
| 100 | $image->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE); |
| 101 | $image->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity, Imagick::CHANNEL_ALPHA); |
| 102 | $image->setImageFormat('png'); |
| 103 | $image->resizeImage($dimensions['width'], $dimensions['height'], Imagick::FILTER_LANCZOS, 1); |
| 104 | return $image->getImageBlob(); |
| 105 | } |
| 106 | |
| 107 | private function scaleDimensions(int $width, int $height): array { |
| 108 | $signatureWidth = $this->signatureTextService->getFullSignatureWidth(); |
| 109 | $signatureHeight = $this->signatureTextService->getFullSignatureHeight(); |
| 110 | |
| 111 | $maxWidth = $signatureWidth * self::SCALE_FACTOR; |
| 112 | $maxHeight = $signatureHeight * self::SCALE_FACTOR; |
| 113 | |
| 114 | if ($width <= $maxWidth && $height <= $maxHeight) { |
| 115 | return ['width' => $width, 'height' => $height]; |
| 116 | } |
| 117 | |
| 118 | $widthRatio = $maxWidth / $width; |
| 119 | $heightRatio = $maxHeight / $height; |
| 120 | $scale = min($widthRatio, $heightRatio); |
| 121 | |
| 122 | $returnWidth = (int)floor($width * $scale); |
| 123 | $returnHeight = (int)floor($height * $scale); |
| 124 | |
| 125 | return ['width' => $returnWidth, 'height' => $returnHeight]; |
| 126 | } |
| 127 | |
| 128 | /** |
| 129 | * @return array{width: int, height: int, x: int, y: int} |
| 130 | */ |
| 131 | private function fitWithinBounds(int $width, int $height, int $maxWidth, int $maxHeight): array { |
| 132 | if ($width <= 0 || $height <= 0 || $maxWidth <= 0 || $maxHeight <= 0) { |
| 133 | return ['width' => 0, 'height' => 0, 'x' => 0, 'y' => 0]; |
| 134 | } |
| 135 | |
| 136 | $scale = min($maxWidth / $width, $maxHeight / $height); |
| 137 | $fitWidth = max(1, (int)floor($width * $scale)); |
| 138 | $fitHeight = max(1, (int)floor($height * $scale)); |
| 139 | |
| 140 | return [ |
| 141 | 'width' => $fitWidth, |
| 142 | 'height' => $fitHeight, |
| 143 | 'x' => (int)floor(max(0, $maxWidth - $fitWidth) / 2), |
| 144 | 'y' => (int)floor(max(0, $maxHeight - $fitHeight) / 2), |
| 145 | ]; |
| 146 | } |
| 147 | |
| 148 | public function delete(): void { |
| 149 | try { |
| 150 | $this->saveSystemBackgroundType('deleted'); |
| 151 | $file = $this->getRootFolder()->getFile('background.png'); |
| 152 | $file->delete(); |
| 153 | } catch (NotFoundException|NotPermittedException) { |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | public function reset(): void { |
| 158 | try { |
| 159 | $this->saveSystemBackgroundType('default'); |
| 160 | $file = $this->getRootFolder()->getFile('background.png'); |
| 161 | $file->delete(); |
| 162 | } catch (NotFoundException|NotPermittedException) { |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | private function saveSystemBackgroundType(string $value): void { |
| 167 | $stampValue = SignatureTextPolicyValue::normalize( |
| 168 | $this->policyService->resolve(SignatureTextPolicy::KEY)->getEffectiveValue(), |
| 169 | ); |
| 170 | $stampValue['background_type'] = $value; |
| 171 | |
| 172 | $allowChildOverride = $this->policyService->getSystemPolicy(SignatureTextPolicy::KEY)?->isAllowChildOverride() ?? false; |
| 173 | $this->policyService->saveSystem( |
| 174 | SignatureTextPolicy::KEY, |
| 175 | SignatureTextPolicyValue::encode($stampValue), |
| 176 | $allowChildOverride, |
| 177 | ); |
| 178 | } |
| 179 | |
| 180 | public function getImage(): ISimpleFile { |
| 181 | try { |
| 182 | $file = $this->getRootFolder()->getFile('background.png'); |
| 183 | } catch (NotFoundException) { |
| 184 | $content = $this->getDefaultImageBlob(); |
| 185 | $file = new InMemoryFile('background.png', $content); |
| 186 | } |
| 187 | return $file; |
| 188 | } |
| 189 | |
| 190 | public function getDefaultImageBlob(): string { |
| 191 | return $this->optmizeImage(file_get_contents(__DIR__ . '/../../img/logo-gray.svg'), 0.15); |
| 192 | } |
| 193 | |
| 194 | public function getCustomImageBlob(): ?string { |
| 195 | try { |
| 196 | return $this->getRootFolder()->getFile('background.png')->getContent(); |
| 197 | } catch (NotFoundException) { |
| 198 | return null; |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | public function getImagePath(): string { |
| 203 | try { |
| 204 | $filePath = $this->getRootFolder()->getFile('background.png'); |
| 205 | $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/'); |
| 206 | return $dataDir . '/' . $this->getInternalPathOfFile($filePath); |
| 207 | } catch (NotFoundException) { |
| 208 | $content = $this->optmizeImage(file_get_contents(__DIR__ . '/../../img/logo-gray.svg'), 0.3); |
| 209 | $filePath = $this->tempManager->getTemporaryFile('.png'); |
| 210 | if (!$filePath) { |
| 211 | throw new Exception('Imposible to write temporary file at temporary folder'); |
| 212 | } |
| 213 | file_put_contents($filePath, $content); |
| 214 | } |
| 215 | return $filePath; |
| 216 | } |
| 217 | } |