open($tmp, ZipArchive::OVERWRITE) !== true) { throw new RuntimeException(t('Le fichier Word n\'a pas pu être créé.')); } $avecPhoto = $photo !== null && is_file($photo); $zip->addFromString('[Content_Types].xml', self::types()); $zip->addFromString('_rels/.rels', self::relsRacine()); $zip->addFromString('docProps/core.xml', self::proprietes($d)); $zip->addFromString('word/document.xml', self::document($d)); $zip->addFromString('word/header1.xml', self::entete($d, $logo, $avecPhoto ? $photo : null)); $zip->addFromString('word/header2.xml', '' . self::p('') . ''); $zip->addFromString('word/_rels/header1.xml.rels', self::relsEntete($avecPhoto)); $zip->addFromString('word/styles.xml', self::styles(['fr' => 'fr-FR', 'en' => 'en-GB', 'de' => 'de-DE'][$d['langue']] ?? 'fr-FR')); $zip->addFromString('word/settings.xml', self::reglages()); $zip->addFromString('word/footer1.xml', self::pied()); $zip->addFromString('word/_rels/document.xml.rels', self::relsDocument()); $zip->addFromString('word/media/logo.png', (string) file_get_contents($logo)); if ($avecPhoto) { $zip->addFromString('word/media/photo.jpg', (string) file_get_contents($photo)); } $zip->close(); $contenu = (string) file_get_contents($tmp); unlink($tmp); return $contenu; } // ----------------------------------------------------------------------- // Briques // ----------------------------------------------------------------------- private static function x(string $texte): string { $texte = preg_replace('/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', '', $texte) ?? ''; return htmlspecialchars($texte, ENT_XML1 | ENT_QUOTES, 'UTF-8'); } /** Options : b, i, caps, color, sz (demi-points). */ private static function r(string $texte, array $o = []): string { $p = ''; if (!empty($o['b'])) { $p .= ''; } if (!empty($o['i'])) { $p .= ''; } if (!empty($o['caps'])) { $p .= ''; } if (isset($o['color'])) { $p .= ''; } if (isset($o['sz'])) { $p .= ''; } return '' . ($p !== '' ? '' . $p . '' : '') . '' . self::x($texte) . ''; } /** Options : style, avant, apres, keepNext, retrait (gauche), suspendu, jc, bordureBas [taille, couleur]. */ private static function p(string $runs, array $o = []): string { $p = ''; if (isset($o['style'])) { $p .= ''; } if (!empty($o['keepNext'])) { $p .= ''; } if (isset($o['bordureBas'])) { $p .= ''; } if (isset($o['avant']) || isset($o['apres'])) { $p .= ''; } if (isset($o['retrait'])) { $p .= ''; } if (isset($o['jc'])) { $p .= ''; } return '' . ($p !== '' ? '' . $p . '' : '') . $runs . ''; } private static function tc(int $largeur, string $contenu, array $o = []): string { $p = ''; if (isset($o['span'])) { $p .= ''; } if (isset($o['bordures'])) { $p .= '' . $o['bordures'] . ''; } if (isset($o['fond'])) { $p .= ''; } if (isset($o['marges'])) { [$h, $dr, $b, $g] = $o['marges']; $p .= '' . ''; } $p .= ''; return '' . $p . '' . $contenu . ''; } private static function tbl(array $colonnes, string $lignes, int $retrait = 0): string { $grille = ''; foreach ($colonnes as $l) { $grille .= ''; } return '' . '' . '' . '' . $grille . '' . $lignes . ''; } private static function filet(string $cote, int $taille, string $couleur): string { return ''; } /** Image en ligne. $l et $h en mm. */ private static function image(string $rid, int $id, string $nom, float $l, float $h): string { $cx = (int) round($l * 36000); $cy = (int) round($h * 36000); return '' . '' . '' . '' . '' . '' . '' . ''; } private static function titre(string $texte): string { return self::p(self::r('● ', ['color' => self::BLEU_CLAIR, 'sz' => 16]) . self::r($texte), ['style' => 'TitreEuropass']); } // ----------------------------------------------------------------------- // Document // ----------------------------------------------------------------------- private static function document(array $d): string { $L = $d['libelles']; $corps = ''; if ($d['resume']) { $corps .= self::titre($L['apropos']); foreach ($d['resume'] as $l) { $corps .= self::p(self::r($l)); } } if ($d['formations']) { $corps .= self::titre($L['formation']); foreach ($d['formations'] as $i => $f) { $dernier = $i === count($d['formations']) - 1; if (trim($f['dates'] . $f['lieu']) !== '') { $corps .= self::p(self::r(trim($f['dates'] . ' ' . $f['lieu']), ['color' => self::GRIS_CLAIR, 'sz' => 17]), ['apres' => 20, 'keepNext' => true]); } $corps .= self::p( self::r($f['diplome'], ['b' => true, 'color' => self::GRIS, 'sz' => 20]) . ($f['etablissement'] !== '' ? self::r(' ' . $f['etablissement'], ['color' => self::GRIS, 'sz' => 20]) : ''), ['apres' => 120] + ($dernier ? [] : ['bordureBas' => [4, self::FILET]]) ); } } if ($d['experiences']) { $corps .= self::titre($L['experience']); foreach ($d['experiences'] as $i => $x) { $dernier = $i === count($d['experiences']) - 1; if ($x['employeur'] !== '' || $x['lieu'] !== '') { $corps .= self::p( self::r($x['employeur'], ['b' => true]) . ($x['employeur'] !== '' && $x['lieu'] !== '' ? self::r(' – ') : '') . self::r($x['lieu']), ['apres' => 20, 'keepNext' => true] ); } $corps .= self::p( self::r($x['poste'], ['b' => true, 'color' => self::GRIS, 'sz' => 20]) . self::r(' – ' . $x['dates'], ['color' => self::BLEU, 'sz' => 20]), ['apres' => 40, 'keepNext' => true] ); foreach ($x['details'] as [$libelle, $valeur]) { $corps .= self::p(self::r($libelle . $d['deux_points'] . ' ', ['b' => true]) . self::r($valeur), ['apres' => 20]); } if ($x['realisations']) { $corps .= self::p(self::r($L['realisations'], ['b' => true]), ['avant' => 40, 'apres' => 20, 'keepNext' => true]); foreach ($x['realisations'] as $r) { $corps .= self::p(self::r('•') . '' . self::r($r), ['retrait' => 600, 'suspendu' => 240, 'apres' => 10]); } } $corps .= self::p('', ['apres' => 100] + ($dernier ? [] : ['bordureBas' => [4, self::FILET]])); } } if ($d['maternelles'] || $d['autres']) { $corps .= self::titre($L['langues']); if ($d['maternelles']) { $corps .= self::p(self::r($L['maternelles'] . ' ') . self::r(implode(' | ', $d['maternelles']), ['b' => true])); } if ($d['autres']) { $corps .= self::p(self::r($L['autres']), ['avant' => 60]); $corps .= $d['avec_grille'] ? self::grille($d) : self::p(implode(self::r(' '), array_map( static fn ($l) => self::r($l['langue'], ['b' => true]) . ($l['niveau'] !== '' ? self::r($d['deux_points'] . ' ' . $l['niveau']) : ''), $d['autres'] ))); $corps .= self::p(self::r($L['niveaux'], ['i' => true, 'color' => self::GRIS_CLAIR, 'sz' => 16]), ['avant' => 40]); } } if ($d['competences']) { $corps .= self::titre($L['competences']); foreach ($d['competences'] as $groupe => $liste) { $corps .= self::p(self::r($groupe, ['b' => true, 'color' => self::GRIS_CLAIR]), ['avant' => 80, 'apres' => 20, 'keepNext' => true]); $corps .= self::p(self::r(implode(' | ', $liste))); } } if ($d['benevolat']) { $corps .= self::titre($L['benevolat']); foreach ($d['benevolat'] as $b) { $corps .= self::p(self::r($b, ['b' => true])); } } if ($d['permis']) { $corps .= self::titre($L['permis']); foreach ($d['permis'] as $p) { $corps .= self::p(self::r($L['permis_ligne'] . ' ', ['b' => true]) . self::r($p)); } } // Première page : le bandeau est dans son en-tête ; les pages suivantes ont un en-tête vide et une marge haute $section = '' . '' . '' . '' . '' . '' . ''; return '' . '' . $corps . $section . ''; } /** En-tête de la première page : bandeau bleu de bord à bord, avec photo, nom, titre, coordonnées et logo. */ private static function entete(array $d, string $logo, ?string $photo): string { $bleu = ['fond' => self::BLEU]; [$lpx, $hpx] = getimagesize($logo) ?: [700, 142]; $logoL = 34.0; $logoH = round($logoL * $hpx / $lpx, 1); $lLogo = 2400; $lPhoto = $photo ? 2000 : 0; $lTexte = self::PAGE_L - 2 * self::MARGE - $lLogo - $lPhoto; $identite = self::p(self::r($d['nom'], ['b' => true, 'color' => 'FFFFFF', 'sz' => 34]), ['apres' => 20]); if ($d['titre'] !== '') { $identite .= self::p(self::r($d['titre'], ['color' => 'DBE9F4', 'sz' => 21]), ['apres' => 60]); } if ($d['coordonnees']) { $runs = ''; foreach ($d['coordonnees'] as $i => $k) { $runs .= ($i ? self::r(' | ', ['color' => '6E95B8', 'sz' => 17]) : '') . self::r($k['libelle'] . $d['deux_points'] . ' ', ['b' => true, 'color' => 'FFFFFF', 'sz' => 17]) . self::r($k['valeur'], ['color' => 'FFFFFF', 'sz' => 17]); } $identite .= self::p($runs, ['apres' => 0]); } $vide = self::p('', ['apres' => 0]); $cellules = self::tc(self::MARGE, $vide, $bleu); if ($photo) { $cellules .= self::tc($lPhoto, self::p(self::image('rId2', 2, 'Photo', 28, 28), ['apres' => 0]), $bleu + ['vAlign' => 'center']); } $cellules .= self::tc($lTexte, $identite, $bleu + ['vAlign' => 'center', 'marges' => [0, 200, 0, 0]]) . self::tc($lLogo, self::p(self::image('rId1', 1, 'europass', $logoL, $logoH), ['apres' => 0, 'jc' => 'right']), $bleu + ['vAlign' => 'top']) . self::tc(self::MARGE, $vide, $bleu); $colonnes = array_values(array_filter([self::MARGE, $lPhoto, $lTexte, $lLogo, self::MARGE])); $lignes = '' . self::tc(self::PAGE_L, $vide, $bleu + ['span' => count($colonnes)]) . '' . '' . $cellules . '' . '' . self::tc(self::PAGE_L, $vide, $bleu + ['span' => count($colonnes)]) . ''; return '' . self::tbl($colonnes, $lignes, -self::MARGE) . self::p('', ['apres' => 0]) . ''; } /** Grille européenne des langues. */ private static function grille(array $d): string { $L = $d['libelles']; $lLangue = 1900; $lCase = intdiv(self::TEXTE_L - $lLangue, 5); $colonnes = [$lLangue, $lCase, $lCase, $lCase, $lCase, $lCase]; $centre = static fn (string $runs): string => self::p($runs, ['jc' => 'center', 'apres' => 0]); $haut = self::filet('top', 6, self::BLEU_CLAIR); $bas = self::filet('bottom', 6, self::BLEU_CLAIR); $marges = ['marges' => [60, 40, 60, 40]]; $lignes = '' . self::tc($lLangue, self::p(''), $marges) . self::tc(2 * $lCase, $centre(self::r(mb_strtoupper($L['comprehension']), ['b' => true, 'sz' => 17])), $marges + ['span' => 2, 'bordures' => $bas]) . self::tc(2 * $lCase, $centre(self::r(mb_strtoupper($L['expression']), ['b' => true, 'sz' => 17])), $marges + ['span' => 2, 'bordures' => $bas]) . self::tc($lCase, $centre(self::r(mb_strtoupper($L['ecrit']), ['b' => true, 'sz' => 17])), $marges + ['bordures' => $bas]) . ''; $sous = self::tc($lLangue, self::p(''), $marges); foreach (array_slice($L['grille'], 0, 4) as $g) { $sous .= self::tc($lCase, $centre(self::r($g, ['sz' => 16])), $marges); } $lignes .= '' . $sous . self::tc($lCase, self::p(''), $marges) . ''; foreach ($d['autres'] as $i => $l) { $fond = $i % 2 === 0 ? ['fond' => self::FOND_LIGNE] : []; $bord = ['bordures' => $haut . ($i === count($d['autres']) - 1 ? $bas : '')]; $cellules = self::tc($lLangue, self::p(self::r($l['langue'], ['b' => true, 'sz' => 17]), ['apres' => 0]), $marges + $fond + $bord + ['vAlign' => 'center']); if ($l['grille']) { foreach ($l['grille'] as $niveau) { $cellules .= self::tc($lCase, $centre(self::r($niveau ?: '–', ['sz' => 17])), $marges + $fond + $bord + ['vAlign' => 'center']); } } else { $cellules .= self::tc(5 * $lCase, $centre(self::r($l['niveau'] !== '' ? $L['niveau'] . ' ' . $l['niveau'] : '–', ['sz' => 17])), $marges + $fond + $bord + ['span' => 5, 'vAlign' => 'center']); } $lignes .= '' . $cellules . ''; } return self::tbl($colonnes, $lignes); } private static function pied(): string { $petit = ['color' => self::GRIS_CLAIR, 'sz' => 14]; $champ = static fn (string $nom): string => '' . self::r('1', $petit) . ''; return '' . '' . self::p($champ('PAGE') . self::r(' / ', $petit) . $champ('NUMPAGES'), ['jc' => 'right', 'apres' => 0]) . ''; } // ----------------------------------------------------------------------- // Fichiers fixes // ----------------------------------------------------------------------- private static function styles(string $langue): string { return '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . ''; } private static function reglages(): string { return '' . '' . '' . '' . ''; } private static function types(): string { $wml = 'application/vnd.openxmlformats-officedocument.wordprocessingml.'; return '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . ''; } private static function relsRacine(): string { return '' . '' . '' . '' . ''; } private static function relsDocument(): string { $t = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/'; return '' . '' . '' . '' . '' . '' . '' . ''; } private static function relsEntete(bool $photo): string { $t = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/'; return '' . '' . '' . ($photo ? '' : '') . ''; } private static function proprietes(array $d): string { $date = gmdate('Y-m-d\TH:i:s\Z'); return '' . '' . '' . self::x($d['fichier']) . 'IT Club' . '' . $date . '' . '' . $date . '' . ''; } }