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..1ca612f --- /dev/null +++ b/src/QrBrandedRenderer.php @@ -0,0 +1,112 @@ + */ + private static function logoCandidates(): array { + return [ + dirname(__DIR__) . '/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/QrHandler.php b/src/QrHandler.php index 62c057c..033b0d2 100644 --- a/src/QrHandler.php +++ b/src/QrHandler.php @@ -30,21 +30,9 @@ final class QrHandler { } private static function renderPng(string $text, int $size): ?string { - $qrencode = trim((string) shell_exec('command -v qrencode 2>/dev/null') ?? ''); - if ($qrencode === '') { - return null; + if (!class_exists('QrBrandedRenderer', false)) { + require_once __DIR__ . '/QrBrandedRenderer.php'; } - $moduleSize = max(2, (int) round($size / 40)); - $cmd = sprintf( - '%s -t PNG -s %d -m 1 -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; + return QrBrandedRenderer::renderPng($text, $size); } } diff --git a/src/bootstrap.php b/src/bootstrap.php index 17b7d4c..d8634c8 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -31,4 +31,5 @@ require_once __DIR__ . '/SignerRepository.php'; require_once __DIR__ . '/LinkRepository.php'; require_once __DIR__ . '/ShortenService.php'; require_once __DIR__ . '/RedirectHandler.php'; +require_once __DIR__ . '/QrBrandedRenderer.php'; require_once __DIR__ . '/QrHandler.php';