feat: build study load PDF
This commit is contained in:
@@ -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<Uint8Array>
|
||||||
|
}
|
||||||
|
|
||||||
|
return { buildStudyLoadPdf };
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user