diff --git a/assets/logo.jpeg b/assets/logo.jpeg new file mode 100644 index 0000000..34be84d Binary files /dev/null and b/assets/logo.jpeg differ diff --git a/pdf.js b/pdf.js index 869dbd1..f908553 100644 --- a/pdf.js +++ b/pdf.js @@ -38,12 +38,58 @@ return out.length ? out : ['']; } + // Compact a degree name for display: + // "BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY" -> "BS IT". + // Multi-word fields become initials; single-word fields stay title-cased. + function compactProgram(program) { + const s = String(program || '').trim().toUpperCase(); + let m = /^BACHELOR OF SCIENCE IN (.+)$/.exec(s); + let prefix = 'BS'; + if (!m) { + m = /^BACHELOR OF ARTS IN (.+)$/.exec(s); + if (m) prefix = 'AB'; + } + if (!m) return String(program || '').trim(); + const field = m[1]; + const words = field.split(/\s+/).filter((w) => !/^(AND|OF|THE|IN)$/.test(w)); + if (words.length <= 1) { + const single = words[0] || field; + return prefix + ' ' + single.charAt(0).toUpperCase() + single.slice(1).toLowerCase(); + } + return prefix + ' ' + words.map((w) => w[0]).join(''); + } + + // Map a semester to the official date range label. + function deriveDateRange(semester) { + const s = String(semester || '').toUpperCase(); + if (/1ST|FIRST/.test(s)) return 'AUGUST - JANUARY'; + if (/2ND|SECOND/.test(s)) return 'JANUARY - MAY'; + if (/SUMMER/.test(s)) return 'JUNE - JULY'; + return ''; + } + + // Load the bundled header logo (JPEG) in the extension background page. + // Returns null in Node (tests) or if the asset is unavailable. + async function loadLogo() { + try { + const url = (typeof browser !== 'undefined' && browser.runtime && browser.runtime.getURL) + ? browser.runtime.getURL('assets/logo.jpeg') + : null; + if (!url) return null; + const resp = await fetch(url); + if (!resp.ok) return null; + return new Uint8Array(await resp.arrayBuffer()); + } catch (e) { + return null; + } + } + // Builds a Letter-size PDF replicating the official ISMIS Study Load layout: - // title block (STUDENT'S STUDY LOAD, semester, date range, date enrolled), - // a student info row (ID, name, program, Year Level), the course table - // (GROUP, COURSE, COURSE DESCRIPTION, TEACHER, UNITS, NOTE), and a footer - // (total units, printed-by). NOTE is the status; UNITS/NOTE are right-aligned; - // schedule is stacked under the course description. + // header logo, title, semester + date range, date enrolled, a student info + // row (ID, name, program, Year Level), the course table (GROUP, COURSE, + // COURSE DESCRIPTION, TEACHER, UNITS, NOTE), and a footer (total under the + // UNITS column, printed-by). NOTE is the status; schedule is stacked under + // the course description. async function buildStudyLoadPdf(data) { const PDFLib = getPDFLib(); const { PDFDocument, StandardFonts, rgb } = PDFLib; @@ -62,6 +108,12 @@ const black = rgb(0, 0, 0); const gray = rgb(0.3, 0.3, 0.3); + const logoBytes = await loadLogo(); + let logoImg = null; + if (logoBytes) { + try { logoImg = await doc.embedJpg(logoBytes); } catch (e) { logoImg = null; } + } + function center(text, size, f, dy) { const w = f.widthOfTextAtSize(text, size); page.drawText(text, { x: (612 - w) / 2, y, size, font: f, color: black }); @@ -76,16 +128,23 @@ } // ---- Header ---- - center("STUDENT'S STUDY LOAD", 9.5, bold, 18); - if (header.semester) center(header.semester, 9, font, 15); - if (header.dateRange) center(header.dateRange, 9, font, 15); + if (logoImg) { + const logoW = 292.5; + const logoH = logoW * (logoImg.height / logoImg.width); + page.drawImage(logoImg, { x: (612 - logoW) / 2, y: y - logoH, width: logoW, height: logoH }); + y -= logoH + 14; + } + center("STUDENT'S STUDY LOAD", 9.5, bold, 16); + if (header.semester) center(String(header.semester).toUpperCase(), 9, font, 14); + const dateRange = header.dateRange || deriveDateRange(header.semester); + if (dateRange) center(dateRange, 9, font, 14); if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 8, italic, margin, 13); - // Student info row: ID (bold), name, program, "Year Level: N" + // Student info row: ID (bold), name, program (compacted), "Year Level: N" const studentParts = []; if (header.studentId) studentParts.push({ text: header.studentId, f: bold }); if (header.fullName) studentParts.push({ text: header.fullName, f: font }); - if (header.program) studentParts.push({ text: header.program, f: font }); + if (header.program) studentParts.push({ text: compactProgram(header.program), f: font }); if (header.yearLevel) studentParts.push({ text: 'Year Level: ' + header.yearLevel, f: font }); if (studentParts.length) { let sx = margin; @@ -137,7 +196,6 @@ } if (isHeader) { - // top line above the header row hline(margin, margin + contentWidth, y); } @@ -159,7 +217,6 @@ y -= rowHeight; if (isHeader) { - // line below the header row hline(margin, margin + contentWidth, y); } return true; @@ -179,7 +236,7 @@ courseCode: course.courseCode, description: course.description, schedule: course.schedule, - teacher: course.teacher, + teacher: course.teacher || course.faculty, units: course.units, status: course.status }; @@ -193,8 +250,10 @@ } y -= 8; if (data.totalUnits) { + // Total sits under the UNITS column, right-aligned within it. + const unitsCol = cols[4]; const w = bold.widthOfTextAtSize(data.totalUnits, 9); - page.drawText(data.totalUnits, { x: margin + contentWidth - w, y, size: 9, font: bold, color: black }); + page.drawText(data.totalUnits, { x: unitsCol.x + unitsCol.w - 3 - w, y, size: 9, font: bold, color: black }); y -= 14; } hline(margin, margin + contentWidth, y); @@ -206,5 +265,5 @@ return doc.save(); // returns Promise } - return { buildStudyLoadPdf }; + return { buildStudyLoadPdf, compactProgram, deriveDateRange }; }); diff --git a/test/pdf.test.js b/test/pdf.test.js index 0f2883d..468c556 100644 --- a/test/pdf.test.js +++ b/test/pdf.test.js @@ -5,7 +5,7 @@ const os = require('node:os'); const path = require('node:path'); const { execFileSync } = require('node:child_process'); global.PDFLib = require('pdf-lib'); -const { buildStudyLoadPdf } = require('../pdf.js'); +const { buildStudyLoadPdf, compactProgram, deriveDateRange } = require('../pdf.js'); const sampleData = { header: { @@ -49,4 +49,21 @@ test('buildStudyLoadPdf returns valid PDF bytes', async () => { const text = extractPdfText(bytes); assert.ok(text.includes('CIS 2101'), 'PDF text should contain course code CIS 2101'); assert.ok(text.includes("STUDENT'S STUDY LOAD"), 'PDF text should contain the title'); + assert.ok(text.includes('CHRISTIAN MADERAZO'), 'PDF text should contain the teacher'); + assert.ok(text.includes('BS IT'), 'PDF text should contain the compacted program'); +}); + +test('compactProgram shortens degree names', () => { + assert.strictEqual(compactProgram('BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY'), 'BS IT'); + assert.strictEqual(compactProgram('BACHELOR OF SCIENCE IN COMPUTER SCIENCE'), 'BS CS'); + assert.strictEqual(compactProgram('BACHELOR OF ARTS IN POLITICAL SCIENCE'), 'AB PS'); + assert.strictEqual(compactProgram('BACHELOR OF SCIENCE IN ACCOUNTANCY'), 'BS Accountancy'); + assert.strictEqual(compactProgram('UNKNOWN PROGRAM'), 'UNKNOWN PROGRAM'); +}); + +test('deriveDateRange maps semesters', () => { + assert.strictEqual(deriveDateRange('1ST Semester 2026'), 'AUGUST - JANUARY'); + assert.strictEqual(deriveDateRange('2ND Semester 2026'), 'JANUARY - MAY'); + assert.strictEqual(deriveDateRange('Summer 2026'), 'JUNE - JULY'); + assert.strictEqual(deriveDateRange(''), ''); });