|
|
@@ -0,0 +1,436 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+namespace App\Mail;
|
|
|
+
|
|
|
+use App\App\Bootstrap;
|
|
|
+use App\Form\FormSchema;
|
|
|
+use setasign\Fpdi\Tcpdf\Fpdi;
|
|
|
+use TCPDF;
|
|
|
+
|
|
|
+final class PdfGenerator
|
|
|
+{
|
|
|
+ private SubmissionFormatter $formatter;
|
|
|
+ private FormSchema $schema;
|
|
|
+ private string $uploadBasePath;
|
|
|
+
|
|
|
+ public function __construct(SubmissionFormatter $formatter, FormSchema $schema)
|
|
|
+ {
|
|
|
+ $this->formatter = $formatter;
|
|
|
+ $this->schema = $schema;
|
|
|
+
|
|
|
+ $app = Bootstrap::config('app');
|
|
|
+ $this->uploadBasePath = rtrim((string) ($app['storage']['uploads'] ?? ''), '/');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Generate PDF with form data and portrait photo.
|
|
|
+ *
|
|
|
+ * @param array<string, mixed> $submission
|
|
|
+ * @return string|null Path to temp PDF file, or null on failure.
|
|
|
+ */
|
|
|
+ public function generateFormDataPdf(array $submission): ?string
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8');
|
|
|
+ $this->configurePdf($pdf, 'Mitgliedsantrag');
|
|
|
+ $pdf->AddPage();
|
|
|
+
|
|
|
+ $formData = (array) ($submission['form_data'] ?? []);
|
|
|
+ $uploads = (array) ($submission['uploads'] ?? []);
|
|
|
+ $email = (string) ($submission['email'] ?? '');
|
|
|
+ $submittedAt = $this->formatDateTime((string) ($submission['submitted_at'] ?? ''));
|
|
|
+ $name = trim(($formData['vorname'] ?? '') . ' ' . ($formData['nachname'] ?? ''));
|
|
|
+
|
|
|
+ $this->renderFormDataHeader($pdf, $name, $email, $submittedAt);
|
|
|
+ $this->renderPortrait($pdf, $uploads);
|
|
|
+ $this->renderFormDataSections($pdf, $formData);
|
|
|
+
|
|
|
+ $tmpPath = $this->tempPath('antragsdaten');
|
|
|
+ $pdf->Output($tmpPath, 'F');
|
|
|
+ return $tmpPath;
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Bootstrap::log('mail', 'PDF-Erstellung (Antragsdaten) fehlgeschlagen: ' . $e->getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Generate PDF combining all attachments except the portrait photo.
|
|
|
+ *
|
|
|
+ * @param array<string, mixed> $submission
|
|
|
+ * @return string|null Path to temp PDF file, or null if no attachments or on failure.
|
|
|
+ */
|
|
|
+ public function generateAttachmentsPdf(array $submission): ?string
|
|
|
+ {
|
|
|
+ $uploads = (array) ($submission['uploads'] ?? []);
|
|
|
+ $formData = (array) ($submission['form_data'] ?? []);
|
|
|
+ $allFields = $this->schema->getAllFields();
|
|
|
+
|
|
|
+ $attachments = [];
|
|
|
+ foreach ($uploads as $fieldKey => $files) {
|
|
|
+ if ($fieldKey === 'portraitfoto' || !is_array($files)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $label = (string) ($allFields[$fieldKey]['label'] ?? $fieldKey);
|
|
|
+ foreach ($files as $file) {
|
|
|
+ if (!is_array($file)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $relativePath = (string) ($file['relative_path'] ?? '');
|
|
|
+ $absolutePath = $this->uploadBasePath . '/' . $relativePath;
|
|
|
+ if ($relativePath !== '' && is_file($absolutePath)) {
|
|
|
+ $attachments[] = [
|
|
|
+ 'path' => $absolutePath,
|
|
|
+ 'filename' => (string) ($file['original_filename'] ?? 'Datei'),
|
|
|
+ 'mime' => (string) ($file['mime'] ?? ''),
|
|
|
+ 'label' => $label,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($attachments === []) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $pdf = new Fpdi('P', 'mm', 'A4', true, 'UTF-8');
|
|
|
+ $this->configurePdf($pdf, 'Anlagen zum Mitgliedsantrag');
|
|
|
+
|
|
|
+ $name = trim(($formData['vorname'] ?? '') . ' ' . ($formData['nachname'] ?? ''));
|
|
|
+ $this->renderAttachmentsTitlePage($pdf, $name, $attachments);
|
|
|
+
|
|
|
+ foreach ($attachments as $att) {
|
|
|
+ $this->addAttachmentToPdf($pdf, $att);
|
|
|
+ }
|
|
|
+
|
|
|
+ $tmpPath = $this->tempPath('anlagen');
|
|
|
+ $pdf->Output($tmpPath, 'F');
|
|
|
+ return $tmpPath;
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Bootstrap::log('mail', 'PDF-Erstellung (Anlagen) fehlgeschlagen: ' . $e->getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function configurePdf(TCPDF $pdf, string $title): void
|
|
|
+ {
|
|
|
+ $pdf->setPrintHeader(false);
|
|
|
+ $pdf->setPrintFooter(true);
|
|
|
+ $pdf->SetCreator('Feuerwehr Freising - Mitgliedsantrag');
|
|
|
+ $pdf->SetAuthor('Feuerwehr Freising');
|
|
|
+ $pdf->SetTitle($title);
|
|
|
+ $pdf->SetAutoPageBreak(true, 20);
|
|
|
+ $pdf->SetMargins(15, 15, 15);
|
|
|
+ $pdf->SetFont('helvetica', '', 10);
|
|
|
+
|
|
|
+ $pdf->setFooterData(['0', '0', '0'], ['200', '200', '200']);
|
|
|
+ $pdf->setFooterFont(['helvetica', '', 8]);
|
|
|
+ $pdf->setFooterMargin(10);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function renderFormDataHeader(TCPDF $pdf, string $name, string $email, string $submittedAt): void
|
|
|
+ {
|
|
|
+ $pdf->SetFont('helvetica', 'B', 16);
|
|
|
+ $pdf->Cell(0, 10, 'Mitgliedsantrag', 0, 1, 'L');
|
|
|
+ $pdf->SetFont('helvetica', '', 11);
|
|
|
+ $pdf->Cell(0, 6, 'Freiwillige Feuerwehr Freising e.V.', 0, 1, 'L');
|
|
|
+ $pdf->Ln(4);
|
|
|
+
|
|
|
+ $pdf->SetFont('helvetica', '', 10);
|
|
|
+ $pdf->SetFillColor(245, 245, 245);
|
|
|
+
|
|
|
+ $metaHtml = '<table cellpadding="4" style="font-size:10px;">';
|
|
|
+ $metaHtml .= '<tr><td style="width:140px;font-weight:bold;">Name:</td><td>' . $this->esc($name) . '</td></tr>';
|
|
|
+ $metaHtml .= '<tr><td style="font-weight:bold;">E-Mail:</td><td>' . $this->esc($email) . '</td></tr>';
|
|
|
+ $metaHtml .= '<tr><td style="font-weight:bold;">Eingereicht am:</td><td>' . $this->esc($submittedAt) . '</td></tr>';
|
|
|
+ $metaHtml .= '</table>';
|
|
|
+
|
|
|
+ $pdf->writeHTML($metaHtml, true, false, true, false, '');
|
|
|
+ $pdf->Ln(2);
|
|
|
+ $pdf->SetDrawColor(180, 0, 0);
|
|
|
+ $pdf->Line(15, $pdf->GetY(), 195, $pdf->GetY());
|
|
|
+ $pdf->SetDrawColor(0, 0, 0);
|
|
|
+ $pdf->Ln(4);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array<string, mixed> $uploads
|
|
|
+ */
|
|
|
+ private function renderPortrait(TCPDF $pdf, array $uploads): void
|
|
|
+ {
|
|
|
+ $portraitFiles = $uploads['portraitfoto'] ?? [];
|
|
|
+ if (!is_array($portraitFiles) || empty($portraitFiles)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $file = $portraitFiles[0];
|
|
|
+ if (!is_array($file)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $relativePath = (string) ($file['relative_path'] ?? '');
|
|
|
+ $absolutePath = $this->uploadBasePath . '/' . $relativePath;
|
|
|
+
|
|
|
+ if ($relativePath === '' || !is_file($absolutePath)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $mime = (string) ($file['mime'] ?? '');
|
|
|
+ $imagePath = $this->resolveImageForPdf($absolutePath, $mime);
|
|
|
+
|
|
|
+ if ($imagePath === null) {
|
|
|
+ $pdf->SetFont('helvetica', 'I', 9);
|
|
|
+ $pdf->Cell(0, 6, 'Portraitfoto: ' . ($file['original_filename'] ?? 'Datei') . ' (WebP-Format, siehe digitale Unterlagen)', 0, 1);
|
|
|
+ $pdf->SetFont('helvetica', '', 10);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $startY = $pdf->GetY();
|
|
|
+ $pdf->Image($imagePath, 150, $startY, 35, 0, '', '', '', true, 150, '', false, false, 0, 'CT');
|
|
|
+
|
|
|
+ if ($imagePath !== $absolutePath && is_file($imagePath)) {
|
|
|
+ @unlink($imagePath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array<string, mixed> $formData
|
|
|
+ */
|
|
|
+ private function renderFormDataSections(TCPDF $pdf, array $formData): void
|
|
|
+ {
|
|
|
+ $steps = $this->formatter->getFormattedSteps($formData);
|
|
|
+
|
|
|
+ foreach ($steps as $step) {
|
|
|
+ if ($pdf->GetY() > 250) {
|
|
|
+ $pdf->AddPage();
|
|
|
+ }
|
|
|
+
|
|
|
+ $pdf->SetFont('helvetica', 'B', 12);
|
|
|
+ $pdf->SetFillColor(180, 0, 0);
|
|
|
+ $pdf->SetTextColor(255, 255, 255);
|
|
|
+ $pdf->Cell(0, 8, ' ' . $step['title'], 0, 1, 'L', true);
|
|
|
+ $pdf->SetTextColor(0, 0, 0);
|
|
|
+ $pdf->Ln(2);
|
|
|
+
|
|
|
+ $pdf->SetFont('helvetica', '', 10);
|
|
|
+
|
|
|
+ foreach ($step['fields'] as $field) {
|
|
|
+ if ($pdf->GetY() > 265) {
|
|
|
+ $pdf->AddPage();
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($field['type'] === 'table') {
|
|
|
+ $pdf->SetFont('helvetica', 'B', 10);
|
|
|
+ $pdf->Cell(0, 6, $field['label'], 0, 1, 'L');
|
|
|
+ $pdf->SetFont('helvetica', '', 9);
|
|
|
+ $pdf->MultiCell(0, 5, $field['value'], 0, 'L');
|
|
|
+ $pdf->Ln(1);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $labelWidth = 80;
|
|
|
+ $pdf->SetFont('helvetica', 'B', 9);
|
|
|
+ $startX = $pdf->GetX();
|
|
|
+ $startY = $pdf->GetY();
|
|
|
+ $pdf->MultiCell($labelWidth, 5, $field['label'] . ':', 0, 'L', false, 0);
|
|
|
+ $pdf->SetFont('helvetica', '', 9);
|
|
|
+ $pdf->SetXY($startX + $labelWidth, $startY);
|
|
|
+ $pdf->MultiCell(0, 5, $field['value'], 0, 'L');
|
|
|
+ $pdf->Ln(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ $pdf->Ln(3);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array<int, array{path: string, filename: string, mime: string, label: string}> $attachments
|
|
|
+ */
|
|
|
+ private function renderAttachmentsTitlePage(Fpdi $pdf, string $name, array $attachments): void
|
|
|
+ {
|
|
|
+ $pdf->AddPage();
|
|
|
+
|
|
|
+ $pdf->SetFont('helvetica', 'B', 16);
|
|
|
+ $pdf->Cell(0, 10, 'Anlagen zum Mitgliedsantrag', 0, 1, 'L');
|
|
|
+ $pdf->SetFont('helvetica', '', 11);
|
|
|
+ $pdf->Cell(0, 6, 'Freiwillige Feuerwehr Freising e.V.', 0, 1, 'L');
|
|
|
+
|
|
|
+ if ($name !== '') {
|
|
|
+ $pdf->Ln(2);
|
|
|
+ $pdf->SetFont('helvetica', '', 10);
|
|
|
+ $pdf->Cell(0, 6, 'Antragsteller: ' . $name, 0, 1, 'L');
|
|
|
+ }
|
|
|
+
|
|
|
+ $pdf->Ln(6);
|
|
|
+ $pdf->SetDrawColor(180, 0, 0);
|
|
|
+ $pdf->Line(15, $pdf->GetY(), 195, $pdf->GetY());
|
|
|
+ $pdf->SetDrawColor(0, 0, 0);
|
|
|
+ $pdf->Ln(6);
|
|
|
+
|
|
|
+ $pdf->SetFont('helvetica', 'B', 11);
|
|
|
+ $pdf->Cell(0, 6, 'Enthaltene Dokumente:', 0, 1, 'L');
|
|
|
+ $pdf->Ln(2);
|
|
|
+
|
|
|
+ $pdf->SetFont('helvetica', '', 10);
|
|
|
+ $index = 1;
|
|
|
+ foreach ($attachments as $att) {
|
|
|
+ $pdf->Cell(10, 6, (string) $index . '.', 0, 0, 'R');
|
|
|
+ $pdf->Cell(0, 6, $att['label'] . ': ' . $att['filename'], 0, 1, 'L');
|
|
|
+ $index++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array{path: string, filename: string, mime: string, label: string} $att
|
|
|
+ */
|
|
|
+ private function addAttachmentToPdf(Fpdi $pdf, array $att): void
|
|
|
+ {
|
|
|
+ $mime = $att['mime'];
|
|
|
+ $path = $att['path'];
|
|
|
+
|
|
|
+ if ($mime === 'application/pdf') {
|
|
|
+ $this->importPdfPages($pdf, $path, $att['filename'], $att['label']);
|
|
|
+ } elseif (str_starts_with($mime, 'image/')) {
|
|
|
+ $this->addImagePage($pdf, $path, $mime, $att['filename'], $att['label']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function importPdfPages(Fpdi $pdf, string $pdfPath, string $filename, string $label): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $pageCount = $pdf->setSourceFile($pdfPath);
|
|
|
+ for ($i = 1; $i <= $pageCount; $i++) {
|
|
|
+ $tplId = $pdf->importPage($i);
|
|
|
+ $size = $pdf->getTemplateSize($tplId);
|
|
|
+ $orientation = ($size['width'] > $size['height']) ? 'L' : 'P';
|
|
|
+ $pdf->AddPage($orientation);
|
|
|
+
|
|
|
+ if ($i === 1) {
|
|
|
+ $this->renderAttachmentLabel($pdf, $label, $filename);
|
|
|
+ }
|
|
|
+
|
|
|
+ $availableHeight = $pdf->getPageHeight() - $pdf->GetY() - 20;
|
|
|
+ $availableWidth = $pdf->getPageWidth() - 30;
|
|
|
+
|
|
|
+ $scale = min(
|
|
|
+ $availableWidth / $size['width'],
|
|
|
+ $availableHeight / $size['height'],
|
|
|
+ 1.0
|
|
|
+ );
|
|
|
+
|
|
|
+ $renderWidth = $size['width'] * $scale;
|
|
|
+ $pdf->useTemplate($tplId, 15, $pdf->GetY(), $renderWidth);
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $pdf->AddPage();
|
|
|
+ $this->renderAttachmentLabel($pdf, $label, $filename);
|
|
|
+ $pdf->SetFont('helvetica', 'I', 10);
|
|
|
+ $pdf->Cell(0, 8, 'PDF konnte nicht eingebettet werden.', 0, 1, 'L');
|
|
|
+ Bootstrap::log('mail', 'PDF-Import fehlgeschlagen für ' . $pdfPath . ': ' . $e->getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function addImagePage(Fpdi|TCPDF $pdf, string $imagePath, string $mime, string $filename, string $label): void
|
|
|
+ {
|
|
|
+ $resolvedPath = $this->resolveImageForPdf($imagePath, $mime);
|
|
|
+
|
|
|
+ $pdf->AddPage();
|
|
|
+ $this->renderAttachmentLabel($pdf, $label, $filename);
|
|
|
+
|
|
|
+ if ($resolvedPath === null) {
|
|
|
+ $pdf->SetFont('helvetica', 'I', 10);
|
|
|
+ $pdf->Cell(0, 8, 'Bild im WebP-Format — siehe digitale Unterlagen (' . $filename . ')', 0, 1, 'L');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $startY = $pdf->GetY();
|
|
|
+ $availableHeight = $pdf->getPageHeight() - $startY - 20;
|
|
|
+ $availableWidth = $pdf->getPageWidth() - 30;
|
|
|
+
|
|
|
+ $pdf->Image(
|
|
|
+ $resolvedPath,
|
|
|
+ 15,
|
|
|
+ $startY,
|
|
|
+ $availableWidth,
|
|
|
+ $availableHeight,
|
|
|
+ '',
|
|
|
+ '',
|
|
|
+ '',
|
|
|
+ true,
|
|
|
+ 150,
|
|
|
+ '',
|
|
|
+ false,
|
|
|
+ false,
|
|
|
+ 0,
|
|
|
+ 'CT'
|
|
|
+ );
|
|
|
+
|
|
|
+ if ($resolvedPath !== $imagePath && is_file($resolvedPath)) {
|
|
|
+ @unlink($resolvedPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function renderAttachmentLabel(TCPDF $pdf, string $label, string $filename): void
|
|
|
+ {
|
|
|
+ $pdf->SetFont('helvetica', 'B', 9);
|
|
|
+ $pdf->SetFillColor(245, 245, 245);
|
|
|
+ $pdf->Cell(0, 6, ' ' . $label . ': ' . $filename, 0, 1, 'L', true);
|
|
|
+ $pdf->Ln(2);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Resolve an image path for TCPDF. TCPDF handles JPEG and PNG natively.
|
|
|
+ * WebP needs GD conversion. Returns null if conversion is not possible.
|
|
|
+ */
|
|
|
+ private function resolveImageForPdf(string $absolutePath, string $mime): ?string
|
|
|
+ {
|
|
|
+ if ($mime !== 'image/webp') {
|
|
|
+ return $absolutePath;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!function_exists('imagecreatefromwebp') || !function_exists('imagejpeg')) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $image = @imagecreatefromwebp($absolutePath);
|
|
|
+ if ($image === false) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $tmpPath = sys_get_temp_dir() . '/antrag_webp_' . bin2hex(random_bytes(8)) . '.jpg';
|
|
|
+ $ok = @imagejpeg($image, $tmpPath, 90);
|
|
|
+ imagedestroy($image);
|
|
|
+
|
|
|
+ return $ok ? $tmpPath : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function formatDateTime(string $isoDate): string
|
|
|
+ {
|
|
|
+ if ($isoDate === '') {
|
|
|
+ return '---';
|
|
|
+ }
|
|
|
+
|
|
|
+ $dt = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $isoDate);
|
|
|
+ if (!$dt) {
|
|
|
+ $dt = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:sP', $isoDate);
|
|
|
+ }
|
|
|
+ if (!$dt) {
|
|
|
+ return $isoDate;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $dt->format('d.m.Y, H:i') . ' Uhr';
|
|
|
+ }
|
|
|
+
|
|
|
+ private function tempPath(string $prefix): string
|
|
|
+ {
|
|
|
+ return sys_get_temp_dir() . '/antrag_' . $prefix . '_' . bin2hex(random_bytes(8)) . '.pdf';
|
|
|
+ }
|
|
|
+
|
|
|
+ private function esc(string $value): string
|
|
|
+ {
|
|
|
+ return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
|
|
+ }
|
|
|
+}
|