diff --git a/assets/ac_qr_logo.png b/assets/ac_qr_logo.png new file mode 100644 index 0000000..d95f9cb Binary files /dev/null and b/assets/ac_qr_logo.png differ diff --git a/src/QrBrandedRenderer.php b/src/QrBrandedRenderer.php new file mode 100644 index 0000000..0e20fb0 --- /dev/null +++ b/src/QrBrandedRenderer.php @@ -0,0 +1,120 @@ + */ + private static function logoCandidates(): array { + return [ + __DIR__ . '/../public/assets/ac_qr_logo.png', + __DIR__ . '/../assets/ac_qr_logo.png', + ]; + } + + private static function logoPath(): ?string { + foreach (self::logoCandidates() as $path) { + if (is_readable($path)) { + return $path; + } + } + return null; + } + + private static function rawQrPng(string $text, int $size): ?string { + $qrencode = trim((string) shell_exec('command -v qrencode 2>/dev/null') ?? ''); + if ($qrencode === '') { + return null; + } + $size = max(128, min(512, $size)); + $moduleSize = max(3, (int) round($size / 33)); + $cmd = sprintf( + '%s -l H -t PNG -s %d -m 2 -o - %s 2>/dev/null', + escapeshellcmd($qrencode), + $moduleSize, + escapeshellarg($text) + ); + $out = shell_exec($cmd); + if (!is_string($out) || strlen($out) < 8) { + return null; + } + return $out; + } + + private static function overlayLogo(string $pngBytes): ?string { + if (!extension_loaded('gd')) { + return null; + } + $logoPath = self::logoPath(); + if ($logoPath === null) { + return null; + } + $qr = @imagecreatefromstring($pngBytes); + if ($qr === false) { + return null; + } + $logo = @imagecreatefrompng($logoPath); + if ($logo === false) { + imagedestroy($qr); + return null; + } + $qrW = imagesx($qr); + $qrH = imagesy($qr); + if ($qrW < 32 || $qrH < 32) { + imagedestroy($qr); + imagedestroy($logo); + return null; + } + + $logoBox = (int) round(min($qrW, $qrH) * self::LOGO_FRACTION); + $border = max(2, (int) round(min($qrW, $qrH) * self::BORDER_FRACTION)); + $plate = $logoBox + 2 * $border; + $plateX = (int) floor(($qrW - $plate) / 2); + $plateY = (int) floor(($qrH - $plate) / 2); + + $white = imagecolorallocate($qr, 255, 255, 255); + imagefilledrectangle($qr, $plateX, $plateY, $plateX + $plate - 1, $plateY + $plate - 1, $white); + + $srcW = imagesx($logo); + $srcH = imagesy($logo); + $dstX = $plateX + $border; + $dstY = $plateY + $border; + imagecopyresampled($qr, $logo, $dstX, $dstY, 0, 0, $logoBox, $logoBox, $srcW, $srcH); + + ob_start(); + imagepng($qr); + $out = ob_get_clean(); + imagedestroy($qr); + imagedestroy($logo); + if (!is_string($out) || strlen($out) < 8) { + return null; + } + return $out; + } +} diff --git a/src/bootstrap.php b/src/bootstrap.php index c386701..a08ee85 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -76,6 +76,7 @@ require_once __DIR__ . '/WireGuardTrafficStats.php'; require_once __DIR__ . '/RsshSessionProvisioner.php'; require_once __DIR__ . '/RsshBastionProvisioner.php'; require_once __DIR__ . '/AuthTwoFactorPage.php'; +require_once __DIR__ . '/QrBrandedRenderer.php'; require_once __DIR__ . '/AnalyticsHead.php'; function cfg(string $key, $default = null) {