Files
ladill-qr-plus/app/Support/Qr/QrScanReliability.php
T
isaaccladandCursor cd6571f199
Deploy Ladill QR Plus / deploy (push) Failing after 1s
Extract Ladill QR Plus as standalone app at qr.ladill.com.
Utility QR types only (URL, WiFi, link list, business, app download) with
SSO, Billing API integration, public /q resolver, and qr-plus:import for
platform migration. vCard and commerce types stay on the platform.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 21:31:24 +00:00

103 lines
3.5 KiB
PHP

<?php
namespace App\Support\Qr;
class QrScanReliability
{
/** @param array<string, mixed> $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<string, mixed> $style
* @return array{level: string, messages: list<string>}
*/
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<string, mixed> $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]);
}
}