diff --git a/pdf.js b/pdf.js new file mode 100644 index 0000000..e9d606e --- /dev/null +++ b/pdf.js @@ -0,0 +1,140 @@ +(function (root, factory) { + if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.ISMIStools = Object.assign(root.ISMIStools || {}, factory()); + } +})(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + + function getPDFLib() { + return (typeof self !== 'undefined' && self.PDFLib) || + (typeof globalThis !== 'undefined' && globalThis.PDFLib); + } + + function wrapText(text, font, size, maxWidth) { + const words = String(text).split(/\s+/).filter(Boolean); + const lines = []; + let line = ''; + for (const word of words) { + const candidate = line ? line + ' ' + word : word; + if (font.widthOfTextAtSize(candidate, size) <= maxWidth) { + line = candidate; + } else { + if (line) lines.push(line); + line = word; + } + } + if (line) lines.push(line); + return lines.length ? lines : ['']; + } + + async function buildStudyLoadPdf(data) { + const PDFLib = getPDFLib(); + const { PDFDocument, StandardFonts, rgb } = PDFLib; + const header = data.header || {}; + const courses = data.courses || []; + + const doc = await PDFDocument.create(); + const page = doc.addPage([612, 792]); // Letter + const font = doc.embedStandardFont(StandardFonts.Helvetica); + const bold = doc.embedStandardFont(StandardFonts.HelveticaBold); + const margin = 50; + const contentWidth = 612 - margin * 2; + let y = 792 - 50; + + 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 }); + y -= dy; + } + function left(text, size, fontToUse, x, dy) { + page.drawText(text, { x, y, size, font: fontToUse, color: black }); + y -= dy; + } + + // 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; + + // Table columns: NOTE, UNITS, TEACHER, COURSE DESCRIPTION, COURSE, GROUP + const cols = [ + { key: 'status', label: 'NOTE', x: margin, w: 40 }, + { key: 'units', label: 'UNITS', x: margin + 40, w: 45 }, + { key: 'teacher', label: 'TEACHER', x: margin + 85, w: 110 }, + { key: 'description', label: 'COURSE DESCRIPTION', x: margin + 195, w: 150 }, + { key: 'courseCode', label: 'COURSE', x: margin + 345, w: 70 }, + { key: 'groupSchedule', label: 'GROUP', x: margin + 415, w: 97 } + ]; + const cellSize = 9; + const headerSize = 9; + + 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.apply(null, linesPerCol.map((l) => l.length)) * 11 + 8; + if (y - rowHeight < 50) { + page.drawText('', { x: margin, y: y, size: 1, font, color: black }); + return false; + } + 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 }); + } + for (let i = 0; i < cols.length; i++) { + const c = cols[i]; + const lines = linesPerCol[i]; + let ty = y - 10; + for (const line of lines) { + page.drawText(line, { x: c.x + 3, y: ty, size, font: f, color: black }); + ty -= 11; + } + } + y -= rowHeight; + return true; + } + + const headerCells = {}; + cols.forEach((c) => { headerCells[c.key] = c.label; }); + drawRow(headerCells, true); + + for (const course of courses) { + const cells = { + status: course.status, + units: course.units, + teacher: course.teacher, + description: course.description, + courseCode: course.courseCode, + groupSchedule: (course.group ? course.group + ' ' : '') + (course.schedule || '') + }; + if (!drawRow(cells, false)) break; + } + + y -= 12; + if (data.totalUnits) { + page.drawText('Total Units: ' + data.totalUnits, { x: margin, y: y, size: 11, font: bold, color: black }); + y -= 16; + } + const printedBy = 'Printed by: ' + (header.studentId || '') + ' on ' + new Date().toLocaleDateString(); + page.drawText(printedBy, { x: margin, y: y, size: 9, font, color: gray }); + + return doc.save(); // returns Promise + } + + return { buildStudyLoadPdf }; +}); diff --git a/test/pdf.test.js b/test/pdf.test.js new file mode 100644 index 0000000..401dc17 --- /dev/null +++ b/test/pdf.test.js @@ -0,0 +1,30 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +global.PDFLib = require('pdf-lib'); +const { buildStudyLoadPdf } = require('../pdf.js'); + +const sampleData = { + header: { + fullName: 'ADRIAN ALFRED CHIONG BONPIN', + studentId: '22103625', + program: 'BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY', + yearLevel: '4', + semester: '1ST Semester 2026', + dateEnrolled: '8/5/2024', + dateRange: 'AUGUST - JANUARY' + }, + courses: [ + { group: '7', courseCode: 'CIS 2101', description: 'DATA STRUCTURES AND ALGORITHMS', units: '3.00', teacher: 'CHRISTIAN MADERAZO', schedule: 'M,W 03:00 PM - 05:30 PM LB443TC', status: 'REGULAR' }, + { group: '2', courseCode: 'IT 3103N', description: 'SYSTEMS INTEGRATION AND ARCHITECTURE', units: '3.00', teacher: 'KHENT DELA PAZ', schedule: 'T,Th 01:30 PM - 03:00 PM DCISMVR3TC', status: 'REGULAR' } + ], + totalUnits: '6.00' +}; + +test('buildStudyLoadPdf returns valid PDF bytes', async () => { + const bytes = await buildStudyLoadPdf(sampleData); + assert.ok(bytes instanceof Uint8Array); + const head = Buffer.from(bytes.slice(0, 5)).toString('latin1'); + assert.strictEqual(head, '%PDF-'); + const doc = await PDFLib.PDFDocument.load(bytes); + assert.strictEqual(doc.getPageCount(), 1); +});