Report cards that print on the school's existing laser printer
A school asks us to build their report card module. They have twelve classes, two terms a year, and the printer is an HP LaserJet 1020 connected to the admin office Windows machine. The headmistress prints the entire batch — six hundred PDFs, collated — and she hand-signs each one before sending it home with students on the last day of term.
The previous system produced A4-sized PDFs that rendered correctly in a browser preview. On paper, the right margin crept into the unprintable edge zone on about twenty sheets per batch, clipping the remarks column. The headmistress's assistant had been manually re-printing those sheets.
The core issue: the printer's hardware margin is 6.35mm on all sides. The PDF generator treated A4 as a flat 210×297mm canvas with content placed right to the edge.
We set the page size explicitly in the generator — wkhtmltopdf, in this case — and define a content box inside the hardware margin.
@dataclass
class ReportCardConfig:
page_width_mm: float = 210.0
page_height_mm: float = 297.0
margin_mm: float = 6.35
# 1mm at 96dpi
px_per_mm: float = 3.7795275591
@property
def margin_px(self) -> float:
return self.margin_mm * self.px_per_mm
@property
def content_width_px(self) -> float:
return (self.page_width_mm - 2 * self.margin_mm) * self.px_per_mm
@property
def content_height_px(self) -> float:
return (self.page_height_mm - 2 * self.margin_mm) * self.px_per_mmWe pass these into the CSS as variables so the layout adapts at generation time rather than in the print dialog.
<style>
@page {
size: A4;
margin: 6.35mm;
}
.report-container {
width: var(--content-width);
min-height: var(--content-height);
padding: 2mm;
box-sizing: border-box;
}
.marks-table {
table-layout: fixed;
width: 100%;
}
.marks-table th, .marks-table td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 1.5mm 1mm;
}
</style>We measured the printer's actual margin by printing a calibration sheet with a 5mm border and checking which edges survived. The spec sheet said 6.35mm; the calibration confirmed it. We use the larger of the two numbers.
The second problem is marks tables that overflow. A class with eight subjects fits cleanly. The optional subjects added in Class 11 push the table past the page break, splitting a student's report across two sheets in a way that looks like two separate reports. We set the row height and font size so a single student's table always fits within one content box, and we force a page break before each student.
.student-report {
break-before: page;
}
.student-report:first-child {
break-before: avoid;
}The third problem is signatures. The headmistress signs by hand, so we leave a 25mm gap at the bottom of each report. But she also wanted a digital signature on the duplicate stored in the system. We render the gap in the print version and fill it with the signature image in the archive version — same layout, different data source.
The tradeoff: table-layout: fixed prevents the browser from auto-sizing columns to fit content. A subject name longer than the column width gets truncated. We set the subject name column to 40mm and abbreviate "Computer Science" to "Comp. Sci." in the data layer. The class teacher reviewed and approved each abbreviation.
We sent the first batch of ten test PDFs to the school. The admin assistant printed them, checked all four edges on every sheet, and confirmed nothing was clipped. We generated the full six hundred. The assistant did not need to re-print any sheets.
The headmistress signed and distributed them on the last Friday of the term.