badge creation

This commit is contained in:
root
2026-06-05 17:16:38 -04:00
parent ad968eaff7
commit a192ed433d
10 changed files with 1740 additions and 142 deletions
BIN
View File
Binary file not shown.
+479 -142
View File
@@ -392,10 +392,11 @@ class BadgePdfService
$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', ''));
$headerMainTitle = $this->headerMainTitle($schoolName);
$schoolLevel = $this->headerSubtitle($schoolName);
$headerMotto = trim($motto) !== ''
? trim($motto)
: trim((string) config('badges.motto_fallback', ''));
: trim((string) config('badges.motto_fallback', 'Learn • Lead • Succeed'));
$infoRows = '';
if ($showGrade) {
@@ -436,7 +437,7 @@ class BadgePdfService
: '<span>AS</span>') .
'</div>
<div class="header-copy">
<div class="school-name">' . $this->escapeHtml($schoolName) . '</div>
<div class="school-name">' . $this->escapeHtml($headerMainTitle) . '</div>
' . ($schoolLevel !== ''
? '<div class="sub-school">' . $this->escapeHtml($schoolLevel) . '</div>'
: '') . '
@@ -508,7 +509,7 @@ class BadgePdfService
body {
margin: 0;
font-family: DejaVu Sans, Arial, Helvetica, sans-serif;
color: #111;
color: #111111;
}
.page {
@@ -536,149 +537,178 @@ body {
.badge {
width: 2.18in;
height: 3.26in;
border: 1.5px solid #166833;
border: 1.2px solid #d8d8d8;
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.10);
box-shadow: 0 0.03in 0.10in rgba(0, 0, 0, 0.16);
}
.header-wrap {
position: relative;
height: 0.80in;
height: 0.96in;
}
.header {
background: linear-gradient(135deg, #0d6b32 0%, #13763a 45%, #0d5a29 100%);
background: linear-gradient(135deg, #063979 0%, #082f68 55%, #05285a 100%);
color: #ffffff;
padding: 0.06in 0.08in 0.04in;
height: 0.58in;
padding: 0.07in 0.08in 0.04in;
height: 0.70in;
border-radius: 0.18in 0.18in 0 0;
}
.header:after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0.64in;
height: 0.08in;
background: #8db7e8;
clip-path: polygon(0 0, 50% 100%, 100% 0, 100% 25%, 50% 100%, 0 25%);
}
.header-accent {
position: absolute;
left: 0.36in;
right: 0.36in;
top: 0.45in;
height: 0.012in;
background: rgba(255, 211, 74, 0.45);
left: 0;
right: 0;
top: 0.67in;
height: 0.20in;
background: #07336f;
clip-path: polygon(0 0, 50% 100%, 100% 0);
}
.header-tail {
width: 1.14in;
height: 0.10in;
margin: -0.01in auto 0;
background: #13763a;
border-radius: 0 0 0.04in 0.04in;
display: none;
}
.header-mark {
float: left;
width: 0.30in;
height: 0.30in;
width: 0.39in;
height: 0.39in;
border: 2px solid #ffffff;
border-radius: 50%;
border-radius: 0.02in;
text-align: center;
overflow: hidden;
background: transparent;
margin-top: 0.02in;
}
.header-mark img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
display: block;
}
.header-mark span {
display: block;
line-height: 0.36in;
font-size: 12pt;
line-height: 0.38in;
font-size: 11pt;
font-weight: 800;
}
.header-copy {
margin-left: 0.38in;
margin-left: 0.45in;
text-align: center;
}
.school-name {
font-size: 7.2pt;
font-weight: 800;
line-height: 1.1;
font-size: 17.5pt;
font-weight: 900;
line-height: 0.90;
margin: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 1px;
color: #ffffff;
}
.sub-school {
font-size: 5.0pt;
font-weight: 800;
font-size: 8.6pt;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 1.9px;
letter-spacing: 1.7px;
margin-top: 0.03in;
color: #ffffff;
}
.sub-school:before,
.sub-school:after {
content: "";
display: inline-block;
width: 0.22in;
border-top: 1px solid #ffffff;
margin: 0 0.05in 0.025in;
}
.motto {
font-size: 4.4pt;
font-weight: 700;
font-size: 6.5pt;
font-weight: 900;
margin-top: 0.03in;
letter-spacing: 0.4px;
letter-spacing: 0.8px;
min-height: 0.08in;
text-transform: uppercase;
color: #f4d34c;
color: #ffd44a;
}
.motto:before,
.motto:after {
content: "";
display: inline-block;
width: 0.20in;
border-top: 1px solid #ffd44a;
margin: 0 0.05in 0.02in;
}
.body {
position: relative;
height: 2.18in;
padding: 0.02in 0.09in 0.42in;
height: 1.94in;
padding: 0.09in 0.09in 0.39in;
}
.student-name {
text-align: center;
color: #15512c;
font-size: 9.6pt;
font-weight: 800;
color: #052d6f;
font-size: 14.0pt;
font-weight: 900;
line-height: 1.05;
text-transform: uppercase;
margin: 0.03in 0 0.02in;
margin: 0.01in 0 0.03in;
letter-spacing: 1px;
}
.name-divider {
text-align: center;
margin-bottom: 0.02in;
margin-bottom: 0.03in;
}
.divider-line {
display: inline-block;
width: 0.48in;
border-top: 1px solid #cfd6d1;
width: 0.65in;
border-top: 1px solid #5b92d1;
vertical-align: middle;
}
.divider-dot {
display: inline-block;
width: 0.04in;
height: 0.04in;
margin: 0 0.04in;
background: #1f6b3a;
width: 0.045in;
height: 0.045in;
margin: 0 0.045in;
background: #064297;
transform: rotate(45deg);
vertical-align: middle;
}
.role {
text-align: center;
color: #1f48b3;
font-size: 5.5pt;
font-weight: 800;
color: #063979;
font-size: 7.4pt;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 1.1px;
margin-bottom: 0.05in;
letter-spacing: 1.5px;
margin-bottom: 0.07in;
}
.content {
@@ -693,14 +723,15 @@ body {
}
.info {
width: 58%;
padding-right: 0.04in;
border-right: 1px solid #d6d6d6;
width: 52%;
padding-right: 0.07in;
border-right: 1px solid #cfcfcf;
}
.info-row {
margin-bottom: 0.06in;
padding-bottom: 0.00in;
margin-bottom: 0.07in;
padding-bottom: 0.04in;
border-bottom: 1px solid #d7d7d7;
}
.info-row:last-child {
@@ -711,55 +742,55 @@ body {
.icon-badge {
display: inline-block;
width: 0.18in;
height: 0.18in;
line-height: 0.18in;
margin-right: 0.03in;
width: 0.22in;
height: 0.22in;
line-height: 0.22in;
margin-right: 0.05in;
border-radius: 50%;
background: #156733;
background: #064297;
color: #ffffff;
text-align: center;
font-size: 4.8pt;
font-weight: 800;
font-size: 5.0pt;
font-weight: 900;
vertical-align: top;
}
.info-copy {
display: inline-block;
width: 0.78in;
width: 0.70in;
}
.label {
display: block;
font-size: 4.5pt;
font-weight: 700;
color: #222222;
font-size: 4.7pt;
font-weight: 900;
color: #111111;
margin-bottom: 0.01in;
text-transform: uppercase;
}
.value {
display: block;
font-size: 7.0pt;
font-weight: 800;
color: #111111;
font-size: 8.2pt;
font-weight: 900;
color: #064297;
word-wrap: break-word;
}
.qr-col {
width: 42%;
width: 48%;
text-align: center;
padding-left: 0.04in;
padding-left: 0.08in;
}
.qr-box {
display: inline-block;
border: 1px solid #1f6b3a;
border-radius: 0.08in;
padding: 0.03in;
border: 1.5px solid #064297;
border-radius: 0.06in;
padding: 0.04in;
background: #ffffff;
min-width: 0.76in;
min-height: 0.76in;
box-shadow: inset 0 0 0 1px #dce8df;
min-width: 0.78in;
min-height: 0.78in;
}
.qr-box img {
@@ -769,15 +800,15 @@ body {
}
.scan-pill {
margin-top: 0.025in;
margin-top: 0.035in;
display: inline-block;
background: #156733;
background: #064297;
color: #ffffff;
border-radius: 999px;
font-size: 4.5pt;
font-weight: 700;
border-radius: 0.03in;
font-size: 4.8pt;
font-weight: 900;
text-transform: uppercase;
padding: 0.025in 0.06in 0.02in;
padding: 0.035in 0.07in 0.025in;
}
.barcode-wrap {
@@ -786,8 +817,8 @@ body {
right: 0.10in;
bottom: 0.06in;
text-align: center;
border-top: 1px solid #d6ddd8;
padding-top: 0.06in;
border-top: 1px solid #3d7fc6;
padding-top: 0.05in;
}
.barcode-bars {
@@ -806,11 +837,7 @@ body {
}
.barcode-text {
margin-top: 0.03in;
text-align: center;
font-size: 5.8pt;
font-weight: 700;
color: #111111;
display: none;
}
.footer {
@@ -818,25 +845,25 @@ body {
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #0d6b32 0%, #0b5c2b 100%);
background: linear-gradient(135deg, #064297 0%, #052b64 100%);
color: #ffffff;
padding: 0.05in 0.06in 0.05in;
font-size: 4.3pt;
padding: 0.055in 0.07in 0.05in;
font-size: 5.2pt;
font-weight: 700;
line-height: 1.2;
}
.footer-mark {
display: inline-block;
width: 0.15in;
height: 0.15in;
line-height: 0.15in;
margin-right: 0.05in;
width: 0.25in;
height: 0.25in;
line-height: 0.25in;
margin-right: 0.07in;
border: 1.5px solid #ffffff;
border-radius: 50%;
text-align: center;
vertical-align: top;
font-size: 6pt;
font-size: 7pt;
overflow: hidden;
background: transparent;
}
@@ -849,13 +876,11 @@ body {
display: block;
}
.footer-mark span {
display: block;
}
.footer-copy {
display: inline-block;
width: 1.72in;
width: 1.65in;
padding-left: 0.07in;
border-left: 1px solid rgba(255,255,255,0.8);
vertical-align: top;
}
CSS;
@@ -914,16 +939,271 @@ CSS;
string $defaultStudentRoleLabel,
string $footerBlock,
?string $logoPath
): void {
$templatePath = $this->badgeTemplatePath($student);
if ($templatePath !== null) {
$this->drawBadgeFromTemplate(
$pdf,
$student,
$x,
$y,
$w,
$h,
$defaultStudentRoleLabel,
$footerBlock,
$templatePath
);
return;
}
// Safety fallback: if the template PNGs are not deployed yet, still render a usable badge.
$this->drawBadgeGeneratedFallback(
$pdf,
$student,
$x,
$y,
$w,
$h,
$schoolName,
$motto,
$defaultStudentRoleLabel,
$footerBlock,
$logoPath
);
}
/**
* Pick the static image template used as the badge background.
*
* Deploy these exact filenames to public/ or one of the supported public asset folders:
* - student_template.png
* - teacher_template.png
* - admin_template.png
*/
protected function badgeTemplatePath(array $row): ?string
{
$kind = strtolower((string) ($row['_badge_kind'] ?? 'student'));
$role = strtolower(trim((string) ($row['role_subline'] ?? $row['role_resolved'] ?? $row['roles'] ?? '')));
if ($kind === 'student') {
return $this->firstExistingPublicTemplate('student_template.png');
}
if (str_contains($role, 'admin')) {
return $this->firstExistingPublicTemplate('admin_template.png');
}
return $this->firstExistingPublicTemplate('teacher_template.png');
}
protected function firstExistingPublicTemplate(string $filename): ?string
{
return $this->formatter->firstExisting([
public_path($filename),
public_path('badges/' . $filename),
public_path('assets/images/' . $filename),
public_path('assets/images/badges/' . $filename),
storage_path('app/public/' . $filename),
storage_path('app/public/badges/' . $filename),
]);
}
protected function drawBadgeFromTemplate(
\FPDF $pdf,
array $row,
float $x,
float $y,
float $w,
float $h,
string $defaultStudentRoleLabel,
string $footerBlock,
string $templatePath
): void {
try {
$pdf->Image($templatePath, $x, $y, $w, $h);
} catch (Throwable) {
return;
}
$kind = strtolower((string) ($row['_badge_kind'] ?? 'student'));
$roleProbe = strtolower((string) ($row['role_subline'] ?? ''));
$isAdmin = $kind !== 'student' && str_contains($roleProbe, 'admin');
$name = strtoupper($this->formatter->toPdf((string) ($row['fullname'] ?? '')));
$firstValue = $this->formatter->toPdf((string) ($row['grade'] ?? ''));
$year = $this->formatter->toPdf((string) ($row['academic_year'] ?? ''));
$id = $this->formatter->toPdf((string) ($row['school_id'] ?? ''));
$qrPath = (string) ($row['_qr_tmp'] ?? '');
$barcodeValue = preg_replace('/[^A-Za-z0-9]/', '', (string) ($row['school_id'] ?? ''));
$barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000';
// Template coordinate map, in inches, relative to the badge's top-left corner.
// The PNG owns the visual design. PHP only overlays dynamic data.
$black = [18, 18, 18];
$white = [255, 255, 255];
// The new template leaves more white space above the name block, so start the overlay lower.
$this->fitCenteredText($pdf, $name, $x + 0.15, $y + 1.24, $w - 0.30, 0.18, 15.0, 7.0, $black, 'B');
// Values beside/under the static labels baked into the template.
$valueX = $x + 0.48;
$valueW = 0.78;
$valueYOffset = 0.18;
$firstValueYOffset = 0.01;
$this->fitLeftText($pdf, $firstValue, $valueX, $y + 2.02 + $valueYOffset + $firstValueYOffset, $valueW, 0.08, 8.0, 5.0, $black, 'B');
$this->fitLeftText($pdf, $year, $valueX, $y + 2.37 + $valueYOffset, $valueW, 0.08, 8.0, 5.0, $black, 'B');
$this->fitLeftText($pdf, $id, $valueX, $y + 2.72 + $valueYOffset, $valueW, 0.08, 8.0, 4.8, $black, 'B');
// QR overlay, inside the template frame.
if ($qrPath !== '' && is_file($qrPath)) {
try {
$pdf->Image($qrPath, $x + 1.45, $y + 1.90, 0.66, 0.66, 'PNG');
} catch (Throwable) {
}
}
// Barcode overlay. Repeat seed so the barcode fills the long template box instead of becoming a sad tiny fence.
$pdf->SetFillColor(17, 17, 17);
$this->drawBarcodeBars($pdf, str_repeat($barcodeValue, 6), $x + 0.35, $y + 3.07, $w - 0.70, 0.13);
// Footer dynamic text. The template already contains the logo and divider.
$pdf->SetTextColor($white[0], $white[1], $white[2]);
$pdf->SetFont('Helvetica', 'B', 4.2);
$lines = preg_split('/\r\n|\r|\n/', $footerBlock) ?: [$footerBlock];
$footerY = $y + $h - 0.25;
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '') {
continue;
}
$pdf->SetXY($x + 0.57, $footerY);
$pdf->Cell($w - 0.65, 0.05, $this->formatter->toPdf($line), 0, 1, 'L');
$footerY += 0.055;
}
}
/**
* Shrink text until it fits in the available width. FPDF, bravely, does not do this for us.
*
* @param array{0:int,1:int,2:int} $rgb
*/
protected function fitCenteredText(
\FPDF $pdf,
string $text,
float $x,
float $y,
float $w,
float $h,
float $maxPt,
float $minPt,
array $rgb,
string $style = ''
): float {
$text = trim($text);
if ($text === '') {
return $y;
}
$size = $maxPt;
$lines = $this->splitCenteredTextIntoTwoLines($text) ?? [$text];
do {
$pdf->SetFont('Helvetica', $style, $size);
$maxLineWidth = 0.0;
foreach ($lines as $line) {
$maxLineWidth = max($maxLineWidth, $pdf->GetStringWidth($line));
}
if ($maxLineWidth <= $w || $size <= $minPt) {
break;
}
$size -= 0.5;
} while ($size >= $minPt);
$pdf->SetFont('Helvetica', $style, $size);
$pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]);
$lineSpacing = $h * 1.18;
foreach ($lines as $index => $line) {
$pdf->SetXY($x, $y + ($index * $lineSpacing));
$pdf->Cell($w, $h, $line, 0, 0, 'C');
}
return $y + ((count($lines) - 1) * $lineSpacing) + $h;
}
/**
* @return array{0:string,1:string}|null
*/
protected function splitCenteredTextIntoTwoLines(string $text): ?array
{
$words = array_values(array_filter(explode(' ', preg_replace('/\s+/', ' ', trim($text)) ?: ''), 'strlen'));
if (count($words) < 2) {
return null;
}
return [
$words[0],
implode(' ', array_slice($words, 1)),
];
}
/**
* @param array{0:int,1:int,2:int} $rgb
*/
protected function fitLeftText(
\FPDF $pdf,
string $text,
float $x,
float $y,
float $w,
float $h,
float $maxPt,
float $minPt,
array $rgb,
string $style = ''
): void {
$text = trim($text);
if ($text === '') {
return;
}
$size = $maxPt;
do {
$pdf->SetFont('Helvetica', $style, $size);
if ($pdf->GetStringWidth($text) <= $w || $size <= $minPt) {
break;
}
$size -= 0.5;
} while ($size >= $minPt);
$pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]);
$pdf->SetXY($x, $y);
$pdf->Cell($w, $h, $text, 0, 0, 'L');
}
protected function drawBadgeGeneratedFallback(
\FPDF $pdf,
array $student,
float $x,
float $y,
float $w,
float $h,
string $schoolName,
string $motto,
string $defaultStudentRoleLabel,
string $footerBlock,
?string $logoPath
): void {
$kind = (string) ($student['_badge_kind'] ?? 'student');
$idLabel = $kind === 'staff' ? 'USER ID' : 'SCHOOL ID';
$showGrade = (bool) ($student['show_grade'] ?? ($kind === 'student'));
$gradeLabel = (string) ($student['grade_label'] ?? ($kind === 'staff' ? 'CLASS / ASSIGNMENT' : 'GRADE'));
$green = [21, 103, 51];
$greenDark = [13, 91, 43];
$greenAccent = [244, 211, 74];
$blueRole = [31, 72, 179];
$green = [6, 66, 151];
$greenDark = [5, 43, 100];
$greenAccent = [255, 212, 74];
$blueRole = [6, 57, 121];
$qrPath = (string) ($student['_qr_tmp'] ?? '');
$fullName = $this->formatter->toPdf((string) ($student['fullname'] ?? ''));
@@ -934,8 +1214,8 @@ CSS;
$barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000';
$barcodeText = $this->formatter->toPdf(str_repeat($barcodeValue, 4));
$schoolNamePdf = $this->formatter->toPdf($schoolName);
$headerSubtitlePdf = $this->formatter->toPdf((string) config('badges.header_subtitle', 'High School'));
$schoolNamePdf = $this->formatter->toPdf($this->headerMainTitle($schoolName));
$headerSubtitlePdf = $this->formatter->toPdf($this->headerSubtitle($schoolName));
$mottoValue = trim($motto) !== ''
? $motto
: (string) config('badges.motto_fallback', 'Learn • Lead • Succeed');
@@ -949,11 +1229,12 @@ CSS;
$headerH = 0.66;
$footerH = 0.36;
$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');
$pdf->Rect($x, $y, $w, $headerH, 'F');
$pdf->SetFillColor($green[0], $green[1], $green[2]);
$pdf->Rect($x, $y + 0.04, $w, 0.20, 'F');
$pdf->SetFillColor(141, 183, 232);
$pdf->Rect($x, $y + $headerH - 0.03, $w, 0.03, 'F');
$sealSize = 0.33;
$sealX = $x + 0.09;
@@ -970,33 +1251,42 @@ CSS;
}
$pdf->SetTextColor(255, 255, 255);
$pdf->SetFont('Helvetica', 'B', 8.2);
$pdf->SetFont('Helvetica', 'B', 13.0);
$pdf->SetXY($x + 0.45, $y + 0.10);
$pdf->Cell($w - 0.55, 0.08, $schoolNamePdf, 0, 1, 'C');
$pdf->Cell($w - 0.55, 0.12, strtoupper($schoolNamePdf), 0, 1, 'C');
$pdf->SetFont('Helvetica', 'B', 5.8);
$pdf->SetFont('Helvetica', 'B', 7.2);
$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->SetFont('Helvetica', 'B', 5.6);
$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.02;
$pdf->SetTextColor(21, 81, 44);
$pdf->SetFont('Helvetica', 'B', 10.2);
$pdf->SetXY($x + 0.07, $bodyTop);
$pdf->Cell($w - 0.14, 0.11, strtoupper($fullName), 0, 1, 'C');
$nameBottomY = $this->fitCenteredText(
$pdf,
strtoupper($fullName),
$x + 0.07,
$bodyTop,
$w - 0.14,
0.11,
13.2,
7.0,
[18, 18, 18],
'B'
);
$pdf->SetTextColor($blueRole[0], $blueRole[1], $blueRole[2]);
$pdf->SetTextColor(18, 18, 18);
$pdf->SetFont('Helvetica', 'B', 6);
$pdf->SetX($x + 0.07);
$roleTop = $nameBottomY + 0.01;
$pdf->SetXY($x + 0.07, $roleTop);
$pdf->Cell($w - 0.14, 0.07, strtoupper($rolePdf), 0, 1, 'C');
$ruleY = $bodyTop + 0.20;
$ruleY = $roleTop + 0.10;
$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);
@@ -1080,7 +1370,7 @@ CSS;
$pdf->Cell($barsW, 0.05, $barcodeText, 0, 0, 'C');
$fy = $y + $h - $footerH;
$pdf->SetFillColor($greenDark[0], $greenDark[1], $greenDark[2]);
$pdf->SetFillColor($green[0], $green[1], $green[2]);
$pdf->Rect($x, $fy, $w, $footerH, 'F');
if ($logoPath !== null && is_readable($logoPath)) {
@@ -1119,8 +1409,9 @@ CSS;
float $height
): void {
$seed = $value !== '' ? strtoupper($value) : 'ID0000';
$cursor = $x;
$gap = 0.01;
$segments = [];
$usedWidth = 0.0;
foreach (str_split($seed) as $char) {
$width = match (ord($char) % 4) {
@@ -1130,20 +1421,38 @@ CSS;
default => 0.025,
};
if (($cursor + $width) > ($x + $maxWidth)) {
$barWidth = $width;
$thinBarWidth = 0.01;
$nextWidth = ($segments === [] ? 0.0 : $gap) + $barWidth + $gap + $thinBarWidth;
if (($usedWidth + $nextWidth) > $maxWidth) {
break;
}
if ($segments !== []) {
$segments[] = ['gap', $gap];
$usedWidth += $gap;
}
$segments[] = ['bar', $barWidth];
$usedWidth += $barWidth;
$segments[] = ['gap', $gap];
$usedWidth += $gap;
$segments[] = ['bar', $thinBarWidth];
$usedWidth += $thinBarWidth;
}
$cursor = $x + max(($maxWidth - $usedWidth) / 2, 0.0);
foreach ($segments as [$type, $segmentWidth]) {
if ($type === 'gap') {
$cursor += $segmentWidth;
continue;
}
$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;
$pdf->Rect($cursor, $y, $segmentWidth, $height, 'F');
$cursor += $segmentWidth;
}
}
@@ -1238,6 +1547,34 @@ CSS;
return 'data:image/png;base64,' . base64_encode($bytes);
}
protected function headerMainTitle(string $schoolName): string
{
$name = trim($schoolName) !== '' ? trim($schoolName) : 'Al Rahma Sunday School';
$normalized = preg_replace('/\s+/', ' ', $name) ?: $name;
if (preg_match('/^(.*?)(\s+Sunday\s+School)$/i', $normalized, $matches)) {
return trim($matches[1]);
}
return $normalized;
}
protected function headerSubtitle(string $schoolName): string
{
$configured = trim((string) config('badges.header_subtitle', ''));
if ($configured !== '') {
return $configured;
}
$name = trim($schoolName) !== '' ? trim($schoolName) : 'Al Rahma Sunday School';
if (preg_match('/Sunday\s+School/i', $name)) {
return 'Sunday School';
}
return 'High School';
}
protected function footerBlock(string $schoolName): string
{
$address = trim((string) config('badges.footer_address', ''));