mirror of
git://f0xx.org/ac/ac-ms-url-shortener
synced 2026-07-29 01:19:38 +03:00
Brand short-link QR PNGs with centered app logo.
Use QrBrandedRenderer with high EC qrencode and white-bordered logo plate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BIN
assets/ac_qr_logo.png
Normal file
BIN
assets/ac_qr_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
112
src/QrBrandedRenderer.php
Normal file
112
src/QrBrandedRenderer.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Branded QR PNG: high error correction + centered app logo with white border. */
|
||||
final class QrBrandedRenderer {
|
||||
private const LOGO_FRACTION = 0.22;
|
||||
private const BORDER_FRACTION = 0.045;
|
||||
|
||||
private function __construct() {
|
||||
}
|
||||
|
||||
public static function renderPng(string $text, int $size = 256): ?string {
|
||||
$text = trim($text);
|
||||
if ($text === '') {
|
||||
return null;
|
||||
}
|
||||
$raw = self::rawQrPng($text, $size);
|
||||
if ($raw === null) {
|
||||
return null;
|
||||
}
|
||||
$branded = self::overlayLogo($raw);
|
||||
return $branded ?? $raw;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user