$style * @return array */ public static function normalize(array $style): array { $style = self::ensureContrast($style); $style = self::ensureQuietZone($style); $style = self::ensureRenderScale($style); $style['error_correction'] = QrStyleDefaults::recommendedEcc($style); return $style; } /** @param array $style */ private static function ensureContrast(array $style): array { $target = 4.5; $attempts = 0; while (QrScanReliability::contrastRatio($style) < $target && $attempts < 16) { $fg = self::hexToRgb((string) $style['foreground']); $bg = self::hexToRgb((string) $style['background']); if (self::relativeLuminance($fg) > self::relativeLuminance($bg)) { $style['foreground'] = self::rgbToHex(self::darkenToward($fg, 0.18)); } else { $style['foreground'] = self::rgbToHex(self::darkenToward($fg, 0.15)); $style['background'] = self::rgbToHex(self::lightenToward($bg, 0.15)); } $attempts++; } if (QrScanReliability::contrastRatio($style) < $target) { $style['foreground'] = '#000000'; $style['background'] = '#ffffff'; } return $style; } /** @param array $style */ private static function ensureQuietZone(array $style): array { $margin = (int) ($style['margin'] ?? 4); $moduleStyle = (string) ($style['module_style'] ?? 'square'); if (QrModuleStyleCatalog::isDecorative($moduleStyle)) { $margin = max($margin, 5); } $style['margin'] = max(4, min(10, $margin)); return $style; } /** @param array $style */ private static function ensureRenderScale(array $style): array { $scale = (int) ($style['scale'] ?? 8); $module = QrModuleStyleCatalog::optionsFor((string) ($style['module_style'] ?? 'square')); if ($module['circular'] ?? false) { $scale = max($scale, 10); } if (! empty($style['logo_path'])) { $scale = max($scale, 10); } $style['scale'] = max(6, min(16, $scale)); return $style; } /** @return array{0: int, 1: int, 2: int} */ private static function hexToRgb(string $hex): array { $hex = ltrim(trim($hex), '#'); if (strlen($hex) === 3) { $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; } if (strlen($hex) !== 6 || ! ctype_xdigit($hex)) { return [0, 0, 0]; } return [hexdec(substr($hex, 0, 2)), hexdec(substr($hex, 2, 2)), hexdec(substr($hex, 4, 2))]; } /** @param array{0: int, 1: int, 2: int} $rgb */ private static function rgbToHex(array $rgb): string { return sprintf('#%02x%02x%02x', $rgb[0], $rgb[1], $rgb[2]); } /** @param array{0: int, 1: int, 2: int} $rgb */ private static function darkenToward(array $rgb, float $amount): array { return [ (int) max(0, $rgb[0] * (1 - $amount)), (int) max(0, $rgb[1] * (1 - $amount)), (int) max(0, $rgb[2] * (1 - $amount)), ]; } /** @param array{0: int, 1: int, 2: int} $rgb */ private static function lightenToward(array $rgb, float $amount): array { return [ (int) min(255, $rgb[0] + ((255 - $rgb[0]) * $amount)), (int) min(255, $rgb[1] + ((255 - $rgb[1]) * $amount)), (int) min(255, $rgb[2] + ((255 - $rgb[2]) * $amount)), ]; } /** @param array{0: int, 1: int, 2: int} $rgb */ private static function relativeLuminance(array $rgb): float { $channels = []; foreach ($rgb as $value) { $v = $value / 255; $channels[] = $v <= 0.03928 ? $v / 12.92 : (($v + 0.055) / 1.055) ** 2.4; } return (0.2126 * $channels[0]) + (0.7152 * $channels[1]) + (0.0722 * $channels[2]); } }