add qr code to certificate

This commit is contained in:
root
2026-05-17 17:24:07 -04:00
parent af75475214
commit 3333e71c94
7 changed files with 531 additions and 72 deletions
+174 -72
View File
@@ -5,20 +5,25 @@ namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\ClassSectionModel;
use App\Models\ConfigurationModel;
use App\Models\CertificateRecordModel;
class CertificateController extends BaseController
{
protected $classSectionModel;
protected $configModel;
protected $certRecordModel;
protected $schoolYear;
public function __construct()
{
$this->classSectionModel = new ClassSectionModel();
$this->configModel = new ConfigurationModel();
$this->certRecordModel = new CertificateRecordModel();
$this->schoolYear = $this->configModel->getConfig('school_year');
}
// ─── Admin UI ──────────────────────────────────────────────────────────────
public function index()
{
$db = \Config\Database::connect();
@@ -59,17 +64,46 @@ class CertificateController extends BaseController
]);
}
public function auditLog()
{
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
$records = $this->certRecordModel->getAuditLog($schoolYear);
$yearSummary = $this->certRecordModel->yearSummary();
return view('admin/certificates/audit_log', [
'records' => $records,
'schoolYear' => $schoolYear,
'yearSummary' => $yearSummary,
]);
}
// ─── Public verification page ──────────────────────────────────────────────
public function verify(string $certNumber)
{
$record = \Config\Database::connect()
->table('certificate_records cr')
->select('cr.*, u.firstname AS admin_firstname, u.lastname AS admin_lastname')
->join('users u', 'u.id = cr.issued_by', 'left')
->where('cr.certificate_number', strtoupper($certNumber))
->get()->getRowArray();
return view('certificates/verify', ['record' => $record ?: null]);
}
// ─── PDF generation ────────────────────────────────────────────────────────
public function generate()
{
$studentIds = $this->request->getPost('student_ids') ?? [];
$certDate = trim($this->request->getPost('cert_date') ?? date('m/d/Y'));
$classSectionId = $this->request->getPost('class_section_id');
$schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear;
if (empty($studentIds)) {
return redirect()->to('administrator/certificates')->with('error', 'Please select at least one student.');
}
// Sanitize inputs
$studentIds = array_filter(array_map('intval', $studentIds));
$certDate = preg_replace('/[^0-9\/\-]/', '', $certDate);
@@ -81,6 +115,7 @@ class CertificateController extends BaseController
$students = [];
foreach ($studentIds as $id) {
$row = null;
if ($classSectionId) {
$row = $db->table('student_class sc')
->select('s.id, s.firstname, s.lastname, cs.class_section_name AS grade')
@@ -89,8 +124,6 @@ class CertificateController extends BaseController
->where('sc.class_section_id', (int) $classSectionId)
->where('s.id', $id)
->get()->getRowArray();
} else {
$row = null;
}
if (!$row) {
@@ -110,7 +143,28 @@ class CertificateController extends BaseController
return redirect()->to('administrator/certificates')->with('error', 'No valid students found.');
}
$pdfData = $this->buildPdf($students, $certDate);
$issuedBy = session()->get('user_id');
$issuedAt = date('Y-m-d H:i:s');
$certDateDb = $this->parseCertDate($certDate);
foreach ($students as &$student) {
$certNumber = $this->certRecordModel->nextNumber($schoolYear);
$this->certRecordModel->insert([
'certificate_number' => $certNumber,
'student_id' => $student['id'],
'student_name' => $student['firstname'] . ' ' . $student['lastname'],
'grade' => $this->formatGrade($student['grade'] ?? ''),
'cert_date' => $certDateDb,
'school_year' => $schoolYear,
'class_section_id' => $classSectionId ?: null,
'issued_by' => $issuedBy,
'issued_at' => $issuedAt,
]);
$student['cert_number'] = $certNumber;
}
unset($student);
$pdfData = $this->buildPdf($students, $certDate);
$filename = 'Certificates_' . date('Ymd_His') . '.pdf';
return $this->response
@@ -119,21 +173,72 @@ class CertificateController extends BaseController
->setBody($pdfData);
}
// ─── PDF generation ────────────────────────────────────────────────────────
// ─── Helpers ───────────────────────────────────────────────────────────────
private function parseCertDate(string $certDate): ?string
{
if (preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $certDate, $m)) {
return $m[3] . '-' . $m[1] . '-' . $m[2];
}
if (preg_match('#^\d{4}-\d{2}-\d{2}$#', $certDate)) {
return $certDate;
}
return null;
}
private function formatGrade(string $raw): string
{
$clean = trim($raw);
$lower = strtolower($clean);
if (preg_match('/^\d+$/', $clean) && (int)$clean >= 1 && (int)$clean <= 9) {
return 'Grade ' . $clean;
}
if ($lower === 'youth') {
return 'Youth';
}
if ($lower === 'kg') {
return 'Kindergarten';
}
return $clean;
}
/**
* Generates a QR code PNG for $url and returns its temporary file path.
* Caller must unlink() the file when done.
*/
private function makeQrTempFile(string $url): ?string
{
try {
$result = \Endroid\QrCode\Builder\Builder::create()
->writer(new \Endroid\QrCode\Writer\PngWriter())
->data($url)
->encoding(new \Endroid\QrCode\Encoding\Encoding('UTF-8'))
->size(300)
->margin(2)
->build();
$tmp = tempnam(sys_get_temp_dir(), 'cert_qr_') . '.png';
$result->saveToFile($tmp);
return $tmp;
} catch (\Throwable $e) {
log_message('error', 'Certificate QR generation failed: ' . $e->getMessage());
return null;
}
}
// ─── PDF rendering ─────────────────────────────────────────────────────────
private function buildPdf(array $students, string $certDate): string
{
$fontDir = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . 'fonts' . DIRECTORY_SEPARATOR;
$imgDir = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR;
// Convert TTF → TCPDF format on first run (cached in TCPDF's own fonts dir).
// K_PATH_FONTS must stay pointing at the TCPDF vendor fonts directory so that
// built-in fonts (helvetica, times, …) remain findable by SetFont().
$edwardianFont = \TCPDF_FONTS::addTTFfont($fontDir . 'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32);
$garamondBold = \TCPDF_FONTS::addTTFfont($fontDir . 'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32);
$ebGaramond = \TCPDF_FONTS::addTTFfont($fontDir . 'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32);
// A4 landscape in points: 841.89 × 595.28
$pdf = new \TCPDF('L', 'pt', 'A4', true, 'UTF-8', false);
$pdf->SetCreator('Al Rahma Sunday School');
$pdf->SetTitle('Student Certificates');
@@ -144,47 +249,45 @@ class CertificateController extends BaseController
$pdf->SetHeaderMargin(0);
$pdf->SetFooterMargin(0);
$W = $pdf->getPageWidth(); // 841.89 pt
$H = $pdf->getPageHeight(); // 595.28 pt
$W = $pdf->getPageWidth();
$H = $pdf->getPageHeight();
$tmpFiles = [];
foreach ($students as $student) {
$pdf->AddPage();
$name = $student['firstname'] . ' ' . $student['lastname'];
$grade = $this->formatGrade($student['grade'] ?? '');
$this->drawCertificate($pdf, $W, $H, $name, $grade, $certDate, $imgDir, $edwardianFont, $garamondBold, $ebGaramond);
$name = $student['firstname'] . ' ' . $student['lastname'];
$grade = $this->formatGrade($student['grade'] ?? '');
$certNumber = $student['cert_number'] ?? '';
// Build verification URL and generate QR temp file
$verifyUrl = base_url('verify/' . rawurlencode($certNumber));
$qrFile = $certNumber !== '' ? $this->makeQrTempFile($verifyUrl) : null;
if ($qrFile) {
$tmpFiles[] = $qrFile;
}
$this->drawCertificate(
$pdf, $W, $H,
$name, $grade, $certDate, $certNumber,
$qrFile,
$imgDir, $edwardianFont, $garamondBold, $ebGaramond
);
}
return $pdf->Output('', 'S');
}
$output = $pdf->Output('', 'S');
private function formatGrade(string $raw): string
{
$clean = trim($raw);
$lower = strtolower($clean);
// Numeric grades 1-9 → "Grade N"
if (preg_match('/^\d+$/', $clean) && (int)$clean >= 1 && (int)$clean <= 9) {
return 'Grade ' . $clean;
foreach ($tmpFiles as $f) {
@unlink($f);
}
// Youth variants
if ($lower === 'youth') {
return 'Youth';
}
// KG → Kindergarten
if ($lower === 'kg') {
return 'Kindergarten';
}
return $clean;
return $output;
}
/**
* Draws one certificate page. Coordinate mapping from the original C# iTextSharp code:
* iTextSharp uses (x, y) with origin at bottom-left, y increases upward.
* TCPDF uses (x, y) with origin at top-left, y increases downward.
* Conversion: y_tcpdf = H - y_iTextSharp
* Coordinate mapping: iTextSharp origin bottom-left → TCPDF origin top-left.
* y_tcpdf ≈ H y_iTextSharp
*/
private function drawCertificate(
\TCPDF $pdf,
@@ -193,32 +296,33 @@ class CertificateController extends BaseController
string $name,
string $grade,
string $certDate,
string $certNumber,
?string $qrFile,
string $imgDir,
string $edwardianFont,
string $garamondBold,
string $ebGaramond
): void {
// ── Title image: iTS SetAbsolutePosition(126, 450), ScaleToFit(600, 300)
// Bottom at y=450 from page bottom → top at H-450 ≈ 145. Place at top of page.
$pdf->Image($imgDir . 'title.png', 126, 0, 600);
// ── Background image: iTS position(280, 79), ScaleToFit(2500, 340)
// background.png is 336×393 px → ScaleToFit gives ≈291×340 pt.
// Top in TCPDF: H-79-340 = ~176
// ── Images
$pdf->Image($imgDir . 'title.png', 126, 0, 600);
$pdf->Image($imgDir . 'background.png', 280, 176, 291, 340);
$pdf->Image($imgDir . 'signature.png', 140, 399, 90, 90);
// ── Signature image: iTS position(140, 106), ScaleToFit(90, 90)
// Top in TCPDF: H-106-90 = ~399
$pdf->Image($imgDir . 'signature.png', 140, 399, 90, 90);
// ── QR code — small, bottom-right, just above the certificate number
$qrSize = 42; // pt
if ($qrFile !== null && file_exists($qrFile)) {
$qrX = $W - 10 - $qrSize; // right-aligned with the cert number text
$qrY = $H - 14 - 3 - $qrSize; // 3 pt gap above the cert number line
$pdf->Image($qrFile, $qrX, $qrY, $qrSize, $qrSize);
}
// ── "Presented to:" — iTS ALIGN_CENTER baseline y=420 → TCPDF y≈155
// ── "Presented to:"
$pdf->SetFont('times', 'B', 24);
$pdf->SetTextColor(0, 0, 0);
$pdf->SetXY(0, 151);
$pdf->Cell($W, 24, 'Presented to:', 0, 0, 'C');
// ── Student name — iTS baseline y=334, 38 pt Edwardian Script → TCPDF y≈219
// Bold effect: fill the glyph AND add a thin stroke around it (Edwardian has no bold variant).
// ── Student name (bold via fill + stroke)
$pdf->SetFont($edwardianFont, '', 38);
$pdf->SetDrawColor(0, 0, 0);
$pdf->setTextRenderingMode(0.4, true, false);
@@ -226,69 +330,67 @@ class CertificateController extends BaseController
$pdf->Cell($W, 38, $name, 0, 0, 'C');
$pdf->setTextRenderingMode(0, true, false);
// ── Line under name — iTS ALIGN_RIGHT at x=600, y=332 → TCPDF y≈243
// ── Line under name
$pdf->SetFont('times', '', 20);
$pdf->SetXY(0, 243);
$pdf->Cell(600, 20, '___________________________________', 0, 0, 'R');
// ── Description — iTS ALIGN_CENTER baseline y=302 → TCPDF y≈273
// ── Description
$pdf->SetFont($ebGaramond, '', 20);
$pdf->SetXY(0, 273);
$pdf->Cell($W, 20, 'for successfully completing the requirements of', 0, 0, 'C');
// ── Grade — iTS ALIGN_CENTER baseline y=265 → TCPDF y≈310
// ── Grade
$pdf->SetFont($garamondBold, '', 20);
$pdf->SetXY(0, 310);
$pdf->Cell($W, 20, $grade, 0, 0, 'C');
// ── "at" — iTS ALIGN_CENTER baseline y=232 → TCPDF y≈343
// ── "at"
$pdf->SetFont('times', '', 20);
$pdf->SetXY(0, 343);
$pdf->Cell($W, 20, 'at', 0, 0, 'C');
// ── School name — iTS ALIGN_CENTER baseline y=200 → TCPDF y≈375
// ── School name
$pdf->SetFont($garamondBold, '', 20);
$pdf->SetXY(0, 375);
$pdf->Cell($W, 20, 'Al Rahma Sunday School', 0, 0, 'C');
// ── Certificate date — iTS DrawGradientText at (598, 116), 26 pt Edwardian
// TCPDF y ≈ H-116-26 = ~453
// ── Date (gradient script)
$this->drawGradientText($pdf, $edwardianFont, 26, $certDate, 598, 453);
// ── Date underline — iTS ALIGN_RIGHT at x=742, y=110 → TCPDF y≈465
// ── Date underline + label
$pdf->SetFont('times', '', 20);
$pdf->SetXY(0, 463);
$pdf->Cell(742, 20, '_________________', 0, 0, 'R');
// ── "Date" label — iTS at (690, 78) → TCPDF y≈497, x≈650
$pdf->SetXY(650, 492);
$pdf->Cell(80, 20, 'Date', 0, 0, 'C');
// ── Signature underline — iTS ALIGN_LEFT at x=106, y=110 → TCPDF y≈465
// ── Signature underline + label
$pdf->SetXY(106, 463);
$pdf->Cell(200, 20, '_________________', 0, 0, 'L');
// ── "Signature" label — iTS at (190, 78) → TCPDF y≈497
$pdf->SetXY(106, 492);
$pdf->Cell(168, 20, 'Signature', 0, 0, 'C');
// ── Certificate number — bottom-right, small gray
if ($certNumber !== '') {
$pdf->SetFont('helvetica', '', 8);
$pdf->SetTextColor(150, 150, 150);
$pdf->SetXY(0, $H - 14);
$pdf->Cell($W - 10, 12, 'Certificate No. ' . $certNumber, 0, 0, 'R');
$pdf->SetTextColor(0, 0, 0);
}
}
/**
* Mimics the C# DrawGradientText — shadow passes give depth, final pass is solid black.
*/
private function drawGradientText(\TCPDF $pdf, string $fontName, float $fontSize, string $text, float $x, float $y): void
{
$pdf->SetFont($fontName, '', $fontSize);
// Shadow passes (progressively more opaque, slightly offset)
for ($i = 0; $i < 6; $i++) {
$alpha = ($i + 1) * 25 / 255; // 0.098 → 0.588
$pdf->setAlpha($alpha);
$pdf->setAlpha(($i + 1) * 25 / 255);
$pdf->SetTextColor(0, 0, 0);
$pdf->Text($x + $i / 5, $y + $i / 5, $text);
}
// Main solid pass
$pdf->setAlpha(1);
$pdf->SetTextColor(0, 0, 0);
$pdf->Text($x, $y, $text);