Générer un document word en PHP, pour de vrai

Nous voici arrivés au bout du chemin, au commencement…d’un nouveau chemin (et un long !). Nous avons vu comment faire une génération de documents Word à partir de modèles, nous avons vu comment insérer des images, nous pouvons de même créer des tableaux, souligner, mettre des entêtes, des pied-de-pages, etc. Il est maintenant grand temps d’avancer et de créer, pour de vrai, from scratch, des documents Word modernes, au format docx (donc notamment compatibles avec Apple Pages et OpenOffice Write).

Qu’est-ce que le format docx ?

Depuis le temps que tout le monde le leur demandait, Microsoft, avec l’apparition du format docx, s’est enfin rangé du côté raisonnable de la Force (sic) en faisant de ses fichiers des dossiers d’archive. Oui, vous avez bien entendu, un fichier docx n’est rien d’autre qu’un fichier zip. Pour paraphraser Léodagan, « si je peux me permettre, la seule différence avec des fichiers zip, c’est que vous appelez ça des fichiers docx ! ».

Preuve en est l’exercice suivant que je vous propose : ouvrez Ms Word, créer un nouveau document, écrivez d’une traite (sans supprimer de caractères, sans sauter de ligne, sans déplacer le curseur avec les flêches…bref, d’une traite) la phrase suivante et absolument originale pour ce genre d’exercices :

Hello World

Une fois cela fait, enregistrez immédiatement votre fichier au format docx.

Renommez votre fichier HelloWorld.docx en HelloWorld.zip, et décompressez-le. Vous allez obtenir un répertoire intitulé « HelloWorld » et donc le contenu est le suivant :

Les répertoires que contient un fichier docx

Beaucoup de bruit pour rien, semble-t-il (autant de fichiers pour écrire Hello World ? À vrai dire, il y aurait eu autant de fichiers si on n’avait pas écrit un seul mot); d’un autre côté, il n’est pas inhabituel de voir le XML multiplier fichiers, répertoires et prises de têtes (et bien sûr nombre d’octets), mais c’est sans doute le prix à payer pour être lisible.

Globalement, on va pouvoir regrouper ces fichiers et répertoires selon trois critères :

  • le contenu même du fichier texte (paragraphes, images, etc.); on appelle ces parties du fichier zip les parties de contenu (pour paraphraser Peter, « on en a chié pour trouver ce nom là »);
  • les parties du fichier zip qui décrivent les relations entre les différents éléments de contenu, ce sont les parties de relations (cf. point précédent pour la remarque cassante);
  • l’arborescence des répertoires qui permet de donner une organisation logique (il paraît) à l’ensemble.

Avant de rentrer dans le détail, je vous invite, si vous le souhaitez, à lire l’article de Wikipedia consacré à l’Office Open XML ainsi que les différents articles connexes (mais, bien sûr, dans un nouvel onglet de votre navigateur, il serait dommage qu’on se perdît de vue).

Voyons maintenant ce que sont ces répertoires et fichiers, et ce que nous devons en faire puisque, bien sûr, notre générateur encore à construire créera lui-même toute cette arborescence.

À la racine

On y trouve trois répertoires ( _rels, docProps et word ) et un fichier ( [Content_types].xml ).

Sans grande surprise, le répertoire _rels contient des parties de relations; pour l’instant, il est vide.

Le répertoire docProps contient plusieurs fichiers qui, tous, décrivent le document d’une manière générale : date de création, auteur, titre, nombre de caractères, de paragraphes, onglet de prévisualisation du document au format JPG, etc. (on va décrire tout ça en détails par la suite).

Le répertoire word, le plus important, contient le document à proprement parler (son contenu, ses feuilles de style, ces pièces insérées telles que des images, etc.).

Enfin, le fichier [Content_types].xml, fichier obligatoire (y compris son nom curieux qui-ne-respecte-pas-la-norme-URI-mais-apparemment-c’est-une-volonté-liée-à-des-décisions-techniques-alors-on-va-rien-dire) et essentiel (comme tous les fichiers en XML, ceci dit). C’est un fichier qui décrit les types de contenu; on va pouvoir y dire, par exemple, qu’un fichier .png est une image PNG, qu’un fichier .jpg est une image JPG, etc. D’une certaine manière, on le fait également en HTML, mais comme les navigateurs gèrent par défaut un vaste ensemble de types de fichiers, on ne l’écrit pour ainsi dire jamais. On tentera bien sûr de limiter le contenu de ce fichier au strict minimum (et ne pas taper l’intégralité des types MIME existant).

