Files
nexus/app/Domains/Unterschrift/Actions/SignDocument/SignDocumentCommand.php
T
2026-07-21 14:35:34 +02:00

151 lines
5.1 KiB
PHP

<?php
namespace App\Domains\Unterschrift\Actions\SignDocument;
use Imagick;
use ImagickPixel;
use RuntimeException;
/**
* imagick-Portierung des falsisign-Ablaufs (sauberer Scan, Modus -c):
* PDF -> Seiten rastern -> Unterschrift auf Zielseite compositen -> zurueck zu PDF.
* Kein shell_exec; nutzt die imagick-Extension (+ ghostscript-Delegate).
*/
class SignDocumentCommand
{
/** Raster-Aufloesung fuer die Ausgabe (analog falsisign density 150). */
private const OUTPUT_DPI = 150;
/** Maximale Seitenzahl, die verarbeitet wird. */
public const MAX_PAGES = 30;
public function __construct(private readonly SignDocumentRequest $request) {}
public function execute(): SignDocumentResponse
{
$signature = $this->prepareOverlay($this->request->signaturPath, 'Unterschrift-Bild');
$stamp = $this->request->stempelPath !== null
? $this->prepareOverlay($this->request->stempelPath, 'Stempel-Bild')
: null;
$doc = new Imagick();
$doc->setResolution(self::OUTPUT_DPI, self::OUTPUT_DPI);
try {
$doc->readImage($this->request->pdfPath);
} catch (\ImagickException $e) {
$signature->clear();
$stamp?->clear();
throw new RuntimeException('Das PDF konnte nicht gelesen werden.', 0, $e);
}
$pageCount = $doc->getNumberImages();
if ($pageCount < 1) {
throw new RuntimeException('Das PDF enthält keine Seiten.');
}
if ($pageCount > self::MAX_PAGES) {
throw new RuntimeException('Das PDF hat zu viele Seiten (max. ' . self::MAX_PAGES . ').');
}
$targetIndex = min($this->request->page, $pageCount) - 1;
for ($i = 0; $i < $pageCount; $i++) {
$doc->setIteratorIndex($i);
// Transparenten PDF-Hintergrund gegen Weiss aufloesen (sonst schwarz).
$doc->setImageBackgroundColor(new ImagickPixel('white'));
if ($doc->getImageAlphaChannel()) {
$doc->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
}
if ($i === $targetIndex) {
// Stempel zuerst, damit die Unterschrift ggf. darueber liegt.
if ($stamp !== null) {
$this->composeOverlay(
$doc,
$stamp,
$this->request->stempelXFrac,
$this->request->stempelYFrac,
$this->request->stempelWidthFrac
);
}
$this->composeOverlay(
$doc,
$signature,
$this->request->xFrac,
$this->request->yFrac,
$this->request->widthFrac
);
}
// Sauberer Scan-Look: Graustufen, aber kein Rauschen/keine Rotation.
if ($this->request->graustufen) {
$doc->transformImageColorspace(Imagick::COLORSPACE_GRAY);
}
// Format pro Seite setzen, damit getImagesBlob() ein mehrseitiges PDF liefert.
$doc->setImageFormat('pdf');
}
$signature->clear();
$stamp?->clear();
$doc->resetIterator();
$pdfContent = $doc->getImagesBlob();
$doc->clear();
return new SignDocumentResponse(
pdfContent: $pdfContent,
filename: $this->request->filename,
);
}
/** Overlay (Unterschrift/Stempel) laden, Weiss transparent machen und auf den Inhalt zuschneiden. */
private function prepareOverlay(string $path, string $label): Imagick
{
$overlay = new Imagick();
try {
$overlay->readImage($path);
} catch (\ImagickException $e) {
throw new RuntimeException('Das ' . $label . ' konnte nicht gelesen werden.', 0, $e);
}
$overlay->setImageFormat('png');
$overlay->setImageAlphaChannel(Imagick::ALPHACHANNEL_ACTIVATE);
$range = $overlay->getQuantumRange()['quantumRangeLong'];
// Weiss (inkl. leicht abweichender Scan-/Foto-Toene) transparent setzen.
$overlay->transparentPaintImage(new ImagickPixel('white'), 0.0, 0.15 * $range, false);
// Ueberfluessigen Rand entfernen, damit die Positionierung exakt ist.
$overlay->trimImage(0.1 * $range);
$overlay->setImagePage(0, 0, 0, 0);
return $overlay;
}
/** Overlay skaliert an der gewuenschten Position auf die aktuelle Seite legen. */
private function composeOverlay(Imagick $doc, Imagick $overlay, float $xFrac, float $yFrac, float $widthFrac): void
{
$pageWidth = $doc->getImageWidth();
$pageHeight = $doc->getImageHeight();
$targetWidth = max(1, (int) round($widthFrac * $pageWidth));
$placed = clone $overlay;
$placed->resizeImage($targetWidth, 0, Imagick::FILTER_LANCZOS, 1);
$x = (int) round($xFrac * $pageWidth);
$y = (int) round($yFrac * $pageHeight);
$doc->compositeImage($placed, Imagick::COMPOSITE_OVER, $x, $y);
$placed->clear();
}
}