151 lines
5.1 KiB
PHP
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();
|
|
}
|
|
}
|