feat(qr): QR-Code-Generator mit Farben, Logo und Live-Vorschau
Erzeugt PNG und SVG ueber endroid/qr-code. Die eingestellte Groesse gilt fuer das fertige Bild inklusive Quiet Zone; der Rand wird in Modulen angegeben und ueber die Matrix in Pixel umgerechnet. Kontrastwarnung nach WCAG, zusaetzlich Hinweis auf invertierte Codes - die erkennen viele Scanner nicht. Mit Logo wird die Fehlerkorrektur zwingend auf Level H gesetzt. Uploads werden gegen GD geprueft (getimagesize allein akzeptiert auch beschaedigte Dateien), in der Groesse und den Abmessungen begrenzt und ausserhalb des Webroots abgelegt. SVGs werden vor dem Speichern von Skripten, Event-Handlern, Entities und externen Referenzen befreit.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* Hex-Farben, Kontrastberechnung (WCAG) fuer die QR-Code-Warnung.
|
||||
*/
|
||||
final class Color
|
||||
{
|
||||
/**
|
||||
* Empfohlenes Mindest-Kontrastverhaeltnis fuer zuverlaessig
|
||||
* scannbare QR-Codes.
|
||||
*/
|
||||
public const MIN_CONTRAST_RATIO = 4.0;
|
||||
|
||||
public static function isValidHex(string $hex): bool
|
||||
{
|
||||
return (bool) preg_match('/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', trim($hex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisiert auf "#rrggbb" (Kleinbuchstaben). Ungueltige Werte
|
||||
* ergeben den Fallback.
|
||||
*/
|
||||
public static function normalizeHex(string $hex, string $fallback = '#000000'): string
|
||||
{
|
||||
$hex = strtolower(trim($hex));
|
||||
if (!self::isValidHex($hex)) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$hex = ltrim($hex, '#');
|
||||
if (strlen($hex) === 3) {
|
||||
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
||||
}
|
||||
|
||||
return '#' . $hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:int,1:int,2:int} r, g, b
|
||||
*/
|
||||
public static function toRgb(string $hex): array
|
||||
{
|
||||
$hex = ltrim(self::normalizeHex($hex), '#');
|
||||
|
||||
return [
|
||||
(int) hexdec(substr($hex, 0, 2)),
|
||||
(int) hexdec(substr($hex, 2, 2)),
|
||||
(int) hexdec(substr($hex, 4, 2)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative Luminanz nach WCAG 2.1.
|
||||
*/
|
||||
public static function luminance(string $hex): float
|
||||
{
|
||||
$channels = array_map(static function (int $value): float {
|
||||
$c = $value / 255;
|
||||
|
||||
return $c <= 0.03928 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4;
|
||||
}, self::toRgb($hex));
|
||||
|
||||
return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Kontrastverhaeltnis zwischen 1.0 und 21.0.
|
||||
*/
|
||||
public static function contrastRatio(string $foreground, string $background): float
|
||||
{
|
||||
$l1 = self::luminance($foreground);
|
||||
$l2 = self::luminance($background);
|
||||
|
||||
$lighter = max($l1, $l2);
|
||||
$darker = min($l1, $l2);
|
||||
|
||||
return round(($lighter + 0.05) / ($darker + 0.05), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* QR-Codes brauchen zusaetzlich einen dunklen Vordergrund auf hellem
|
||||
* Grund - invertierte Codes werden von vielen Scannern nicht erkannt.
|
||||
*/
|
||||
public static function isInverted(string $foreground, string $background): bool
|
||||
{
|
||||
return self::luminance($foreground) > self::luminance($background);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert eine Warnung oder null, wenn die Kombination unbedenklich ist.
|
||||
*/
|
||||
public static function qrWarning(string $foreground, string $background): ?string
|
||||
{
|
||||
$ratio = self::contrastRatio($foreground, $background);
|
||||
|
||||
if ($ratio < self::MIN_CONTRAST_RATIO) {
|
||||
return sprintf(
|
||||
'Zu geringer Kontrast (%.1f:1, empfohlen mindestens %.1f:1). Der QR-Code ist moeglicherweise nicht zuverlaessig scannbar.',
|
||||
$ratio,
|
||||
self::MIN_CONTRAST_RATIO,
|
||||
);
|
||||
}
|
||||
|
||||
if (self::isInverted($foreground, $background)) {
|
||||
return 'Heller Vordergrund auf dunklem Hintergrund: viele Scanner erkennen invertierte QR-Codes nicht. Besser dunkle Module auf hellem Grund.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user