badge creation
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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 |
Reference in New Issue
Block a user