PDF Erstellung anpassung

This commit is contained in:
2026-07-20 00:50:29 +02:00
parent e64370aea0
commit c9ab82e97a
+151 -95
View File
@@ -38,99 +38,80 @@ $footerText = trim((string)($settings['pdf_footer_text'] ?? ''));
// Ab dieser Mitgliederzahl wird auf zwei Seiten (Vorder-/Rueckseite) statt // Ab dieser Mitgliederzahl wird auf zwei Seiten (Vorder-/Rueckseite) statt
// einer einzelnen Seite gedruckt. // einer einzelnen Seite gedruckt.
define('EXPORT_MULTI_PAGE_THRESHOLD', 50); define('EXPORT_MULTI_PAGE_THRESHOLD', 50);
// Platzbudget in TCPDF-HTML-"px" fuer die Datenzeilen einer Seite, kalibriert // TCPDF's writeHTML() ignoriert CSS-Zeilenhoehen/-innenabstaende auf
// an der bisherigen Ausgabe (63 Zeilen a 16px auf einer vollen A4-Seite). Ist // Tabellenzeilen vollstaendig (getestet: weder "height" auf <tr> noch
// eine Fusszeile konfiguriert, wird ihr reservierter Platz abgezogen, damit // "padding" auf <td> haben irgendeinen Einfluss auf die gerenderte Zeilen-
// die letzte Tabellenzeile sie nicht ueberlappt. // hoehe). Deshalb wird die Tabelle unten mit TCPDFs nativer Cell()-Methode
define('EXPORT_FOOTER_RESERVE_PX', 40); // gezeichnet, die den Hoehen-Parameter zuverlaessig exakt umsetzt.
define('EXPORT_PAGE_ROW_BUDGET_PX', (63 * 16) - ($footerText !== '' ? EXPORT_FOOTER_RESERVE_PX : 0)); define('EXPORT_PAGE_WIDTH_MM', 200.0); // A4 (210mm) minus 5mm Rand links/rechts
// Technischer Mindestwert, nur als letzte Bremse gegen 0/negative Hoehen bei define('EXPORT_COL_NAME_MM', 40.0);
// absurd hohen Teilnehmerzahlen - kein gestalterisches Minimum. define('EXPORT_COL_BALANCE_MM', 20.0);
define('EXPORT_HARD_MIN_ROW_HEIGHT_PX', 6); define('EXPORT_COL_MARKS_MM', EXPORT_PAGE_WIDTH_MM - EXPORT_COL_NAME_MM - EXPORT_COL_BALANCE_MM);
define('EXPORT_TITLE_ROW_MM', 8.0);
define('EXPORT_HEADER_ROW_MM', 6.0);
define('EXPORT_HINT_ROW_MM', 6.0);
// 1px (CSS/96dpi) = 25.4/96 mm - fuer die Umrechnung der Admin-Einstellung
// (weiterhin in "px" gepflegt, siehe mandant-einstellungen.php) in mm.
define('EXPORT_PX_TO_MM', 25.4 / 96);
define('EXPORT_HARD_MIN_ROW_HEIGHT_MM', 2.0);
// TCPDFs Cell() verbraucht pro gezeichneter Zeile durchgaengig etwas mehr
// vertikalen Platz als die angeforderte Hoehe (mutmasslich die
// Rahmenlinienbreite), gemessen ueber 61 Testzeilen a 4.2333mm: ca. 0,157mm
// Overhead je Zeile. Ohne Beruecksichtigung summiert sich das ueber viele
// Zeilen zu mehreren mm und kann einen ungewollten Seitenumbruch ausloesen.
define('EXPORT_ROW_OVERHEAD_MM', 0.2);
$allActive = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]); $allActive = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
$memberCount = count($allActive); $memberCount = count($allActive);
$isMultiPage = $memberCount >= EXPORT_MULTI_PAGE_THRESHOLD; $isMultiPage = $memberCount >= EXPORT_MULTI_PAGE_THRESHOLD;
/** /**
* Ermittelt Zeilenanzahl und -hoehe fuer eine Seite: Ausgangspunkt ist die * Verfuegbare Hoehe (mm) fuer die Datenzeilen einer A4-Seite: Gesamthoehe
* vom Admin gewuenschte Zeilenhoehe. Sind Fuellzeilen erwuenscht, wird die * minus oberem/unterem Rand, Titelzeile, Spaltenkopf und optional
* Seite bis zu dieser Hoehe komplett aufgefuellt (weniger Mitglieder -> * reservierter Fusszeile bzw. Vorder-/Rueckseiten-Hinweiszeile.
*/
function export_page_row_budget_mm(bool $hasFooter, bool $hasSeitenHinweis): float
{
// Kleiner zusaetzlicher Sicherheitspuffer obendrauf (der zeilenweise
// Cell()-Overhead wird bereits separat in export_compute_layout()
// eingerechnet).
$budget = 297.0 - 5.0 - 5.0 - EXPORT_TITLE_ROW_MM - EXPORT_HEADER_ROW_MM - 2.0;
if ($hasFooter) {
$budget -= 12.0;
}
if ($hasSeitenHinweis) {
$budget -= EXPORT_HINT_ROW_MM;
}
return max(EXPORT_HARD_MIN_ROW_HEIGHT_MM, $budget);
}
/**
* Ermittelt Zeilenanzahl und -hoehe (mm) fuer eine Seite: Ausgangspunkt ist
* die vom Admin gewuenschte Zeilenhoehe. Sind Fuellzeilen erwuenscht, wird
* die Seite bis zu dieser Hoehe komplett aufgefuellt (weniger Mitglieder ->
* groessere, statt immer gleich viele Zeilen). Passt die Mitgliederzahl bei * groessere, statt immer gleich viele Zeilen). Passt die Mitgliederzahl bei
* der gewuenschten Hoehe nicht auf eine Seite, wird bei jeder Generierung * der gewuenschten Hoehe nicht auf eine Seite, wird bei jeder Generierung
* automatisch so weit verkleinert, dass garantiert alle Teilnehmer passen. * automatisch so weit verkleinert, dass garantiert alle Teilnehmer passen.
* *
* @return array{0: int, 1: float} [targetRows, rowHeightPx] * @return array{0: int, 1: float} [targetRows, rowHeightMm]
*/ */
function export_compute_layout(int $memberCount, bool $showEmptyRows, int $adminRowHeightPx): array function export_compute_layout(int $memberCount, bool $showEmptyRows, int $adminRowHeightPx, float $pageRowBudgetMm): array
{ {
$adminRowHeightPx = max(1, $adminRowHeightPx); $adminRowHeightMm = max(0.1, $adminRowHeightPx * EXPORT_PX_TO_MM);
$capacityAtAdminHeight = (int)floor(EXPORT_PAGE_ROW_BUDGET_PX / $adminRowHeightPx); // Jede Zeile verbraucht real "Hoehe + Overhead" (siehe EXPORT_ROW_OVERHEAD_MM),
// das muss bei der Kapazitaets- und Hoehenberechnung mit einfliessen.
$capacityAtAdminHeight = (int)floor($pageRowBudgetMm / ($adminRowHeightMm + EXPORT_ROW_OVERHEAD_MM));
$targetRows = $showEmptyRows ? max($memberCount, $capacityAtAdminHeight) : $memberCount; $targetRows = $showEmptyRows ? max($memberCount, $capacityAtAdminHeight) : $memberCount;
$targetRows = max(1, $targetRows); $targetRows = max(1, $targetRows);
$rowHeight = min((float)$adminRowHeightPx, EXPORT_PAGE_ROW_BUDGET_PX / $targetRows); $rowHeight = min($adminRowHeightMm, ($pageRowBudgetMm / $targetRows) - EXPORT_ROW_OVERHEAD_MM);
$rowHeight = max(EXPORT_HARD_MIN_ROW_HEIGHT_PX, $rowHeight); $rowHeight = max(EXPORT_HARD_MIN_ROW_HEIGHT_MM, $rowHeight);
return [$targetRows, $rowHeight]; return [$targetRows, $rowHeight];
} }
/**
* Rendert eine Teilnehmerseite als HTML-Tabelle fuer TCPDF.
*
* @param list<array> $teilnehmer
*/
function export_render_page_html(array $teilnehmer, string $titel, string $hinweisText, string $markPrice, bool $showEmptyRows, int $adminRowHeightPx, ?string $seitenHinweis): string
{
[$targetRows, $rowHeight] = export_compute_layout(count($teilnehmer), $showEmptyRows, $adminRowHeightPx);
$html = '
<table border="1" style="white-space:nowrap;">
<tr style="height: 25px;' . $hinweisText . '" valign="bottom">
<th style="width:30%;font-size: 13px;" colspan="2"><b>' . $titel . '</b></th>
<th style="width:70%;" valign="bottom">1 Strich = ' . $markPrice . '€. Bitte bezahlen bei 10 € zahlen. ' . date("d.m.Y H:s") . '</th>
</tr>
<tr>
<td style="width:20%"><b>Name</b></td>
<td style="width:10%"><b>Guthaben</b></td>
<td><b>Striche</b></td>
</tr>';
$rowCount = 0;
foreach ($teilnehmer as $person) {
$rowCount++;
$name = saas_html($person['display_name']);
$balanceCents = (int)$person['balance_cents'];
$balance = number_format($balanceCents / 100, 2, ',', '.');
$html .= '<tr style="height: ' . $rowHeight . 'px;">';
$html .= "<td>{$name}</td>";
if ($balanceCents < -1000) {
$html .= '<td style="background-color: rgb(204, 0, 0); color: rgb(255, 255, 255);">' . $balance . ' €</td>';
} elseif ($balanceCents > 500) {
$html .= '<td style="background-color: rgb(0, 102, 0); color: rgb(255, 255, 255);">' . $balance . ' €</td>';
} else {
$html .= "<td>{$balance} €</td>";
}
$html .= "<td></td>";
$html .= "</tr>";
}
if ($showEmptyRows) {
for ($i = $rowCount; $i < $targetRows; $i++) {
$html .= '<tr style="height: ' . $rowHeight . 'px;"><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>';
}
}
if ($seitenHinweis !== null) {
$html .= '<tr><td>&nbsp;</td><td>&nbsp;</td><td style="background-color: rgb(255, 255, 0);text-align: right;"><b>' . $seitenHinweis . '</b></td></tr>';
}
$html .= '</table>';
return $html;
}
// TCPDF-Bibliothek einbinden // TCPDF-Bibliothek einbinden
require_once(__DIR__ . '/TCPDF/tcpdf.php'); require_once(__DIR__ . '/TCPDF/tcpdf.php');
@@ -146,15 +127,25 @@ class MyCustomPDFWithWatermark extends TCPDF {
$bMargin = $this->getBreakMargin(); $bMargin = $this->getBreakMargin();
$auto_page_break = $this->AutoPageBreak; $auto_page_break = $this->AutoPageBreak;
$this->SetAutoPageBreak(false, 0); $this->SetAutoPageBreak(false, 0);
$x = $this->x;
$y = $this->y;
$this->SetAlpha(0.35); $this->SetAlpha(0.35);
$this->SetFont('helvetica', '', 14); $this->SetFont('helvetica', '', 14);
$this->SetTextColor(0, 0, 0); $this->SetTextColor(0, 0, 0);
foreach ([40, 140, 240] as $y) { foreach ([40, 140, 240] as $wy) {
$this->MultiCell(90, 0, $this->watermarkText, 0, 'L', false, 1, 110, $y); $this->MultiCell(90, 0, $this->watermarkText, 0, 'L', false, 1, 110, $wy);
} }
$this->SetAlpha(1); $this->SetAlpha(1);
$this->SetFont('helvetica', '', 9.5); $this->SetFont('helvetica', '', 9.5);
$this->SetTextColor(0, 0, 0);
// MultiCell(..., $ln=1, ...) verschiebt den Seiten-Cursor als
// Nebeneffekt - ohne dieses Zuruecksetzen wuerde der eigentliche
// Tabelleninhalt an der Position des letzten Wasserzeichen-Absatzes
// statt oben auf der Seite beginnen.
$this->x = $x;
$this->y = $y;
$this->SetAutoPageBreak($auto_page_break, $bMargin); $this->SetAutoPageBreak($auto_page_break, $bMargin);
$this->setPageMark(); $this->setPageMark();
@@ -173,6 +164,88 @@ class MyCustomPDFWithWatermark extends TCPDF {
} }
} }
/**
* Zeichnet eine Teilnehmerseite mit TCPDFs nativer Cell()-Methode (siehe
* Kommentar oben zu EXPORT_PAGE_WIDTH_MM: writeHTML() ignoriert Zeilenhoehen
* auf <tr>/<td> komplett, Cell() setzt die uebergebene Hoehe dagegen exakt
* um).
*
* @param list<array> $teilnehmer
* @param array{0:int,1:int,2:int} $titelFarbe Hintergrundfarbe der Titelzeile
* @param array{0:int,1:int,2:int} $titelTextFarbe Textfarbe der Titelzeile
*/
function export_render_page(
TCPDF $pdf,
array $teilnehmer,
string $titel,
array $titelFarbe,
array $titelTextFarbe,
string $markPrice,
bool $showEmptyRows,
int $adminRowHeightPx,
?string $seitenHinweis
): void {
$rowBudgetMm = export_page_row_budget_mm(trim($pdf->footerText) !== '', $seitenHinweis !== null);
[$targetRows, $rowHeight] = export_compute_layout(count($teilnehmer), $showEmptyRows, $adminRowHeightPx, $rowBudgetMm);
// Titelzeile: Name/Guthaben-Breite + Strichebreite, Hintergrundfarbe je nach Seite.
$pdf->SetFont('helvetica', 'B', 13);
$pdf->SetFillColor($titelFarbe[0], $titelFarbe[1], $titelFarbe[2]);
$pdf->SetTextColor($titelTextFarbe[0], $titelTextFarbe[1], $titelTextFarbe[2]);
$pdf->Cell(EXPORT_COL_NAME_MM + EXPORT_COL_BALANCE_MM, EXPORT_TITLE_ROW_MM, $titel, 1, 0, 'L', true);
$pdf->SetFont('helvetica', '', 9);
$hinweisText = '1 Strich = ' . $markPrice . '€. Bitte bezahlen bei 10 € zahlen. ' . date('d.m.Y H:i');
$pdf->Cell(EXPORT_COL_MARKS_MM, EXPORT_TITLE_ROW_MM, $hinweisText, 1, 1, 'L', true);
$pdf->SetTextColor(0, 0, 0);
// Spaltenkopf.
$pdf->SetFont('helvetica', 'B', 9.5);
$pdf->Cell(EXPORT_COL_NAME_MM, EXPORT_HEADER_ROW_MM, 'Name', 1, 0, 'L');
$pdf->Cell(EXPORT_COL_BALANCE_MM, EXPORT_HEADER_ROW_MM, 'Guthaben', 1, 0, 'L');
$pdf->Cell(EXPORT_COL_MARKS_MM, EXPORT_HEADER_ROW_MM, 'Striche', 1, 1, 'L');
// Datenzeilen.
$pdf->SetFont('helvetica', '', 9.5);
$rowCount = 0;
foreach ($teilnehmer as $person) {
$rowCount++;
$name = (string)$person['display_name'];
$balanceCents = (int)$person['balance_cents'];
$balance = number_format($balanceCents / 100, 2, ',', '.') . ' €';
$pdf->Cell(EXPORT_COL_NAME_MM, $rowHeight, $name, 1, 0, 'L');
if ($balanceCents < -1000) {
$pdf->SetFillColor(204, 0, 0);
$pdf->SetTextColor(255, 255, 255);
$pdf->Cell(EXPORT_COL_BALANCE_MM, $rowHeight, $balance, 1, 0, 'L', true);
$pdf->SetTextColor(0, 0, 0);
} elseif ($balanceCents > 500) {
$pdf->SetFillColor(0, 102, 0);
$pdf->SetTextColor(255, 255, 255);
$pdf->Cell(EXPORT_COL_BALANCE_MM, $rowHeight, $balance, 1, 0, 'L', true);
$pdf->SetTextColor(0, 0, 0);
} else {
$pdf->Cell(EXPORT_COL_BALANCE_MM, $rowHeight, $balance, 1, 0, 'L');
}
$pdf->Cell(EXPORT_COL_MARKS_MM, $rowHeight, '', 1, 1, 'L');
}
if ($showEmptyRows) {
for ($i = $rowCount; $i < $targetRows; $i++) {
$pdf->Cell(EXPORT_COL_NAME_MM, $rowHeight, '', 1, 0, 'L');
$pdf->Cell(EXPORT_COL_BALANCE_MM, $rowHeight, '', 1, 0, 'L');
$pdf->Cell(EXPORT_COL_MARKS_MM, $rowHeight, '', 1, 1, 'L');
}
}
if ($seitenHinweis !== null) {
$pdf->Cell(EXPORT_COL_NAME_MM + EXPORT_COL_BALANCE_MM, EXPORT_HINT_ROW_MM, '', 1, 0, 'L');
$pdf->SetFont('helvetica', 'B', 9.5);
$pdf->SetFillColor(255, 255, 0);
$pdf->Cell(EXPORT_COL_MARKS_MM, EXPORT_HINT_ROW_MM, $seitenHinweis, 1, 1, 'R', true);
}
}
$pdf = new MyCustomPDFWithWatermark(PDF_PAGE_ORIENTATION, 'mm', 'A4', true, 'UTF-8', false); $pdf = new MyCustomPDFWithWatermark(PDF_PAGE_ORIENTATION, 'mm', 'A4', true, 'UTF-8', false);
$pdf->watermarkText = $watermarkText; $pdf->watermarkText = $watermarkText;
$pdf->footerText = $footerText; $pdf->footerText = $footerText;
@@ -184,18 +257,7 @@ $pdf->SetFont('helvetica', '', 9.5);
if (!$isMultiPage) { if (!$isMultiPage) {
$pdf->AddPage(); $pdf->AddPage();
$pdf->writeHTML( export_render_page($pdf, $allActive, 'Kaffeeliste', [0, 94, 63], [255, 255, 255], $markPrice, $showEmptyRows, $adminRowHeightPx, null);
export_render_page_html(
$allActive,
'Kaffeeliste',
'background-color: rgb(0, 94, 63);color: rgb(255, 255, 255);',
$markPrice,
$showEmptyRows,
$adminRowHeightPx,
null
),
true, false, true, false, ''
);
} else { } else {
if ($splitMode === 'alphabetical') { if ($splitMode === 'alphabetical') {
$mid = (int)ceil($memberCount / 2); $mid = (int)ceil($memberCount / 2);
@@ -211,16 +273,10 @@ if (!$isMultiPage) {
} }
$pdf->AddPage(); $pdf->AddPage();
$pdf->writeHTML( export_render_page($pdf, $front, $frontTitel, [0, 94, 63], [255, 255, 255], $markPrice, $showEmptyRows, $adminRowHeightPx, 'Rückseite beachten!');
export_render_page_html($front, $frontTitel, 'background-color: rgb(0, 94, 63);color: rgb(255, 255, 255);', $markPrice, $showEmptyRows, $adminRowHeightPx, 'Rückseite beachten!'),
true, false, true, false, ''
);
$pdf->AddPage(); $pdf->AddPage();
$pdf->writeHTML( export_render_page($pdf, $back, $backTitel, [91, 209, 215], [0, 0, 0], $markPrice, $showEmptyRows, $adminRowHeightPx, 'Vorderseite beachten!');
export_render_page_html($back, $backTitel, 'background-color: rgb(91, 209, 215);color: rgb(0, 0, 0);', $markPrice, $showEmptyRows, $adminRowHeightPx, 'Vorderseite beachten!'),
true, false, true, false, ''
);
} }
$pdf->Output('Kaffeestrichliste.pdf', 'D'); $pdf->Output('Kaffeestrichliste.pdf', 'D');