$style */ public static function contrastRatio(array $style): float { $fg = self::relativeLuminance((string) ($style['foreground'] ?? '#000000')); $bg = self::relativeLuminance((string) ($style['background'] ?? '#ffffff')); $lighter = max($fg, $bg); $darker = min($fg, $bg); return ($lighter + 0.05) / ($darker + 0.05); } /** * @param array $style * @return array{level: string, messages: list} */ public static function assess(array $style): array { $style = QrStyleDefaults::merge($style); $messages = []; $level = 'good'; $contrast = self::contrastRatio($style); $isClassic = self::isClassicBlackOnWhite($style); $moduleStyle = (string) ($style['module_style'] ?? 'square'); $moduleMeta = QrModuleStyleCatalog::optionsFor($moduleStyle); if ($moduleMeta['scan_risk'] === 'medium') { $messages[] = 'This module style may not scan on every phone camera. Square is the safest choice.'; $level = 'fair'; } if ($moduleMeta['scan_risk'] === 'high') { $messages[] = 'Decorative styles like ' . $moduleMeta['label'] . ' often fail on Samsung Camera. Use square for print and signage.'; $level = 'poor'; } if (! $isClassic) { $messages[] = 'Colored QR codes often fail on Samsung Camera and other basic scanners. Black on white works everywhere.'; $level = $level === 'good' ? 'fair' : 'poor'; } if ($contrast < 4.5) { $messages[] = sprintf( 'Contrast is low (%.1f:1). Aim for at least 4.5:1 — dark foreground, white background.', $contrast, ); $level = 'poor'; } if ($moduleMeta['scan_risk'] !== 'low' && ! $isClassic) { $level = 'poor'; } if ($messages === []) { $messages[] = 'Black square modules on white give the best compatibility with Samsung, iPhone, and printed codes.'; } return ['level' => $level, 'messages' => $messages]; } /** @param array $style */ public static function isClassicBlackOnWhite(array $style): bool { $fg = strtolower(ltrim((string) ($style['foreground'] ?? ''), '#')); $bg = strtolower(ltrim((string) ($style['background'] ?? ''), '#')); $fg = strlen($fg) === 3 ? $fg[0] . $fg[0] . $fg[1] . $fg[1] . $fg[2] . $fg[2] : $fg; $bg = strlen($bg) === 3 ? $bg[0] . $bg[0] . $bg[1] . $bg[1] . $bg[2] . $bg[2] : $bg; return in_array($fg, ['000000', '0f172a', '111111', '1a1a1a'], true) && in_array($bg, ['ffffff', 'fff'], true); } private static function relativeLuminance(string $hex): float { $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; } $channels = []; foreach ([0, 2, 4] as $offset) { $value = hexdec(substr($hex, $offset, 2)) / 255; $channels[] = $value <= 0.03928 ? $value / 12.92 : (($value + 0.055) / 1.055) ** 2.4; } return (0.2126 * $channels[0]) + (0.7152 * $channels[1]) + (0.0722 * $channels[2]); } }