De plus, ce fichier va pouvoir également surcharger (ou override, si on est bilingue — « amazing », dit Norman, qui fait des vidéos) certains types pour les faire correspondre à des types spécifiques.

Ainsi, notre fichier HelloWorld.docx contient le fichier [Content_types].xml suivant :

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
	<Default Extension="jpeg" ContentType="image/jpeg"/>
	<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
	<Default Extension="xml" ContentType="application/xml"/>
	<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
	<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
	<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
	<Override PartName="/word/fontTable.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"/>
	<Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/>
	<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
	<Override PartName="/word/webSettings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"/>
	<Override PartName="/word/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
</Types>

Avant de vous mettre immédiatement  à gueuler que vous n’avez pas ce contenu, merci de constater que j’ai simplement réorganiser les types de façon lisible (d’abord les types par défaut (en ordre alpha), puis les exceptions (également en ordre alpha).

Ce que nous raconte ce fichier, ce sont les points suivants :

  • si je vois passer un fichier dont l’extension est « jpeg », je le traite comme un fichier de type image/jpeg;
  • si je vois passer un fichier dont l’extension est « rels », je le traite comme un fichier de type application/vnd.openxmlformats-package.relationships+xml, que Ms Word est capable d’interpréter ensuite comme une relation;
  • si je vois passer un fichier dont l’extension est « xml », je le traite comme un fichier de type application/xml, à savoir comme un fichier xml, donc.

Ensuite, il dit : Attention ! Ce que je viens d’expliquer pour les fichiers XML n’est pas valable pour la liste des fichiers xml suivants (que je ne vais pas reprendre) qui correspondent tous à quelque chose en particulier complètement spécifique au traitement de Word et que je ne peux donc pas décemment traiter comme un fichier XML de base qui tenterait de se crasher dans mon répertoire pour la soirée.

Le répertoire docProps

trois fichiers, pour l’instant : app.xml, core.xml et thumbnail.jpeg. On va de suite mettre ce dernier de côté, on voit bien de quoi il s’agit (il suffit de l’ouvrir pour constater qu’il s’agit d’une vignette représentant le document) et, pour s’éviter une migraine immédiate, on évite de se demander comment diable on va être capable de générer une telle image.

Les deux fichiers xml vont décrire ce qu’on appelle communément les méta-données (ou meta-data) du document, c’est-à-dire les propriétés du document lui-même et pas de son contenu (d’où le nom du répertoire, docProps).

app.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
    <Template>Normal.dotm</Template>
    <TotalTime>0</TotalTime>
    <Pages>1</Pages>
    <Words>0</Words>
    <Characters>0</Characters>
    <Application>Microsoft Macintosh Word</Application>
    <DocSecurity>0</DocSecurity>
    <Lines>1</Lines>
    <Paragraphs>1</Paragraphs>
    <ScaleCrop>false</ScaleCrop>
    <LinksUpToDate>false</LinksUpToDate>
    <CharactersWithSpaces>0</CharactersWithSpaces>
    <SharedDoc>false</SharedDoc>
    <HyperlinksChanged>false</HyperlinksChanged>
    <AppVersion>12.0000</AppVersion>
</Properties>

On va rapidement faire le tour du contenu même si, globalement, tout ceci semble assez naturel :

Avant toute chose, pour vous amuser, visitez cette page du MSDN de Microsoft qui décrit l’ensemble des propriétés décrites ci-dessous (sous forme de classe associée au XML, mais bon, on s’y retrouve).

Le fichier XML, déclaré comme tel ligne 1, contient un gros tag, Properties. Celui-ci contient les informations suivantes :

  • Template : c’est le fichier modèle de document (classiquement, Normal.dotm);
  • TotalTime : c’est le temps total (en minutes) d’édition du document; si vous êtes normalement agile, il devrait être égal à 0;
  • Pages : le nombre de pages du document;
  • Words : le nombre de mots du document; là, vous allez me dire : « comment cela peut-il faire 0 alors qu’il y a deux mots ? »; pour le coup — et cela sera valable pour les prochaines informations du même genre — le problème est que cette propriété n’est pas explicite; elle n’enregistre pas le nombre de mots du documents, mais le nombre de mots du documents lors de la dernière sauvegarde (donc 0 lors d’une création);
  • Characters : le nombre de signes du document;
  • Application : le nom de l’application (eh oui, je travaille sous Mac…évidemment que je travaille sous Mac);
  • DocSecurity : représente, sous la forme d’un entier, le niveau de sécurité d’un document Word; les valeurs possibles sont les suivantes :
    • 0 – Aucune sécurité particulière;
    • 1 – Le document est protégé par un mot de passe;
    • 2 – Le document est « recommandé » pour une ouverture en lecture seule;
    • 4 – Le document est « forcé » en lecture seule;
    • 8 – Le document est verrouillé pour annotations seulement.
  • Lines : le nombre de lignes du document;
  • Paragraphs : le nombre de paragraphes du document;
  • ScaleCrop : voilà bien une propriété curieuse…Cette propriété détermine si oui (true) ou non (false) le document peut être redimensionné (scaling) pour l’affichage de la vignette thumbnail, ou si la vignette en question contient l’affichage tel quel (non redimensionné, c’est-à-dire « rogné » ou cropped); cela peut sembler n’avoir strictement aucun intérêt, d’autant qu’on ne s’intéresse pas pour l’instant à la vignette, mais qui vivra verra;
  • LinksUpToDate : Microsoft définit cette propriété comme indiquant si les liens sont « à jour » (up to date), auquel cas la valeur est true, ou pas (auquel cas, la valeur est false); ils n’indiquent par contre pas ce que signifie pour le document qu’un lien est à jour ou non (cela signifie-t-il qu’un lien a été modifié ? que la connexion vers l’url donnée a été validée ?);
  • CharactersWithSpaces : comme pour Characters et Words, la valeur est celle du dernier enregistrement (donc 0 pour une création); cette propriété représente, comme son nom l’indique, les caractères avec les espaces (c’est-à-dire en incluant les espaces; on verra par la suite ce qu’il en est des tabulations, des retours chariot, de la ponctuation, des puces de numérotation, des sauts de lignes, de pages, etc.);
  • SharedDoc : le document est-il partagé ?
  • HyperlinksChanged : les liens ont-ils été modifiés ?
  • AppVersion : numéro de version de l’application, nommée dans le tag Application, qui a généré ce document.
core.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <dc:title></dc:title>
    <dc:subject></dc:subject>
    <dc:creator></dc:creator>
    <cp:keywords></cp:keywords>
    <cp:lastModifiedBy>Bruce</cp:lastModifiedBy>
    <cp:revision>1</cp:revision>
    <dcterms:created xsi:type="dcterms:W3CDTF">2011-07-26T16:34:00Z</dcterms:created>
    <dcterms:modified xsi:type="dcterms:W3CDTF">2011-07-26T16:34:00Z</dcterms:modified>
</cp:coreProperties>

Alors là, attention, c’est le core, maintenant, c’est pour de vrai; le fichier app.xml décrivait le document, alors que core.xml décrit…le document. Ben oui, on ne comprend pas trop pourquoi faire deux fichiers distincts; on pourrait croire que le premier donne des infos techniques (nombre de signes, de mots, etc.) tandis que le second donne des infos plutôt de contenu (titre, mot-clés, auteur, etc.), mais ce n’est pas tout à fait vrai, comme nous l’allons voir. Pour info, côté Microsoft, on nous explique que app.xml décrit les propriétés liées à l’application tandis que core.xml décrit les propriétés liées au document lui-même…no comment.

Bon, tout d’abord on  a maintenant trois namespaces, que Microsoft appelle dc, cp et dcterms, le tout dans un gros tag de properties, sous le namespace cp, intelligemment nommé CoreProperties (puisque cp est l’acronyme de CoreProperties).

Pour les namespaces, on va rapidement faire le tour, parce que c’est vrai que cp était facile, les autres, c’est moins le cas :

  • cp : CoreProperties, donc; ce qui est marrant, c’est d’imaginer que, dans le tag CoreProperties, il va y avoir autre chose que du CoreProperties;
  • dc : Document…CoreProperties ? Content ? À votre avis…? Non, en fait, cela signifie Dublin Core qui est un schema de méta-données générique;
  • dcterms : Dublin Core Terms, donc, Terms au sens « termes » visiblement.
Bon, ceci étant vu, les données maintenant :
  • dc:title : bon, le titre; ok, jusque là ça va, on peut simplement constater qu’il n’y a aucune valeur pour l’instant (donc pas de titre automatique);
  • dc:subject : le thème, pour être exact;
  • dc:creator : le créateur du document; ce qui est drôle, c’est que ce champ n’est pas renseigné, alors que le champ lastModifiedBy, comme nous le verrons, est renseigné;
  • cp:keywords : les mots-clés;
  • cp:lastModifiedBy : l’utilisateur qui a, en dernier, modifié le document;
  • cp:revision : version du document, incrémenté à chaque sauvegarde différente de la version précédente;
  • dcterms:created : date de création dans un format W3CDTF (W3C Date and Time Formats); pour plus d’infos à ce sujet, bon appétit;
  • dcterms:modified : date de dernière modification du document (à la création, c’est la date de création).

Cet article est un peu long et chiant jusque là

Je comprends, jeune scarabée, mais il faut apprendre à marcher avant de savoir…marcher. Le prochain article décrira en détails le contenu du répertoire word/ (quoi ! un article juste pour ça ?) et nous pourrons ensuite attaquer le développement de notre générateur; on fera ça bien, vous verrez; d’abord la conception, puis le développement, et enfin la mise à disposition pour une période donnée (et oui, une des règles de base de tous les business de création est que ce qui n’a pas de prix n’a pas de valeur; c’est triste, mais c’est ainsi).

 
 

7 réponses à to “Générer un document word en PHP, pour de vrai”

  • Salut Bruce !

    Je viens à peine de découvrir tes articles sur la génération d’un docx en PHP, et j’ai une question dont je n’arrive pas à trouver la réponse…

    En fait, j’ai avancé un peu plus loin que là où en est le tuto. Mon but est de générer un docx à partir d’un docx décompressé. Je ne fait que modifier le fichier « document.xml » puis je rezip le tout et envoi une en-tête forçant le téléchargement du fichier.

    Pour le moment, je n’ai fait aucune modification, mais le fichier recomposer ne s’ouvre pas => Word 2007 me dit que le fichier est corrompu ! O_o (alors que c’est exactement les mêmes fichiers !)

    Quoiqu’il en soit, je pense que le soucis vient de l’en-tête que j’envoie:

    header(‘Content-Type: application/x-zip’);
    header(‘Content-Disposition: attachment; filename=archive.docx’);

    J’ai fait des recherches, et j’ai trouvé que le type MIME des docx est: « application/vnd.openxmlformats-officedocument.wordprocessingml.document »

    Le soucis, c’est que ça ne fonctionne pas…

    Pourtant, quand je met: « application/msword », il me dit que le lancement du convertisseur a échoué, preuve que l’en-tête a son importance à l’ouverture du fichier, non ?

    As-tu une idée de l’origine du problème ?

    Merci d’avance !

    PS: Je t’aurais bien envoyé un mail plutôt, mais je n’ai pas trouvé d’adresse…

  • Plusieurs choses : tout d’abord, on va couper court au suspense, je n’ai pas la réponse à ta question.

    D’après ce que tu expliques, tu décompresses le docx, modifies le document et re-compresses le tout. En soi, ce n’est pas franchement de l’avance sur le tuto, c’est peut-être simplement une autre façon de réaliser une étape précédente de ce dossier; quoi qu’il en soit, d’expérience, lorsque Word gueule qu’un fichier généré est corrompu, c’est généralement qu’il y a des caractères qu’il n’aime pas. Question idiote : tu dis « alors que c’est exactement les mêmes fichiers », n’y a-t-il aucune modification entre le fichier document.xml original et celui généré ? Le docs en question, as-tu essayé de l’enregistrer (hors téléchargement) et de voir si tu pouvais l’ouvrir dans Word ? Si le problème est le même, du coup ton problème d’entête est annexe. Il faudrait comparer l’original de document.xml et le généré pour voir ce qui est modifié; en tout état de cause, sans le dit fichier, difficile de pouvoir t’apporter une réponse…Si tu as une URL qui permette de récupérer le fichier en question, je suis preneur…

  • Bonjour !

    Merci d’avoir répondu à ma question !

    Pour ce qui est de la réponse aux tiennes, comme je ne modifie pas le « document.xml » (ça, je le ferais plus tard), il n’y a pas de raison pour que la fichier contienne des caractères que Word n’aime pas.

    D’autre part, j’ai bien évidemment essayé de compresser directement les fichiers « à la main » (avec 7zip) puis de renommer le fichier ainsi construit en .doc, et ça fonctionne bien, je n’ai aucun message d’erreur ! O_o

    Bref, l’avantage, dans tout ça, c’est qu’après m’avoir dit que le fichier est corrompu, Word me propose quand même d’essayer de le récupérer, et il m’ouvre le fichier comme si de rien n’était => et ça fonctionne également après avoir modifié le « document.xml ».

    Bien sûr, ce serait mieux si je n’avais pas ce fameux message, mais je crois que je ne vais avoir le choix… :(

  • Il ne s’agit sans doute pas de ça, mais on ne sait jamais, alors je le note : si Word dit que le fichier est corrompu, mais qu’il arrive ensuite à le reconstruire, il n’est pas impossible qu’il y ait un problème d’encodage (bienvenue dans le plus grand cauchemar des fichiers Windows); en effet, si ton fichier xml passe de ISO-8859-1 à UTF-8, quitte à ensuite faire le chemin inverse, il peut y avoir des problèmes.

    De la même manière, si ton fichier XML Windows transite à un moment entre Linux (ou assimilé, tel Mac OS X) et Windows, tu peux aussi avoir des problèmes avec les retours chariot (c’est loin, mais de mémoire, sous linux, un retour chariot correspond à un caractère : « \n », tandis que sous Windows, il correspond techniquement à deux caractères : fin de ligne « \r » et nouvelle ligne « \n »); d’expérience, ça peut gêner la lecture que Word fera ensuite des fichiers s’il ne comprend pas où sont les fins de lignes.

    Tiens-moi au courant si ça résout le problème, ça peut être utile à plus d’une personne…

  • Un complément au dernier commentaire; j’ai fait un test simple (si simple que je ne l’avais pas fait jusque là) : j’ai créé un fichier docx avec Word; je l’ai renommé en zip, je l’ai décompressé puis immédiatement recompressé; enfin, je l’ai renommé en docx.

    En effet, Word refuse de l’ouvrir; il ne s’agit donc pas d’un problème d’encodage ou assimilé, je vais enquêter.

    Ce qui est étonnant, c’est que, lorsqu’on crée, from scratch — comme nous allons le faire — un fichier word (en PHP, ou même manuellement, fichier par fichier), Word l’ouvre sans problème.

    Ah, Microsoft…Qu’est-ce qu’on (s/f)erait sans toi ?

  • Addendum, encore une fois : en fait, sur mon test, j’avais fait une connerie, je zippais le répertoire qui contient les fichiers, alors qu’ils faut compresser les fichiers eux-mêmes (la règle : [Content_Types].xml doit être à la racine du zip).

    Ceci étant, Word a quand même besoin de reconstruire le fichier pour que ça marche.

    J’ai comparé, en binaire, le contenu du fichier docx initial ainsi que le contenu du docx reconstitué, et j’ai constaté qu’il y a des différences, une douzaine de différences, mais toutes identiques, qui ne concernent que quelques octets.

    Du coup, l’enquête continue pour trouver la source de ces différences; pour info, une comparaison sur les deux fichiers dézippés démontre que les contenus, une fois décompresses, sont absolument identiques.

    Mon idée, à ce stade, est qu’il doit y avoir une option au niveau-même de la compression. À suivre…

  • Le problème est résolu lorsqu’on zippe à la ligne de commande :

    zip -r fichierACreer.zip * (depuis l’intérieur du répertoire)

    là, ça fonctionne. Le problème vient donc d’au moins un des points suivants :
    - option de compression;
    - inclusion, dans le zip, des fichiers de ressources liés à l’OS (comme, par exemple, .DS_Store dans Mac OS X)

Laisser un commentaire

*