|
|
|
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|
|
|
|
namespace App\Services\Badges;
|
|
|
|
|
|
|
|
|
|
use chillerlan\QRCode\Output\QRGdImagePNG;
|
|
|
|
|
use chillerlan\QRCode\Output\QRMarkupSVG;
|
|
|
|
|
use chillerlan\QRCode\QRCode;
|
|
|
|
|
use chillerlan\QRCode\QROptions;
|
|
|
|
|
use Dompdf\Dompdf;
|
|
|
|
@@ -56,19 +57,21 @@ class BadgePdfService
|
|
|
|
|
$motto = (string) config('badges.motto', '');
|
|
|
|
|
$studentRoleLabel = (string) config('badges.role_label', 'Student');
|
|
|
|
|
$footerBlock = $this->footerBlock($schoolName);
|
|
|
|
|
$logoPath = $this->logoFilePath();
|
|
|
|
|
$logoPath = $this->preparedLogoPath();
|
|
|
|
|
|
|
|
|
|
$rows = [];
|
|
|
|
|
|
|
|
|
|
$students = $this->studentLookupService->fetchForBadges($studentIds, $schoolYear);
|
|
|
|
|
foreach ($students as $row) {
|
|
|
|
|
try {
|
|
|
|
|
$qrPayload = (string) ($row['school_id'] ?? '');
|
|
|
|
|
$rows[] = array_merge($row, [
|
|
|
|
|
'_badge_kind' => 'student',
|
|
|
|
|
'role_subline' => $studentRoleLabel,
|
|
|
|
|
'show_grade' => true,
|
|
|
|
|
'grade_label' => 'Grade',
|
|
|
|
|
'_qr_base64' => base64_encode($this->renderQrPng((string) $row['school_id'])),
|
|
|
|
|
'_qr_src' => $this->renderQrSvgDataUri($qrPayload),
|
|
|
|
|
'_qr_base64' => $this->renderQrPngBase64($qrPayload),
|
|
|
|
|
]);
|
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
continue;
|
|
|
|
@@ -89,23 +92,32 @@ class BadgePdfService
|
|
|
|
|
|
|
|
|
|
$rows[] = array_merge($staffRow, [
|
|
|
|
|
'_badge_kind' => 'staff',
|
|
|
|
|
'_qr_base64' => base64_encode($this->renderQrPng($qrPayload)),
|
|
|
|
|
'_qr_src' => $this->renderQrSvgDataUri($qrPayload),
|
|
|
|
|
'_qr_base64' => $this->renderQrPngBase64($qrPayload),
|
|
|
|
|
]);
|
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use the fixed-coordinate FPDF renderer for badges so the output
|
|
|
|
|
// always stays at a strict 4x2 layout (8 badges per page).
|
|
|
|
|
$binary = $this->renderWithFpdf(
|
|
|
|
|
$rows,
|
|
|
|
|
$schoolName,
|
|
|
|
|
$motto,
|
|
|
|
|
$studentRoleLabel,
|
|
|
|
|
$footerBlock,
|
|
|
|
|
$logoPath
|
|
|
|
|
);
|
|
|
|
|
try {
|
|
|
|
|
$binary = $this->renderWithDompdf(
|
|
|
|
|
$rows,
|
|
|
|
|
$schoolName,
|
|
|
|
|
$motto,
|
|
|
|
|
$footerBlock
|
|
|
|
|
);
|
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
// Keep the fixed-coordinate FPDF renderer as a fallback when Dompdf fails.
|
|
|
|
|
$binary = $this->renderWithFpdf(
|
|
|
|
|
$rows,
|
|
|
|
|
$schoolName,
|
|
|
|
|
$motto,
|
|
|
|
|
$studentRoleLabel,
|
|
|
|
|
$footerBlock,
|
|
|
|
|
$logoPath
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$logIds = array_values(array_unique(array_merge($studentIds, $userIds)));
|
|
|
|
|
$this->printLogService->logSafely(
|
|
|
|
@@ -308,7 +320,7 @@ class BadgePdfService
|
|
|
|
|
'academic_year' => $year !== '' ? $year : '—',
|
|
|
|
|
'school_id' => $schoolId,
|
|
|
|
|
'role_subline' => $display,
|
|
|
|
|
'show_grade' => $isTeacherish && !$isAdmin && $gradeCol !== '—',
|
|
|
|
|
'show_grade' => true,
|
|
|
|
|
'grade_label' => 'Class / Grade',
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
@@ -373,11 +385,16 @@ class BadgePdfService
|
|
|
|
|
$grade = $this->escapeHtml((string) ($row['grade'] ?? '—'));
|
|
|
|
|
$year = $this->escapeHtml((string) ($row['academic_year'] ?? '—'));
|
|
|
|
|
$schoolId = $this->escapeHtml((string) ($row['school_id'] ?? '—'));
|
|
|
|
|
$qrBase64 = trim((string) ($row['_qr_base64'] ?? ''));
|
|
|
|
|
$qrSrc = trim((string) ($row['_qr_src'] ?? ''));
|
|
|
|
|
$footer = nl2br($this->escapeHtml($footerBlock));
|
|
|
|
|
$barcodeDigits = preg_replace('/[^A-Za-z0-9]/', '', (string) ($row['school_id'] ?? ''));
|
|
|
|
|
$barcodeDigits = $barcodeDigits !== '' ? str_repeat($barcodeDigits, 4) : '12345678901234567890';
|
|
|
|
|
$barcodeValue = preg_replace('/[^A-Za-z0-9]/', '', (string) ($row['school_id'] ?? ''));
|
|
|
|
|
$barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000';
|
|
|
|
|
$barcodeText = $this->escapeHtml(str_repeat($barcodeValue, 4));
|
|
|
|
|
$showGrade = (bool) ($row['show_grade'] ?? ($kind === 'student'));
|
|
|
|
|
$schoolLevel = trim((string) config('badges.header_subtitle', ''));
|
|
|
|
|
$headerMotto = trim($motto) !== ''
|
|
|
|
|
? trim($motto)
|
|
|
|
|
: trim((string) config('badges.motto_fallback', ''));
|
|
|
|
|
|
|
|
|
|
$infoRows = '';
|
|
|
|
|
if ($showGrade) {
|
|
|
|
@@ -419,8 +436,12 @@ class BadgePdfService
|
|
|
|
|
'</div>
|
|
|
|
|
<div class="header-copy">
|
|
|
|
|
<div class="school-name">' . $this->escapeHtml($schoolName) . '</div>
|
|
|
|
|
<div class="sub-school">High School</div>
|
|
|
|
|
<div class="motto">' . $this->escapeHtml($motto) . '</div>
|
|
|
|
|
' . ($schoolLevel !== ''
|
|
|
|
|
? '<div class="sub-school">' . $this->escapeHtml($schoolLevel) . '</div>'
|
|
|
|
|
: '') . '
|
|
|
|
|
' . ($headerMotto !== ''
|
|
|
|
|
? '<div class="motto">' . $this->escapeHtml($headerMotto) . '</div>'
|
|
|
|
|
: '') . '
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="header-accent"></div>
|
|
|
|
@@ -444,8 +465,8 @@ class BadgePdfService
|
|
|
|
|
|
|
|
|
|
<td class="qr-col">
|
|
|
|
|
<div class="qr-box">'
|
|
|
|
|
. ($qrBase64 !== ''
|
|
|
|
|
? '<img src="data:image/png;base64,' . $qrBase64 . '" alt="QR Code">'
|
|
|
|
|
. ($qrSrc !== ''
|
|
|
|
|
? '<img src="' . $qrSrc . '" alt="QR Code">'
|
|
|
|
|
: '') .
|
|
|
|
|
'</div>
|
|
|
|
|
<div class="scan-pill">Scan to verify</div>
|
|
|
|
@@ -453,8 +474,10 @@ class BadgePdfService
|
|
|
|
|
</tr>
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
<div class="barcode-rule"></div>
|
|
|
|
|
<div class="barcode">' . $this->escapeHtml($barcodeDigits) . '</div>
|
|
|
|
|
<div class="barcode-wrap">
|
|
|
|
|
<div class="barcode-bars">' . $this->buildBarcodeHtml($barcodeValue) . '</div>
|
|
|
|
|
<div class="barcode-text">' . $barcodeText . '</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="footer">
|
|
|
|
@@ -510,63 +533,64 @@ body {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.badge {
|
|
|
|
|
width: 2.12in;
|
|
|
|
|
height: 3.18in;
|
|
|
|
|
border: 1.2px solid #1f6b3a;
|
|
|
|
|
width: 2.18in;
|
|
|
|
|
height: 3.26in;
|
|
|
|
|
border: 1.5px solid #166833;
|
|
|
|
|
border-radius: 0.18in;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
position: relative;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
box-shadow: 0 0.03in 0.10in rgba(0, 0, 0, 0.15);
|
|
|
|
|
box-shadow: 0 0.03in 0.10in rgba(0, 0, 0, 0.10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-wrap {
|
|
|
|
|
position: relative;
|
|
|
|
|
height: 0.92in;
|
|
|
|
|
height: 0.80in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
background: #1f6b3a;
|
|
|
|
|
background: linear-gradient(135deg, #0d6b32 0%, #13763a 45%, #0d5a29 100%);
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
padding: 0.10in 0.10in 0.06in;
|
|
|
|
|
height: 0.64in;
|
|
|
|
|
padding: 0.06in 0.08in 0.04in;
|
|
|
|
|
height: 0.58in;
|
|
|
|
|
border-radius: 0.18in 0.18in 0 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-accent {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 0.28in;
|
|
|
|
|
right: 0.28in;
|
|
|
|
|
top: 0.64in;
|
|
|
|
|
height: 0.02in;
|
|
|
|
|
background: #9fd6b0;
|
|
|
|
|
left: 0.36in;
|
|
|
|
|
right: 0.36in;
|
|
|
|
|
top: 0.45in;
|
|
|
|
|
height: 0.012in;
|
|
|
|
|
background: rgba(255, 211, 74, 0.45);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-tail {
|
|
|
|
|
width: 0;
|
|
|
|
|
height: 0;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
border-left: 0.92in solid transparent;
|
|
|
|
|
border-right: 0.92in solid transparent;
|
|
|
|
|
border-top: 0.22in solid #1f6b3a;
|
|
|
|
|
width: 1.14in;
|
|
|
|
|
height: 0.10in;
|
|
|
|
|
margin: -0.01in auto 0;
|
|
|
|
|
background: #13763a;
|
|
|
|
|
border-radius: 0 0 0.04in 0.04in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-mark {
|
|
|
|
|
float: left;
|
|
|
|
|
width: 0.40in;
|
|
|
|
|
height: 0.40in;
|
|
|
|
|
width: 0.30in;
|
|
|
|
|
height: 0.30in;
|
|
|
|
|
border: 2px solid #ffffff;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
text-align: center;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: rgba(255,255,255,0.08);
|
|
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-mark img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-mark span {
|
|
|
|
@@ -577,29 +601,29 @@ body {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-copy {
|
|
|
|
|
margin-left: 0.48in;
|
|
|
|
|
margin-left: 0.38in;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.school-name {
|
|
|
|
|
font-size: 8.8pt;
|
|
|
|
|
font-size: 7.2pt;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
line-height: 1.1;
|
|
|
|
|
margin: 0;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.8px;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sub-school {
|
|
|
|
|
font-size: 5.8pt;
|
|
|
|
|
font-size: 5.0pt;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 2px;
|
|
|
|
|
margin-top: 0.02in;
|
|
|
|
|
letter-spacing: 1.9px;
|
|
|
|
|
margin-top: 0.03in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.motto {
|
|
|
|
|
font-size: 4.8pt;
|
|
|
|
|
font-size: 4.4pt;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
margin-top: 0.03in;
|
|
|
|
|
letter-spacing: 0.4px;
|
|
|
|
@@ -609,35 +633,37 @@ body {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.body {
|
|
|
|
|
padding: 0.00in 0.10in 0.40in;
|
|
|
|
|
position: relative;
|
|
|
|
|
height: 2.18in;
|
|
|
|
|
padding: 0.02in 0.09in 0.42in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.student-name {
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #15512c;
|
|
|
|
|
font-size: 10.6pt;
|
|
|
|
|
font-size: 9.6pt;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
line-height: 1.05;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
margin: 0.08in 0 0.03in;
|
|
|
|
|
margin: 0.03in 0 0.02in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.name-divider {
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-bottom: 0.03in;
|
|
|
|
|
margin-bottom: 0.02in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.divider-line {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 0.60in;
|
|
|
|
|
border-top: 1px solid #8fc7a0;
|
|
|
|
|
width: 0.48in;
|
|
|
|
|
border-top: 1px solid #cfd6d1;
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.divider-dot {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 0.05in;
|
|
|
|
|
height: 0.05in;
|
|
|
|
|
width: 0.04in;
|
|
|
|
|
height: 0.04in;
|
|
|
|
|
margin: 0 0.04in;
|
|
|
|
|
background: #1f6b3a;
|
|
|
|
|
transform: rotate(45deg);
|
|
|
|
@@ -646,18 +672,19 @@ body {
|
|
|
|
|
|
|
|
|
|
.role {
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #1f6b3a;
|
|
|
|
|
font-size: 6.2pt;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #1f48b3;
|
|
|
|
|
font-size: 5.5pt;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 1.1px;
|
|
|
|
|
margin-bottom: 0.08in;
|
|
|
|
|
margin-bottom: 0.05in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.content {
|
|
|
|
|
width: 100%;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
table-layout: fixed;
|
|
|
|
|
margin-top: 0.01in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.content td {
|
|
|
|
@@ -666,14 +693,13 @@ body {
|
|
|
|
|
|
|
|
|
|
.info {
|
|
|
|
|
width: 58%;
|
|
|
|
|
padding-right: 0.06in;
|
|
|
|
|
padding-right: 0.04in;
|
|
|
|
|
border-right: 1px solid #d6d6d6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-row {
|
|
|
|
|
margin-bottom: 0.07in;
|
|
|
|
|
padding-bottom: 0.04in;
|
|
|
|
|
border-bottom: 1px solid #d9d9d9;
|
|
|
|
|
margin-bottom: 0.06in;
|
|
|
|
|
padding-bottom: 0.00in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-row:last-child {
|
|
|
|
@@ -684,94 +710,105 @@ body {
|
|
|
|
|
|
|
|
|
|
.icon-badge {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 0.22in;
|
|
|
|
|
height: 0.22in;
|
|
|
|
|
line-height: 0.22in;
|
|
|
|
|
margin-right: 0.04in;
|
|
|
|
|
width: 0.18in;
|
|
|
|
|
height: 0.18in;
|
|
|
|
|
line-height: 0.18in;
|
|
|
|
|
margin-right: 0.03in;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: #1f6b3a;
|
|
|
|
|
background: #156733;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 5pt;
|
|
|
|
|
font-size: 4.8pt;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-copy {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 0.92in;
|
|
|
|
|
width: 0.78in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.label {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 4.8pt;
|
|
|
|
|
font-size: 4.5pt;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
color: #333333;
|
|
|
|
|
color: #222222;
|
|
|
|
|
margin-bottom: 0.01in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.value {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 7.6pt;
|
|
|
|
|
font-size: 7.0pt;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
color: #1f6b3a;
|
|
|
|
|
color: #111111;
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.qr-col {
|
|
|
|
|
width: 42%;
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding-left: 0.06in;
|
|
|
|
|
padding-left: 0.04in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.qr-box {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
border: 1px solid #1f6b3a;
|
|
|
|
|
border-radius: 0.08in;
|
|
|
|
|
padding: 0.04in;
|
|
|
|
|
padding: 0.03in;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
min-width: 0.92in;
|
|
|
|
|
min-height: 0.92in;
|
|
|
|
|
min-width: 0.76in;
|
|
|
|
|
min-height: 0.76in;
|
|
|
|
|
box-shadow: inset 0 0 0 1px #dce8df;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.qr-box img {
|
|
|
|
|
width: 0.84in;
|
|
|
|
|
height: 0.84in;
|
|
|
|
|
width: 0.70in;
|
|
|
|
|
height: 0.70in;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scan {
|
|
|
|
|
margin-top: 0.04in;
|
|
|
|
|
font-size: 5.5pt;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
color: #1f6b3a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scan-pill {
|
|
|
|
|
margin-top: 0.03in;
|
|
|
|
|
margin-top: 0.025in;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
background: #1f6b3a;
|
|
|
|
|
background: #156733;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
border-radius: 0.06in;
|
|
|
|
|
font-size: 5pt;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
font-size: 4.5pt;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
padding: 0.025in 0.08in;
|
|
|
|
|
padding: 0.025in 0.06in 0.02in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barcode-rule {
|
|
|
|
|
margin-top: 0.07in;
|
|
|
|
|
border-top: 1px solid #8fc7a0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barcode {
|
|
|
|
|
margin: 0.04in auto 0;
|
|
|
|
|
.barcode-wrap {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 0.10in;
|
|
|
|
|
right: 0.10in;
|
|
|
|
|
bottom: 0.06in;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-family: "Courier New", monospace;
|
|
|
|
|
font-size: 9pt;
|
|
|
|
|
letter-spacing: 0.2px;
|
|
|
|
|
border-top: 1px solid #d6ddd8;
|
|
|
|
|
padding-top: 0.06in;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barcode-bars {
|
|
|
|
|
height: 0.14in;
|
|
|
|
|
line-height: 0;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barcode-bar {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
height: 0.14in;
|
|
|
|
|
margin-right: 1px;
|
|
|
|
|
background: #111111;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barcode-text {
|
|
|
|
|
margin-top: 0.03in;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 5.8pt;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #111111;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -780,19 +817,19 @@ body {
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: #1f6b3a;
|
|
|
|
|
background: linear-gradient(135deg, #0d6b32 0%, #0b5c2b 100%);
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
padding: 0.06in 0.06in;
|
|
|
|
|
font-size: 4.8pt;
|
|
|
|
|
padding: 0.05in 0.06in 0.05in;
|
|
|
|
|
font-size: 4.3pt;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.footer-mark {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 0.18in;
|
|
|
|
|
height: 0.18in;
|
|
|
|
|
line-height: 0.18in;
|
|
|
|
|
width: 0.15in;
|
|
|
|
|
height: 0.15in;
|
|
|
|
|
line-height: 0.15in;
|
|
|
|
|
margin-right: 0.05in;
|
|
|
|
|
border: 1.5px solid #ffffff;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
@@ -800,13 +837,15 @@ body {
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
font-size: 6pt;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: rgba(255,255,255,0.08);
|
|
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.footer-mark img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.footer-mark span {
|
|
|
|
@@ -815,12 +854,32 @@ body {
|
|
|
|
|
|
|
|
|
|
.footer-copy {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 1.64in;
|
|
|
|
|
width: 1.72in;
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
}
|
|
|
|
|
CSS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function buildBarcodeHtml(string $value): string
|
|
|
|
|
{
|
|
|
|
|
$seed = $value !== '' ? strtoupper($value) : 'ID0000';
|
|
|
|
|
$bars = [];
|
|
|
|
|
|
|
|
|
|
foreach (str_split($seed) as $char) {
|
|
|
|
|
$width = match (ord($char) % 4) {
|
|
|
|
|
0 => '1px',
|
|
|
|
|
1 => '2px',
|
|
|
|
|
2 => '3px',
|
|
|
|
|
default => '4px',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$bars[] = '<span class="barcode-bar" style="width:' . $width . '"></span>';
|
|
|
|
|
$bars[] = '<span class="barcode-bar" style="width:1px"></span>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return implode('', $bars);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function escapeHtml(string $value): string
|
|
|
|
|
{
|
|
|
|
|
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
|
|
|
@@ -860,57 +919,89 @@ CSS;
|
|
|
|
|
$showGrade = (bool) ($student['show_grade'] ?? ($kind === 'student'));
|
|
|
|
|
$gradeLabel = (string) ($student['grade_label'] ?? ($kind === 'staff' ? 'CLASS / ASSIGNMENT' : 'GRADE'));
|
|
|
|
|
|
|
|
|
|
$blue = [31, 107, 58];
|
|
|
|
|
$blueLight = [53, 140, 81];
|
|
|
|
|
$green = [21, 103, 51];
|
|
|
|
|
$greenDark = [13, 91, 43];
|
|
|
|
|
$greenAccent = [244, 211, 74];
|
|
|
|
|
$blueRole = [31, 72, 179];
|
|
|
|
|
|
|
|
|
|
$qrPath = (string) ($student['_qr_tmp'] ?? '');
|
|
|
|
|
$fullName = $this->formatter->toPdf((string) ($student['fullname'] ?? ''));
|
|
|
|
|
$grade = $this->formatter->toPdf((string) ($student['grade'] ?? ''));
|
|
|
|
|
$year = $this->formatter->toPdf((string) ($student['academic_year'] ?? ''));
|
|
|
|
|
$schoolId = $this->formatter->toPdf((string) ($student['school_id'] ?? ''));
|
|
|
|
|
$barcodeValue = preg_replace('/[^A-Za-z0-9]/', '', (string) ($student['school_id'] ?? ''));
|
|
|
|
|
$barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000';
|
|
|
|
|
$barcodeText = $this->formatter->toPdf(str_repeat($barcodeValue, 4));
|
|
|
|
|
|
|
|
|
|
$schoolNamePdf = $this->formatter->toPdf($schoolName);
|
|
|
|
|
$mottoPdf = $this->formatter->toPdf($motto);
|
|
|
|
|
$headerSubtitlePdf = $this->formatter->toPdf((string) config('badges.header_subtitle', 'High School'));
|
|
|
|
|
$mottoValue = trim($motto) !== ''
|
|
|
|
|
? $motto
|
|
|
|
|
: (string) config('badges.motto_fallback', 'Learn • Lead • Succeed');
|
|
|
|
|
$mottoPdf = $this->formatter->toPdf($mottoValue);
|
|
|
|
|
$rolePdf = $this->formatter->toPdf((string) ($student['role_subline'] ?? $defaultStudentRoleLabel));
|
|
|
|
|
|
|
|
|
|
$pdf->SetDrawColor($blue[0], $blue[1], $blue[2]);
|
|
|
|
|
$pdf->SetLineWidth(0.003);
|
|
|
|
|
$pdf->SetDrawColor($green[0], $green[1], $green[2]);
|
|
|
|
|
$pdf->SetLineWidth(0.01);
|
|
|
|
|
$pdf->Rect($x, $y, $w, $h);
|
|
|
|
|
|
|
|
|
|
$headerH = 0.55;
|
|
|
|
|
$footerH = 0.38;
|
|
|
|
|
$headerH = 0.66;
|
|
|
|
|
$footerH = 0.36;
|
|
|
|
|
|
|
|
|
|
$pdf->SetFillColor($blue[0], $blue[1], $blue[2]);
|
|
|
|
|
$pdf->SetFillColor($green[0], $green[1], $green[2]);
|
|
|
|
|
$pdf->Rect($x, $y, $w, $headerH, 'F');
|
|
|
|
|
$pdf->SetFillColor($greenDark[0], $greenDark[1], $greenDark[2]);
|
|
|
|
|
$pdf->Rect($x, $y + 0.18, $w, 0.18, 'F');
|
|
|
|
|
$pdf->Rect($x + 0.22, $y + $headerH - 0.03, $w - 0.44, 0.10, 'F');
|
|
|
|
|
|
|
|
|
|
$sealSize = 0.33;
|
|
|
|
|
$sealX = $x + 0.09;
|
|
|
|
|
$sealY = $y + 0.07;
|
|
|
|
|
$pdf->SetFillColor(255, 255, 255);
|
|
|
|
|
$pdf->Rect($sealX, $sealY, $sealSize, $sealSize, 'F');
|
|
|
|
|
$pdf->SetDrawColor(255, 255, 255);
|
|
|
|
|
$pdf->Rect($sealX, $sealY, $sealSize, $sealSize, 'D');
|
|
|
|
|
if ($logoPath !== null && is_readable($logoPath)) {
|
|
|
|
|
try {
|
|
|
|
|
$pdf->Image($logoPath, $sealX + 0.02, $sealY + 0.02, $sealSize - 0.04, $sealSize - 0.04);
|
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$pdf->SetTextColor(255, 255, 255);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 8.5);
|
|
|
|
|
$pdf->SetXY($x + 0.06, $y + 0.12);
|
|
|
|
|
$pdf->Cell($w - 0.12, 0.1, $schoolNamePdf, 0, 0, 'C');
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 8.2);
|
|
|
|
|
$pdf->SetXY($x + 0.45, $y + 0.10);
|
|
|
|
|
$pdf->Cell($w - 0.55, 0.08, $schoolNamePdf, 0, 1, 'C');
|
|
|
|
|
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 5.8);
|
|
|
|
|
$pdf->SetXY($x + 0.45, $y + 0.27);
|
|
|
|
|
$pdf->Cell($w - 0.55, 0.06, strtoupper($headerSubtitlePdf), 0, 1, 'C');
|
|
|
|
|
|
|
|
|
|
if ($mottoPdf !== '') {
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 5.2);
|
|
|
|
|
$pdf->SetXY($x + 0.06, $y + $headerH - 0.12);
|
|
|
|
|
$pdf->Cell($w - 0.12, 0.08, $mottoPdf, 0, 0, 'C');
|
|
|
|
|
$pdf->SetTextColor($greenAccent[0], $greenAccent[1], $greenAccent[2]);
|
|
|
|
|
$pdf->SetXY($x + 0.45, $y + 0.40);
|
|
|
|
|
$pdf->Cell($w - 0.55, 0.06, strtoupper($mottoPdf), 0, 1, 'C');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$bodyTop = $y + $headerH + 0.06;
|
|
|
|
|
$bodyTop = $y + $headerH + 0.02;
|
|
|
|
|
$pdf->SetTextColor(21, 81, 44);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 9.5);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 10.2);
|
|
|
|
|
$pdf->SetXY($x + 0.07, $bodyTop);
|
|
|
|
|
$pdf->Cell($w - 0.14, 0.11, strtoupper($fullName), 0, 1, 'C');
|
|
|
|
|
|
|
|
|
|
$pdf->SetTextColor(27, 86, 177);
|
|
|
|
|
$pdf->SetTextColor($blueRole[0], $blueRole[1], $blueRole[2]);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 6);
|
|
|
|
|
$pdf->SetX($x + 0.07);
|
|
|
|
|
$pdf->Cell($w - 0.14, 0.07, strtoupper($rolePdf), 0, 1, 'C');
|
|
|
|
|
|
|
|
|
|
$ruleY = $bodyTop + 0.22;
|
|
|
|
|
$pdf->SetDrawColor(215, 222, 234);
|
|
|
|
|
$pdf->Line($x + 0.08, $ruleY, $x + $w - 0.08, $ruleY);
|
|
|
|
|
$ruleY = $bodyTop + 0.20;
|
|
|
|
|
$pdf->SetDrawColor(210, 214, 210);
|
|
|
|
|
$pdf->Line($x + 0.10, $ruleY, $x + 0.58, $ruleY);
|
|
|
|
|
$pdf->Line($x + $w - 0.58, $ruleY, $x + $w - 0.10, $ruleY);
|
|
|
|
|
|
|
|
|
|
$infoLeft = $x + 0.08;
|
|
|
|
|
$infoW = $w * 0.52;
|
|
|
|
|
$infoW = $w * 0.45;
|
|
|
|
|
$labelY = $ruleY + 0.05;
|
|
|
|
|
$infoRows = [];
|
|
|
|
|
if ($showGrade) {
|
|
|
|
@@ -920,14 +1011,25 @@ CSS;
|
|
|
|
|
$infoRows[] = [$idLabel, $schoolId];
|
|
|
|
|
|
|
|
|
|
foreach ($infoRows as $index => [$label, $value]) {
|
|
|
|
|
$iconY = $labelY + 0.03;
|
|
|
|
|
$pdf->SetFillColor($green[0], $green[1], $green[2]);
|
|
|
|
|
$pdf->SetDrawColor($green[0], $green[1], $green[2]);
|
|
|
|
|
$pdf->Rect($infoLeft, $iconY - 0.07, 0.14, 0.14, 'F');
|
|
|
|
|
$pdf->SetTextColor(255, 255, 255);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 5);
|
|
|
|
|
$iconLabel = $label === 'ACADEMIC YEAR' ? 'Y' : ($label === $idLabel ? 'ID' : 'G');
|
|
|
|
|
$pdf->SetXY($infoLeft + 0.01, $iconY - 0.03);
|
|
|
|
|
$pdf->Cell(0.12, 0.06, $iconLabel, 0, 0, 'C');
|
|
|
|
|
|
|
|
|
|
$textLeft = $infoLeft + 0.16;
|
|
|
|
|
$pdf->SetTextColor(51, 51, 51);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 5);
|
|
|
|
|
$pdf->SetXY($infoLeft, $labelY);
|
|
|
|
|
$pdf->SetXY($textLeft, $labelY);
|
|
|
|
|
$pdf->Cell($infoW, 0.05, $label, 0, 1, 'L');
|
|
|
|
|
|
|
|
|
|
$pdf->SetTextColor($blueLight[0], $blueLight[1], $blueLight[2]);
|
|
|
|
|
$pdf->SetTextColor(17, 17, 17);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 7.5);
|
|
|
|
|
$pdf->SetX($infoLeft);
|
|
|
|
|
$pdf->SetX($textLeft);
|
|
|
|
|
if ($index === array_key_last($infoRows)) {
|
|
|
|
|
$pdf->MultiCell($infoW, 0.09, $value, 0, 'L');
|
|
|
|
|
$labelY = $pdf->GetY() + 0.015;
|
|
|
|
@@ -937,51 +1039,113 @@ CSS;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$qrSize = 0.38;
|
|
|
|
|
$qrX = $x + $w - 0.08 - $qrSize;
|
|
|
|
|
$qrY = $ruleY + 0.04;
|
|
|
|
|
$dividerX = $x + ($w * 0.53);
|
|
|
|
|
$pdf->SetDrawColor(214, 214, 214);
|
|
|
|
|
$pdf->Line($dividerX, $ruleY + 0.02, $dividerX, $y + $h - $footerH - 0.30);
|
|
|
|
|
|
|
|
|
|
$qrSize = 0.48;
|
|
|
|
|
$qrX = $x + $w - 0.18 - $qrSize;
|
|
|
|
|
$qrY = $ruleY + 0.07;
|
|
|
|
|
|
|
|
|
|
if ($qrPath !== '' && is_file($qrPath)) {
|
|
|
|
|
try {
|
|
|
|
|
$pdf->SetDrawColor($blueLight[0], $blueLight[1], $blueLight[2]);
|
|
|
|
|
$pdf->Rect($qrX - 0.03, $qrY - 0.03, $qrSize + 0.06, $qrSize + 0.06);
|
|
|
|
|
$pdf->SetDrawColor($green[0], $green[1], $green[2]);
|
|
|
|
|
$pdf->Rect($qrX - 0.05, $qrY - 0.05, $qrSize + 0.10, $qrSize + 0.10, 'D');
|
|
|
|
|
$pdf->Image($qrPath, $qrX, $qrY, $qrSize, $qrSize, 'PNG');
|
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$pdf->SetTextColor($blueLight[0], $blueLight[1], $blueLight[2]);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 4.5);
|
|
|
|
|
$pdf->SetXY($qrX - 0.05, $qrY + $qrSize + 0.02);
|
|
|
|
|
$pdf->Cell($qrSize + 0.1, 0.05, 'SCAN TO VERIFY', 0, 0, 'C');
|
|
|
|
|
$pillW = 0.56;
|
|
|
|
|
$pillH = 0.10;
|
|
|
|
|
$pillX = $qrX + ($qrSize / 2) - ($pillW / 2);
|
|
|
|
|
$pillY = $qrY + $qrSize + 0.06;
|
|
|
|
|
$pdf->SetFillColor($green[0], $green[1], $green[2]);
|
|
|
|
|
$pdf->Rect($pillX, $pillY, $pillW, $pillH, 'F');
|
|
|
|
|
$pdf->SetTextColor(255, 255, 255);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 4.4);
|
|
|
|
|
$pdf->SetXY($pillX, $pillY + 0.02);
|
|
|
|
|
$pdf->Cell($pillW, 0.05, 'SCAN TO VERIFY', 0, 0, 'C');
|
|
|
|
|
|
|
|
|
|
$barcodeY = $y + $h - $footerH - 0.34;
|
|
|
|
|
$barsX = $x + 0.14;
|
|
|
|
|
$barsW = $w - 0.28;
|
|
|
|
|
$pdf->SetDrawColor(214, 221, 216);
|
|
|
|
|
$pdf->Line($barsX, $barcodeY - 0.03, $barsX + $barsW, $barcodeY - 0.03);
|
|
|
|
|
$this->drawBarcodeBars($pdf, $barcodeValue, $barsX, $barcodeY, $barsW, 0.14);
|
|
|
|
|
$pdf->SetTextColor(17, 17, 17);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 5.6);
|
|
|
|
|
$pdf->SetXY($barsX, $barcodeY + 0.16);
|
|
|
|
|
$pdf->Cell($barsW, 0.05, $barcodeText, 0, 0, 'C');
|
|
|
|
|
|
|
|
|
|
$fy = $y + $h - $footerH;
|
|
|
|
|
$pdf->SetFillColor($blue[0], $blue[1], $blue[2]);
|
|
|
|
|
$pdf->SetFillColor($greenDark[0], $greenDark[1], $greenDark[2]);
|
|
|
|
|
$pdf->Rect($x, $fy, $w, $footerH, 'F');
|
|
|
|
|
|
|
|
|
|
if ($logoPath !== null && is_readable($logoPath)) {
|
|
|
|
|
try {
|
|
|
|
|
$smallLogo = 0.1;
|
|
|
|
|
$pdf->Image($logoPath, $x + $w / 2 - $smallLogo / 2, $fy + 0.03, $smallLogo, $smallLogo);
|
|
|
|
|
$smallLogo = 0.16;
|
|
|
|
|
$pdf->SetFillColor(255, 255, 255);
|
|
|
|
|
$pdf->Rect($x + 0.09, $fy + 0.05, 0.18, 0.18, 'F');
|
|
|
|
|
$pdf->Image($logoPath, $x + 0.10, $fy + 0.06, $smallLogo, $smallLogo);
|
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$pdf->SetTextColor(255, 255, 255);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 4.5);
|
|
|
|
|
$pdf->SetFont('Helvetica', 'B', 4.3);
|
|
|
|
|
$lines = preg_split('/\r\n|\r|\n/', $footerBlock) ?: [$footerBlock];
|
|
|
|
|
$lineY = $fy + 0.15;
|
|
|
|
|
$lineY = $fy + 0.08;
|
|
|
|
|
$textX = $x + 0.30;
|
|
|
|
|
$textW = $w - 0.36;
|
|
|
|
|
foreach ($lines as $line) {
|
|
|
|
|
$line = trim($line);
|
|
|
|
|
if ($line === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$pdf->SetXY($x + 0.04, $lineY);
|
|
|
|
|
$pdf->Cell($w - 0.08, 0.055, $this->formatter->toPdf($line), 0, 1, 'C');
|
|
|
|
|
$pdf->SetXY($textX, $lineY);
|
|
|
|
|
$pdf->Cell($textW, 0.055, $this->formatter->toPdf($line), 0, 1, 'L');
|
|
|
|
|
$lineY += 0.055;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function drawBarcodeBars(
|
|
|
|
|
\FPDF $pdf,
|
|
|
|
|
string $value,
|
|
|
|
|
float $x,
|
|
|
|
|
float $y,
|
|
|
|
|
float $maxWidth,
|
|
|
|
|
float $height
|
|
|
|
|
): void {
|
|
|
|
|
$seed = $value !== '' ? strtoupper($value) : 'ID0000';
|
|
|
|
|
$cursor = $x;
|
|
|
|
|
$gap = 0.01;
|
|
|
|
|
|
|
|
|
|
foreach (str_split($seed) as $char) {
|
|
|
|
|
$width = match (ord($char) % 4) {
|
|
|
|
|
0 => 0.01,
|
|
|
|
|
1 => 0.015,
|
|
|
|
|
2 => 0.02,
|
|
|
|
|
default => 0.025,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (($cursor + $width) > ($x + $maxWidth)) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$pdf->SetFillColor(17, 17, 17);
|
|
|
|
|
$pdf->Rect($cursor, $y, $width, $height, 'F');
|
|
|
|
|
$cursor += $width + $gap;
|
|
|
|
|
|
|
|
|
|
if (($cursor + 0.01) > ($x + $maxWidth)) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$pdf->Rect($cursor, $y, 0.01, $height, 'F');
|
|
|
|
|
$cursor += 0.01 + $gap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function renderQrPng(string $payload): string
|
|
|
|
|
{
|
|
|
|
|
$qrOptions = new QROptions([
|
|
|
|
@@ -997,14 +1161,64 @@ CSS;
|
|
|
|
|
return $qr->render($payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function renderQrPngBase64(string $payload): string
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
return base64_encode($this->renderQrPng($payload));
|
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function renderQrSvgDataUri(string $payload): string
|
|
|
|
|
{
|
|
|
|
|
$qrOptions = new QROptions([
|
|
|
|
|
'version' => 5,
|
|
|
|
|
'outputInterface' => QRMarkupSVG::class,
|
|
|
|
|
'outputBase64' => true,
|
|
|
|
|
'eccLevel' => QRCode::ECC_M,
|
|
|
|
|
'drawLightModules' => false,
|
|
|
|
|
'svgAddXmlHeader' => false,
|
|
|
|
|
'connectPaths' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$qr = new QRCode($qrOptions);
|
|
|
|
|
|
|
|
|
|
return $qr->render($payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function logoFilePath(): ?string
|
|
|
|
|
{
|
|
|
|
|
return $this->formatter->firstExisting([
|
|
|
|
|
public_path('logo-circle.png'),
|
|
|
|
|
public_path('logo.png'),
|
|
|
|
|
public_path('assets/images/school_logo.png'),
|
|
|
|
|
public_path('assets/images/logo.png'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function preparedLogoPath(): ?string
|
|
|
|
|
{
|
|
|
|
|
$path = $this->logoFilePath();
|
|
|
|
|
if ($path === null || !is_readable($path)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$png = $this->makeCircularPngBytesFromPath($path);
|
|
|
|
|
if ($png === null) {
|
|
|
|
|
return $path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tmpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'badge_logo_' . bin2hex(random_bytes(8)) . '.png';
|
|
|
|
|
if (@file_put_contents($tmpPath, $png) === false) {
|
|
|
|
|
return $path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->tempQrFiles[] = $tmpPath;
|
|
|
|
|
|
|
|
|
|
return $tmpPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function logoDataUri(): ?string
|
|
|
|
|
{
|
|
|
|
|
$path = $this->logoFilePath();
|
|
|
|
@@ -1012,20 +1226,15 @@ CSS;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
|
|
|
|
$mime = match ($ext) {
|
|
|
|
|
'jpg', 'jpeg' => 'image/jpeg',
|
|
|
|
|
'gif' => 'image/gif',
|
|
|
|
|
'webp' => 'image/webp',
|
|
|
|
|
default => 'image/png',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$bytes = @file_get_contents($path);
|
|
|
|
|
$bytes = $this->makeCircularPngBytesFromPath($path);
|
|
|
|
|
if ($bytes === null) {
|
|
|
|
|
$bytes = @file_get_contents($path);
|
|
|
|
|
}
|
|
|
|
|
if ($bytes === false || $bytes === '') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'data:' . $mime . ';base64,' . base64_encode($bytes);
|
|
|
|
|
return 'data:image/png;base64,' . base64_encode($bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function footerBlock(string $schoolName): string
|
|
|
|
@@ -1037,4 +1246,78 @@ CSS;
|
|
|
|
|
|
|
|
|
|
return $schoolName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function makeCircularPngBytesFromPath(string $path): ?string
|
|
|
|
|
{
|
|
|
|
|
if (!function_exists('imagecreatetruecolor') || !function_exists('imagepng')) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$bytes = @file_get_contents($path);
|
|
|
|
|
if ($bytes === false || $bytes === '') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$source = @imagecreatefromstring($bytes);
|
|
|
|
|
if ($source === false) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$srcW = imagesx($source);
|
|
|
|
|
$srcH = imagesy($source);
|
|
|
|
|
if ($srcW <= 0 || $srcH <= 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$size = min($srcW, $srcH);
|
|
|
|
|
$srcX = (int) floor(($srcW - $size) / 2);
|
|
|
|
|
$srcY = (int) floor(($srcH - $size) / 2);
|
|
|
|
|
|
|
|
|
|
$square = imagecreatetruecolor($size, $size);
|
|
|
|
|
if ($square === false) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imagealphablending($square, false);
|
|
|
|
|
imagesavealpha($square, true);
|
|
|
|
|
$transparent = imagecolorallocatealpha($square, 0, 0, 0, 127);
|
|
|
|
|
imagefill($square, 0, 0, $transparent);
|
|
|
|
|
imagecopyresampled($square, $source, 0, 0, $srcX, $srcY, $size, $size, $size, $size);
|
|
|
|
|
|
|
|
|
|
$circle = imagecreatetruecolor($size, $size);
|
|
|
|
|
if ($circle === false) {
|
|
|
|
|
imagedestroy($square);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imagealphablending($circle, false);
|
|
|
|
|
imagesavealpha($circle, true);
|
|
|
|
|
$circleTransparent = imagecolorallocatealpha($circle, 0, 0, 0, 127);
|
|
|
|
|
imagefill($circle, 0, 0, $circleTransparent);
|
|
|
|
|
|
|
|
|
|
$radius = $size / 2;
|
|
|
|
|
for ($x = 0; $x < $size; $x++) {
|
|
|
|
|
for ($y = 0; $y < $size; $y++) {
|
|
|
|
|
$dx = ($x + 0.5) - $radius;
|
|
|
|
|
$dy = ($y + 0.5) - $radius;
|
|
|
|
|
if ((($dx * $dx) + ($dy * $dy)) <= ($radius * $radius)) {
|
|
|
|
|
$color = imagecolorat($square, $x, $y);
|
|
|
|
|
imagesetpixel($circle, $x, $y, $color);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ob_start();
|
|
|
|
|
imagepng($circle);
|
|
|
|
|
$png = ob_get_clean();
|
|
|
|
|
|
|
|
|
|
imagedestroy($square);
|
|
|
|
|
imagedestroy($circle);
|
|
|
|
|
|
|
|
|
|
return is_string($png) && $png !== '' ? $png : null;
|
|
|
|
|
} finally {
|
|
|
|
|
imagedestroy($source);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|