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…)

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