Générer un document OpenXML à partir d'un modèle (pptx, xlsx,docx) et de variables: où comment générer vos documents à la volée simplement
Vous avez produit pour un client un document powerpoint vraiment bien, avec l'entête de votre entreprise, votre logo... sur un sujet qui revient souvent pour vous.
Le temps passe, et voilà qu'on vous redemande le même sujet ou très similaire mais avec quelques informations différentes ou bien à actualiser.
Si vous êtes capable d'extraire dans un fichier toute les informations qui ont changé, vous trouverez ci-dessous un script qui peut vous faire gagner du temps pour re-générer le document mis à jour.
Principe du script
Les fichiers OpenXML (*.pptx, *.docx, *.xlsx) sont en fait des fichiers zippés qui contient des répertoires et des fichiers xml (entre autres). Après avoir extrait le zip (avec l'extension PHP Zip, qui est donc nécessaire !), il est donc facile de remplacer des mots ou "variables" dans les fichiers xml (texte). Le script ci-dessous utilise la fonction preg_match_all() pour trouver les occurences de l'expression '${*}' ou '*' est le nom d'une variable. Si vous mettez donc '${mavariable}' n'importe où dans votre document (avec votre bien vieux microsoft Office), elle sera trouvée et remplacée par la valeur que vous aurez indiquée pour cette variable dans un fichier de configuration (ini).
Le principe est simple, limité, mais fonctionnel. Abusez-en !
Création du fichier template
Commencez par créer / modifier votre fichier pptx (ou docx ou xlsx) en ajoutant des '${mavariable}' à la place des mots, phrases, ou valeurs que voulez modifier dynamiquement, appellons ce fichier 'template.pptx' (par exemple)
Création du fichier de propriétés
Créer un fichier 'properties.ini' (par exemple) qui contient la liste de toute les variables que vou voulez utiliser dans votre document :
doc.title = Le titre du document ici doc.author.name = Mon nom ici ...
Création du script de génération
Copier le script ci-dessous dans le fichier 'openxml.php'
<?php
/**
* Content of the openxml.php file
*
* @usage php openxml.php <iniFile> <templateFile> <finalFile>
*
* @copyright none.
*/
/**
* Replaces all ${variable.name} by the value of 'variable.name' of your configuration file in the specified file
*
* @param string $file the source file
* @param array $vars the list of variable to replace
*
* @return void
*/
function parseFile($file,$vars)
{
$content = file_get_contents($file);
$matches = null;
if(preg_match_all('/\$\{([^\}]+)\}/',$content,$matches)>0) {
for($i=0;$i<count($matches[0]);$i++){
$v = '$vars';
$vl='';
$found = trim($matches[1][$i]);
foreach(explode('.',$found) as $t){
$v.= '["'.strip_tags($t).'"]';
$vl.=($vl?'.':'').strip_tags($t);
}
eval('$value = isset('.$v.') ? '.$v.' : (isset($vars["'.$vl.'"]) ? $vars["'.$vl.'"] : "[Variable not found: \'$vl\']");');
$content = str_replace('${'.$matches[1][$i].'}',$value,$content);
}
}
file_put_contents($file,$content);
}
/**
* Lists all files in specified location
*
* @param string $location the location to list files in
*
* @return array
*/
function listFiles($location,$root=null)
{
if(null==$root) {
$root = $location;
}
$files = array();
if (is_dir($location)){
$dh = opendir($location);
while(false !== ($file = readdir($dh))){
if ('.'===$file || '..' === $file) continue;
$files = array_merge($files,listFiles($location.'/'.$file,$root));
}
closedir($dh);
}
$files[] = $location;
sort($files);
if($root === $location) {
array_shift($files);
}
return $files;
}
/**
* Deletes all files in specified locaton
*
* @param string $location
*
* @return void
*/
function delete($location)
{
if (is_dir($location)){
$dh = opendir($location);
while(false !== ($file = readdir($dh))){
if ('.'===$file || '..' === $file) continue;
delete($location.'/'.$file);
}
closedir($dh);
}
@unlink($location);
}
$iniFile = $_SERVER['argv'][1];
$templateFile = $_SERVER['argv'][2];
$finalFile = $_SERVER['argv'][3];
// 1. imports ini file
$vars = parse_ini_file($iniFile);
// 2. unzips the file into a temporary directory
$tempDir = sys_get_temp_dir().'/openxml';
if(!file_exists($tempDir)){
mkdir($tempDir,0777,true);
}
$zip = new ZipArchive();
if (true !== $zip->open($templateFile)) {
throw new RuntimeException("Unable to read zip file '$templateFile'");
}
$zip->extractTo($tempDir);
$zip->close();
// 3. parses extracted files to replace variables
$files = listFiles($tempDir);
foreach($files as $file) {
if(substr($file,strlen($file)-4)!=='.xml') continue;
parseFile($file,$vars);
}
// 4. Re-zip all files into the final pptx file
$zip = new ZipArchive();
if(file_exists($finalFile)){
unlink($finalFile);
}
if ($zip->open($finalFile, ZIPARCHIVE::CREATE)!==TRUE) {
throw new RuntimeException("Unable to create zip archive '$finalFile'");
}
foreach($files as $file) {
$fileName = str_replace('\\','/',substr($file,strlen($tempDir)+1));
$zip->addFile($file,$fileName);
}
$zip->close();
// 5. Deletes all artifacts
delete($tempDir);
echo "Your openxl document has been generated in '$finalFile' from template '$templateFile' and variables set in configuration file '$iniFile'\n";
Exécutez le script !
$ php openxml.php properties.ini template.pptx document.pptx
Vous devriez avoir un fichier 'document.pptx' créé à partir du fichier template (modèle) et dont les variables ont été remplacé par leur valeur trouvée dans le fichier de configuation.
Pour aller plus loin
Ce script n'est pas parfait, mais il fonctionne très bien pour les cas simples. Sous excel, vous aurez peut être quelques petits soucis car les valeurs remplacées sont considérées comme du texte et pas des chiffres, ce qu posent problèmes ou formule (il y a un palliatif)
Je vous laisse le soin de faire évoluer ce script pour :
- ajouter des images générées dynamiquement par php
- réaliser une api objet à intégrer dans vos scripts et applications/sites.
N'hésitez pas à poster vos commentaires surtout si vous trouvez des outils plus complets ailleurs, ou que vous faites évoluer ce script !
Commentaires
Merci, c'est un article interessant pour un sujet qui est d'actualité pour moi. Comment inserer des images dans un documents docx ?
@spm: C'est un peu plus compliqué mais possible, j'utilise ce type de routine (qui contient certainement des bugs, mais qui fonctionne pour mes générations de rapports avec OpenXML) :
// parses for pics if (preg_match_all('/(.*)<\/p\:pic>/U',$content,$matches)>0) { for($i=0;$i
/U',$matches[1][$i],$matches2)>0){ $matches3 = null; if(preg_match_all('/\$\{([^\}]+)\}/',$matches2[3][0],$matches3)>0) { $matches4 = null; if(preg_match_all('/0){ $relFile = dirname($file).'/_rels/'.basename($file).'.rels'; if (true === file_exists($relFile)) { $x = simplexml_load_file($relFile); foreach($x->Relationship as $relationship){ if ((string)$relationship['Id'] === $matches4[1][0]){ $picFile = (string)$relationship['Target']; if(!$picFile) continue; $picFile = realpath(dirname($file).'/'.$picFile); if(!$picFile) continue; unlink($picFile); try { $tplPicFile = $this->parseVar($matches3[0][0],$matches3[1][0],$vars); } catch(Exception $e) { $tplPicFile = dirname(__FILE__).'/resources/no-data.png'; } copy($tplPicFile,$picFile); } } } } } } } } $content = $this->parseString($content,$vars);
Globalement, je mets le nom de la variable dans l'attribute "description" texte de l'image dans mon document openxml (par exemple dans powerpoint) puis je parse le xml du document pour remplacer les occurences des variables par les vrais images sachant que les balises xml existent déjà (puisque créé par powerpoint) sauf sont à ajouter les balises xml des resources (le fichier image complémentaire)
Sorti du contexte, ca doit être un peu rebutant par contre, je le conçois...
olivier