diff --git a/pdf.js b/pdf.js index 0bdbfdd..869dbd1 100644 --- a/pdf.js +++ b/pdf.js @@ -29,6 +29,21 @@ return lines.length ? lines : ['']; } + // Wraps multi-line text (segments split on \n) into wrapped lines. + function wrapLines(text, font, size, maxWidth) { + const out = []; + String(text == null ? '' : text).split(/\n/).forEach((seg) => { + out.push(...wrapText(seg, font, size, maxWidth)); + }); + return out.length ? out : ['']; + } + + // 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. async function buildStudyLoadPdf(data) { const PDFLib = getPDFLib(); const { PDFDocument, StandardFonts, rgb } = PDFLib; @@ -39,6 +54,7 @@ let page = doc.addPage([612, 792]); // Letter const font = doc.embedStandardFont(StandardFonts.Helvetica); const bold = doc.embedStandardFont(StandardFonts.HelveticaBold); + const italic = doc.embedStandardFont(StandardFonts.HelveticaOblique); const margin = 50; const contentWidth = 612 - margin * 2; let y = 792 - 50; @@ -46,98 +62,146 @@ const black = rgb(0, 0, 0); const gray = rgb(0.3, 0.3, 0.3); - function center(text, size, fontToUse, dy) { - const w = fontToUse.widthOfTextAtSize(text, size); - page.drawText(text, { x: (612 - w) / 2, y: y, size, font: fontToUse, color: black }); + 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 }); y -= dy; } - function left(text, size, fontToUse, x, dy) { - page.drawText(text, { x, y, size, font: fontToUse, color: black }); + function left(text, size, f, x, dy) { + page.drawText(text, { x, y, size, font: f, color: black }); y -= dy; } + function hline(x1, x2, atY) { + page.drawRectangle({ x: x1, y: atY, width: x2 - x1, height: 0.75, color: black }); + } - // Header - center(header.semester || '', 14, bold, 20); - center("STUDENT'S STUDY LOAD", 16, bold, 24); - center(header.dateRange || '', 11, font, 18); - if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 10, font, margin, 16); - y -= 6; - if (header.studentId) left('Student ID: ' + header.studentId, 10, font, margin, 14); - if (header.fullName) left('Name: ' + header.fullName, 10, font, margin, 14); - if (header.program) left('Program: ' + header.program, 10, font, margin, 14); - if (header.yearLevel) left('Year Level: ' + header.yearLevel, 10, font, margin, 14); - y -= 10; + // ---- 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 (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 8, italic, margin, 13); - // Table columns: NOTE, UNITS, TEACHER, COURSE DESCRIPTION, COURSE, GROUP + // Student info row: ID (bold), name, program, "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.yearLevel) studentParts.push({ text: 'Year Level: ' + header.yearLevel, f: font }); + if (studentParts.length) { + let sx = margin; + for (const part of studentParts) { + const w = part.f.widthOfTextAtSize(part.text, 9); + page.drawText(part.text, { x: sx, y, size: 9, font: part.f, color: black }); + sx += w + 18; + } + y -= 16; + } + + // ---- Table: GROUP, COURSE, COURSE DESCRIPTION, TEACHER, UNITS, NOTE ---- const cols = [ - { key: 'status', label: 'NOTE', x: margin, w: 50 }, - { key: 'units', label: 'UNITS', x: margin + 50, w: 45 }, - { key: 'teacher', label: 'TEACHER', x: margin + 95, w: 110 }, - { key: 'description', label: 'COURSE DESCRIPTION', x: margin + 205, w: 150 }, - { key: 'courseCode', label: 'COURSE', x: margin + 355, w: 70 }, - { key: 'groupSchedule', label: 'GROUP', x: margin + 425, w: 87 } + { key: 'group', label: 'GROUP', x: margin, w: 26, align: 'left' }, + { key: 'courseCode', label: 'COURSE', x: margin + 26, w: 46, align: 'left' }, + { key: 'description', label: 'COURSE DESCRIPTION', x: margin + 72, w: 189, align: 'left' }, + { key: 'teacher', label: 'TEACHER', x: margin + 261, w: 123, align: 'left' }, + { key: 'units', label: 'UNITS', x: margin + 384, w: 31, align: 'right' }, + { key: 'status', label: 'NOTE', x: margin + 415, w: 97, align: 'right' } ]; - const cellSize = 9; - const headerSize = 9; + const cellSize = 8; + const headerSize = 8; + const lineH = 10; + + function rowLines(cells) { + return cols.map((c) => { + let lines; + if (c.key === 'description') { + lines = wrapLines(cells.description, font, cellSize, c.w - 6); + const sched = String(cells.schedule || ''); + if (sched) lines.push(...wrapText(sched, font, cellSize, c.w - 6)); + } else { + lines = wrapLines(cells[c.key], font, cellSize, c.w - 6); + } + return lines; + }); + } function drawRow(cells, isHeader) { const f = isHeader ? bold : font; const size = isHeader ? headerSize : cellSize; - const linesPerCol = cols.map((c) => wrapText(cells[c.key] || '', f, size, c.w - 6)); - const rowHeight = Math.max(...linesPerCol.map((l) => l.length)) * 11 + 8; - if (!isHeader && y - rowHeight < 50) { - // Row doesn't fit on the current page: start a new page and re-draw the header. + const linesPerCol = rowLines(cells); + const rowHeight = Math.max(...linesPerCol.map((l) => l.length)) * lineH + 8; + + if (y - rowHeight < 50) { page = doc.addPage([612, 792]); y = 792 - 50; drawHeaderRow(); } + if (isHeader) { - page.drawRectangle({ x: margin, y: y - rowHeight, width: contentWidth, height: rowHeight, color: rgb(0.85, 0.9, 0.95) }); - } - page.drawRectangle({ x: margin, y: y - rowHeight, width: contentWidth, height: rowHeight, borderColor: black, borderWidth: 0.5 }); - for (const c of cols) { - page.drawRectangle({ x: c.x, y: y - rowHeight, width: c.w, height: rowHeight, borderColor: black, borderWidth: 0.5 }); + // top line above the header row + hline(margin, margin + contentWidth, y); } + for (let i = 0; i < cols.length; i++) { const c = cols[i]; const lines = linesPerCol[i]; - let ty = y - 10; + let ty = y - 9; for (const line of lines) { - page.drawText(line, { x: c.x + 3, y: ty, size, font: f, color: black }); - ty -= 11; + if (c.align === 'right') { + const w = f.widthOfTextAtSize(line, size); + page.drawText(line, { x: c.x + c.w - 3 - w, y: ty, size, font: f, color: black }); + } else { + page.drawText(line, { x: c.x + 3, y: ty, size, font: f, color: black }); + } + ty -= lineH; } } + y -= rowHeight; + + if (isHeader) { + // line below the header row + hline(margin, margin + contentWidth, y); + } return true; } function drawHeaderRow() { - const headerCells = {}; - cols.forEach((c) => { headerCells[c.key] = c.label; }); - drawRow(headerCells, true); + const cells = {}; + cols.forEach((c) => { cells[c.key] = c.label; }); + drawRow(cells, true); } drawHeaderRow(); for (const course of courses) { const cells = { - status: course.status, - units: course.units, - teacher: course.teacher, - description: course.description, + group: course.group, courseCode: course.courseCode, - groupSchedule: (course.group ? course.group + ' ' : '') + (course.schedule || '') + description: course.description, + schedule: course.schedule, + teacher: course.teacher, + units: course.units, + status: course.status }; - if (!drawRow(cells, false)) break; + drawRow(cells, false); } - y -= 12; - if (data.totalUnits) { - page.drawText('Total Units: ' + data.totalUnits, { x: margin, y: y, size: 11, font: bold, color: black }); - y -= 16; + // ---- Footer ---- + if (y < 90) { + page = doc.addPage([612, 792]); + y = 792 - 50; } + y -= 8; + if (data.totalUnits) { + const w = bold.widthOfTextAtSize(data.totalUnits, 9); + page.drawText(data.totalUnits, { x: margin + contentWidth - w, y, size: 9, font: bold, color: black }); + y -= 14; + } + hline(margin, margin + contentWidth, y); + y -= 12; const printedBy = 'Printed by: ' + (header.studentId || '') + ' on ' + new Date().toLocaleDateString(); - page.drawText(printedBy, { x: margin, y: y, size: 9, font, color: gray }); + const pw = font.widthOfTextAtSize(printedBy, 8); + page.drawText(printedBy, { x: margin + contentWidth - pw, y, size: 8, font, color: gray }); return doc.save(); // returns Promise }