zu fabricjs gewechselt, noch nicht funktionsfähig

This commit is contained in:
Codex Agent
2025-10-31 20:19:09 +01:00
parent 06df61f706
commit eb0c31c90b
33 changed files with 7718 additions and 2062 deletions

View File

@@ -42,7 +42,7 @@ class EventJoinTokenLayoutController extends Controller
abort(404, 'Layout nicht gefunden.');
}
if (! in_array($format, ['pdf', 'svg'], true)) {
if (! in_array($format, ['pdf', 'png'], true)) {
abort(404, 'Unbekanntes Exportformat.');
}
@@ -69,18 +69,15 @@ class EventJoinTokenLayoutController extends Controller
'qrPngDataUri' => $qrPngDataUri,
'backgroundStyle' => $backgroundStyle,
'customization' => $joinToken->metadata['layout_customization'] ?? null,
'advancedLayout' => $this->buildAdvancedLayout(
$layoutConfig,
$joinToken->metadata['layout_customization'] ?? null,
$qrPngDataUri,
$tokenUrl,
$eventName
),
];
$filename = sprintf('%s-%s.%s', Str::slug($eventName ?: 'event'), $layoutConfig['id'], $format);
if ($format === 'svg') {
$svg = view('layouts.join-token.svg', $viewData)->render();
return response($svg)
->header('Content-Type', 'image/svg+xml')
->header('Content-Disposition', 'attachment; filename="'.$filename.'"');
}
$html = view('layouts.join-token.pdf', $viewData)->render();
$options = new Options;
@@ -93,9 +90,46 @@ class EventJoinTokenLayoutController extends Controller
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->render();
return response($dompdf->output())
$pdfBinary = $dompdf->output();
$filenameStem = sprintf('%s-%s', Str::slug($eventName ?: 'event'), $layoutConfig['id']);
if ($format === 'png') {
if (! class_exists(\Imagick::class)) {
abort(500, 'PNG-Export erfordert Imagick.');
}
try {
$imagick = new \Imagick;
$imagick->setResolution(300, 300);
$imagick->setBackgroundColor(new \ImagickPixel('white'));
$imagick->setOption('pdf:use-cropbox', 'true');
$imagick->readImageBlob($pdfBinary);
$imagick->setIteratorIndex(0);
$flattened = $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
$flattened->setImageFormat('png');
$flattened->setImageUnits(\Imagick::RESOLUTION_PIXELSPERINCH);
$flattened->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
$pngBinary = $flattened->getImagesBlob();
$flattened->clear();
$flattened->destroy();
$imagick->clear();
$imagick->destroy();
} catch (\Throwable $exception) {
report($exception);
abort(500, 'PNG-Export konnte nicht erzeugt werden.');
}
return response($pngBinary)
->header('Content-Type', 'image/png')
->header('Content-Disposition', 'attachment; filename="'.$filenameStem.'.png"');
}
return response($pdfBinary)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="'.$filename.'"');
->header('Content-Disposition', 'attachment; filename="'.$filenameStem.'.pdf"');
}
private function ensureBelongsToEvent(Event $event, EventJoinToken $joinToken): void
@@ -182,4 +216,168 @@ class EventJoinTokenLayoutController extends Controller
return $layout['background'] ?? '#FFFFFF';
}
private function buildAdvancedLayout(array $layout, $customization, string $qrPngDataUri, string $tokenUrl, string $eventName): ?array
{
if (! is_array($customization)) {
return null;
}
if (($customization['mode'] ?? null) !== 'advanced') {
return null;
}
$elements = $customization['elements'] ?? null;
if (! is_array($elements) || empty($elements)) {
return null;
}
$width = (int) ($layout['svg']['width'] ?? 1080);
$height = (int) ($layout['svg']['height'] ?? 1520);
$accent = $layout['accent'] ?? '#6366F1';
$text = $layout['text'] ?? '#0F172A';
$secondary = $layout['secondary'] ?? '#1F2937';
$badge = $layout['badge'] ?? $accent;
$resolved = [];
foreach ($elements as $element) {
if (! is_array($element) || empty($element['id']) || empty($element['type'])) {
continue;
}
$type = (string) $element['type'];
$dimensions = $this->normalizeElementDimensions($type, $element, $width, $height);
$content = $this->resolveElementContent($type, $customization, $layout, $eventName, $tokenUrl, $element['content'] ?? null);
$style = [
'position' => 'absolute',
'left' => sprintf('%dpx', $dimensions['x']),
'top' => sprintf('%dpx', $dimensions['y']),
'width' => sprintf('%dpx', $dimensions['width']),
'height' => sprintf('%dpx', $dimensions['height']),
];
if (isset($element['rotation']) && is_numeric($element['rotation']) && (float) $element['rotation'] !== 0.0) {
$style['transform'] = sprintf('rotate(%sdeg)', (float) $element['rotation']);
$style['transform-origin'] = 'center center';
}
$fontSize = isset($element['font_size']) && is_numeric($element['font_size'])
? max(8, min(160, (float) $element['font_size']))
: null;
$lineHeight = isset($element['line_height']) && is_numeric($element['line_height'])
? max(0.5, min(3.0, (float) $element['line_height']))
: null;
$letterSpacing = isset($element['letter_spacing']) && is_numeric($element['letter_spacing'])
? (float) $element['letter_spacing']
: null;
$resolved[] = [
'id' => (string) $element['id'],
'type' => $type,
'content' => $content,
'align' => $element['align'] ?? null,
'font_size' => $fontSize,
'line_height' => $lineHeight,
'letter_spacing' => $letterSpacing,
'font_family' => $element['font_family'] ?? null,
'fill' => $element['fill'] ?? null,
'style' => $style,
'style_string' => $this->styleToString($style),
'width' => $dimensions['width'],
'height' => $dimensions['height'],
'asset' => match ($type) {
'qr' => $qrPngDataUri,
'logo' => $customization['logo_data_url'] ?? $customization['logo_url'] ?? $layout['logo_url'] ?? null,
default => null,
},
];
}
if (empty($resolved)) {
return null;
}
return [
'width' => $width,
'height' => $height,
'background' => $layout['background'] ?? '#FFFFFF',
'background_gradient' => $layout['background_gradient'] ?? null,
'accent' => $accent,
'text' => $text,
'secondary' => $secondary,
'badge' => $badge,
'qr_src' => $qrPngDataUri,
'logo_src' => $customization['logo_data_url'] ?? $customization['logo_url'] ?? $layout['logo_url'] ?? null,
'elements' => $resolved,
];
}
private function resolveElementContent(string $type, array $customization, array $layout, string $eventName, string $tokenUrl, $fallback = null): ?string
{
return match ($type) {
'headline' => $customization['headline'] ?? $eventName,
'subtitle' => $customization['subtitle'] ?? ($layout['subtitle'] ?? ''),
'description' => $customization['description'] ?? ($layout['description'] ?? ''),
'link' => (isset($customization['link_label']) && trim($customization['link_label']) !== '')
? $customization['link_label']
: $tokenUrl,
'badge' => $customization['badge_label'] ?? ($layout['badge_label'] ?? 'Digitale Gästebox'),
'cta' => $customization['cta_label'] ?? ($layout['cta_label'] ?? 'Scan mich & starte direkt'),
default => $fallback !== null ? (string) $fallback : null,
};
}
private function styleToString(array $style): string
{
return implode(';', array_map(
static fn ($key, $value) => sprintf('%s:%s', $key, $value),
array_keys($style),
$style
));
}
private function normalizeElementDimensions(string $type, array $element, int $canvasWidth, int $canvasHeight): array
{
$minDimensions = match ($type) {
'qr' => ['width' => 240, 'height' => 240],
'logo' => ['width' => 140, 'height' => 100],
'badge' => ['width' => 160, 'height' => 60],
'cta' => ['width' => 200, 'height' => 80],
'text' => ['width' => 200, 'height' => 120],
default => ['width' => 160, 'height' => 80],
};
$width = (int) round((float) ($element['width'] ?? $minDimensions['width']));
$height = (int) round((float) ($element['height'] ?? $minDimensions['height']));
$width = max($minDimensions['width'], min($width, $canvasWidth));
$height = max($minDimensions['height'], min($height, $canvasHeight));
if ($type === 'qr') {
$size = (int) round(max($width, $height));
$size = max(240, min($size, min($canvasWidth, $canvasHeight)));
$width = $height = $size;
}
$maxX = max($canvasWidth - $width, 0);
$maxY = max($canvasHeight - $height, 0);
$x = (int) round((float) ($element['x'] ?? 0));
$y = (int) round((float) ($element['y'] ?? 0));
$x = max(0, min($x, $maxX));
$y = max(0, min($y, $maxY));
return [
'x' => $x,
'y' => $y,
'width' => $width,
'height' => $height,
];
}
}