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 . ''
. '';
}
}