From 3320a26ada0e6a9491e46bffcf486df4e01ec4e2 Mon Sep 17 00:00:00 2001 From: stephan Date: Sat, 25 Jul 2026 10:22:43 -0400 Subject: [PATCH] Add layout management functions for dossard PDF generation. Implement features for loading, saving, and normalizing layouts with support for dynamic box configurations. Enhance field catalog for extensibility and improve comments for clarity. Increment version code. --- assets/dossard_templates/default.layout.json | 27 + php/inc_fx_dossard_print.php | 378 +++++++++++-- superadm/ajax_dossard_layout.php | 36 ++ superadm/dossard_layout.php | 526 +++++++++++++++++++ 4 files changed, 910 insertions(+), 57 deletions(-) create mode 100644 assets/dossard_templates/default.layout.json create mode 100644 superadm/ajax_dossard_layout.php create mode 100644 superadm/dossard_layout.php diff --git a/assets/dossard_templates/default.layout.json b/assets/dossard_templates/default.layout.json new file mode 100644 index 0000000..03392e3 --- /dev/null +++ b/assets/dossard_templates/default.layout.json @@ -0,0 +1,27 @@ +{ + "version": 2, + "page": "8.5x5.5", + "boxes": [ + { + "id": "bib", + "field": "no_bib", + "align": "C", + "font_max": 220, + "left": 0.2, + "right": 0.78, + "top": 0.36, + "bottom": 0.66 + }, + { + "id": "prenom", + "field": "par_prenom", + "align": "L", + "font_max": 28, + "left": 0.32, + "right": 0.68, + "top": 0.72, + "bottom": 0.84 + } + ], + "updated_at": "2026-07-25T00:00:00+00:00" +} diff --git a/php/inc_fx_dossard_print.php b/php/inc_fx_dossard_print.php index a740e1f..b483ec6 100644 --- a/php/inc_fx_dossard_print.php +++ b/php/inc_fx_dossard_print.php @@ -172,45 +172,310 @@ function fxDossardPrintBuildQrTempFile($intEveId) } /** - * Layout calibré pour le fond comic default (8.5" × 5.5"). - * Deux boîtes indépendantes en % de page (références user annotées). + * Fichier layout calibré (GUI drag & drop → %) — vérité pour le PDF. * + * @param int $intEveId 0 = default + * @return string + */ +function fxDossardPrintLayoutPath($intEveId = 0) +{ + $strDir = fxDossardPrintTemplatesDir(); + $intEveId = intval($intEveId); + if ($intEveId > 0) { + $strEve = $strDir . '/eve_' . $intEveId . '.layout.json'; + if (is_file($strEve)) { + return $strEve; + } + } + + return $strDir . '/default.layout.json'; +} + +/** + * Catalogue des champs exploitables dans une boîte (extensible). + * + * @return array + */ +function fxDossardPrintFieldCatalog() +{ + return array( + 'no_bib' => array('label' => 'N° dossard', 'sample' => '2347', 'font_max' => 220), + 'par_prenom' => array('label' => 'Prénom', 'sample' => 'Cynthia', 'font_max' => 28), + 'par_nom' => array('label' => 'Nom', 'sample' => 'Tremblay', 'font_max' => 28), + 'par_prenom_nom' => array('label' => 'Prénom + nom', 'sample' => 'Cynthia Tremblay', 'font_max' => 22), + // Réservés pour plus tard (valeur vide tant que non chargé en SQL) + 'par_age' => array('label' => 'Âge', 'sample' => '42', 'font_max' => 24), + 'par_sexe' => array('label' => 'Sexe', 'sample' => 'F', 'font_max' => 24), + 'epr_nom' => array('label' => 'Épreuve', 'sample' => '10K', 'font_max' => 22), + ); +} + +/** + * Layout défaut : 2 boîtes (n° + prénom) — modèle extensible N boîtes. + * + * @return array{version:int,page:string,boxes:array} + */ +function fxDossardPrintDefaultLayout() +{ + return array( + 'version' => 2, + 'page' => '8.5x5.5', + 'boxes' => array( + array( + 'id' => 'bib', + 'field' => 'no_bib', + 'align' => 'C', + 'font_max' => 220, + 'left' => 0.20, + 'right' => 0.78, + 'top' => 0.36, + 'bottom' => 0.66, + ), + array( + 'id' => 'prenom', + 'field' => 'par_prenom', + 'align' => 'L', + 'font_max' => 28, + 'left' => 0.32, + 'right' => 0.68, + 'top' => 0.72, + 'bottom' => 0.84, + ), + ), + ); +} + +/** + * Normalise JSON v1 (bib/name) ou v2 (boxes) → layout v2. + * + * @param array|null $arrData + * @return array + */ +function fxDossardPrintNormalizeLayout($arrData) +{ + $arrDefault = fxDossardPrintDefaultLayout(); + if (!is_array($arrData)) { + return $arrDefault; + } + + // v1 legacy + if (empty($arrData['boxes']) && !empty($arrData['bib']) && !empty($arrData['name'])) { + $arrBoxes = $arrDefault['boxes']; + foreach (array('left', 'right', 'top', 'bottom') as $strEdge) { + if (isset($arrData['bib'][$strEdge])) { + $arrBoxes[0][$strEdge] = floatval($arrData['bib'][$strEdge]); + } + if (isset($arrData['name'][$strEdge])) { + $arrBoxes[1][$strEdge] = floatval($arrData['name'][$strEdge]); + } + } + $arrDefault['boxes'] = $arrBoxes; + return $arrDefault; + } + + if (empty($arrData['boxes']) || !is_array($arrData['boxes'])) { + return $arrDefault; + } + + $arrCatalog = fxDossardPrintFieldCatalog(); + $arrBoxes = array(); + foreach ($arrData['boxes'] as $intIdx => $arrBox) { + if (!is_array($arrBox)) { + continue; + } + $strField = isset($arrBox['field']) ? (string)$arrBox['field'] : ''; + if ($strField === '' || !isset($arrCatalog[$strField])) { + continue; + } + foreach (array('left', 'right', 'top', 'bottom') as $strEdge) { + if (!isset($arrBox[$strEdge])) { + continue 2; + } + $flt = floatval($arrBox[$strEdge]); + if ($flt < 0 || $flt > 1) { + continue 2; + } + $arrBox[$strEdge] = $flt; + } + if ($arrBox['right'] <= $arrBox['left'] || $arrBox['bottom'] <= $arrBox['top']) { + continue; + } + $strAlign = strtoupper((string)($arrBox['align'] ?? 'C')); + if ($strAlign !== 'L' && $strAlign !== 'R') { + $strAlign = 'C'; + } + $intFontMax = isset($arrBox['font_max']) + ? max(8, intval($arrBox['font_max'])) + : intval($arrCatalog[$strField]['font_max']); + $strId = trim((string)($arrBox['id'] ?? '')); + if ($strId === '') { + $strId = 'box_' . ($intIdx + 1); + } + $arrBoxes[] = array( + 'id' => $strId, + 'field' => $strField, + 'align' => $strAlign, + 'font_max' => $intFontMax, + 'left' => round($arrBox['left'], 5), + 'right' => round($arrBox['right'], 5), + 'top' => round($arrBox['top'], 5), + 'bottom' => round($arrBox['bottom'], 5), + ); + } + + if (count($arrBoxes) < 1) { + return $arrDefault; + } + + return array( + 'version' => 2, + 'page' => '8.5x5.5', + 'boxes' => $arrBoxes, + 'updated_at' => isset($arrData['updated_at']) ? (string)$arrData['updated_at'] : null, + ); +} + +/** + * Lit le layout calibré (JSON GUI) ou défaut. + * + * @param int $intEveId + * @return array + */ +function fxDossardPrintLoadLayout($intEveId = 0) +{ + $strPath = fxDossardPrintLayoutPath($intEveId); + if (!is_file($strPath)) { + return fxDossardPrintDefaultLayout(); + } + $strJson = @file_get_contents($strPath); + $arrData = is_string($strJson) ? json_decode($strJson, true) : null; + + return fxDossardPrintNormalizeLayout($arrData); +} + +/** + * @deprecated alias GUI ancienne — préfère fxDossardPrintLoadLayout + * @return array + */ +function fxDossardPrintLoadLayoutPct($intEveId = 0) +{ + return fxDossardPrintLoadLayout($intEveId); +} + +/** + * Enregistre le layout calibré (liste de boîtes). + * + * @param array $arrLayout version + boxes[] + * @param int $intEveId 0 = default.layout.json + * @return array{ok:bool,error?:string,path?:string} + */ +function fxDossardPrintSaveLayout($arrLayout, $intEveId = 0) +{ + $intEveId = intval($intEveId); + $strDir = fxDossardPrintTemplatesDir(); + if (!is_dir($strDir)) { + return array('ok' => false, 'error' => 'Dossier templates absent.'); + } + + $arrNorm = fxDossardPrintNormalizeLayout($arrLayout); + if (empty($arrNorm['boxes'])) { + return array('ok' => false, 'error' => 'Aucuneune boîte valide.'); + } + + $strPath = ($intEveId > 0) + ? $strDir . '/eve_' . $intEveId . '.layout.json' + : $strDir . '/default.layout.json'; + + $arrOut = array( + 'version' => 2, + 'page' => '8.5x5.5', + 'boxes' => $arrNorm['boxes'], + 'updated_at' => date('c'), + ); + $strJson = json_encode($arrOut, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($strJson === false || @file_put_contents($strPath, $strJson) === false) { + return array('ok' => false, 'error' => 'Écriture impossible (droits fichier?).'); + } + + return array('ok' => true, 'path' => $strPath); +} + +/** + * @deprecated — accepte encore {bib,name} ou {boxes} + * @param array $arrPct + * @param int $intEveId + * @return array + */ +function fxDossardPrintSaveLayoutPct($arrPct, $intEveId = 0) +{ + if (isset($arrPct['boxes'])) { + return fxDossardPrintSaveLayout($arrPct, $intEveId); + } + + return fxDossardPrintSaveLayout($arrPct, $intEveId); +} + +/** + * Valeur texte pour un champ boîte. + * + * @param array $arrRow + * @param string $strField + * @return string + */ +function fxDossardPrintResolveFieldValue($arrRow, $strField) +{ + $fnUn = function ($str) { + $str = (string)$str; + return function_exists('fxUnescape') ? fxUnescape($str) : $str; + }; + + switch ((string)$strField) { + case 'no_bib': + $str = trim((string)($arrRow['no_bib'] ?? '')); + return ($str === '' || $str === '0') ? '-' : $str; + case 'par_prenom': + return trim($fnUn($arrRow['par_prenom'] ?? '')); + case 'par_nom': + return trim($fnUn($arrRow['par_nom'] ?? '')); + case 'par_prenom_nom': + return trim($fnUn($arrRow['par_prenom'] ?? '') . ' ' . $fnUn($arrRow['par_nom'] ?? '')); + case 'par_age': + return trim((string)($arrRow['par_age'] ?? $arrRow['age'] ?? '')); + case 'par_sexe': + return trim((string)($arrRow['par_sexe'] ?? '')); + case 'epr_nom': + return trim($fnUn($arrRow['epr_nom'] ?? $arrRow['epr_libelle'] ?? '')); + default: + return ''; + } +} + +/** + * Layout mm pour le PDF — lit le JSON calibré (N boîtes). + * + * @param int $intEveId * @return array */ -function fxDossardPrintLayout() +function fxDossardPrintLayout($intEveId = 0) { $arrSize = fxDossardPrintPageSizeMm(); $fltW = $arrSize[0]; $fltH = $arrSize[1]; + $arrLayout = fxDossardPrintLoadLayout($intEveId); - // --- Boîte NUMÉRO (grande) — % page, d'après annotation rouge --- - $arrBibPct = array( - 'left' => 0.20, - 'right' => 0.78, - 'top' => 0.36, - 'bottom' => 0.66, - ); - // --- Boîte PRÉNOM (petite, plus bas, centrée) — PAS collée au n° --- - $arrNamePct = array( - 'left' => 0.32, - 'right' => 0.68, - 'top' => 0.72, - 'bottom' => 0.84, - ); - - $arrBib = fxDossardPrintBoxFromPct($fltW, $fltH, $arrBibPct); - $arrName = fxDossardPrintBoxFromPct($fltW, $fltH, $arrNamePct); + $arrBoxesMm = array(); + foreach ($arrLayout['boxes'] as $arrBox) { + $arrRect = fxDossardPrintBoxFromPct($fltW, $fltH, $arrBox); + $arrBoxesMm[] = array_merge($arrBox, $arrRect); + } $fltQr = 28.0; return array( 'page_w' => $fltW, 'page_h' => $fltH, - 'bib' => $arrBib, - 'name' => $arrName, + 'boxes' => $arrBoxesMm, 'ascent_ratio' => 0.72, - 'bib_font_max' => 220, - 'name_font_max' => 28, 'qr_size' => $fltQr, 'qr_x' => $fltW - $fltQr - 11.5, 'qr_y' => $fltH - $fltQr - 14.5, @@ -278,15 +543,20 @@ function fxDossardPrintFitBibFont($pdf, $strText, $fltMaxW, $fltMaxH, $intFontMa } /** - * Texte centré dans une boîte (Cell = centre H + V FPDF). + * Texte dans une boîte (Cell FPDF : align H + centre V de la cellule). * * @param FPDF $pdf * @param array $arrBox x,y,w,h * @param string $strText * @param int $intFont + * @param string $strAlign C|L|R */ -function fxDossardPrintDrawCenteredInBox($pdf, $arrBox, $strText, $intFont) +function fxDossardPrintDrawInBox($pdf, $arrBox, $strText, $intFont, $strAlign = 'C') { + $strAlign = strtoupper((string)$strAlign); + if ($strAlign !== 'L' && $strAlign !== 'R') { + $strAlign = 'C'; + } $pdf->SetFont('Helvetica', 'B', $intFont); $pdf->SetXY($arrBox['x'], $arrBox['y']); $pdf->Cell( @@ -295,10 +565,16 @@ function fxDossardPrintDrawCenteredInBox($pdf, $arrBox, $strText, $intFont) fxDossardPrintPdfText($strText), 0, 0, - 'C' + $strAlign ); } +/** @deprecated alias — préfère fxDossardPrintDrawInBox(..., 'C') */ +function fxDossardPrintDrawCenteredInBox($pdf, $arrBox, $strText, $intFont) +{ + fxDossardPrintDrawInBox($pdf, $arrBox, $strText, $intFont, 'C'); +} + /** * Génère et envoie le PDF (exit). * @@ -328,14 +604,7 @@ function fxDossardPrintOutputPdf($arrRow) } require_once $strFpdf; - $arrLayout = fxDossardPrintLayout(); - $strBib = trim((string)($arrRow['no_bib'] ?? '')); - if ($strBib === '' || $strBib === '0') { - $strBib = '-'; - } - // MSIN-4485 — prénom seul, centré dans sa boîte (%). - $strName = trim(function_exists('fxUnescape') ? fxUnescape($arrRow['par_prenom'] ?? '') : (string)($arrRow['par_prenom'] ?? '')); - + $arrLayout = fxDossardPrintLayout($intEveId); $strQrFile = fxDossardPrintBuildQrTempFile($intEveId); $strTplType = fxDossardPrintImageType($strTemplate); @@ -355,30 +624,25 @@ function fxDossardPrintOutputPdf($arrRow) $pdf->SetTextColor(0, 0, 0); - // 1) Numéro → grande boîte, centré, police max - $arrBibBox = $arrLayout['bib']; - $intBibFont = fxDossardPrintFitBibFont( - $pdf, - $strBib, - $arrBibBox['w'] * 0.98, - $arrBibBox['h'] * 0.92, - $arrLayout['bib_font_max'], - $arrLayout['ascent_ratio'] - ); - fxDossardPrintDrawCenteredInBox($pdf, $arrBibBox, $strBib, $intBibFont); - - // 2) Prénom → petite boîte, centré, police max - if ($strName !== '') { - $arrNameBox = $arrLayout['name']; - $intNameFont = fxDossardPrintFitBibFont( + // MSIN-4485 — N boîtes configurables (champ + align + %) + $strBibForFile = '-'; + foreach ($arrLayout['boxes'] as $arrBox) { + $strText = fxDossardPrintResolveFieldValue($arrRow, $arrBox['field']); + if ($arrBox['field'] === 'no_bib') { + $strBibForFile = $strText; + } + if ($strText === '') { + continue; + } + $intFont = fxDossardPrintFitBibFont( $pdf, - $strName, - $arrNameBox['w'] * 0.95, - $arrNameBox['h'] * 0.90, - $arrLayout['name_font_max'], + $strText, + $arrBox['w'] * 0.98, + $arrBox['h'] * 0.92, + intval($arrBox['font_max']), $arrLayout['ascent_ratio'] ); - fxDossardPrintDrawCenteredInBox($pdf, $arrNameBox, $strName, $intNameFont); + fxDossardPrintDrawInBox($pdf, $arrBox, $strText, $intFont, $arrBox['align']); } if ($strQrFile !== false) { @@ -393,7 +657,7 @@ function fxDossardPrintOutputPdf($arrRow) @unlink($strQrFile); } - $strSafeBib = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $strBib); + $strSafeBib = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $strBibForFile); $strFilename = 'dossard-' . $strSafeBib . '-par' . intval($arrRow['par_id']) . '.pdf'; $pdf->Output('I', $strFilename); diff --git a/superadm/ajax_dossard_layout.php b/superadm/ajax_dossard_layout.php new file mode 100644 index 0000000..b30c94d --- /dev/null +++ b/superadm/ajax_dossard_layout.php @@ -0,0 +1,36 @@ + 0) { + ob_end_clean(); +} +header('Content-Type: application/json; charset=utf-8'); + +$strRaw = file_get_contents('php://input'); +$arrIn = is_string($strRaw) ? json_decode($strRaw, true) : null; +if (!is_array($arrIn)) { + http_response_code(400); + echo json_encode(array('ok' => false, 'error' => 'JSON invalide.')); + exit; +} + +$intEveId = isset($arrIn['eve_id']) ? intval($arrIn['eve_id']) : 0; +$arrResult = fxDossardPrintSaveLayout($arrIn, $intEveId); +if (empty($arrResult['ok'])) { + http_response_code(400); +} +echo json_encode($arrResult); +exit; diff --git a/superadm/dossard_layout.php b/superadm/dossard_layout.php new file mode 100644 index 0000000..0c7003f --- /dev/null +++ b/superadm/dossard_layout.php @@ -0,0 +1,526 @@ + 0) { + foreach (array('jpg', 'jpeg', 'png') as $strExt) { + $strCand = dirname(__DIR__) . '/assets/dossard_templates/eve_' . $intEveId . '.' . $strExt; + if (is_file($strCand)) { + $strTplWeb = '../assets/dossard_templates/eve_' . $intEveId . '.' . $strExt; + break; + } + } +} + +$strLayoutJson = json_encode($arrLayout, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); +$strCatalogJson = json_encode($arrCatalog, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); +?> + + + + + +MSIN-4485 — Calibrage dossard + + + +
+
+

Calibrage dossard — boîtes + champs

+

Glisse / resize · choisis le champ et l’alignement · ajoute des boîtes pour plus tard (nom, âge…)

+
+
+ + + + +
+
+
+ +
+
+
+
+
+ + +