Files
ismis-helper/pdf.js
T

268 lines
9.8 KiB
JavaScript

(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 : [''];
}
// 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 : [''];
}
// 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:
// 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;
const header = data.header || {};
const courses = data.courses || [];
const doc = await PDFDocument.create();
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 = 36;
const contentWidth = 612 - margin * 2; // 540; right edge = 576
let y = 792 - 10; // near the very top, matching the reference logo position
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 });
y -= dy;
}
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 ----
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 + 10;
}
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, laid out in fixed zones across the page (matching the
// reference): ID (bold) left, name at ~100, program at ~338, and
// "Year Level: N" right-aligned to the right content edge (576).
const yearLevelText = header.yearLevel ? 'Year Level: ' + header.yearLevel : '';
if (header.studentId) page.drawText(header.studentId, { x: margin, y, size: 9, font: bold, color: black });
if (header.fullName) page.drawText(header.fullName, { x: 100, y, size: 9, font, color: black });
if (header.program) page.drawText(compactProgram(header.program), { x: 338, y, size: 9, font, color: black });
if (yearLevelText) {
const w = font.widthOfTextAtSize(yearLevelText, 9);
page.drawText(yearLevelText, { x: margin + contentWidth - w, y, size: 9, font, color: black });
}
if (header.studentId || header.fullName || header.program || yearLevelText) y -= 16;
// ---- Table: GROUP, COURSE, COURSE DESCRIPTION, TEACHER, UNITS, NOTE ----
// Column left edges + widths, tuned to the reference geometry (right edge 576).
const cols = [
{ key: 'group', label: 'GROUP', x: margin, w: 38, align: 'left' },
{ key: 'courseCode', label: 'COURSE', x: margin + 38, w: 42, align: 'left' },
{ key: 'description', label: 'COURSE DESCRIPTION', x: margin + 80, w: 201, align: 'left' },
{ key: 'teacher', label: 'TEACHER', x: margin + 281, w: 128, align: 'left' },
{ key: 'units', label: 'UNITS', x: margin + 409, w: 31, align: 'right' },
{ key: 'status', label: 'NOTE', x: margin + 440, w: 100, align: 'right' }
];
const cellSize = 8;
const headerSize = 8;
const lineH = 12;
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 = rowLines(cells);
const rowHeight = Math.max(...linesPerCol.map((l) => l.length)) * lineH + 12;
if (y - rowHeight < 50) {
page = doc.addPage([612, 792]);
y = 792 - 10;
drawHeaderRow();
}
if (isHeader) {
hline(margin, margin + contentWidth, y);
}
for (let i = 0; i < cols.length; i++) {
const c = cols[i];
const lines = linesPerCol[i];
let ty = y - 9;
for (const line of lines) {
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) {
hline(margin, margin + contentWidth, y);
}
return true;
}
function drawHeaderRow() {
const cells = {};
cols.forEach((c) => { cells[c.key] = c.label; });
drawRow(cells, true);
}
drawHeaderRow();
for (const course of courses) {
const cells = {
group: course.group,
courseCode: course.courseCode,
description: course.description,
schedule: course.schedule,
teacher: course.teacher || course.faculty,
units: course.units,
status: course.status
};
drawRow(cells, false);
}
// ---- Footer ----
if (y < 90) {
page = doc.addPage([612, 792]);
y = 792 - 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: unitsCol.x + unitsCol.w - 3 - 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();
const pw = font.widthOfTextAtSize(printedBy, 8);
page.drawText(printedBy, { x: margin + contentWidth - pw, y, size: 8, font, color: gray });
return doc.save(); // returns Promise<Uint8Array>
}
return { buildStudyLoadPdf, compactProgram, deriveDateRange };
});