Introduction au SDK Open XML

 

Par Julien Chable, développeur/consultant chez Wygwam France sur les technologies Office, Open XML et SharePoint.


Blog de Julien.


Manipuler efficacement des documents Office directement, et de plus côté serveur, a toujours été un souhait de beaucoup d’entreprises et de chef de projet. Les solutions à base d’automation – nécessitant une instance d’Office sur le serveur – a toujours montré ses limites en termes d’intégration, de qualité de service et de performances. Aujourd’hui avec le format Open XML, format normalisé ECMA et ISO, les entreprises redeviennent propriétaires du format de données de leurs documents et peuvent créer des applications consommant ces documents, le SDK Open XML a été créé dans ce but.

Des articles de plus en plus nombreux expliquent les objectifs d’Open XML et la technique permettant aux applications de stocker leurs documents dans un format entièrement décrit en XML et compressé ensuite en Zip. Cet article passera rapidement sur la structure d’un document Open XML, tout simplement car le SDK Open XML apporte justement une abstraction au mécanisme interne de structuration du format, aussi appelé structure Open Packaging Convention.

Open Packaging Convention et les différents formats de documents

Un document Open XML est un fichier compressé Zip, et peut donc être ouvert avec n’importe quel outil de compression gérant ce format, renfermant un ensemble de fichiers définissant les données du document. Ce conteneur Zip est également appelé le paquet ou « package » en Anglais. Chaque fichier que contient l’archive Zip est appelée « une partie » du document, et chaque partie est reliée à une autre à l’aide d’une relation (par exemple un paragraphe d’un contenu avec une image).

Chaque type de document possède sa propre structure de relation que nous détaillerons par la suite dans les sections se rapportant aux différents formats.

Voici une décomposition simple d’un fichier WordprocessingML avec ses parties et ses relations :

La partie de relations /.rels est la partie d’origine de tous les documents Open XML, c’est cette partie qui liste toutes les relations initiales vers lesquelles Office – ou tout autre implémentation d’Open XML – va se servir pour récupérer toutes les parties contenant l’ensemble des données d’un document.

Chaque produit de la suite Office propose un outil pour chacun des besoins de l’utilisateur en entreprise. Chaque produit possède donc un objectif et donc une structure du format bien spécifique.

Dans la suite de cet article nous allons aborder l’utilisation de chaque format avec le SDK Open XML.

Le SDK Open XML et System.IO.Packaging

L’objectif de l’Open Packaging Convention est d’offrir une structure logique et flexible à un format aussi extensible et complexe que celui des documents bureautiques. L’OPC offre une abstraction des données du document sous la forme de partie. La manipulation des parties, des relations et des types de contenu a déjà été adressé par l’espace de nom System.IO.Packaging de l’assembly WindowsBase.dll livré en standard depuis .NET 3.

Cette API permet déjà de faire beaucoup de choses mais n’adresse aucunement d’autre chose que la partie Open Packaging Convention du format Open XML. En effet toutes les parties quelque soit leur origine, sont considérées comme un seule et unique type d’objet : les PackagePart. Or lorsque vous naviguez dans les parties, aucune méthode n’adresse la récupération ou la navigation aisée d’une partie vers une autre. Par exemple, l’utilisation de l’espace de nom System.IO.Packaging ne rend pas aisé de retrouver la partie d’une présentation PowerPoint puis de naviguer vers chaque diapositive, ou de naviguer vers les images à partir du contenu d’un document Word.

L’objectif du SDK Open XML est justement d’abstraire la couche Open Packaging Convention – et d’autant plus les concepts abstraits de l’espace de nom System.IO.Packaging - et d’offrir un maximum de méthodes et de propriétés pour récupérer les parties et naviguer rapidement et efficacement entre les différentes parties. Pour résumer, le SDK Open XML, et ce n’est pas peu, apporte le typage fort des parties là où l’espace de nom System.IO.Packaging nous laissait avec des objets génériques :

using (Package pkg = Package.Open("Demo.docx", FileMode.Open))
{
    PackagePart mainPart;
    // Récupération de la partie principale du contenu du document
    foreach (PackageRelationship rel in package.GetRelationshipsByType(
    "https://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"))
    {
        mainPart = pkg.GetPart(PackUriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri));
        break;
    }
}

Vous allez pouvoir faire de même avec le SDK Open XML en quelques lignes seulement :

using (WordprocessingDocument wordDoc = 
WordprocessingDocument.Open("MonDoc.docx", false))
{
    MainDocumentPart mainPart = wordDoc.MainDocumentPart;
    
}

Ces deux exemples permettent de se rendre compte du typage qui a lieu aussi bien au niveau du paquet (Package -> WordprocessingDocument) que des parties (PackagePart -> MainDocumentPart). Ce typage apporte évidemment des méthodes, propriétés et comportement adéquate selon les documents et parties manipulés.

Le SDK Open XML s’utilise en référençant une assembly nommée DocumentFormat.OpenXml.dll et en utilisant l’espace de nom DocumentFormat.OpenXml.Packaging. Vous remarquerez que c’est une DLL totalement autonome, ce qui vous permettra de la redistribuer facilement et de ne pas vous appuyez sur d’autre dépendance que celle de .NET 3 (l’assembly WindowsBase est évidemment indispensable à son exécution).

 

Remarque : le nom de la DLL et de l’espace de nom ont changés entre les différentes CTP et la version finale du SDK. Il se peut que vous trouviez des exemples ou des morceaux de codes faisant référence aux anciens noms. Modifiez simplement ces derniers pour qu’ils correspondent à la version finale pour faire fonctionner ces applications.

L’architecture du SDK Open XML permet d’observer le haut niveau d’abstraction :

 

Word : les documents WordprocessingML

La structure d’un document WordprocessingML est relativement simple comme le montre le schéma suivant :

 

Au cœur d’un document WordprocessingML se trouve peu de parties véritablement primordiales. La plus importante de toute est celle nommée document.xml et décrit le contenu complet des paragraphes d’un document de traitement de texte. La manipulation de ce type de document n’en est que plus aisée avec l’utilisation du SDK Open XML. Par exemple, voici un code pour créer un document Word avec Linq To XML :

using (WordprocessingDocument wordDoc =
WordprocessingDocument.Create("MonDoc.docx", WordprocessingDocumentType.Document))
{
    // On ajoute la partie principale 
    MainDocumentPart mainPart = wordDoc.AddMainDocumentPart();

    // Création de la structure XML WordprocessingML
    XNamespace wNamespace = "https://schemas.openxmlformats.org/wordprocessingml/2006/main";
    XDocument xmlDoc = new XDocument(
        new XDeclaration("1.0", "utf-8", "yes"),
            new XElement(wNamespace + "document",
                new XElement(wNamespace + "body",
                    new XElement(wNamespace + "p",
                        new XElement(wNamespace + "r",
                            new XElement(wNamespace + "t",
                                new XText("Texte de mon document ici !")))))));
    xmlDoc.Save(new StreamWriter(mainPart.GetStream()));
}

L’ouverture d’un document de type WordprocessingML se fait logiquement avec la classe WordprocessingDocument qui permet ensuite de naviguer rapidement vers la partie principale contenant l’ensemble des paragraphes. Les parties étant typées, la partie principale porte le nom de MainDocumentPart.

L’exécution de ce code produira le document suivant :

Un autre exemple permettant de récupérer les images d’un document WordprocessingML :

using (WordprocessingDocument wordDoc = 
WordprocessingDocument.Open("Demo.docx", false))
{
    MainDocumentPart mainPart = wordDoc.MainDocumentPart;
    foreach (ImagePart imgPart in mainPart.ImageParts)
    {
        Image img = Image.FromStream(imgPart.GetStream());
        Console.WriteLine("Extraction de l'image '{0}' ({1}x{2})",
            imgPart.Uri,
            img.Width,
            img.Height);
        img.Save(Path.GetFileName(imgPart.Uri.ToString()));
    }
}

Après exécution de ce code, vous devriez retrouver toutes les images du document dans le répertoire courant d’exécution :

Un dernier exemple qui vous montre comment changer le style d’un document :

using (WordprocessingDocument wordDoc =
WordprocessingDocument.Open("Demo.docx", true))
{
    MainDocumentPart mainPart = wordDoc.MainDocumentPart;
    StyleDefinitionsPart stylePart = mainPart.StyleDefinitionsPart;
    stylePart.FeedData(new FileStream("styles.xml", FileMode.Open));
}

Le peu de ligne pour réaliser des opérations rendues complexes avec Open Packaging Convention deviennent étonnamment simples avec les SDK Open XML. Notez que la structure simple de ce format de document y est pour beaucoup. Comme nous le verrons pour les deux autres formats de fichier, cela est loin d’être vrai pour ceux-ci.

Excel : les documents SpreadsheetML

Le format Excel est un peu plus complexe que Word mais rien en comparaison de PowerPoint que nous aborderons dans le prochain chapitre. Pour commencer avec ce format, voyons tout d’abord la structure d’un document SpreadsheetML simple.

Le format de document SpreadsheetML se décompose de la façon suivante : un classeur possède une ou plusieurs feuilles, chaque feuille possède des définitions de tables, des images, etc …

En utilisant le SDK Open XML et les classes SpreadsheetDocument vous allez pouvoir naviguer entre les différentes parties aisément pour par exemple retrouver les valeurs des cellules :

static void Main(string[] args)
{
    using (SpreadsheetDocument xlsxDoc = SpreadsheetDocument.Open("Demo.xlsx", false))
    {
        WorkbookPart workbook = xlsxDoc.WorkbookPart;

        foreach (WorksheetPart sheet in workbook.WorksheetParts)
        {
            XDocument xdoc = XDocument.Load(new XmlTextReader(sheet.GetStream()));
            string cellref = "A2";

            Console.WriteLine("La valeur de la cellule {0} est {1}",
                cellref,
                GetCellValue(workbook, xdoc, cellref));
            break; // On ne veut que la première feuille
        }
    }
}

