Files
ms1inscription-v5/php/inc_fx_dossard_print.php

663 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* MSIN-4485 — Impression dossard PDF (1 participant).
*
* Zone utile : 8.5" × 5.5" (demi-Lettre coupée sur le 11").
* Format feuille PDF : à trancher selon le mode dimpression (voir discussion produit).
*
* Impression type : échelle 100 %, sans « ajuster à la page » si le papier = taille PDF.
*/
define('MSIN_DOSSARD_PRINT_PERM', 'inscriptions_gestion.dossard_print');
/**
* Zone utile dossard (pouces → mm) : 8.5" × 5.5".
*
* @return array{0:float,1:float} [largeur, hauteur]
*/
function fxDossardPrintPageSizeMm()
{
return array(8.5 * 25.4, 5.5 * 25.4);
}
/** Dossier des fonds JPG/PNG (même ratio 8.5:5.5 recommandé). */
function fxDossardPrintTemplatesDir()
{
return dirname(__DIR__) . '/assets/dossard_templates';
}
/**
* Fond : eve_{id}.jpg|png sinon default.jpg|png.
*
* @return string|null
*/
function fxDossardPrintResolveTemplatePath($intEveId)
{
$strDir = fxDossardPrintTemplatesDir();
$intEveId = intval($intEveId);
$arrCandidates = array();
if ($intEveId > 0) {
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.jpg';
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.jpeg';
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.png';
}
$arrCandidates[] = $strDir . '/default.jpg';
$arrCandidates[] = $strDir . '/default.jpeg';
$arrCandidates[] = $strDir . '/default.png';
foreach ($arrCandidates as $strPath) {
if (is_file($strPath)) {
return $strPath;
}
}
return null;
}
/**
* Type FPDF d'après le fichier réel.
*
* @return string JPG|PNG|GIF|''
*/
function fxDossardPrintImageType($strPath)
{
$arrInfo = @getimagesize($strPath);
if (!is_array($arrInfo) || empty($arrInfo[2])) {
return '';
}
if ((int)$arrInfo[2] === IMAGETYPE_JPEG) {
return 'JPG';
}
if ((int)$arrInfo[2] === IMAGETYPE_PNG) {
return 'PNG';
}
if ((int)$arrInfo[2] === IMAGETYPE_GIF) {
return 'GIF';
}
return '';
}
/** Texte FPDF (core fonts = Windows-1252). */
function fxDossardPrintPdfText($str)
{
$str = (string)$str;
if (function_exists('iconv')) {
$strConverted = @iconv('UTF-8', 'Windows-1252//TRANSLIT', $str);
if ($strConverted !== false) {
return $strConverted;
}
}
if (function_exists('mb_convert_encoding')) {
return (string)mb_convert_encoding($str, 'Windows-1252', 'UTF-8');
}
return $str;
}
/** Droit impression (hors kit total). */
function fxDossardPrintCan($intComId, $intEveId)
{
if (!function_exists('fxEveAccesHasPermission')) {
require_once __DIR__ . '/inc_fx_eve_acces.php';
}
return fxEveAccesHasPermission(intval($intComId), intval($intEveId), MSIN_DOSSARD_PRINT_PERM);
}
/**
* @return array|null
*/
function fxDossardPrintLoadParticipant($intParId, $intEveId)
{
global $objDatabase;
$intParId = intval($intParId);
$intEveId = intval($intEveId);
if ($intParId <= 0 || $intEveId <= 0) {
return null;
}
$sql = 'SELECT p.par_id, p.par_prenom, p.par_nom, p.no_bib, p.eve_id, p.epr_id, p.pec_id,
e.pec_id_original, e.pec_actif, e.is_cancelled
FROM resultats_participants p
INNER JOIN resultats_epreuves_commandees e
ON e.pec_id_original = p.pec_id AND e.epr_id = p.epr_id
WHERE p.par_id = ' . $intParId . '
AND p.eve_id = ' . $intEveId . '
LIMIT 1';
$row = $objDatabase->fxGetRow($sql);
return ($row === null || $row === false) ? null : $row;
}
function fxDossardPrintQrUrl($intEveId)
{
if (!function_exists('fxKcEveResultatsQrUrl')) {
require_once __DIR__ . '/inc_fx_eve_extra.php';
}
return fxKcEveResultatsQrUrl(intval($intEveId));
}
/**
* @return string|false chemin PNG temp
*/
function fxDossardPrintBuildQrTempFile($intEveId)
{
require_once __DIR__ . '/../libs/phpqrcode-master/qrlib.php';
$strUrl = fxDossardPrintQrUrl($intEveId);
if ($strUrl === '') {
return false;
}
$strTmp = tempnam(sys_get_temp_dir(), 'ms1dossardqr_');
if ($strTmp === false) {
return false;
}
$strPng = $strTmp . '.png';
@unlink($strTmp);
QRcode::png($strUrl, $strPng, QR_ECLEVEL_M, 6, 1);
if (!is_file($strPng)) {
return false;
}
return $strPng;
}
/**
* 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<string,array{label:string,sample:string,font_max:int}>
*/
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<string,mixed>
*/
function fxDossardPrintLayout($intEveId = 0)
{
$arrSize = fxDossardPrintPageSizeMm();
$fltW = $arrSize[0];
$fltH = $arrSize[1];
$arrLayout = fxDossardPrintLoadLayout($intEveId);
$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,
'boxes' => $arrBoxesMm,
'ascent_ratio' => 0.72,
'qr_size' => $fltQr,
'qr_x' => $fltW - $fltQr - 11.5,
'qr_y' => $fltH - $fltQr - 14.5,
);
}
/**
* Rectangle mm depuis fractions 01 de la page.
*
* @param float $fltPageW
* @param float $fltPageH
* @param array<string,float> $arrPct left|right|top|bottom
* @return array{x:float,y:float,w:float,h:float}
*/
function fxDossardPrintBoxFromPct($fltPageW, $fltPageH, $arrPct)
{
$fltX = $fltPageW * floatval($arrPct['left']);
$fltY = $fltPageH * floatval($arrPct['top']);
$fltW = $fltPageW * (floatval($arrPct['right']) - floatval($arrPct['left']));
$fltH = $fltPageH * (floatval($arrPct['bottom']) - floatval($arrPct['top']));
return array(
'x' => $fltX,
'y' => $fltY,
'w' => $fltW,
'h' => $fltH,
);
}
/**
* Plus grande police Helvetica Bold qui tient dans une boîte (largeur + hauteur).
*
* @param FPDF $pdf
* @param string $strText
* @param float $fltMaxW
* @param float $fltMaxH
* @param int $intFontMax
* @param float $fltAscentRatio
* @return int
*/
function fxDossardPrintFitBibFont($pdf, $strText, $fltMaxW, $fltMaxH, $intFontMax, $fltAscentRatio = 0.72)
{
$strPdf = fxDossardPrintPdfText($strText);
$fltAscentRatio = ($fltAscentRatio > 0) ? $fltAscentRatio : 0.72;
$fltPtPerMm = 72 / 25.4;
$intCapByH = ($fltMaxH > 0)
? (int)floor($fltMaxH * $fltPtPerMm / $fltAscentRatio)
: intval($intFontMax);
$intHi = max(12, min(intval($intFontMax), $intCapByH));
$intLo = 12;
$intBest = $intLo;
while ($intLo <= $intHi) {
$intMid = (int)floor(($intLo + $intHi) / 2);
$pdf->SetFont('Helvetica', 'B', $intMid);
if ($pdf->GetStringWidth($strPdf) <= $fltMaxW) {
$intBest = $intMid;
$intLo = $intMid + 1;
} else {
$intHi = $intMid - 1;
}
}
return $intBest;
}
/**
* 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 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(
$arrBox['w'],
$arrBox['h'],
fxDossardPrintPdfText($strText),
0,
0,
$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).
*
* @param array $arrRow
*/
function fxDossardPrintOutputPdf($arrRow)
{
$intEveId = intval($arrRow['eve_id'] ?? 0);
$strTemplate = fxDossardPrintResolveTemplatePath($intEveId);
if ($strTemplate === null) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'Template dossard introuvable (assets/dossard_templates/default.jpg).';
exit;
}
$strFpdf = dirname(__DIR__) . '/fpdf182/fpdf.php';
if (!is_file($strFpdf)) {
$strDocRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
$strFpdf = rtrim((string)$strDocRoot, '/\\') . '/fpdf182/fpdf.php';
}
if (!is_file($strFpdf)) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'FPDF introuvable (fpdf182/fpdf.php).';
exit;
}
require_once $strFpdf;
$arrLayout = fxDossardPrintLayout($intEveId);
$strQrFile = fxDossardPrintBuildQrTempFile($intEveId);
$strTplType = fxDossardPrintImageType($strTemplate);
$pdf = new FPDF('L', 'mm', array($arrLayout['page_w'], $arrLayout['page_h']));
$pdf->SetAutoPageBreak(false);
$pdf->SetMargins(0, 0, 0);
$pdf->AddPage();
$pdf->Image(
$strTemplate,
0,
0,
$arrLayout['page_w'],
$arrLayout['page_h'],
$strTplType
);
$pdf->SetTextColor(0, 0, 0);
// 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,
$strText,
$arrBox['w'] * 0.98,
$arrBox['h'] * 0.92,
intval($arrBox['font_max']),
$arrLayout['ascent_ratio']
);
fxDossardPrintDrawInBox($pdf, $arrBox, $strText, $intFont, $arrBox['align']);
}
if ($strQrFile !== false) {
$pdf->Image(
$strQrFile,
$arrLayout['qr_x'],
$arrLayout['qr_y'],
$arrLayout['qr_size'],
$arrLayout['qr_size'],
'PNG'
);
@unlink($strQrFile);
}
$strSafeBib = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $strBibForFile);
$strFilename = 'dossard-' . $strSafeBib . '-par' . intval($arrRow['par_id']) . '.pdf';
$pdf->Output('I', $strFilename);
exit;
}