MSIN-4471 — Update PDF distribution labels for participant names and checkboxes. Introduce new functions for handling logo paths and inline text rendering in PDF documents. Adjust SQL insert statements for improved clarity and idempotency. Increment version code.

This commit is contained in:
2026-07-22 07:50:44 -04:00
parent 2d706fb628
commit b97fa8a337
3 changed files with 362 additions and 28 deletions

View File

@ -3708,7 +3708,7 @@ function fxBibStaticFallback($clef) {
'bib_v4_dist_print_col_prenom' => ['fr' => 'Prénom', 'en' => 'First name'],
// MSIN-4471 — Feuille bénévole : nom+prénom + case remise.
'bib_v4_dist_print_col_nom_prenom' => ['fr' => 'Nom, Prénom', 'en' => 'Last name, First name'],
'bib_v4_dist_print_col_check' => ['fr' => '', 'en' => ''],
'bib_v4_dist_print_col_check' => ['fr' => 'OK', 'en' => 'OK'],
'bib_v4_dist_print_col_sexe' => ['fr' => 'Sexe', 'en' => 'Gender'],
'bib_v4_dist_print_empty_epr' => ['fr' => 'Aucun dossard assigné pour cette épreuve.', 'en' => 'No bib assigned for this race.'],
'bib_v4_dist_print_last' => ['fr' => 'Dernière impression : %s — %s', 'en' => 'Last print: %s — %s'],
@ -9510,6 +9510,71 @@ function fxBibDistPrintIntroPageClefs() {
);
}
/** MSIN-4471 — Chemin absolu du logo MS1 (pied / bandeau PDF). */
function fxBibDistPrintMs1LogoPath() {
$strDocRoot = $_SERVER['DOCUMENT_ROOT'] ?? dirname(__DIR__);
$strPath = rtrim($strDocRoot, '/\\') . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'logoms1inscription.png';
return is_file($strPath) ? $strPath : '';
}
/**
* MSIN-4471 — Texte brut d'un nœud DOM (espaces normalisés).
*/
function fxBibDistPrintDomPlainText($node) {
if (!$node) {
return '';
}
$str = $node->textContent ?? '';
$str = preg_replace('/\s+/u', ' ', $str);
return trim((string)$str);
}
/**
* MSIN-4471 — Écrit un fragment inline (texte + strong/em) en mode Write.
*/
function fxBibDistPrintWriteInline($pdf, $node, $fltLineH = 5, $fltFontSize = 10) {
if (!$node) {
return;
}
foreach ($node->childNodes as $child) {
if ($child->nodeType === XML_TEXT_NODE) {
$strText = preg_replace('/\s+/u', ' ', $child->nodeValue);
if ($strText !== null && $strText !== '') {
$pdf->SetFont('Arial', '', $fltFontSize);
$pdf->Write($fltLineH, fxBibDistPrintPdfText($strText));
}
continue;
}
if ($child->nodeType !== XML_ELEMENT_NODE) {
continue;
}
$strTag = strtolower($child->nodeName);
if ($strTag === 'br') {
$pdf->Ln($fltLineH);
continue;
}
if (in_array($strTag, array('strong', 'b'), true)) {
$strText = fxBibDistPrintDomPlainText($child);
if ($strText !== '') {
$pdf->SetFont('Arial', 'B', $fltFontSize);
$pdf->Write($fltLineH, fxBibDistPrintPdfText($strText));
$pdf->SetFont('Arial', '', $fltFontSize);
}
continue;
}
if (in_array($strTag, array('em', 'i'), true)) {
$strText = fxBibDistPrintDomPlainText($child);
if ($strText !== '') {
$pdf->SetFont('Arial', 'I', $fltFontSize);
$pdf->Write($fltLineH, fxBibDistPrintPdfText($strText));
$pdf->SetFont('Arial', '', $fltFontSize);
}
continue;
}
fxBibDistPrintWriteInline($pdf, $child, $fltLineH, $fltFontSize);
}
}
/**
* MSIN-4471 — Résout un src img vers un fichier sous images/doc/ uniquement.
* Accepte /images/doc/x.png, images/doc/x.png (refuse tout autre chemin).
@ -9570,6 +9635,207 @@ function fxBibDistPrintWriteImage($pdf, $strPath) {
$pdf->Ln(max(8, $fltH + 2));
}
/**
* MSIN-4471 — Rendu flyer des pages intro (bandeau déjà dans Header).
* Structure attendue : lead <p> puis blocs <h3>+<p> (cartes numérotées).
* Si pas de h3 : repli sur fxBibDistPrintWriteHtml.
*/
function fxBibDistPrintWriteIntroFlyer($pdf, $strHtml) {
$strHtml = trim((string)$strHtml);
if ($strHtml === '') {
return;
}
if (!class_exists('DOMDocument', false)) {
fxBibDistPrintWriteHtml($pdf, $strHtml);
return;
}
$dom = new DOMDocument();
$blnPrev = libxml_use_internal_errors(true);
$dom->loadHTML(
'<?xml encoding="UTF-8"><div id="ms1-bib-dist-root">' . $strHtml . '</div>',
LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors($blnPrev);
$root = $dom->getElementById('ms1-bib-dist-root');
if (!$root) {
foreach ($dom->getElementsByTagName('div') as $el) {
if ($el->getAttribute('id') === 'ms1-bib-dist-root') {
$root = $el;
break;
}
}
}
if (!$root) {
fxBibDistPrintWriteHtml($pdf, $strHtml);
return;
}
// Déballer <article> si présent.
$article = null;
foreach ($root->childNodes as $child) {
if ($child->nodeType === XML_ELEMENT_NODE && strtolower($child->nodeName) === 'article') {
$article = $child;
break;
}
}
$scope = $article ?: $root;
$tabBlocks = array();
foreach ($scope->childNodes as $child) {
if ($child->nodeType !== XML_ELEMENT_NODE) {
continue;
}
$strTag = strtolower($child->nodeName);
if (in_array($strTag, array('p', 'h2', 'h3', 'h4', 'ul', 'ol', 'blockquote', 'img'), true)) {
$tabBlocks[] = $child;
}
}
$blnHasH3 = false;
foreach ($tabBlocks as $el) {
if (strtolower($el->nodeName) === 'h3') {
$blnHasH3 = true;
break;
}
}
if (!$blnHasH3) {
fxBibDistPrintWriteHtml($pdf, $strHtml);
return;
}
$fltLeft = method_exists($pdf, 'GetLeftMargin') ? $pdf->GetLeftMargin() : 12;
$fltRight = method_exists($pdf, 'GetRightMargin') ? $pdf->GetRightMargin() : 12;
$fltBottom = method_exists($pdf, 'GetBreakMargin') ? $pdf->GetBreakMargin() : 18;
$fltPageW = $pdf->GetPageWidth();
$fltContentW = $fltPageW - $fltLeft - $fltRight;
$intTip = 0;
$blnLeadDone = false;
$i = 0;
$intN = count($tabBlocks);
while ($i < $intN) {
$el = $tabBlocks[$i];
$strTag = strtolower($el->nodeName);
// Premier <p> = encadré d'intro (lead).
if ($strTag === 'p' && !$blnLeadDone) {
$blnLeadDone = true;
$strLead = fxBibDistPrintDomPlainText($el);
$fltInnerW = $fltContentW - 10;
$pdf->SetFont('Arial', 'I', 10);
$intLines = max(2, (int)ceil($pdf->GetStringWidth(fxBibDistPrintPdfText($strLead)) / max(1, $fltInnerW)) + 1);
$fltBoxH = max(12, 4 + ($intLines * 4.5) + 3);
if ($pdf->GetY() + $fltBoxH > $pdf->GetPageHeight() - $fltBottom) {
$pdf->AddPage();
}
$fltY0 = $pdf->GetY();
$pdf->SetFillColor(232, 240, 254);
$pdf->Rect($fltLeft, $fltY0, $fltContentW, $fltBoxH, 'F');
$pdf->SetFillColor(47, 95, 208);
$pdf->Rect($fltLeft, $fltY0, 2.5, $fltBoxH, 'F');
$pdf->SetXY($fltLeft + 6, $fltY0 + 3.5);
$pdf->SetFont('Arial', 'I', 10);
$pdf->SetTextColor(40, 55, 90);
$pdf->MultiCell($fltInnerW, 4.5, fxBibDistPrintPdfText($strLead), 0, 'L');
$pdf->SetY($fltY0 + $fltBoxH + 5);
$pdf->SetTextColor(0, 0, 0);
$i++;
continue;
}
if ($strTag === 'h3' || $strTag === 'h2' || $strTag === 'h4') {
$intTip++;
$strTitleRaw = fxBibDistPrintDomPlainText($el);
$strTitle = $strTitleRaw;
$strNum = (string)$intTip;
if (preg_match('/^(\d+)\s*[\.\)\—\-]\s*(.+)$/u', $strTitleRaw, $m)) {
$strNum = $m[1];
$strTitle = $m[2];
}
$bodyNode = null;
if (($i + 1) < $intN && strtolower($tabBlocks[$i + 1]->nodeName) === 'p') {
$bodyNode = $tabBlocks[$i + 1];
$i += 2;
} else {
$i++;
}
$strBodyPlain = $bodyNode ? fxBibDistPrintDomPlainText($bodyNode) : '';
$fltBadge = 12;
$fltPad = 4.5;
$fltInnerLeft = $fltLeft + 6 + $fltBadge + 5;
$fltInnerW = ($fltLeft + $fltContentW) - $fltPad - $fltInnerLeft;
$pdf->SetFont('Arial', 'B', 11);
$intTitleLines = max(1, (int)ceil($pdf->GetStringWidth(fxBibDistPrintPdfText($strTitle)) / max(1, $fltInnerW)));
$fltTitleH = $intTitleLines * 5.5;
$pdf->SetFont('Arial', '', 10);
$fltBodyH = 0;
if ($strBodyPlain !== '') {
$intBodyLines = max(1, (int)ceil($pdf->GetStringWidth(fxBibDistPrintPdfText($strBodyPlain)) / max(1, $fltInnerW)));
$fltBodyH = $intBodyLines * 4.8;
}
$fltCardH = max(24, $fltPad + $fltTitleH + 2 + $fltBodyH + $fltPad + 1);
if ($pdf->GetY() + $fltCardH > $pdf->GetPageHeight() - $fltBottom) {
$pdf->AddPage();
}
$fltY0 = $pdf->GetY();
$pdf->SetFillColor(247, 249, 252);
$pdf->SetDrawColor(200, 210, 228);
$pdf->Rect($fltLeft, $fltY0, $fltContentW, $fltCardH, 'FD');
$pdf->SetFillColor(47, 95, 208);
$pdf->Rect($fltLeft, $fltY0, 3.2, $fltCardH, 'F');
$fltBadgeX = $fltLeft + 7;
$fltBadgeY = $fltY0 + ($fltCardH - $fltBadge) / 2;
$pdf->SetFillColor(47, 95, 208);
$pdf->Rect($fltBadgeX, $fltBadgeY, $fltBadge, $fltBadge, 'F');
$pdf->SetTextColor(255, 255, 255);
$pdf->SetFont('Arial', 'B', 12);
$pdf->SetXY($fltBadgeX, $fltBadgeY + 2.8);
$pdf->Cell($fltBadge, 6, fxBibDistPrintPdfText($strNum), 0, 0, 'C');
$pdf->SetTextColor(25, 40, 70);
$pdf->SetFont('Arial', 'B', 11);
$pdf->SetXY($fltInnerLeft, $fltY0 + $fltPad);
$pdf->MultiCell($fltInnerW, 5.5, fxBibDistPrintPdfText($strTitle), 0, 'L');
if ($bodyNode) {
$pdf->SetTextColor(55, 65, 80);
$pdf->SetX($fltInnerLeft);
$pdf->SetLeftMargin($fltInnerLeft);
fxBibDistPrintWriteInline($pdf, $bodyNode, 4.8, 10);
$pdf->SetLeftMargin($fltLeft);
$pdf->Ln(2);
}
$pdf->SetY($fltY0 + $fltCardH + 3.5);
$pdf->SetTextColor(0, 0, 0);
$pdf->SetX($fltLeft);
continue;
}
// Autres blocs (ul/ol/img…) : rendu classique.
if ($strTag === 'p') {
// p après les cartes (rare) : texte simple.
$pdf->SetFont('Arial', '', 10);
$pdf->MultiCell($fltContentW, 5, fxBibDistPrintPdfText(fxBibDistPrintDomPlainText($el)), 0, 'L');
$pdf->Ln(2);
$i++;
continue;
}
fxBibDistPrintWriteHtml($pdf, $dom->saveHTML($el));
$i++;
}
}
/**
* MSIN-4471 — Rendu HTML simplifié (doc_html) vers FPDF.
* Tags supportés : h2h4, p, ul/ol/li, strong/b, em/i, br, img, blockquote, article/div/span.
@ -9715,7 +9981,9 @@ function fxBibDistPrintWriteHtml($pdf, $strHtml) {
}
}
/** MSIN-4471 — Dessine le QR résultats événement (kc.php?t=eve) centré. */
/**
* MSIN-4471 — Dessine le QR résultats événement (kc.php?t=eve) centré.
*/
function fxBibDistPrintDrawEventQr($pdf, $int_eve_id) {
$int_eve_id = (int)$int_eve_id;
if ($int_eve_id <= 0 || !function_exists('fxKcEveResultatsQrPngBinary')) {
@ -9763,18 +10031,25 @@ function fxBibOutputDistPrintIntroPages($pdf, $int_eve_id, $strLangue = 'fr') {
$pdf->blnIntroMode = true;
$pdf->eprTitle = '';
$pdf->documentSubtitle = '';
$strDefaultDocTitle = fxBibTexte('bib_v4_dist_print_title', 0);
// Marges flyer : place pour bandeau + logo pied de page.
$pdf->SetMargins(12, 40, 12);
$pdf->SetAutoPageBreak(true, 18);
foreach (fxBibDistPrintIntroPageClefs() as $strClef) {
$row = function_exists('fxDocGetPageRow') ? fxDocGetPageRow($strClef, 0) : array();
$strTitre = trim((string)($row['doc_titre'] ?? ''));
$strSous = trim((string)($row['doc_sous_titre'] ?? ''));
$strHtml = trim((string)($row['doc_html'] ?? ''));
if ($strTitre === '' && $strHtml === '') {
continue;
}
$pdf->documentTitle = ($strTitre !== '') ? $strTitre : $strClef;
$pdf->documentSubtitle = $strSous;
$pdf->AddPage();
fxBibDistPrintWriteHtml($pdf, $strHtml);
fxBibDistPrintWriteIntroFlyer($pdf, $strHtml);
if ($strClef === 'bib_dist_print_qr') {
fxBibDistPrintDrawEventQr($pdf, $int_eve_id);
}
@ -9782,7 +10057,10 @@ function fxBibOutputDistPrintIntroPages($pdf, $int_eve_id, $strLangue = 'fr') {
$pdf->blnIntroMode = false;
$pdf->documentTitle = $strDefaultDocTitle;
$pdf->documentSubtitle = '';
$pdf->eprTitle = '';
$pdf->SetMargins(10, 12, 10);
$pdf->SetAutoPageBreak(true, 16);
}
/** MSIN-4433 — Génère et envoie le PDF (format lettre). */
@ -9827,8 +10105,9 @@ function fxBibOutputDistPrintPdf($int_eve_id, $strLangue = 'fr', $strPrintedBy =
public $pageText = 'Page %s / %s';
public $eventTitle = '';
public $documentTitle = '';
public $documentSubtitle = '';
public $eprTitle = '';
/** MSIN-4471 — Pages intro : pas de sous-titre épreuve. */
/** MSIN-4471 — Pages intro : bandeau flyer, pas de sous-titre épreuve. */
public $blnIntroMode = false;
/** MSIN-4471 — Accès public aux marges (protégées dans FPDF 1.82). */
@ -9846,12 +10125,44 @@ function fxBibOutputDistPrintPdf($int_eve_id, $strLangue = 'fr', $strPrintedBy =
}
function Header() {
if ($this->blnIntroMode) {
$fltW = $this->GetPageWidth();
// Bandeau flyer (bleu MS1 + filet or).
$this->SetFillColor(47, 95, 208);
$this->Rect(0, 0, $fltW, 32, 'F');
$this->SetFillColor(255, 193, 7);
$this->Rect(0, 32, $fltW, 2, 'F');
$fltTextX = 12;
$strLogo = function_exists('fxBibDistPrintMs1LogoPath') ? fxBibDistPrintMs1LogoPath() : '';
if ($strLogo !== '') {
$fltLogoW = 34;
$this->Image($strLogo, 10, 9, $fltLogoW);
$fltTextX = 10 + $fltLogoW + 5;
}
$this->SetTextColor(255, 255, 255);
$this->SetXY($fltTextX, 6);
$this->SetFont('Arial', 'B', 14);
$this->Cell($fltW - $fltTextX - 10, 7, fxBibDistPrintPdfText($this->eventTitle), 0, 2, 'L');
$this->SetFont('Arial', 'B', 12);
$this->Cell($fltW - $fltTextX - 10, 6, fxBibDistPrintPdfText($this->documentTitle), 0, 2, 'L');
if (trim((string)$this->documentSubtitle) !== '') {
$this->SetFont('Arial', 'I', 9);
$this->SetTextColor(220, 230, 255);
$this->Cell($fltW - $fltTextX - 10, 5, fxBibDistPrintPdfText($this->documentSubtitle), 0, 2, 'L');
}
$this->SetTextColor(0, 0, 0);
$this->SetY(38);
return;
}
$this->SetFont('Arial', 'B', 14);
$this->Cell(0, 8, fxBibDistPrintPdfText($this->eventTitle), 0, 1, 'C');
$this->SetFont('Arial', $this->blnIntroMode ? 'B' : '', $this->blnIntroMode ? 11 : 10);
$this->SetFont('Arial', '', 10);
$this->Cell(0, 6, fxBibDistPrintPdfText($this->documentTitle), 0, 1, 'C');
$this->Ln(2);
if (!$this->blnIntroMode && $this->eprTitle !== '') {
if ($this->eprTitle !== '') {
$this->SetFont('Arial', 'B', 11);
$this->Cell(0, 7, fxBibDistPrintPdfText($this->eprTitle), 0, 1, 'L');
$this->Ln(1);
@ -9859,12 +10170,32 @@ function fxBibOutputDistPrintPdf($int_eve_id, $strLangue = 'fr', $strPrintedBy =
}
function Footer() {
$strLogo = function_exists('fxBibDistPrintMs1LogoPath') ? fxBibDistPrintMs1LogoPath() : '';
$fltLogoW = 20;
$fltLogoReserve = ($strLogo !== '') ? ($fltLogoW + 6) : 0;
$fltPageW = $this->GetPageWidth();
if ($strLogo !== '') {
// Petit logo bas droite (toutes les pages).
$this->Image(
$strLogo,
$fltPageW - $this->rMargin - $fltLogoW,
$this->GetPageHeight() - 11,
$fltLogoW
);
}
$this->SetY(-12);
$this->SetFont('Arial', 'I', 7);
$this->Cell(0, 4, fxBibDistPrintPdfText($this->footerText), 0, 0, 'L');
$this->SetX($this->lMargin);
$this->SetTextColor(90, 90, 90);
$fltTextW = $fltPageW - $this->lMargin - $this->rMargin - $fltLogoReserve - 22;
if ($fltTextW < 40) {
$fltTextW = 40;
}
$this->Cell($fltTextW, 4, fxBibDistPrintPdfText($this->footerText), 0, 0, 'L');
$strPage = sprintf($this->pageText, $this->PageNo(), '{nb}');
$this->Cell(0, 4, fxBibDistPrintPdfText($strPage), 0, 0, 'R');
$this->Cell(22, 4, fxBibDistPrintPdfText($strPage), 0, 0, 'R');
$this->SetTextColor(0, 0, 0);
}
}
}
@ -9875,15 +10206,16 @@ function fxBibOutputDistPrintPdf($int_eve_id, $strLangue = 'fr', $strPrintedBy =
$pdf->eventTitle = fxBibDistPrintTruncate($strEventTitle, 80);
$pdf->documentTitle = fxBibTexte('bib_v4_dist_print_title', 0);
$pdf->SetMargins(10, 12, 10);
$pdf->SetAutoPageBreak(true, 14);
$pdf->SetAutoPageBreak(true, 16);
$pdf->AliasNbPages();
// MSIN-4471 — Pages d'explication (doc) avant les tableaux d'épreuves.
fxBibOutputDistPrintIntroPages($pdf, $int_eve_id, $strLangue);
// MSIN-4471 — Feuille bénévole : case remise + dossard gros + nom/prénom + ~2025 lignes/page.
// MSIN-4471 — Feuille bénévole : recherche par nom, puis dossard à remettre.
// Ordre : ☐ | Nom, Prénom | Dossard | Sexe | questions
$fltPageW = $pdf->GetPageWidth() - 20;
$fltColCheck = 9;
$fltColCheck = 10;
$fltColBib = 28;
$fltColSexe = 12;
$fltFixedW = $fltColCheck + $fltColBib + $fltColSexe;
@ -9947,9 +10279,10 @@ function fxBibOutputDistPrintPdf($int_eve_id, $strLangue = 'fr', $strPrintedBy =
$fnDrawTableHeader = function () use ($pdf, $fltColCheck, $fltColBib, $fltNameW, $fltColSexe, $fltQEach, $tabQueCols, $intNbQ, $fltHeadH) {
$pdf->SetFont('Arial', 'B', 10);
$pdf->SetFillColor(220, 220, 220);
// Case remise | Nom (recherche) | Dossard | Sexe | …
$pdf->Cell($fltColCheck, $fltHeadH, fxBibDistPrintPdfText(fxBibTexte('bib_v4_dist_print_col_check', 0)), 1, 0, 'C', true);
$pdf->Cell($fltColBib, $fltHeadH, fxBibDistPrintPdfText(fxBibTexte('bib_v4_dist_print_col_bib', 0)), 1, 0, 'C', true);
$pdf->Cell($fltNameW, $fltHeadH, fxBibDistPrintPdfText(fxBibTexte('bib_v4_dist_print_col_nom_prenom', 0)), 1, 0, 'L', true);
$pdf->Cell($fltColBib, $fltHeadH, fxBibDistPrintPdfText(fxBibTexte('bib_v4_dist_print_col_bib', 0)), 1, 0, 'C', true);
$pdf->Cell($fltColSexe, $fltHeadH, fxBibDistPrintPdfText(fxBibTexte('bib_v4_dist_print_col_sexe', 0)), 1, $intNbQ === 0 ? 1 : 0, 'C', true);
foreach ($tabQueCols as $i => $col) {
$blnLast = ($i === $intNbQ - 1);
@ -9987,14 +10320,14 @@ function fxBibOutputDistPrintPdf($int_eve_id, $strLangue = 'fr', $strPrintedBy =
$pdf->SetFont('Arial', '', 11);
$pdf->Cell($fltColCheck, $fltRowH, '', 1, 0, 'C', $blnFill);
// Dossard — signal principal.
// Nom, Prénom — colonne de recherche principale.
$pdf->SetFont('Arial', 'B', 11);
$pdf->Cell($fltNameW, $fltRowH, fxBibDistPrintPdfText(fxBibDistPrintTruncate($strNomPrenom, 42)), 1, 0, 'L', $blnFill);
// Dossard — numéro à remettre.
$pdf->SetFont('Arial', 'B', 14);
$pdf->Cell($fltColBib, $fltRowH, fxBibDistPrintPdfText((string)(int)($row['no_bib'] ?? 0)), 1, 0, 'C', $blnFill);
// Nom, Prénom.
$pdf->SetFont('Arial', '', 11);
$pdf->Cell($fltNameW, $fltRowH, fxBibDistPrintPdfText(fxBibDistPrintTruncate($strNomPrenom, 42)), 1, 0, 'L', $blnFill);
$pdf->SetFont('Arial', '', 11);
$pdf->Cell($fltColSexe, $fltRowH, fxBibDistPrintPdfText(fxBibDistPrintSexeLabel($row['par_sexe'] ?? '', $strLangue)), 1, $intNbQ === 0 ? 1 : 0, 'C', $blnFill);