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.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
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', ''));
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB