Créer un fichier Word OpenXML avec .Net

Florian Casabianca

Avec l'arrivée de la nouvelle version de Microsoft Office 2007, Microsoft introduit le nouveau format de document Office Open XML pour Word, Excel et PowerPoint et qui succèdent aux formats de fichier binaires d'Office (.doc, .xls et .ppt) apparus avec la sortie d'Office 97.
Cet article présente les bases pour la création d'un fichier Word OpenXML en .Net.

N'hésitez pas à commenter cet article ! Commentez

- Titre : Créer un fichier Word OpenXML avec .Net

- Auteur : Florian Casabianca

- Parution : 5 février 2007

- Mise à jour : 13 juillet 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2007 Florian Casabianca. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- I. Introduction

- II. Rappel sur la structure d'un document WordProcessingML

- III. Prérequis nécessaires

- IV. Création d'un premier document WordprocessingML

- IV-A. Analyse du code

- IV-B. Analyse du fichier généré

- V. La Part principale du document

- VI. Paragraphes et formatage

- VI-A. Les paragraphes

- VI-B. Les runs

- VII. Les styles

- VII-A. La Part styles

- VII-B. Définition d'un style

- VII-C. Styles s'appliquant à un paragraphe

- VII-D. Styles s'appliquant à un run

- VII-E. Les propriétés par défaut

- VII-F. Ordre d'application des styles

- VII-G. Un peu de code

- VIII. Les fichiers de propriétés

- IX. Ajouter une image au document

- IX-A. DrawingML

- IX-B. Le code

- X. Pour aller plus loin

- XI. Conclusion

- Liens

- Remerciements

- Contact

I - Introduction

Avec l'arrivée de la nouvelle version de Microsoft Office 2007, Microsoft introduit le nouveau format de document Office Open XML pour Word, Excel et PowerPoint et qui succèdent aux formats de fichier binaires d'Office (.doc, .xls et .ppt) apparus avec la sortie d'Office 97.

Grâce à ce nouveau format récemment standardisé à l'ECMA, les fichiers Word deviennent de simples packages zip contenant des fichiers XML. Ainsi, il n'est plus nécessaire de posséder Microsoft Word pour créer ou visualiser des fichiers, un simple éditeur de texte ou une application « maison » suffit.

Dans cet article nous allons tout d'abord découvrir le code de base nécessaire pour créer un document Word au format Open XML contenant un simple texte. Nous étofferons ensuite ce code pour modifier l'apparence (police, couleur, font) du texte. Enfin, nous verrons comment ajouter n'importe quel objet (textuel ou binaire) à notre document (dans notre cas ce sera une image).

II - Rappel sur la structure d'un document WordProcessingML

WordprocessingML est un ensemble de conventions pour représenter un document Word au format Open XML. Pour les documents Excel il existe SpreadsheetML et pour les documents PowerPoint il s'agit de PresentationML.

Cet article n'a pas pour but de vous présenter l'architecture et la structure d'un document au format Open XML. Vous devez en avoir pris connaissance avant de lire cet article. Si tel n'est pas le cas, je vous conseille d'aller visiter ces quelques liens : les livres blancs et Structure d'un document Open XML. Cependant, nous allons tout de même faire un petit rappel sur la structure d'un document (ou package) Open XML de type WordprocessingML que vous pouvez visualiser notamment en ajoutant l'extension .zip à un fichier .docx et en l'ouvrant avec votre lecteur de fichiers zip.

Les trois principaux composants du nouveau format sont :

- Les parts : chaque fichier contenu dans l'arborescence est une Part. La plupart sont des fichiers XML, mais il peut aussi y avoir des fichiers binaires (images, vidéos, objets OLE etc.) ou même d'autres fichiers Open XML multimédia si le document Word en contient.

- Les éléments type de contenu : ce sont des métadonnées contenues dans le fichier [Content_Types].xml et permettant de décrire le type de contenu stocké dans une Part (fichier jpeg, fichier de styles, fichier de relations, etc.). On peut ainsi savoir quelle méthode de lecture employer pour lire une Part.

- Les éléments relation : ils permettent de définir les associations entre une Part source et une partie cible. Les relations spécifient comment les parts se mêlent pour former un document. Les relations sont définies dans les fichiers .rels.

Le dossier docProps contient les fichiers de propriétés du document.
Le fichier document.xml est la Part principale d'un document WordprocessingML et contient le texte du corps du document.

III - Prérequis nécessaires

Pour nous faciliter la tâche, le Framework 3.0 de Microsoft .NET inclut la nouvelle API de packaging fournie dans l'assembly WindowsBase.dll. Les classes qui constituent l'API packaging sont contenues dans l'espace de noms System.IO.Package.

Vous aurez donc besoin du Framework 3.0 et de son SDK.

Pour ajouter une référence à votre projet sous Visual Studio : dans le menu projet, cliquez sur ajouter une référence. Si la dll WindowsBase ne se trouve pas dans l'onglet .Net, choisissez l'onglet Parcourir et aller la chercher dans \Program Files\Reference Assemblies\Microsoft\Framework\v3.0.

Vous devrez aussi référencer les espaces de noms System.IO et System.Xml dans votre projet :

using System.IO;
using System.Xml;
using System.IO.Packaging;

IV - Création d'un premier document WordprocessingML

Le but de cette première étape est de créer un premier document docx dont le contenu du fichier document.xml (voir l'arborescence) sera le suivant :

<?xml version="1.0" encoding="utf-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p>
<w:r>
<w:t>Salut !</w:t>
</w:r>
</w:p>
</w:body>
</w:document>

Nous avons ici le contenu minimum que doit avoir le fichier document.xml. Les explications concernant les différentes balises seront évoquées plus tard.

IV-A - Analyse du code

Au niveau du code C#, rien de vraiment compliqué. En effet, il suffit pour l'instant de créer un objet XmlDocument et de le remplir avec les nœuds nécessaires afin d'obtenir l'arborescence souhaitée (de la manipulation standard de XML en somme):

// Utilisation du NameSpace WordprocessingML:
string WordprocessingML = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";

// Création du WordML
XmlDocument xmlStartPart = new XmlDocument();
XmlElement tagDocument = xmlStartPart.CreateElement("w:document", WordprocessingML);
xmlStartPart.AppendChild(tagDocument);
XmlElement tagBody = xmlStartPart.CreateElement("w:body", WordprocessingML);
tagDocument.AppendChild(tagBody);
XmlElement tagParagraph = xmlStartPart.CreateElement("w:p", WordprocessingML);
tagBody.AppendChild(tagParagraph);
XmlElement tagRun = xmlStartPart.CreateElement("w:r", WordprocessingML);
tagParagraph.AppendChild(tagRun);
XmlElement tagText = xmlStartPart.CreateElement("w:t", WordprocessingML);
tagRun.AppendChild(tagText);

// Insertion du texte
XmlNode nodeText = xmlStartPart.CreateNode(XmlNodeType.Text, "w:t", WordprocessingML);
nodeText.Value = "Salut !";
tagText.AppendChild(nodeText);

C'est maintenant que l'espace de noms System.IO.Packaging va nous servir. Il contient en effet la classe Package qui présente une méthode static Open permettant de créer de nouveaux packages et d'ouvrir des packages existants. Attention, l'appel de la méthode Open devra se faire suivre par un appel à la méthode Close. Nous passons ici en paramètre le nom de notre fichier, le fait que s'il existe déjà on le supprime et le fait qu'on y accède en lecture/écriture :

// Création d'un nouveau package
Package pkgOutputDoc = null;
pkgOutputDoc = Package.Open("monFichier.docx", FileMode.Create, FileAccess.ReadWrite);

Une fois le package créé, nous allons maintenant créer une Part (souvenez-vous, c'est l'une des trois composantes que nous avons évoquées lors du rappel sur la structure d'un fichier docx). Cette Part sera le document document.xml contenu dans le répertoire word. Pour cela, nous avons la méthode CreatPart qui attend en paramètre un Uri permettant de spécifier où doit se localiser le fichier document.xml dans le package et un string spécifiant le type de contenu du fichier. Nous spécifions ici « application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml » qui signifie que cette Part contient l'information principale de notre document docx (c'est-à-dire là où sera stocké le texte contenu dans le document Word, dans notre cas :  « salut ! »).

// Création d'une part
Uri uri = new Uri("/word/document.xml", UriKind.Relative);
PackagePart partDocumentXML = pkgOutputDoc.CreatePart(uri,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml");

Pour finir cette étape, il reste à sérialiser le contenu de la Part dans le package :

StreamWriter streamStartPart = new StreamWriter(partDocumentXML.GetStream(FileMode.Create, FileAccess.Write));
xmlStartPart.Save(streamStartPart);
streamStartPart.Close();

L'écriture de notre fichier document.xml est maintenant terminée. Mais il reste encore une dernière étape pour finir la création de notre document WordprocessingML. En effet, si nous reprenons les trois principaux composants que nous avons évoqués au début, nous en avons oublié un : l'élément relation. Insérer le fichier document.xml dans le package ne suffit pas, il faut aussi le lier au package. Pour cela, nous disposons de la méthode CreateRelationship que l'on appelle depuis notre objet Package. Nous lui passons en premier paramètre l'objet Uri construit précédemment qui représente la cible de la relation. Le deuxième paramètre permet de spécifier si cette cible se situe à l'intérieur ou à l'extérieur du package. Le troisième paramètre définit le type de relation. Ici nous mettons une relation de type officeDocument car c'est le type de relation qui correspond à la Part principale du package (document.docx). Enfin, le dernier paramètre sert à donner un identifiant à cette relation.

// Création de la RelationShip
pkgOutputDoc.CreateRelationship(uri, TargetMode.Internal,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument","rId1");
pkgOutputDoc.Flush();

// Fermeture du package
pkgOutputDoc.Close();

Voilà, la création de notre premier document Word est terminée. Si vous exécutez ce code vous obtiendrez un document monFichier.docx dont voici la visualisation dans Word 2007 :

IV-B - Analyse du fichier généré

Jetons un coup d'œil sur les fichiers créés. Si vous extrayez le contenu du document monFichier.docx vous allez retrouver l'arborescence décrite au début (même si elle est pour le moment pas mal allégée).

A la racine vous allez retrouver le fichier [Content_Types].xml qui décrit les types de contenu des différentes parts du package :

<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="xml" ContentType="application/vnd.openxmlformats-   officedocument.wordprocessingml.document.main+xml" />
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
</Types>

Vous pouvez voir ici le type de contenu « application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml » que nous avons défini précédemment dans le code C#.

Vous trouverez également un répertoire _rels qui contient le fichier .rels contenant les éléments relation :

<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
Target="/word/document.xml" Id="rId1" />
</Relationships>

On y retrouve bien la relation rId1 que nous avions définie dans le code C# ayant pour cible le fichier document.xml.

V - La Part principale du document

Avant de pouvoir étoffer notre texte (ajout de paragraphes, de couleurs, etc.) il nous faut d'abord comprendre la structure de la Part principale d'un document Open XML : le fichier document.xml. Comme vous pouvez vous en douter, les différents changements que nous souhaitons apporter à notre texte se feront grâce à l'ajout de balises XML dans le fichier documents.xml. L'ajout de ces balises supplémentaires ne posera pas de réelles difficultés (il suffit en effet de modifier le code C# vu précédemment et plus particulièrement l'objet XmlDocument afin de lui rajouter les balises souhaitées, le reste du code restant identique). Le problème réside plutôt dans le fait de savoir quelles balises ajouter et où les insérer.

Voyons la structure de base de la Part principale d'un document Open XML:

<?xml version="1.0" encoding="utf-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:background w:color="CEB966"/>
<w:body>
<w:p>
<w:r>
<w:t>Salut !</w:t>
</w:r>
</w:p>
</w:body>
</w:document>

La balise document est l'élément root du document. Il spécifie le contenu de la Part principale d'un document Open XML. Cet élément ne peut avoir que 2 éléments enfants : l'élément body et l'élément background.

L'élément background indique les informations concernant le fond d'un document WordprocessingML (par exemple la couleur de fond).

La balise body indique le contenu du corps du document. Elle peut contenir un certain nombre de types d'éléments dont tbl (tableau) et p (paragraphe).

VI - Paragraphes et formatage

VI-A - Les paragraphes

Le paragraphe (qui est défini avec l'élément p) est l'unité la plus fondamentale pour stocker un bloc de contenu dans un document WordprocessingML. Un paragraphe définit une division distincte du contenu qui commence avec une nouvelle ligne. Un paragraphe peut contenir trois types d'informations : les propriétés du paragraphe (facultatives), le contenu du paragraphe (typiquement une balise run), et un ensemble d'identifications facultatives de révision utilisées pour comparer le contenu de deux documents.

Nous pouvons évidemment mettre plusieurs paragraphes dans un document. Ainsi, si l'on veut que notre texte contienne deux paragraphes, le fichier document.xml devra ressembler à ceci :

<?xml version="1.0" encoding="utf-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:background w:color="CEB966"/>
<w:body>
<w:p>
<w:r>
<w:t>Paragraphe 1</w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t>Paragraphe 2</w:t>
</w:r>
</w:p>
</w:body>
</w:document>

Il est possible de spécifier un ensemble de propriétés qui s'appliqueront à tout le paragraphe. Ces propriétés doivent être déclarées dans un élément pPr au sein de l'élément p.

Par exemple :

L'élément jc permet de spécifier l'alignement du texte. La valeur « both » correspond à un alignement justifié (il existe aussi « right », « left » et « center »).

L'élément pageBreakBefore permet d'insérer un saut de page juste avant le paragraphe concerné.

Ainsi, un paragraphe dont l'alignement du texte est justifié et qui commence sur une nouvelle page se déclarera de cette façon :

<?xml version="1.0" encoding="utf-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:background w:color="CEB966"/>
<w:body>
<w:p>
<w:pPr>
<w:jc w:val="both"/>
<w:pageBreakBefore/>
</w:pPr>
<w:r>
<w:t>Salut !</w:t>
</w:r>
</w:p>
</w:body>
</w:document>

Le code C# pour réaliser ceci n'est pas compliqué. Il suffit en effet simplement de modifier notre objet XmlDocument que nous avions déclaré plus haut. Le reste du code (création du package, des relations, etc.) demeure strictement identique. Voici le code pour créer deux paragraphes et déclarer les propriétés justifié et saut de page au deuxième paragraphe :

// Utilisation du NameSpace WordprocessingML:
string WordprocessingML =
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";

// Création du WordML
XmlDocument xmlStartPart = new XmlDocument();
XmlElement tagDocument = xmlStartPart.CreateElement("w:document",
WordprocessingML);
xmlStartPart.AppendChild(tagDocument);
XmlElement tagBody = xmlStartPart.CreateElement("w:body", WordprocessingML);
tagDocument.AppendChild(tagBody);

// déclaration du paragraphe 1
XmlElement tagParagraph = xmlStartPart.CreateElement("w:p", WordprocessingML);
tagBody.AppendChild(tagParagraph);
XmlElement tagRun = xmlStartPart.CreateElement("w:r", WordprocessingML);
tagParagraph.AppendChild(tagRun);
XmlElement tagText = xmlStartPart.CreateElement("w:t", WordprocessingML);
tagRun.AppendChild(tagText);
// Insertion du texte
XmlNode nodeText = xmlStartPart.CreateNode(XmlNodeType.Text, "w:t", WordprocessingML);
nodeText.Value = "Salut !";
tagText.AppendChild(nodeText);
// déclaration du paragraphe 2
tagParagraph = xmlStartPart.CreateElement("w:p", WordprocessingML);
tagBody.AppendChild(tagParagraph);

// déclaration des propriétés du paragraphe
XmlElement tagParagraphProp = xmlStartPart.CreateElement("w:pPr", WordprocessingML);
tagParagraph.AppendChild(tagParagraphProp);

//propriété alignement
XmlElement tagAlignement = xmlStartPart.CreateElement("w:jc", WordprocessingML);
//attribut du noeud w:jc
XmlAttribute attribut1 = xmlStartPart.CreateAttribute("w:val", WordprocessingML);
attribut1.InnerText = "both";
tagAlignement.SetAttributeNode(attribut1);
tagParagraphProp.AppendChild(tagAlignement);

//propriété saut de page
XmlElement tagBreak = xmlStartPart.CreateElement("w:pageBreakBefore", WordprocessingML);
tagParagraphProp.AppendChild(tagBreak);

tagRun = xmlStartPart.CreateElement("w:r", WordprocessingML);
tagParagraph.AppendChild(tagRun);
tagText = xmlStartPart.CreateElement("w:t", WordprocessingML);
tagRun.AppendChild(tagText);
nodeText = xmlStartPart.CreateNode(XmlNodeType.Text, "w:t", WordprocessingML);
nodeText.Value = "Salut !";
tagText.AppendChild(nodeText);

//reste du code...

VI-B - Les runs

Un run (balise r) est une région de textes ayant un ensemble commun de propriétés. Un élément r permet de combiner des styles ou des propriétés de formatage, s'appliquant de la même façon à toutes parties du run. Le texte dans un document WordprocessingML doit être contenu dans un ou plusieurs runs. Un paragraphe est une collection d'un ou plusieurs runs. Un run doit être contenu dans un paragraphe. Le texte à l'intérieur d'un run s'écrit dans une balise t.

De même qu'un paragraphe peut avoir des propriétés, un run peut en avoir aussi. Tous les éléments à l'intérieur d'un élément r ont leurs propriétés contrôlées par un élément rPr facultatif et qui doit être le premier enfant de l'élément r. En d'autres termes, l'élément rPr est un conteneur pour un ensemble d'éléments de propriété qui sont appliqués au reste des enfants de l'élément r. Les éléments de propriété à l'intérieur de l'élément conteneur rPr permettent de contrôler si le texte dans les éléments t suivants est en gras, souligné ou en italique, par exemple. Quelques exemples de propriétés : gras, bordure, style de caractère, couleur, police, taille de police, italique, direction du texte ou encore soulignement.

Le contenu le plus commun du run est l'élément t (mais il en existe d'autres !), qui est le conteneur pour le texte qui compose le contenu du document. Un élément t peut contenir une quantité arbitraire de texte, allant même jusqu'à pouvoir contenir le contenu entier du document. Un élément t doit être inclus dans un élément r. Un élément r peut contenir de multiples éléments t, entremêlés avec d'autres éléments.

Ainsi, la phrase suivante : « Open XML avec .NET c'est facile. » qui contient du texte en rouge, en gras et surligné sera décomposé ainsi :

<w:r>
<w:rPr>
<w:color w:val="FF0000"/>
</w:rPr>
<w:t>Open XML</w:t>
</w:r>
<w:r>
<w:t xml:space="preserve"> avec </w:t>
</w:r>
<w:r>
<w:rPr>
<w:b/>
</w:rPr>
<w:t>.NET</w:t>
</w:r>
<w:r>
<w:t xml:space="preserve"> c'est </w:t>
</w:r>
<w:r>
<w:rPr>
<w:highlight w:val="yellow"/>
</w:rPr>
<w:t>facile</w:t>
</w:r>

Notez l'utilisation de la propriété xml:space=« preserve » qui permet de conserver les espaces se trouvant dans le texte à l'intérieur de la balise t.

Voici un peu de code pour agrémenter tout cela. Notez ici que je ne mets que le code pour la création du premier run. En effet, il vous suffit de répéter l'opération pour les autres, vous savez faire maintenant, c'est de la simple manipulation d'XML.

// Utilisation du NameSpace WordprocessingML:
string WordprocessingML = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";

// Création du WordML
XmlDocument xmlStartPart = new XmlDocument();
XmlElement tagDocument = xmlStartPart.CreateElement("w:document",
WordprocessingML);
xmlStartPart.AppendChild(tagDocument);
XmlElement tagBody = xmlStartPart.CreateElement("w:body", WordprocessingML);
tagDocument.AppendChild(tagBody);

// déclaration du paragraphe
XmlElement tagParagraph = xmlStartPart.CreateElement("w:p", WordprocessingML);
tagBody.AppendChild(tagParagraph);

//création du premier run
XmlElement tagRun = xmlStartPart.CreateElement("w:r", WordprocessingML);
tagParagraph.AppendChild(tagRun);

//Elément de propriétés du premier run
XmlElement tagRunProp = xmlStartPart.CreateElement("w:rPr", WordprocessingML);
tagRun.AppendChild(tagRunProp);
//propriété couleur
XmlElement tagCouleur = xmlStartPart.CreateElement("w:color", WordprocessingML);
//attribut de l'élément w:color
XmlAttribute valeurCouleur = xmlStartPart.CreateAttribute("w:val", WordprocessingML);
valeurCouleur.InnerText = "FF0000";
tagCouleur.SetAttributeNode(valeurCouleur);
tagRunProp.AppendChild(tagCouleur);

//création de l'élément t
XmlElement tagText = xmlStartPart.CreateElement("w:t", WordprocessingML);
tagRun.AppendChild(tagText);
tagText.InnerText= "Open XML";

//reste du code...

VII - Les styles

Après avoir vu notamment comment définir un paragraphe et des propriétés de formatage pouvant lui être appliquées, peut-être vous posez vous la question suivante : Est-il possible, à l'instar des feuilles de styles CSS en programmation Web, de pouvoir séparer le contenu de sa présentation ? Question pertinente à laquelle on peut heureusement répondre par l'affirmative grâce à l'utilisation des styles.

Dans un document WordprocessingML, les styles sont des ensembles prédéfinis de propriétés qui peuvent être appliquées au texte dans le document. Ceci permet aux propriétés de formatage d'être stockées et gérées indépendamment du contenu, permettant à la présentation du contenu du document d'être changé à un seul endroit (plutôt que de changer les propriétés de chaque paragraphe un à un).

VII-A - La Part styles

À l'instar du contenu d'un document WordprocessingML qui est stocké dans la Part principale (document.xml), les styles seront aussi stockés dans une Part : le fichier styles.xml situé au même niveau que le fichier document.xml. Comme pour la création de la Part principale, la création de la Part contenant les styles nécessitera de définir son type de contenu (application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml) dans le fichier [Content_Types].xml et de créer un élément relation (http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles) pour la lier au package.

Voici la structure du fichier styles.xml :

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">

<w:docDefaults></w:docDefaults>
<w:latentStyles></w:latentStyles>
<w:style></w:style>
<w:style></w:style>
</w:styles>

Elle se compose d'au plus un élément docDefauts (facultatif), d'au plus un élément latentStyles (facultatif) et d'autant d'éléments style que voulus.

VII-B - Définition d'un style

Un style est défini en utilisant un élément style. La définition d'un style peut se découper en trois segments :

- Un ensemble de propriétés communes à tous les styles

- Le type du style

- Les propriétés spécifiques au style (liées au type de style)

Les propriétés communes à tous les styles sont un ensemble de propriétés qui peuvent être employées indépendamment du type de style ; par exemple, le nom du style, l'identifiant du style, si le style est caché, si le style est verrouillé, etc.

Le type du style permet de définir à quel type de bloc va s'appliquer le style. WordprocessingML contient six types de style:

- Les styles s'appliquant à un paragraphe

- Les styles s'appliquant à un run

- Les styles liés (paragraphe + texte)

- Les styles s'appliquant à un tableau

- Les styles s'appliquant à une énumération

- Les propriétés par défaut pour les paragraphes et le texte

Voici à quoi peut ressembler la définition d'un style (sans les propriétés spécifiques au style) :

<w:style w:type="paragraph" w:default="1" w:styleId="MonStyle1">
<w:name w:val="mon style 1"/>
<w:basedOn w:val="MonStyleDeBase"/>
<w:next w:val="MonStyle2"/>
...
</w:style>

On voit ici la déclaration de quelques propriétés communes à tous les styles telles que l'identifiant (« MonStyle1 »), le nom (« mon style 1 ») ou encore le style de base (héritage) à partir duquel est construit ce style (« MonStyleDeBase »). Le type du style est défini grâce à l'attribut type ; ici il s'agit d'un style s'appliquant à un paragraphe. Notez l'attribut default qui permet de spécifier que ce style sera le style s'appliquant par défaut à tous les paragraphes ne faisant pas référence à un style particulier.

Voyons maintenant les propriétés spécifiques au style. Elles dépendent bien entendu du type de style déclaré. Nous n'étudierons ici que les styles s'appliquant aux paragraphes et au texte.

VII-C - Styles s'appliquant à un paragraphe

Ces types de style s'appliquent à un paragraphe en entier. Cela implique que le style peut définir à la fois des propriétés de texte (propriétés qui s'appliquent au texte dans le document) et des propriétés de paragraphe (les propriétés qui s'appliquent au positionnement et à l'aspect du paragraphe). Les styles s'appliquant à un paragraphe (élément p) ne peuvent pas être référencés par des éléments run dans un document. Ces styles doivent être référencés par l'élément pStyle dans l'élément pPr d'un paragraphe.

Consideront le style suivant :

<w:style w:type="paragraph" w:styleId="MonStyle1">
<w:name w:val="mon style 1"/>
<w:pPr>
<w:spacing w:line="480" w:lineRule="auto"/>
<w:ind w:firstLine="1440"/>
</w:pPr>
<w:rPr>
<w:rFonts w:ascii="Algerian" w:hAnsi="Algerian"/>
<w:color w:val="ED1C24"/>
<w:sz w:val="40"/>
</w:rPr>
</w:style>

Ce style définit des propriétés qui vont s'appliquer au paragraphe grâce à l'élément pPr et des propriétés qui vont s'appliquer à l'élément run contenu dans ce paragraphe grâce à l'élément rPr.

Ce style sera référencé ainsi par un paragraphe :

<w:p>
<w:pPr>
<w:pStyle w:val="MonStyle1"/>
</w:pPr>
<w:r>
<w:t>Open XML avec .Net c'est facile</w:t>
</w:r>
</w:p>p>

Le style est référencé grâce à l'élément pStyle à l'intérieur de l'élément pPr du paragraphe qui, rappelons-le, définit les propriétés s'appliquant à un paragraphe

Voici le résultat produit :

VII-D - Styles s'appliquant à un run

Ces styles ne peuvent s'appliquer qu'à des runs à l'intérieur d'un document et non à un paragraphe. Cela implique donc qu'ils ne peuvent définir que des propriétés s'appliquant au texte à l'intérieur d'un paragraphe. Ces styles sont référencés par l'élément rStyle à l'intérieur de l'élément de propriété du run (rPr).

Considerons le style suivant (de type character) :

<w:style w:type="character" w:styleId="monStyledeRun">
<w:name w:val="Style pour run"/>
<w:priority w:val="99"/>
<w:rPr>
<w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/>
<w:color w:val="FFF200"/>
<w:u w:val="single"/>
</w:rPr>
</w:style>

Vous noterez l'utilisation de l'élément rPr pour définir les propriétés qui vont s'appliquer.

Ce style sera référencé ainsi par un paragraphe :

<w:p>
<w:r>
<w:rPr>
<w:rStyle w:val="monStyledeRun"/>
</w:rPr>
<w:t>Un style est appliqué à ce texte</w:t>
</w:r>
</w:p>

Le style « monStyledeRun » est référencé grâce à l'élément rStyle à l'intérieur de l'élément de propriétés rPp du run.

VII-E - Les propriétés par défaut

Bien qu'on ne puisse par vraiment parler de style au sens strict du terme (ces propriétés ne pouvant être directement appliquées à du texte), elles permettent de définir les propriétés de formatage qui seront appliquées par défaut à tous les paragraphes et runs du document. Pour cela, on n'utilise pas la balise style, mais un nouvel élément : docDefaults. Ce dernier peut contenir deux éléments enfants : pPrDefault qui définit les propriétés par défaut pour les paragraphes et rPrDefault qui définit les propriétés par défaut pour les runs.

Voici un exemple de définition pour l'élément docDefaults :

<w:docDefaults>
<w:pPrDefault>
<w:pPr>
<w:jc w:val="center"/>
</w:pPr>
</w:pPrDefault>
<w:rPrDefault>
<w:rPr>
<w:b/>
</w:rPr>
</w:rPrDefault>
</w:docDefaults>

Plus d'explications seraient superflues, il suffit simplement de définir l'élément pPr dans pPrDefault et l'élément rPr dans rPrDefault puis d'y déclarer les propriétés voulues.

VII-F - Ordre d'application des styles

Avec les différents types de styles existants, il est possible d'appliquer plusieurs styles différents à un même contenu. Considérons l'extrait suivant :

<w:p>
<w:pPr>
<w:pStyle w:val="MonStyle1"/>
</w:pPr>
<w:r>
<w:rPr>
<w:rStyle w:val="monStyledeRun"/>
<w:color w:val="FF0000"/>
</w:rPr>
<w:t>Un style est appliqué à ce texte</w:t>
</w:r>
</w:p>

Dans cet exemple un premier style « MonStyle1 » est appliqué au paragraphe. Un second style « monStyleDeRun » est ensuite appliqué à un run contenu dans ce même paragraphe. Enfin, une couleur non déclarée dans un style est appliqué au texte. Supposons que « MonStyle1 » et « monStyleDeRun » définissent chacun une autre couleur pour le texte : de quelle couleur sera finalement le texte ? Pour résoudre ce problème, les différents styles sont appliqués dans un ordre précis et résumé dans le tableau suivant :

Ce tableau peut être décrit de la façon suivante :

- Tout d'abord, les styles par défaut sont appliqués à tous les paragraphes et runs du document.

- Ensuite, les styles spécifiques aux tableaux sont appliqués à tous les tableaux du document.

- Ensuite, on applique les styles définis pour les énumérations

- Puis, les styles s'appliquant aux paragraphes (et au runs contenus dans ces paragraphes) sont appelés.

- Puis, les styles spécifiques aux runs sont appliqués.

- Et pour finir, on applique les propriétés de formatage directes (non contenues dans des styles).

Ainsi, si l'on reprend notre exemple précédent, c'est finalement la couleur définie directement (FF0000) qui aura le dernier mot.

VII-G - Un peu de code

Après avoir vu les bases concernant les styles, mettons tout cela en application. Nous allons créer un document Open XML qui contiendra un paragraphe (avec du texte) auquel nous appliquerons un style. Celui-ci sera de type « paragraph », c'est-à-dire définissant des propriétés s'appliquant à un paragraphe (ici le paragraphe sera centré), mais aussi au texte contenu dans ce paragraphe (ici la couleur).

Voici le code XML de ce style (contenu dans le fichier styles.xml):

<?xml version="1.0" encoding="utf-8"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:styleId="MonStyle1">
<w:name w:val="MonStyle1" />
<w:pPr>
<w:jc w:val="center" />
</w:pPr>
<w:rPr>
<w:color w:val="FF0000" />
</w:rPr>
</w:style>
</w:styles>

Pour réaliser cela, rien de bien compliqué. Comme toujours, il faut partir d'un XmlDocument et construire son arborescence en y ajoutant les nœuds nécessaires. Voici le code :

// Création du style
XmlDocument xmlStylePart = new XmlDocument();

XmlElement tagStyles = xmlStylePart.CreateElement("w:styles", WordprocessingML);
xmlStylePart.AppendChild(tagStyles);
XmlElement tagStyle = xmlStylePart.CreateElement("w:style", WordprocessingML);
tagStyles.AppendChild(tagStyle);

/****attributs du style****/
//type de style
XmlAttribute attributType = xmlStylePart.CreateAttribute("w:type", WordprocessingML);
attributType.InnerText = "paragraph";
tagStyle.SetAttributeNode(attributType);
//identifiant du style
XmlAttribute attributId = xmlStylePart.CreateAttribute("w:styleId", WordprocessingML);
attributId.InnerText = "MonStyle1";
tagStyle.SetAttributeNode(attributId);

//noeud Name
XmlElement tagName = xmlStylePart.CreateElement("w:name", WordprocessingML);
tagStyle.AppendChild(tagName);
XmlAttribute attributName = xmlStylePart.CreateAttribute("w:val", WordprocessingML);
attributName.InnerText = "MonStyle1";
tagName.SetAttributeNode(attributName);

//noeud pPr
XmlElement tagpPr = xmlStylePart.CreateElement("w:pPr", WordprocessingML);
tagStyle.AppendChild(tagpPr);
//propriété alignement
XmlElement tagAlignement = xmlStylePart.CreateElement("w:jc", WordprocessingML);
//attribut du noeud w:jc
XmlAttribute attributJc = xmlStylePart.CreateAttribute("w:val", WordprocessingML);
attributJc.InnerText = "center";
tagAlignement.SetAttributeNode(attributJc);
tagpPr.AppendChild(tagAlignement);

//noeud rPr
XmlElement tagrPr = xmlStylePart.CreateElement("w:rPr", WordprocessingML);
tagStyle.AppendChild(tagrPr);
//propriété couleur
XmlElement tagCouleur = xmlStylePart.CreateElement("w:color", WordprocessingML);
//attribut de l'élément w:color
XmlAttribute valeurCouleur = xmlStylePart.CreateAttribute("w:val", WordprocessingML);
valeurCouleur.InnerText = "FF0000";
tagCouleur.SetAttributeNode(valeurCouleur);
tagrPr.AppendChild(tagCouleur);

Passons maintenant au fichier document.xml dont voici le contenu…

<?xml version="1.0" encoding="utf-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p>
<w:pPr>
<w:pStyle w:val="MonStyle1" />
</w:pPr>
<w:r>
<w:t>hello</w:t>
</w:r>
</w:p>
<w:p />
</w:body>
</w:document>

…et le code pour le construire :

// Création du WordML
XmlDocument xmlStartPart = new XmlDocument();

XmlElement tagDocument = xmlStartPart.CreateElement("w:document", WordprocessingML);
xmlStartPart.AppendChild(tagDocument);
XmlElement tagBody = xmlStartPart.CreateElement("w:body", WordprocessingML);
tagDocument.AppendChild(tagBody);
XmlElement tagParagraph = xmlStartPart.CreateElement("w:p", WordprocessingML);
tagBody.AppendChild(tagParagraph);

//propriétés du paragraphe
XmlElement tagParagraphProp = xmlStartPart.CreateElement("w:pPr", WordprocessingML);
tagParagraph.AppendChild(tagParagraphProp);

//style du paragraphe
XmlElement tagpStyle = xmlStartPart.CreateElement("w:pStyle", WordprocessingML);
//attribut de l'élément w:pStyle
XmlAttribute valeurStyle = xmlStartPart.CreateAttribute("w:val", WordprocessingML);
valeurStyle.InnerText = "MonStyle1";
tagStyle.SetAttributeNode(valeurStyle);
tagParagraphProp.AppendChild(tagpStyle);

//création du run
XmlElement tagRun = xmlStartPart.CreateElement("w:r", WordprocessingML);
tagParagraph.AppendChild(tagRun);

//création de l'élément t
XmlElement tagText = xmlStartPart.CreateElement("w:t", WordprocessingML);
tagRun.AppendChild(tagText);
tagText.InnerText = "hello";

Rien d'exceptionnel par rapport à ce qui a déjà été vu. La seule nouveauté réside dans l'ajout de l'élément pStyle.

On crée ensuite un nouveau package…

// Création d'un nouveau fichier
Package pkgOutputDoc = null;
pkgOutputDoc = Package.Open("monFichier.docx", FileMode.Create, FileAccess.ReadWrite);

…puis une nouvelle Part et une relation pour la lier au package :

// Création de la part principale
Uri uri = new Uri("document.xml", UriKind.Relative);
PackagePart partDocumentXML = pkgOutputDoc.CreatePart(uri,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml");
StreamWriter streamStartPart =
new StreamWriter(partDocumentXML.GetStream(FileMode.Create, FileAccess.Write));

xmlStartPart.Save(streamStartPart);
streamStartPart.Close();

// Création de la RelationShip
pkgOutputDoc.CreateRelationship(uri, TargetMode.Internal,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
"rId1");

Tout cela est du code que nous avons déjà vu et qui nous permet de créer la Part principale du document.

Il nous reste maintenant à nous occuper de notre fichier de styles. Tout comme le fichier document.xml, le fichier de style va être une Part du document WordprocessingML. Le code pour le créer va donc être très ressemblant.

Nous créons tout d'abord une nouvelle Part (PackagePart) en spécifiant son emplacement (Uri) et son type de contenu : « application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml » qui est le type à spécifier pour la Part styles. Puis on sérialise le tout dans le package :

// Sauvegarde du style généré dans le fichier styles.xml dans le package
uri = new Uri("styles.xml", UriKind.Relative);
PackagePart partStylesXML = pkgOutputDoc.CreatePart(uri,
"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml");
streamStartPart =
new StreamWriter(partStylesXML.GetStream(FileMode.Create, FileAccess.Write));

xmlStylePart.Save(streamStartPart);
streamStartPart.Close();

Nous avons créé la Part et déclaré son type de contenu, il ne nous reste maintenant plus qu'à la lier en créant un élément relation. Attention, c'est ici un peu différent que lors de la création de la relation pour la Part principale du document. En effet, avec le fichier document.xml il fallait créer une relation entre le package et ce fichier pour spécifier où était la Part principale quand on lisait le package. Or ici nous voulons spécifier qu'il existe une relation entre la Part styles et la Part principale du document.

Pour créer la relation, nous n'allons donc pas utiliser l'objet Package, mais l'objet partDocumentXML :

// Création de la RelationShip
partDocumentXML.CreateRelationship(uri, TargetMode.Internal,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
"rId1");

Le type de relation est défini par « http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles » qui caractérise une relation de type styles.

Cela aura pour conséquence de créer un nouveau répertoire _rels dans le répertoire word avec à l'intérieur un fichier document.xml.rels contenant cette relation. En voici le contenu :

<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
Target="/word/styles.xml" Id="rId1" />
</Relationships>

Enfin, il ne reste plus qu'à fermer le package :

pkgOutputDoc.Flush();
pkgOutputDoc.Close();

VIII - Les fichiers de propriétés

Ceux-ci sont stockés dans le répertoire docProps situé à la racine du package.

Le fichier core.xml contient un ensemble de propriétés communes à tous les fichiers Open XML. Ces propriétés incluent le nom du créateur, la date de création, le titre, et la description. Ainsi, que vous traitiez un document docx, xlsx ou encore pptx, ces propriétés seront toujours placées à cet endroit.

Le fichier app.xml contient des propriétés spécifiques pour chaque type de packages Open XML. Par exemple, pour un package WordprocessingML, ces propriétés incluent le nombre de caractères, de mots, de lignes, de paragraphes, et de pages dans le document. Pour un package de type Spreadsheet (xlsx), ces propriétés incluent les titres des feuilles. Pour un package de type Presentation (pptx), ces propriétés incluent le format de présentation, le nombre de diapositives, le nombre de notes.

Nous allons uniquement nous intéresser dans cette partie au fichier core.xml.

Voici un exemple de ce que pourrait être le contenu de ce fichier :

<?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>OpenXML et dotnet</dc:title>
<dc:subject>créer un fichier Word 2007</dc:subject>
<dc:creator>Moi</dc:creator>
<dc:description>commentaires</dc:description>
<dcterms:created xsi:type="dcterms:W3CDTF">2006-12-26T10:16:00Z</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2006-12-26T10:16:00Z</dcterms:modified>
<cp:category>Article dotnet</cp:category>
<cp:contentStatus>En cours</cp:contentStatus>
<cp:keywords>dotnet OpenXML</cp:keywords>
<cp:revision>2</cp:revision>
</cp:coreProperties>

Notez l'utilisation de différents namespaces.

Les éléments de propriété ne peuvent pas être répétés. Un élément en double corromprait le package.

Les éléments de propriété ne sont pas obligatoires et peuvent être omis si vous ne voulez pas renseigner le champ correspondant (la présence du fichier core.xml n'est d'ailleurs elle même pas obligatoire dans le package).

Créons tout d'abord le XmlDocument correspondant (on se limite ici à la création des éléments title, creator, category et created):

string cp = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties";
string dc = "http://purl.org/dc/elements/1.1/";
string dcterms = "http://purl.org/dc/terms/";
string xsi = "http://www.w3.org/2001/XMLSchema-instance";

XmlDocument xmlCorePart = new XmlDocument();

XmlElement tagCore = xmlCorePart.CreateElement("cp:coreProperties", "cp");
xmlCorePart.AppendChild(tagCore);

XmlElement tagTitle = xmlCorePart.CreateElement("dc:title", "dc");
tagTitle.InnerText = "Titre";
tagCore.AppendChild(tagTitle);

XmlElement tagCreator = xmlCorePart.CreateElement("dc:creator", "dc");
tagCreator.InnerText = "Creator";
tagCore.AppendChild(tagCreator);

XmlElement tagCat = xmlCorePart.CreateElement("cp:category", "cp");
tagCat.InnerText = "Catégorie";
tagCore.AppendChild(tagCat);

XmlElement tagCreated = xmlCorePart.CreateElement("dcterms:created", dcterms);

//formatage de la date
DateTimeFormatInfo myDTFI = new CultureInfo("fr-FR", false).DateTimeFormat;
tagCreated.InnerText = DateTime.Now.ToString(myDTFI.SortableDateTimePattern);
tagCore.AppendChild(tagCreated);
//attribut
XmlAttribute attributType = xmlCorePart.CreateAttribute("xsi:type", xsi);
attributType.InnerText = "dcterms:W3CDTF";
tagCreated.SetAttributeNode(attributType);

Les seules petites difficultés ici se limitent à l'utilisation de différents namespaces et d'un encodage spécial pour la date (ne pas oublier « using System.Globalization; » pour pouvoir utiliser DateTimeFormatInfo).

Passons maintenant à l'écriture du fichier core.xml. Celui-ci sera stocké à l'emplacement « /docProps/core.xml » et aura pour type de contenu « application/vnd.openxmlformats-package.core-properties+xml ». Il ne faudra ensuite pas oublier, comme toujours, de créer une relation de type « http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties », entre cette Part et le package. Attention ici à l'identifiant de cette relation. En effet, n'oublions pas que nous avons déjà une relation entre le fichier document.xml et le package (identifiant rId1). L'identifiant devant être unique, nous mettrons ici « rId2 ».

Voici le code correspondant :

// Sauvegarde du core généré dans le fichier core.xml dans le package
uri = new Uri("/docProps/core.xml", UriKind.Relative);
PackagePart partCoreXML = pkgOutputDoc.CreatePart(uri,
"application/vnd.openxmlformats-package.core-properties+xml");
streamStartPart =
new StreamWriter(partCoreXML.GetStream(FileMode.Create, FileAccess.Write));
xmlCorePart.Save(streamStartPart);
streamStartPart.Close();
// Création de la RelationShip
pkgOutputDoc.CreateRelationship(uri, TargetMode.Internal,
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-
properties", "rId2");

Pour finir, voici la liste complète des différentes propriétés disponibles :

Propriétés

Descriptions

Utilisations

title

Titre du document.

<dc:title>OpenXML et dotnet</dc:title>

subject

Sujet du document.

<dc:subject>objet du document</dc:subject>

creator

Nom du créateur.

<dc:creator>Moi</dc:creator>

description

Description du document.

<dc:description>commentaires</dc:description>

identifier

<dc:identifier>id</dc:identifier>

language

Langue du document.

<dc:language>fr</dc:language>

category

Catégorie du document.
Ex : lettre, rapport, etc.

<cp:category>Article</cp:category>

contentStatus

Statut du document.
Ex : en cours, final, etc.

<cp:contentStatus>En cours</cp:contentStatus>

contentType

Type de contenu.

<cp:contentType>dotnet</cp:contentType>

keywords

Mots clefs, pour l'indexation.

<cp:keywords>dotnet OpenXML</cp:keywords>

revision

Numéro de révision.

<cp:revision>2</cp:revision>

lastModifiedBy

Nom de la dernière personne ayant modifié le document.

<cp:lastModifiedBy>Moi</cp:lastModifiedBy>

version

Version du document.

<cp:version>1.0</cp:version>

created

Date de création du document.

<dcterms:created xsi:type=« dcterms:W3CDTF »>
2006-12-26T10:16:00Z</dcterms:created>

modified

Date de la dernière modification.

<dcterms:modified xsi:type=« dcterms:W3CDTF »>
2006-12-26T10:16:00Z</dcterms:modified>

IX - Ajouter une image au document

IX-A - DrawingML

Le namespace DrawingML définit tous les éléments permettant de construire des objets graphiques (images, diagrammes, graphiques, formes, etc.). Ce namespace n'est pas spécifique aux documents WordprocessingML, mais peut aussi être utilisé dans des documents SpreadsheetML ou PresentationML.

Dans un document WordprocessingML, il est possible d'inclure des objets graphiques DrawingML en utilisant l'élément drawing au sein d'un élément run. Quand ces objets sont présents dans un document WordprocessingML, il est nécessaire d'inclure l'information qui indique comment les objets seront placés relativement au document. L'élément drawing possède cette capacité, indiquant l'information nécessaire pour ancrer et afficher des objets DrawingML dans un document WordprocessingML.

Voici un exemple de ce que pourrait donner l'insertion d'une image dans un document WordprocessingML (notez les différents namespaces utilisés):

<w:drawing>
<wp:inline>
<wp:extent cx="3000000" cy="3000000" />
<wp:docPr name="monImage.png" id="1" />
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:nvPicPr>
<pic:cNvPr id="2" name=" monImage.png" />
<pic:cNvPicPr />
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="rId2" />
<a:stretch>
<a:fillRect />
</a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:off x="0" y="0" />
<a:ext cx="3000000" cy="3000000" />
</a:xfrm>
<a:prstGeom prst="rect" />
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>

Nous allons donner quelques explications concernant les divers éléments utilisés.

L'élément drawing peut contenir un élément inline ou un élément anchor ayant chacun des propriétés différentes. Ces éléments enfants permettent de définir comment sera disposée l'image par rapport au texte (aligné sur le texte ou ancré).

Nous avons ici choisi une disposition de type inline. L'élément inline contient un certain nombre d'éléments enfants dont docPr (obligatoire), graphic (obligatoire) et extent (facultatif).

L'élément docPr permet en autre de spécifier deux propriétés obligatoires : name (spécifie un nom pour l'objet DrawingML) et id (spécifie un identifiant unique pour l'objet DrawingML).

L'élément extent permet de spécifier la place que va prendre l'objet DrawingML dans le document. Il possède deux attributs cx et cy qui définissent respectivement la hauteur et la largeur.

L'unité de mesure est l'EMU (English Metric Unit). Les valeurs possibles vont de 0 à 27273042316900. Il y a 360000 EMUs par centimètre, 914400 EMU par pouce et 12700 EMU par point.

L'élément graphic indique l'existence d'un objet graphique. Les spécifications pour cet objet graphique sont référencées dans l'élément enfant graphicData. Ce dernier définit quel sera le type de contenu de l'objet graphique. Cette information est indiquée en utilisant l'attribut uri. Le reste du contenu de l'élément graphicData dépend de la valeur de cet attribut. Voici quelques-unes des valeurs possibles pour cet attribut :

- http://schemas.openxmlformats.org/drawingml/2006/chart

- http://schemas.openxmlformats.org/drawingml/2006/compatibility

- http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas

- http://schemas.openxmlformats.org/drawingml/2006/picture

- http://schemas.openxmlformats.org/drawingml/2006/table

- http://schemas.openxmlformats.org/drawingml/2006/ole

Ici nous utilisons un objet picture dans l'objet graphique. Un objet picture est défini par l'élément pic. Celui-ci peut contenir un certain nombre de nœuds enfant dont trois sont obligatoires : nvPicPr, blipFill et spPr :

L'élément nvPicPr est un conteneur pour les propriétés non visuelles associées à une image. Il contient deux nœuds obligatoires cNvPicPr et cNvPr.

L'élément blipFill définit comment sera rempli l'objet picture. Le nœud blip fait référence au fichier image qui sera utilisé. Ce fichier étant stocké dans le package on utilise l'attribut embed qui spécifie l'identifiant d'un élément relation contenu dans le fichier document.xml.rels. Le nœud stretch indique que l'image sera étirée pour remplir l'espace disponible.

L'élément spPr permet de définir un certain nombre d'effets qui changent l'aspect de l'image. On y retrouve par exemple les effets de rotation, de réflexion, d'ombre, etc. Le nœud enfant prstGeom permet d'utiliser une forme géométrique préréglée. Ici nous utilisons une forme rectangulaire, mais nous aurions pu utiliser un cœur, un trapèze, un rond ou toute autre forme disponible (la liste se trouve dans les spécifications du format au chapitre 5.1.12.56 ST_ShapeType).

IX-B - Le code

Voyons maintenant le code nécessaire pour réaliser tout cela.

Nous allons tout d'abord créer un XmlElement qui contiendra la structure XML définie ci-dessus (élément drawing). La seule difficulté pour l'instant peut se résumer à la longueur du code nécessaire… Attention toutefois aux différents namespaces utilisés.

string WordprocessingML = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
string RelationShips =  "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
string Drawing = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
string DrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main";
string Pic = "http://schemas.openxmlformats.org/drawingml/2006/picture";

// Création du WordML
XmlDocument xmlStartPart = new XmlDocument();

XmlElement tagDrawing = xmlStartPart.CreateElement("w:drawing", WordprocessingML);
XmlElement tagInline = xmlStartPart.CreateElement("wp:inline", Drawing);
tagDrawing.AppendChild(tagInline);

//Element extent
XmlElement tagExtent = xmlStartPart.CreateElement("wp:extent", Drawing);
tagInline.AppendChild(tagExtent);
//attributs de extent
XmlAttribute attributcx = xmlStartPart.CreateAttribute("cx");
attributcx.InnerText = "1300000";
tagExtent.SetAttributeNode(attributcx);
XmlAttribute attributcy = xmlStartPart.CreateAttribute("cy");
attributcy.InnerText = "1300000";
tagExtent.SetAttributeNode(attributcy);

//Element docPr
XmlElement tagdocPr = xmlStartPart.CreateElement("wp:docPr", Drawing);
tagInline.AppendChild(tagdocPr);
//attributs de docPr
XmlAttribute attributname = xmlStartPart.CreateAttribute("name");
attributname.InnerText = "openxml.png";
tagdocPr.SetAttributeNode(attributname);
XmlAttribute attributid = xmlStartPart.CreateAttribute("id");
attributid.InnerText = "1";
tagdocPr.SetAttributeNode(attributid);

//Element graphic
XmlElement taggraphic = xmlStartPart.CreateElement("a:graphic", DrawingML);
tagInline.AppendChild(taggraphic);
//Element graphicData
XmlElement taggraphicData = xmlStartPart.CreateElement("a:graphicData", DrawingML);
taggraphic.AppendChild(taggraphicData);
//attributs de graphicData
XmlAttribute attributuri = xmlStartPart.CreateAttribute("uri");
attributuri.InnerText = "http://schemas.openxmlformats.org/drawingml/2006/picture";
taggraphicData.SetAttributeNode(attributuri);

//Element pic
XmlElement tagpic = xmlStartPart.CreateElement("pic:pic", Pic);
taggraphicData.AppendChild(tagpic);

//Element nvPicPr
XmlElement tagnvPicPr = xmlStartPart.CreateElement("pic:nvPicPr", Pic);
tagpic.AppendChild(tagnvPicPr);
//Element cNvPr
XmlElement tagcNvPr = xmlStartPart.CreateElement("pic:cNvPr", Pic);
tagnvPicPr.AppendChild(tagcNvPr);
//attributs de cNvPr
XmlAttribute attributnamecNvPr = xmlStartPart.CreateAttribute("name");
attributnamecNvPr.InnerText = "openxml.jpg";
tagcNvPr.SetAttributeNode(attributnamecNvPr);
XmlAttribute attributidcNvPr = xmlStartPart.CreateAttribute("id");
attributidcNvPr.InnerText = "2";
tagcNvPr.SetAttributeNode(attributidcNvPr);
//Element cNvPicPr
XmlElement tagcNvPicPr = xmlStartPart.CreateElement("pic:cNvPicPr", Pic);
tagnvPicPr.AppendChild(tagcNvPicPr);

//Element blipFill
XmlElement tagblipFill = xmlStartPart.CreateElement("pic:blipFill", Pic);
tagpic.AppendChild(tagblipFill);
//Element blip
XmlElement tagblip = xmlStartPart.CreateElement("a:blip", DrawingML);
tagblipFill.AppendChild(tagblip);
//attribut de blip
XmlAttribute attributembed = xmlStartPart.CreateAttribute("r:embed", RelationShips);
attributembed.InnerText = "rId1";
tagblip.SetAttributeNode(attributembed);
//Element stretch
XmlElement tagstretch = xmlStartPart.CreateElement("a:stretch", DrawingML);
tagblipFill.AppendChild(tagstretch);
//Element fillRect
XmlElement tagfillRect = xmlStartPart.CreateElement("a:fillRect", DrawingML);
tagstretch.AppendChild(tagfillRect);

//Element spPr
XmlElement tagspPr = xmlStartPart.CreateElement("pic:spPr", Pic);
tagpic.AppendChild(tagspPr);
//Element xfrm
XmlElement tagxfrm = xmlStartPart.CreateElement("a:xfrm", DrawingML);
tagspPr.AppendChild(tagxfrm);

//Element off
XmlElement tagoff = xmlStartPart.CreateElement("a:off", DrawingML);
tagxfrm.AppendChild(tagoff);
//attributs de off
XmlAttribute attributx = xmlStartPart.CreateAttribute("x");
attributx.InnerText = "0";
tagoff.SetAttributeNode(attributx);
XmlAttribute attributy = xmlStartPart.CreateAttribute("y");
attributy.InnerText = "0";
tagoff.SetAttributeNode(attributy);
//Element ext
XmlElement tagext = xmlStartPart.CreateElement("a:ext", DrawingML);
tagxfrm.AppendChild(tagext);
//attributs de ext
XmlAttribute attributcxext = xmlStartPart.CreateAttribute("cx");
attributcxext.InnerText = "1300000";
tagext.SetAttributeNode(attributcxext);
XmlAttribute attributcyext = xmlStartPart.CreateAttribute("cy");
attributcyext.InnerText = "1300000";
tagext.SetAttributeNode(attributcyext);

//Element prstGeom
XmlElement tagprstGeom = xmlStartPart.CreateElement("a:prstGeom", DrawingML);
tagspPr.AppendChild(tagprstGeom);
//attributs de prstGeom
XmlAttribute attributprst = xmlStartPart.CreateAttribute("prst");
attributprst.InnerText = "rect";
tagprstGeom.SetAttributeNode(attributprst);

Une fois l'arborescence créée, il nous faut l'intégrer dans un élément run au sein d'un XmlDocument. On crée ici un XmlDocument qui contiendra deux paragraphes : un pour du texte et un pour l'image. L'insertion du XmlElement précédemment créé se fait à la dernière ligne :

XmlElement tagDocument = xmlStartPart.CreateElement("w:document", WordprocessingML);
xmlStartPart.AppendChild(tagDocument);
XmlElement tagBody = xmlStartPart.CreateElement("w:body", WordprocessingML);
tagDocument.AppendChild(tagBody);
XmlElement tagParagraph = xmlStartPart.CreateElement("w:p", WordprocessingML);
tagBody.AppendChild(tagParagraph);
//création du premier run
XmlElement tagRun = xmlStartPart.CreateElement("w:r", WordprocessingML);
tagParagraph.AppendChild(tagRun);
//un peu de texte
XmlElement tagText = xmlStartPart.CreateElement("w:t", WordprocessingML);
tagRun.AppendChild(tagText);
tagText.InnerText = "Voici une image :";
tagRun.AppendChild(tagText);

//Nouveau paragraphe avec une image
XmlElement tagParagraph2 = xmlStartPart.CreateElement("w:p", WordprocessingML);
tagBody.AppendChild(tagParagraph2);
//création du 2e run
XmlElement tagRun2 = xmlStartPart.CreateElement("w:r", WordprocessingML);
tagParagraph2.AppendChild(tagRun2);

//insertion de l'image dans le run
tagRun2.AppendChild(tagDrawing);

Il nous faut maintenant sauvegarder ce XmlDocument dans un nouveau package comme on a maintenant l'habitude de le faire…

// Création d'un nouveau package
Package pkgOutputDoc = null;
pkgOutputDoc = Package.Open("monFichier.docx", FileMode.Create, FileAccess.ReadWrite);
// Sauvegarde du XmlDocument dans le un fichier document.xml dans le package
Uri uri = new Uri("/word/document.xml", UriKind.Relative);
PackagePart partDocumentXML = pkgOutputDoc.CreatePart(uri,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml");
StreamWriter streamStartPart =
new StreamWriter(partDocumentXML.GetStream(FileMode.Create, FileAccess.Write));
xmlStartPart.Save(streamStartPart);
streamStartPart.Close();

// Création de la RelationShip
pkgOutputDoc.CreateRelationship(uri, TargetMode.Internal,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
"rId1");

Il ne nous reste plus qu'à insérer l'image (openxml.png) dans le package. Celle-ci sera stockée dans un répertoire media à l'intérieur du répertoire word. À l'instar des fichiers document.xml ou styles.xml, une image est une Part. Nous allons donc devoir définir son type de contenu ainsi qu'une relation la liant à une autre Part (ici, le fichier document.xml).

Commençons tout d'abord par déclarer une Part comme nous l'avons toujours fait. On crée un objet PackagePart et on lui passe en paramètre la localisation du fichier dans le package et le type de contenu (ici « image/png »). On enregistre ensuite l'image dans le package en utilisant un FileStream.

//sauvegarde de l'image dans le package
uri = new Uri("/word/media/openxml.png", UriKind.Relative);
PackagePart partImage = pkgOutputDoc.CreatePart(uri, "image/png");
using (Stream targetStream = partImage.GetStream())
{
using (FileStream sourceStream = new FileStream("openxml.png",
FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[1024];
int nrBytesWritten = sourceStream.Read(buffer, 0, 1024);
while (nrBytesWritten > 0)
{
targetStream.Write(buffer, 0, nrBytesWritten);
nrBytesWritten = sourceStream.Read(buffer, 0, 1024);
}
}
}

Enfin, il nous reste à créer la relation. De même qu'avec le fichier de styles, on ne lie pas l'image au package, mais à la Part principale (le fichier document.xml). On utilise donc l'objet partDocumentXML pour créer cette relation, et non pkgOutputDoc. C'est donc dans le fichier document.xml.rels du répertoire word\_rels qu'elle sera stockée.

// Création de la RelationShip entre l'image et la part principale
partDocumentXML.CreateRelationship(uri, TargetMode.Internal,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
"rId1");

http://schemas.openxmlformats.org/officeDocument/2006/relationships/image est le type de la relation qui correspond au type de relation pour les images. Nous spécifions ici un identifiant de relation rId1 car c'est la seule relation que nous déclarons dans le fichier document.xml.rels. S'il existe déjà d'autres relations (pour les styles par exemple), il vaudra vérifier que l'identifiant est bien unique.

Enfin, on ferme le package :

// Fermeture du package
pkgOutputDoc.Flush();
pkgOutputDoc.Close();

Et voici le résultat obtenu :

X - Pour aller plus loin

Microsoft a sorti un package de snippets utilisable avec Visual Studio 2005 destiné au format de documents Open XML (Word, Excel et PowerPoint).
Et ça se trouve ici : snipets.

Bien que la création de fichiers Open XML soit relativement simple, il n'en demeure pas moins qu'il faut quand même taper un nombre assez important de lignes de codes (et notamment pour la création de la structure XML). C'est pourquoi un certain nombre de librairies permettant de simplifier le code nécessaire ont commencé à émerger un peu partout sur le Web.

XI - Conclusion

Nous avons donc vu au travers de cet article les bases pour la génération de documents Word au format Open XML en utilisant les briques du Framework 3.0 autour de l'assembly System.IO.Package.

La génération de documents Office (Word dans cet exemple) peut désormais être exécutée sans qu'Office ne soit installé et sans devoir utilisé les Primary Interop Assemblies d'Office comme c'était le cas avec les anciens formats.

Liens

Le site Open XML chez Microsoft

Introducing the Office (2007) Open XML File Formats

How to: Manipulate Office Open XML Formats Documents

Structure d'un document Open XML

Open XML sur le blog de Julien Chable

openxmldeveloper.org

Spécifications du format à l'ECMA

Les webcast sur Open XML

Structure des fichiers OpenXML (developpez.com)

Lecture des fichiers OpenXML avec PHP 5 (developpez.com)

Remerciements

J'adresse ici tous mes remerciements à l'équipe de rédaction de « developpez.com » et tout particulièrement à Emilie Guittier pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article.

Contact

Si vous constatez une erreur dans le tutoriel, dans les sources, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par le forum.