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'; $barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000';
$barcodeText = $this->escapeHtml(str_repeat($barcodeValue, 4)); $barcodeText = $this->escapeHtml(str_repeat($barcodeValue, 4));
$showGrade = (bool) ($row['show_grade'] ?? ($kind === 'student')); $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) !== '' $headerMotto = trim($motto) !== ''
? trim($motto) ? trim($motto)
: trim((string) config('badges.motto_fallback', '')); : trim((string) config('badges.motto_fallback', 'Learn • Lead • Succeed'));
$infoRows = ''; $infoRows = '';
if ($showGrade) { if ($showGrade) {
@@ -436,7 +437,7 @@ class BadgePdfService
: '<span>AS</span>') . : '<span>AS</span>') .
'</div> '</div>
<div class="header-copy"> <div class="header-copy">
<div class="school-name">' . $this->escapeHtml($schoolName) . '</div> <div class="school-name">' . $this->escapeHtml($headerMainTitle) . '</div>
' . ($schoolLevel !== '' ' . ($schoolLevel !== ''
? '<div class="sub-school">' . $this->escapeHtml($schoolLevel) . '</div>' ? '<div class="sub-school">' . $this->escapeHtml($schoolLevel) . '</div>'
: '') . ' : '') . '
@@ -508,7 +509,7 @@ class BadgePdfService
body { body {
margin: 0; margin: 0;
font-family: DejaVu Sans, Arial, Helvetica, sans-serif; font-family: DejaVu Sans, Arial, Helvetica, sans-serif;
color: #111; color: #111111;
} }
.page { .page {
@@ -536,149 +537,178 @@ body {
.badge { .badge {
width: 2.18in; width: 2.18in;
height: 3.26in; height: 3.26in;
border: 1.5px solid #166833; border: 1.2px solid #d8d8d8;
border-radius: 0.18in; border-radius: 0.18in;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
background: #ffffff; 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 { .header-wrap {
position: relative; position: relative;
height: 0.80in; height: 0.96in;
} }
.header { .header {
background: linear-gradient(135deg, #0d6b32 0%, #13763a 45%, #0d5a29 100%); background: linear-gradient(135deg, #063979 0%, #082f68 55%, #05285a 100%);
color: #ffffff; color: #ffffff;
padding: 0.06in 0.08in 0.04in; padding: 0.07in 0.08in 0.04in;
height: 0.58in; height: 0.70in;
border-radius: 0.18in 0.18in 0 0; 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 { .header-accent {
position: absolute; position: absolute;
left: 0.36in; left: 0;
right: 0.36in; right: 0;
top: 0.45in; top: 0.67in;
height: 0.012in; height: 0.20in;
background: rgba(255, 211, 74, 0.45); background: #07336f;
clip-path: polygon(0 0, 50% 100%, 100% 0);
} }
.header-tail { .header-tail {
width: 1.14in; display: none;
height: 0.10in;
margin: -0.01in auto 0;
background: #13763a;
border-radius: 0 0 0.04in 0.04in;
} }
.header-mark { .header-mark {
float: left; float: left;
width: 0.30in; width: 0.39in;
height: 0.30in; height: 0.39in;
border: 2px solid #ffffff; border: 2px solid #ffffff;
border-radius: 50%; border-radius: 0.02in;
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
background: transparent; background: transparent;
margin-top: 0.02in;
} }
.header-mark img { .header-mark img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
border-radius: 50%;
display: block; display: block;
} }
.header-mark span { .header-mark span {
display: block; display: block;
line-height: 0.36in; line-height: 0.38in;
font-size: 12pt; font-size: 11pt;
font-weight: 800; font-weight: 800;
} }
.header-copy { .header-copy {
margin-left: 0.38in; margin-left: 0.45in;
text-align: center; text-align: center;
} }
.school-name { .school-name {
font-size: 7.2pt; font-size: 17.5pt;
font-weight: 800; font-weight: 900;
line-height: 1.1; line-height: 0.90;
margin: 0; margin: 0;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 1px;
color: #ffffff;
} }
.sub-school { .sub-school {
font-size: 5.0pt; font-size: 8.6pt;
font-weight: 800; font-weight: 900;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1.9px; letter-spacing: 1.7px;
margin-top: 0.03in; 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 { .motto {
font-size: 4.4pt; font-size: 6.5pt;
font-weight: 700; font-weight: 900;
margin-top: 0.03in; margin-top: 0.03in;
letter-spacing: 0.4px; letter-spacing: 0.8px;
min-height: 0.08in; min-height: 0.08in;
text-transform: uppercase; 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 { .body {
position: relative; position: relative;
height: 2.18in; height: 1.94in;
padding: 0.02in 0.09in 0.42in; padding: 0.09in 0.09in 0.39in;
} }
.student-name { .student-name {
text-align: center; text-align: center;
color: #15512c; color: #052d6f;
font-size: 9.6pt; font-size: 14.0pt;
font-weight: 800; font-weight: 900;
line-height: 1.05; line-height: 1.05;
text-transform: uppercase; text-transform: uppercase;
margin: 0.03in 0 0.02in; margin: 0.01in 0 0.03in;
letter-spacing: 1px;
} }
.name-divider { .name-divider {
text-align: center; text-align: center;
margin-bottom: 0.02in; margin-bottom: 0.03in;
} }
.divider-line { .divider-line {
display: inline-block; display: inline-block;
width: 0.48in; width: 0.65in;
border-top: 1px solid #cfd6d1; border-top: 1px solid #5b92d1;
vertical-align: middle; vertical-align: middle;
} }
.divider-dot { .divider-dot {
display: inline-block; display: inline-block;
width: 0.04in; width: 0.045in;
height: 0.04in; height: 0.045in;
margin: 0 0.04in; margin: 0 0.045in;
background: #1f6b3a; background: #064297;
transform: rotate(45deg); transform: rotate(45deg);
vertical-align: middle; vertical-align: middle;
} }
.role { .role {
text-align: center; text-align: center;
color: #1f48b3; color: #063979;
font-size: 5.5pt; font-size: 7.4pt;
font-weight: 800; font-weight: 900;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1.1px; letter-spacing: 1.5px;
margin-bottom: 0.05in; margin-bottom: 0.07in;
} }
.content { .content {
@@ -693,14 +723,15 @@ body {
} }
.info { .info {
width: 58%; width: 52%;
padding-right: 0.04in; padding-right: 0.07in;
border-right: 1px solid #d6d6d6; border-right: 1px solid #cfcfcf;
} }
.info-row { .info-row {
margin-bottom: 0.06in; margin-bottom: 0.07in;
padding-bottom: 0.00in; padding-bottom: 0.04in;
border-bottom: 1px solid #d7d7d7;
} }
.info-row:last-child { .info-row:last-child {
@@ -711,55 +742,55 @@ body {
.icon-badge { .icon-badge {
display: inline-block; display: inline-block;
width: 0.18in; width: 0.22in;
height: 0.18in; height: 0.22in;
line-height: 0.18in; line-height: 0.22in;
margin-right: 0.03in; margin-right: 0.05in;
border-radius: 50%; border-radius: 50%;
background: #156733; background: #064297;
color: #ffffff; color: #ffffff;
text-align: center; text-align: center;
font-size: 4.8pt; font-size: 5.0pt;
font-weight: 800; font-weight: 900;
vertical-align: top; vertical-align: top;
} }
.info-copy { .info-copy {
display: inline-block; display: inline-block;
width: 0.78in; width: 0.70in;
} }
.label { .label {
display: block; display: block;
font-size: 4.5pt; font-size: 4.7pt;
font-weight: 700; font-weight: 900;
color: #222222; color: #111111;
margin-bottom: 0.01in; margin-bottom: 0.01in;
text-transform: uppercase;
} }
.value { .value {
display: block; display: block;
font-size: 7.0pt; font-size: 8.2pt;
font-weight: 800; font-weight: 900;
color: #111111; color: #064297;
word-wrap: break-word; word-wrap: break-word;
} }
.qr-col { .qr-col {
width: 42%; width: 48%;
text-align: center; text-align: center;
padding-left: 0.04in; padding-left: 0.08in;
} }
.qr-box { .qr-box {
display: inline-block; display: inline-block;
border: 1px solid #1f6b3a; border: 1.5px solid #064297;
border-radius: 0.08in; border-radius: 0.06in;
padding: 0.03in; padding: 0.04in;
background: #ffffff; background: #ffffff;
min-width: 0.76in; min-width: 0.78in;
min-height: 0.76in; min-height: 0.78in;
box-shadow: inset 0 0 0 1px #dce8df;
} }
.qr-box img { .qr-box img {
@@ -769,15 +800,15 @@ body {
} }
.scan-pill { .scan-pill {
margin-top: 0.025in; margin-top: 0.035in;
display: inline-block; display: inline-block;
background: #156733; background: #064297;
color: #ffffff; color: #ffffff;
border-radius: 999px; border-radius: 0.03in;
font-size: 4.5pt; font-size: 4.8pt;
font-weight: 700; font-weight: 900;
text-transform: uppercase; text-transform: uppercase;
padding: 0.025in 0.06in 0.02in; padding: 0.035in 0.07in 0.025in;
} }
.barcode-wrap { .barcode-wrap {
@@ -786,8 +817,8 @@ body {
right: 0.10in; right: 0.10in;
bottom: 0.06in; bottom: 0.06in;
text-align: center; text-align: center;
border-top: 1px solid #d6ddd8; border-top: 1px solid #3d7fc6;
padding-top: 0.06in; padding-top: 0.05in;
} }
.barcode-bars { .barcode-bars {
@@ -806,11 +837,7 @@ body {
} }
.barcode-text { .barcode-text {
margin-top: 0.03in; display: none;
text-align: center;
font-size: 5.8pt;
font-weight: 700;
color: #111111;
} }
.footer { .footer {
@@ -818,25 +845,25 @@ body {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: linear-gradient(135deg, #0d6b32 0%, #0b5c2b 100%); background: linear-gradient(135deg, #064297 0%, #052b64 100%);
color: #ffffff; color: #ffffff;
padding: 0.05in 0.06in 0.05in; padding: 0.055in 0.07in 0.05in;
font-size: 4.3pt; font-size: 5.2pt;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
} }
.footer-mark { .footer-mark {
display: inline-block; display: inline-block;
width: 0.15in; width: 0.25in;
height: 0.15in; height: 0.25in;
line-height: 0.15in; line-height: 0.25in;
margin-right: 0.05in; margin-right: 0.07in;
border: 1.5px solid #ffffff; border: 1.5px solid #ffffff;
border-radius: 50%; border-radius: 50%;
text-align: center; text-align: center;
vertical-align: top; vertical-align: top;
font-size: 6pt; font-size: 7pt;
overflow: hidden; overflow: hidden;
background: transparent; background: transparent;
} }
@@ -849,13 +876,11 @@ body {
display: block; display: block;
} }
.footer-mark span {
display: block;
}
.footer-copy { .footer-copy {
display: inline-block; 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; vertical-align: top;
} }
CSS; CSS;
@@ -914,16 +939,271 @@ CSS;
string $defaultStudentRoleLabel, string $defaultStudentRoleLabel,
string $footerBlock, string $footerBlock,
?string $logoPath ?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 { ): void {
$kind = (string) ($student['_badge_kind'] ?? 'student'); $kind = (string) ($student['_badge_kind'] ?? 'student');
$idLabel = $kind === 'staff' ? 'USER ID' : 'SCHOOL ID'; $idLabel = $kind === 'staff' ? 'USER ID' : 'SCHOOL ID';
$showGrade = (bool) ($student['show_grade'] ?? ($kind === 'student')); $showGrade = (bool) ($student['show_grade'] ?? ($kind === 'student'));
$gradeLabel = (string) ($student['grade_label'] ?? ($kind === 'staff' ? 'CLASS / ASSIGNMENT' : 'GRADE')); $gradeLabel = (string) ($student['grade_label'] ?? ($kind === 'staff' ? 'CLASS / ASSIGNMENT' : 'GRADE'));
$green = [21, 103, 51]; $green = [6, 66, 151];
$greenDark = [13, 91, 43]; $greenDark = [5, 43, 100];
$greenAccent = [244, 211, 74]; $greenAccent = [255, 212, 74];
$blueRole = [31, 72, 179]; $blueRole = [6, 57, 121];
$qrPath = (string) ($student['_qr_tmp'] ?? ''); $qrPath = (string) ($student['_qr_tmp'] ?? '');
$fullName = $this->formatter->toPdf((string) ($student['fullname'] ?? '')); $fullName = $this->formatter->toPdf((string) ($student['fullname'] ?? ''));
@@ -934,8 +1214,8 @@ CSS;
$barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000'; $barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000';
$barcodeText = $this->formatter->toPdf(str_repeat($barcodeValue, 4)); $barcodeText = $this->formatter->toPdf(str_repeat($barcodeValue, 4));
$schoolNamePdf = $this->formatter->toPdf($schoolName); $schoolNamePdf = $this->formatter->toPdf($this->headerMainTitle($schoolName));
$headerSubtitlePdf = $this->formatter->toPdf((string) config('badges.header_subtitle', 'High School')); $headerSubtitlePdf = $this->formatter->toPdf($this->headerSubtitle($schoolName));
$mottoValue = trim($motto) !== '' $mottoValue = trim($motto) !== ''
? $motto ? $motto
: (string) config('badges.motto_fallback', 'Learn • Lead • Succeed'); : (string) config('badges.motto_fallback', 'Learn • Lead • Succeed');
@@ -949,11 +1229,12 @@ CSS;
$headerH = 0.66; $headerH = 0.66;
$footerH = 0.36; $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->SetFillColor($greenDark[0], $greenDark[1], $greenDark[2]);
$pdf->Rect($x, $y + 0.18, $w, 0.18, 'F'); $pdf->Rect($x, $y, $w, $headerH, 'F');
$pdf->Rect($x + 0.22, $y + $headerH - 0.03, $w - 0.44, 0.10, '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; $sealSize = 0.33;
$sealX = $x + 0.09; $sealX = $x + 0.09;
@@ -970,33 +1251,42 @@ CSS;
} }
$pdf->SetTextColor(255, 255, 255); $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->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->SetXY($x + 0.45, $y + 0.27);
$pdf->Cell($w - 0.55, 0.06, strtoupper($headerSubtitlePdf), 0, 1, 'C'); $pdf->Cell($w - 0.55, 0.06, strtoupper($headerSubtitlePdf), 0, 1, 'C');
if ($mottoPdf !== '') { if ($mottoPdf !== '') {
$pdf->SetFont('Helvetica', 'B', 5.2); $pdf->SetFont('Helvetica', 'B', 5.6);
$pdf->SetTextColor($greenAccent[0], $greenAccent[1], $greenAccent[2]); $pdf->SetTextColor($greenAccent[0], $greenAccent[1], $greenAccent[2]);
$pdf->SetXY($x + 0.45, $y + 0.40); $pdf->SetXY($x + 0.45, $y + 0.40);
$pdf->Cell($w - 0.55, 0.06, strtoupper($mottoPdf), 0, 1, 'C'); $pdf->Cell($w - 0.55, 0.06, strtoupper($mottoPdf), 0, 1, 'C');
} }
$bodyTop = $y + $headerH + 0.02; $bodyTop = $y + $headerH + 0.02;
$pdf->SetTextColor(21, 81, 44); $nameBottomY = $this->fitCenteredText(
$pdf->SetFont('Helvetica', 'B', 10.2); $pdf,
$pdf->SetXY($x + 0.07, $bodyTop); strtoupper($fullName),
$pdf->Cell($w - 0.14, 0.11, strtoupper($fullName), 0, 1, 'C'); $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->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'); $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->SetDrawColor(210, 214, 210);
$pdf->Line($x + 0.10, $ruleY, $x + 0.58, $ruleY); $pdf->Line($x + 0.10, $ruleY, $x + 0.58, $ruleY);
$pdf->Line($x + $w - 0.58, $ruleY, $x + $w - 0.10, $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'); $pdf->Cell($barsW, 0.05, $barcodeText, 0, 0, 'C');
$fy = $y + $h - $footerH; $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'); $pdf->Rect($x, $fy, $w, $footerH, 'F');
if ($logoPath !== null && is_readable($logoPath)) { if ($logoPath !== null && is_readable($logoPath)) {
@@ -1119,8 +1409,9 @@ CSS;
float $height float $height
): void { ): void {
$seed = $value !== '' ? strtoupper($value) : 'ID0000'; $seed = $value !== '' ? strtoupper($value) : 'ID0000';
$cursor = $x;
$gap = 0.01; $gap = 0.01;
$segments = [];
$usedWidth = 0.0;
foreach (str_split($seed) as $char) { foreach (str_split($seed) as $char) {
$width = match (ord($char) % 4) { $width = match (ord($char) % 4) {
@@ -1130,20 +1421,38 @@ CSS;
default => 0.025, 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; 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->SetFillColor(17, 17, 17);
$pdf->Rect($cursor, $y, $width, $height, 'F'); $pdf->Rect($cursor, $y, $segmentWidth, $height, 'F');
$cursor += $width + $gap; $cursor += $segmentWidth;
if (($cursor + 0.01) > ($x + $maxWidth)) {
break;
}
$pdf->Rect($cursor, $y, 0.01, $height, 'F');
$cursor += 0.01 + $gap;
} }
} }
@@ -1238,6 +1547,34 @@ CSS;
return 'data:image/png;base64,' . base64_encode($bytes); 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 protected function footerBlock(string $schoolName): string
{ {
$address = trim((string) config('badges.footer_address', '')); $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