Inleiding
Zo rond oktober/november gaan mijn vrouw en ik op zoek naar een aantal nieuwe kalenders voor het volgende jaar. Vanaf 2019 maken we de grote weekkalender zelf. Dit jaar werd die uitgebreid met een kleiner broertje op A5-formaat. Toch ontbrak er nog één onderdeel: een compacte maandkalender met weeknummers. Ik ging opnieuw aan de slag en nam de code van de weekkalender als basis. Het resultaat is te vinden op GitHub.
Eenvoudig
De weekkalender heeft 52-53 pagina's met veel ruimte voor elke dag. Deze maandkalender moest daarentegen compact worden en liefst het hele jaar in één of twee oogopslagen samenvatten. Uiteindelijk koos ik voor een raster van 3×2 maanden, 2 kantjes A5; zie dit voorbeeld.
Opnieuw maakte ik gebruik van mPDF voor het omzetten van HTML naar PDF. Zo kon ik met relatief eenvoudige opmaak (lees: tabellen) toch het gewenste resultaat bereiken.
Opmaak
De eerste maand van 2023 ziet er als volgt uit:
januari 2023
----------------------
wk | 52 01 02 03 04 05
---+------------------
ma | 2 9 16 23 30
di | 3 10 17 24 31
wo | 4 11 18 25
do | 5 12 19 26
vr | 6 13 20 27
za | 7 14 21 28
zo | 1 8 15 22 29
Elke maand is één tabel van in totaal 7 kolommen en 9 rijen. Daarvan is
de eerste rij de titel (maandnaam + jaar). De volgende rij is gevuld met de
weeknummers. De eerste kolom bevat de weekdagnamen (ma-di-wo-do-vr-za-zo).
De rest van de tabel wordt gevuld met de dagnummers zelf. Voor het omliggende
raster van 3×2 nestte ik de maandtabellen in één omliggende tabel
genaamd scaffold
.
Code in/uitklappen
table.scaffold{
margin: 0 auto;
}
table.month{
font-family: sans-serif;
}
table.month th,
table.month td {
border-right: solid 0.3mm black;
border-bottom: solid 0.3mm black;
padding: 1mm 2mm;
text-align: center;
}
table.month tr.title{
background-color: #999999;
}
table.month tr.week{
background-color: #cccccc;
}
Code
Met onderstaande code wordt op basis van een maandnummer ($m
) en het
ingestelde jaar ($this->_year
) een HTML-tabel gegenereerd. De functie
_dtWrap()
is een hulpje om gemakkelijk met meerdere van elkaar afgeleide
datums te kunnen werken.
Code in/uitklappen
private function _generateMonthTable(int $m) : string
{
$firstThisMonth = new \DateTime($this->_year.'-'.$m.'-01');
$loopDate = clone $firstThisMonth;
$html = '<table class="month">';
$html .= sprintf(
'<tr class="title"><th colspan="7">%s %d</th></tr>',
strftime('%B', $firstThisMonth->getTimestamp()),
$this->_year
);
while ($loopDate->format('N') > 1) {
$loopDate->sub(new \DateInterval('P1D'));
}
// Array of 8 rows for weeknumbers (1x) + weekdays (7x)
$rows = [
-1 => [], 0 => [], 1 => [], 2 => [],
3 => [], 4 => [], 5 => [], 6 => []
];
// Construct first row (for weeknumbers)
for ($weekLoop = -1; $weekLoop < 6; $weekLoop++) {
if ($weekLoop == -1) {
$rows[-1][] = '<th>wk</th>';
continue;
}
$dt = self::_dtWrap($firstThisMonth, 7*$weekLoop);
$rows[-1][] = strftime('<td>%V</td>', $dt->getTimestamp());
}
// Construct second to eighth rows
foreach ($rows as $rowIx => &$row) {
if ($rowIx == -1) {
continue;
}
for ($colIx = -1; $colIx < 6; $colIx++) {
$dt = self::_dtWrap($loopDate, 7*$colIx + $rowIx);
if ($colIx == -1) {
// Print abbreviated weekday name
$row[] = strftime('<th>%a</th>', $dt->getTimestamp());
continue;
}
if ($dt->format('m') != $m) {
// Print empty cell
$row[] = '<td> </td>';
continue;
}
// Print day of month
$row[] = strftime('<td>%e</td>', $dt->getTimestamp());
}
}
foreach ($rows as $rowIx => &$row) {
if ($rowIx == -1) {
$html .= '<tr class="week">';
} else {
$html .= '<tr>';
}
$html .= implode('', $row);
$html .= '</tr>';
}
$html .= '</table>';
return $html;
}
private static function _dtWrap(\DateTime $dt, int $days = 0) : \DateTime
{
$outDt = clone $dt;
$di = new \DateInterval('P'.abs($days).'D');
if ($days < 0) {
$outDt->sub($di);
} else {
$outDt->add($di);
}
return $outDt;
}
Finale
Tot slot wordt in onderstaande functie het daadwerkelijke PDF-document gegenereerd. Er wordt gekozen voor een A5-formaat in landschap-ligging. Daarna wordt de stylesheet toegevoegd en – in 4 stappen van 3 – de maanden per stuk als tabel in een cel aan het omliggende raster toegevoegd.
Code in/uitklappen
public function getPDF() : void
{
$pdfConfig = array(
'format' => 'A5',
'margin_left' => 5,
'margin_right' => 5,
'margin_top' => 5,
'margin_bottom' => 5,
'margin_header' => 0,
'margin_footer' => 0,
'orientation' => 'L',
);
$pdf = new \Mpdf\Mpdf($pdfConfig);
$pdf->SetTitle('Maandkalender '.$this->_year);
$pdf->SetAuthor('Frans-Willem Post (FWieP)');
$css = file_get_contents('style.css');
$pdf->WriteHTML($css, \Mpdf\HTMLParserMode::HEADER_CSS);
$pdf->AddPage();
$html = '<table class="scaffold"><tr>';
for ($m = 1; $m <= 3; $m++) {
// Add january - march to the first page's first row
$html .= '<td>'.$this->_generateMonthTable($m).'</td>';
}
$html .= '</tr><tr>';
for ($m = 4; $m <= 6; $m++) {
// Add april - june to the first page's second row
$html .= '<td>'.$this->_generateMonthTable($m).'</td>';
}
$html .= '</tr></table>';
$pdf->WriteHTML($html, \Mpdf\HTMLParserMode::HTML_BODY);
$pdf->AddPage();
$html = '<table class="scaffold"><tr>';
for ($m = 7; $m <= 9; $m++) {
// Add july - september to the second page's first row
$html .= '<td>'.$this->_generateMonthTable($m).'</td>';
}
$html .= '</tr><tr>';
for ($m = 10; $m <= 12; $m++) {
// Add october - december to the second page's second row
$html .= '<td>'.$this->_generateMonthTable($m).'</td>';
}
$html .= '</tr></table>';
$pdf->WriteHTML($html, \Mpdf\HTMLParserMode::HTML_BODY);
$pdf->Output('Maandkalender '.$this->_year.'.pdf', D::DOWNLOAD);
}
Epiloog
Nog een kleine tip achteraf: bij het afdrukken van deze maandkalender op één vel van A4-formaat, ben je afhankelijk van de printerinstellingen. De meeste programma's en besturingssystemen bieden een mogelijkheid om 2, 4, 8 of 16 pagina's op één vel te printen.
In mijn geval moest ik dan wel het schaal-percentage op 141% zetten. Laat dat nou precies de verhouding tussen A5 en A4 zijn (√2 × 100% = 141%)!