ISMIS Helper: cross-browser build, CI, PDF fixes, Chrome download fix #1
@@ -29,6 +29,21 @@
|
|||||||
return lines.length ? lines : [''];
|
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) {
|
async function buildStudyLoadPdf(data) {
|
||||||
const PDFLib = getPDFLib();
|
const PDFLib = getPDFLib();
|
||||||
const { PDFDocument, StandardFonts, rgb } = PDFLib;
|
const { PDFDocument, StandardFonts, rgb } = PDFLib;
|
||||||
@@ -39,6 +54,7 @@
|
|||||||
let page = doc.addPage([612, 792]); // Letter
|
let page = doc.addPage([612, 792]); // Letter
|
||||||
const font = doc.embedStandardFont(StandardFonts.Helvetica);
|
const font = doc.embedStandardFont(StandardFonts.Helvetica);
|
||||||
const bold = doc.embedStandardFont(StandardFonts.HelveticaBold);
|
const bold = doc.embedStandardFont(StandardFonts.HelveticaBold);
|
||||||
|
const italic = doc.embedStandardFont(StandardFonts.HelveticaOblique);
|
||||||
const margin = 50;
|
const margin = 50;
|
||||||
const contentWidth = 612 - margin * 2;
|
const contentWidth = 612 - margin * 2;
|
||||||
let y = 792 - 50;
|
let y = 792 - 50;
|
||||||
@@ -46,98 +62,146 @@
|
|||||||
const black = rgb(0, 0, 0);
|
const black = rgb(0, 0, 0);
|
||||||
const gray = rgb(0.3, 0.3, 0.3);
|
const gray = rgb(0.3, 0.3, 0.3);
|
||||||
|
|
||||||
function center(text, size, fontToUse, dy) {
|
function center(text, size, f, dy) {
|
||||||
const w = fontToUse.widthOfTextAtSize(text, size);
|
const w = f.widthOfTextAtSize(text, size);
|
||||||
page.drawText(text, { x: (612 - w) / 2, y: y, size, font: fontToUse, color: black });
|
page.drawText(text, { x: (612 - w) / 2, y, size, font: f, color: black });
|
||||||
y -= dy;
|
y -= dy;
|
||||||
}
|
}
|
||||||
function left(text, size, fontToUse, x, dy) {
|
function left(text, size, f, x, dy) {
|
||||||
page.drawText(text, { x, y, size, font: fontToUse, color: black });
|
page.drawText(text, { x, y, size, font: f, color: black });
|
||||||
y -= dy;
|
y -= dy;
|
||||||
}
|
}
|
||||||
|
function hline(x1, x2, atY) {
|
||||||
|
page.drawRectangle({ x: x1, y: atY, width: x2 - x1, height: 0.75, color: black });
|
||||||
|
}
|
||||||
|
|
||||||
// Header
|
// ---- Header ----
|
||||||
center(header.semester || '', 14, bold, 20);
|
center("STUDENT'S STUDY LOAD", 9.5, bold, 18);
|
||||||
center("STUDENT'S STUDY LOAD", 16, bold, 24);
|
if (header.semester) center(header.semester, 9, font, 15);
|
||||||
center(header.dateRange || '', 11, font, 18);
|
if (header.dateRange) center(header.dateRange, 9, font, 15);
|
||||||
if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 10, font, margin, 16);
|
if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 8, italic, margin, 13);
|
||||||
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
|
// 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 = [
|
const cols = [
|
||||||
{ key: 'status', label: 'NOTE', x: margin, w: 50 },
|
{ key: 'group', label: 'GROUP', x: margin, w: 26, align: 'left' },
|
||||||
{ key: 'units', label: 'UNITS', x: margin + 50, w: 45 },
|
{ key: 'courseCode', label: 'COURSE', x: margin + 26, w: 46, align: 'left' },
|
||||||
{ key: 'teacher', label: 'TEACHER', x: margin + 95, w: 110 },
|
{ key: 'description', label: 'COURSE DESCRIPTION', x: margin + 72, w: 189, align: 'left' },
|
||||||
{ key: 'description', label: 'COURSE DESCRIPTION', x: margin + 205, w: 150 },
|
{ key: 'teacher', label: 'TEACHER', x: margin + 261, w: 123, align: 'left' },
|
||||||
{ key: 'courseCode', label: 'COURSE', x: margin + 355, w: 70 },
|
{ key: 'units', label: 'UNITS', x: margin + 384, w: 31, align: 'right' },
|
||||||
{ key: 'groupSchedule', label: 'GROUP', x: margin + 425, w: 87 }
|
{ key: 'status', label: 'NOTE', x: margin + 415, w: 97, align: 'right' }
|
||||||
];
|
];
|
||||||
const cellSize = 9;
|
const cellSize = 8;
|
||||||
const headerSize = 9;
|
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) {
|
function drawRow(cells, isHeader) {
|
||||||
const f = isHeader ? bold : font;
|
const f = isHeader ? bold : font;
|
||||||
const size = isHeader ? headerSize : cellSize;
|
const size = isHeader ? headerSize : cellSize;
|
||||||
const linesPerCol = cols.map((c) => wrapText(cells[c.key] || '', f, size, c.w - 6));
|
const linesPerCol = rowLines(cells);
|
||||||
const rowHeight = Math.max(...linesPerCol.map((l) => l.length)) * 11 + 8;
|
const rowHeight = Math.max(...linesPerCol.map((l) => l.length)) * lineH + 8;
|
||||||
if (!isHeader && y - rowHeight < 50) {
|
|
||||||
// Row doesn't fit on the current page: start a new page and re-draw the header.
|
if (y - rowHeight < 50) {
|
||||||
page = doc.addPage([612, 792]);
|
page = doc.addPage([612, 792]);
|
||||||
y = 792 - 50;
|
y = 792 - 50;
|
||||||
drawHeaderRow();
|
drawHeaderRow();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHeader) {
|
if (isHeader) {
|
||||||
page.drawRectangle({ x: margin, y: y - rowHeight, width: contentWidth, height: rowHeight, color: rgb(0.85, 0.9, 0.95) });
|
// top line above the header row
|
||||||
}
|
hline(margin, margin + contentWidth, y);
|
||||||
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++) {
|
for (let i = 0; i < cols.length; i++) {
|
||||||
const c = cols[i];
|
const c = cols[i];
|
||||||
const lines = linesPerCol[i];
|
const lines = linesPerCol[i];
|
||||||
let ty = y - 10;
|
let ty = y - 9;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
page.drawText(line, { x: c.x + 3, y: ty, size, font: f, color: black });
|
if (c.align === 'right') {
|
||||||
ty -= 11;
|
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;
|
y -= rowHeight;
|
||||||
|
|
||||||
|
if (isHeader) {
|
||||||
|
// line below the header row
|
||||||
|
hline(margin, margin + contentWidth, y);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawHeaderRow() {
|
function drawHeaderRow() {
|
||||||
const headerCells = {};
|
const cells = {};
|
||||||
cols.forEach((c) => { headerCells[c.key] = c.label; });
|
cols.forEach((c) => { cells[c.key] = c.label; });
|
||||||
drawRow(headerCells, true);
|
drawRow(cells, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawHeaderRow();
|
drawHeaderRow();
|
||||||
|
|
||||||
for (const course of courses) {
|
for (const course of courses) {
|
||||||
const cells = {
|
const cells = {
|
||||||
status: course.status,
|
group: course.group,
|
||||||
units: course.units,
|
|
||||||
teacher: course.teacher,
|
|
||||||
description: course.description,
|
|
||||||
courseCode: course.courseCode,
|
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;
|
// ---- Footer ----
|
||||||
if (data.totalUnits) {
|
if (y < 90) {
|
||||||
page.drawText('Total Units: ' + data.totalUnits, { x: margin, y: y, size: 11, font: bold, color: black });
|
page = doc.addPage([612, 792]);
|
||||||
y -= 16;
|
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();
|
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<Uint8Array>
|
return doc.save(); // returns Promise<Uint8Array>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user