static string GetCellValue(WorkbookPart workbook, XDocument document, string cellRef)
{
    var targetedCellValue = (from c in document.Root.Descendants()
        .Where(c => c.Name.LocalName == "v")
        .Where(c => c.Parent.Attribute("r").Value == cellRef)
    select c).Single();

    if (targetedCellValue.Parent.Attribute("t").Value == "s")
    {
        // Nous devons aller chercher la valeur dans la table des chaines partagées
        SharedStringTablePart sharedStringPart = workbook.SharedStringTablePart;
        XDocument xdoc = XDocument.Load(new XmlTextReader(sharedStringPart.GetStream()));
        return (from c in xdoc.Root.Descendants()
            .Where(c => c.Name.LocalName == "t")
            select c).ElementAt(Int32.Parse(targetedCellValue.Value)).Value;
    }
    else
        return targetedCellValue.Value;
}

Le mécanisme d’optimisation du stockage (lecture et écriture) du format SpreadsheetML stock les valeurs de type chaines de caractères, susceptibles de se répéter, dans une partie à part. En effet, rien ne sert de stocker 100 000 fois un même nom de département dans un document SpreadsheetML.

Pour savoir si la valeur réelle de la cellule est celle de la valeur de la définition de la cellule d’une feuille, nous regardons le type de valeur stockée dans la cellule. Si le type – attribut t – est égal à s – pour string- cela signifie que la valeur réelle de la cellule est stockée dans la partie de chaines partagées. Sinon cela signifie que la valeur – élément fils v – est la valeur réelle et non une référence/index dans la table des chaines partagées.

Par exemple, dans ce cas la valeur de la cellule n’est pas égale à 2 mais à la valeur du 3ème élément de la table des chaines partagées :

C’est exactement ce que fait le code en récupérant une instance de SharedStringTablePart – la partie des chaines de caractères partagées - pour aller chercher la valeur réelle de la cellule.

L’exemple ci-dessus est suffisamment explicite pour illustrer du temps gagné avec le SDK Open XML, chaque partie est obtenue immédiatement au travers d’une propriété de classe.

PowerPoint : les documents PresentationML

Voici le format le plus complexe de tout Open XML. Non pas par sa nature mais par le fait que ce format de document de présentation est une véritable toile d’araignée au vu des nombreuses relations et interconnexions entre les parties. Voici un résumé de la structure d’un document au format PresentationML :

L’architecture de PresentationML devrait vous donner une idée globale des interdépendances qui se retrouveront bien évidemment dans l’architecture du SDK Open XML :

L’exemple ci-après illustre de l’utilisation du SDK Open XML en listant les diapositives d’une présentation en affichant le titre de la diapositive :

using (PresentationDocument pptxDoc = PresentationDocument.Open(
    "Demo.pptx", false))
{
    PresentationPart presentation = pptxDoc.PresentationPart;
    Console.WriteLine("Liste des diapositives :");
    foreach (SlidePart slide in presentation.SlideParts)
    {
        XDocument slideDoc = XDocument.Load(new XmlTextReader(slide.GetStream()));
        var pars = slideDoc
            .Descendants(pNS + "sp")
            .Where(n => n.Descendants(pNS + "ph").Single().Attribute("type") != null && (
            n.Descendants(pNS + "ph").Single().Attribute("type").Value == "title" ||
            n.Descendants(pNS + "ph").Single().Attribute("type").Value == "ctrTitle"))
            .Descendants(aNS + "p");

        foreach (var par in pars)
            foreach (var t in par.Descendants(aNS + "t"))
                Console.Write(t.Value);

        Console.WriteLine();
    }
}

Le résultat de l’exécution de ce bout de code vont montre également (et cela est également vrai avec SpreadsheetML) que les parties ne sont pas forcément renvoyé dans l’ordre de leur apparition tel que défini dans la partie de présentation.

Conclusion

Indéniablement le SDK Open XML apporte une API permettant de gagner significativement en productivité lorsque l’on veut créer ou manipuler des documents Open XML. Néanmoins, et l’espace de nom l’indique bien, cette version du SDK Open XML est uniquement une abstraction de l’enveloppe – du packaging – d’un document. Le SDK ne s’appuie pas sur le contenu des parties pour retrouver les parties mais uniquement sur les relations (d’où l’ordre incorrect des diapositives et des feuilles Excel) qui existent entre elles. Cette version 1 s’arrête donc au niveau de la couche Open Packaging Convention et ne fourni pas encore de modèle objets pour manipuler le contenu d’un document. Il vous faudra donc connaître vous aussi la structure interne d’un document – dont un exemple a été présenté pour chaque format dans cet article – et du schéma XML des parties pour modifier vos documents.

En attendant la prochaine version de cette API qui devrait arriver dans les prochains mois selon la roadmap, nous voici en possession d’une librairie qui devient vite indispensable une fois que l’on y a goûté ! En vous souhaitant de bons développements, je vous laisse télécharger le SDK Open XML et sa documentation avec les liens ci-dessous.

Références