XSL/FO et .NET

Vous êtes-vous déjà demandé comment faire pour générer un PDF en XSL-FO depuis ASP.NET, ou pour le coup n’importe quelle application .NET ? J’ai eu beau chercher, je n’ai pas trouvé de moteur XSL-FO écrit en code managé et open-source.

La solution glanée sur différents sites web est de faire de l’interop avec ApacheFOP, qui est écrit en Java. Mais là, deux voies sont possibles :

– Ou bien on recompile le code Java en J#, et on obtient des assemblages .NET nécessitant les redistribuables J# pour fonctionner. Avantage : le tout ne pèse que quelques Mo. Inconvénient : ça n’a aucune chance de fonctionner sous Mono.

– Ou bien on exécute en mémoire les JAR en passant par IKVM, qui est une machine virtuelle embarquée dans un processus .NET. Avantage : c’est propre et tout tourne en mémoire, de plus pas de problème théoriquement avec Mono, vu qu’IKVM est carrément livré avec. Inconvénient : ça pèse une trentaine de Mo, car il faut livrer avec la totalité des classes Java. Il doit y avoir moyen de faire autrement en générant une dll IKVM.ClassPath.dll avec seulement ce qui est nécessaire, mais je n’ai pas essayé.

On va expliquer sur ce post la deuxième méthode, qui de mon point de vue est la meilleure. Oui, je sais, ça a déjà été fait sur d’autres pages web, mais je n’ai rien trouvé qui était à jour avec la dernière version de FOP et avec la liste des références nécessaires.

Récupération des librairies

On commence par récupérer FOP sur le site Apache, http://xmlgraphics.apache.org/fop/download.html (la toute dernière version stable est la 0.94). On va également télécharger la dernière version en date de IKVM, à savoir la 0.36.0.11 sur http://sourceforge.net/project/showfiles.php?group_id=69637.

Petite remarque au passage : je suis toujours autant émerveillé de la modestie des équipes capables de réaliser un développement aussi remarquable, et de laisser un numéro de version inférieur à 1. Quand on voit des logiciels soi-disant professionnels toujours aussi buggés en version 10, on se dit que beaucoup d’entre eux devraient prendre exemple sur les développements OpenSource.

On décompresse ensuite tout ça dans deux répertoires /FOP et /IKVM.

Recompilation des librairies FOP par IKVM

IKVM fournit un environnement d’exécution .NET d’une VM Java sous forme d’un exécutable, mais également un compilateur permettant de générer des assemblages .NET à partir des JAR, et qui se nomme IKVMC.

Dans notre cas, on se place dans /IKVM/bin, et on lance successivement les commandes suivantes :

ikvmc -target:library
    -reference:IKVM.OpenJDK.ClassLibrary.dll
    /fop-0.94/lib/xml-apis-1.3.02.jar

ikvmc -target:library
    -reference:IKVM.OpenJDK.ClassLibrary.dll
    -reference:xml-apis-1.3.02.dll
    /fop-0.94/lib/xercesImpl-2.7.1.jar

ikvmc -target:library
    -reference:IKVM.OpenJDK.ClassLibrary.dll
    -reference:xml-apis-1.3.02.dll
    /fop-0.94/lib/avalon-framework-4.2.0.jar

ikvmc -target:library
    -reference:IKVM.OpenJDK.ClassLibrary.dll
    -reference:xml-apis-1.3.02.dll
    /fop-0.94/lib/batik-all-1.6.jar

ikvmc -target:library
    -reference:IKVM.OpenJDK.ClassLibrary.dll
    /fop-0.94/lib/commons-logging-1.0.4.jar

ikvmc -target:library
    -reference:IKVM.OpenJDK.ClassLibrary.dll
    /fop-0.94/lib/commons-io-1.3.1.jar

ikvmc -target:library
    -reference:IKVM.OpenJDK.ClassLibrary.dll
    /fop-0.94/lib/xmlgraphics-commons-1.2.jar

ikvmc -target:library
    -reference:IKVM.OpenJDK.ClassLibrary.dll
    -reference:xml-apis-1.3.02.dll
    -reference:batik-all-1.6.dll
    -reference:commons-logging-1.0.4.dll
    -reference:xmlgraphics-commons-1.2.dll
    -reference:commons-io-1.3.1.dll
    -reference:avalon-framework-4.2.0.dll
    /fop-0.94/build/fop.jar

Si vous avez besoin d’assemblages signés, il suffit de spécifier la directive -keyfile:[votre fichier .snk] ou bien -key:[le nom de votre container de clé]. IKVMC contient de nombreuses autres options qui peuvent se réveler très utiles. La prise en compte de la signature par nom fort, dans mon cas, était très importante, et ce fut un vrai soulagement de voir que le paramètre était présent. Je me voyais déjà désassembler l’IL depuis la DLL puis recompiler en ajoutant la signature, et je ne suis même pas sûr que ça soit possible…

Utilisation des assemblages créés

Une fois ceci fait, vous pouvez créer un projet .NET dans lequel vous allez simplement mettre en référence les librairies fop.dll et IKVM.ClassPath.dll générées précédemment. Placer au même endroit toutes les autres références, qui ne seront pas nécessaires pour la compilation, mais indispensables lors de l’exécution. Vous pouvez ensuite taper le genre de code suivant pour vérifier que tout fonctionne. J’ai enlevé tout ce qui est gestion des exceptions et génération des noms de fichiers pour simplifier le code et ne faire voir que ce qui a vraiment du sens pour un tutoriel sur XSL-FO.

Note : je préfère écrire systématiquement les noms de classes complets dans ce genre d’exemple. Je trouve qu’on a toujours du mal à savoir d’où viennent les classes sinon, surtout qu’il est très facile d’oublier de donner les using lorsqu’on copie une simple portion de classe. Dans la pratique, il est bien sûr plus clair d’utiliser des noms de classe simples avec des using.

public void GenererPDF(System.Xml.XmlDocument Source, string FichierXSLFO, string FichierResultatPDF)
{
    // Récupération du fichier XSL
    System.Xml.Xsl.XslCompiledTransform Moteur = new System.Xml.Xslt.XslCompiledTransform();
    Moteur.Load(FichierXSLFO);

    // Application de la transformation
    string FichierResultatFO = System.IO.Path.GetTempFileName();
    System.Xml.XmlTextWriter Scribe = new System.Xml.XmlTextWriter(FichierResultatFO, System.Text.Encoding.Default);

    Moteur.Transform(Source.CreateNavigator(), Scribe);
    Scribe.Close();

    // Création du PDF par interop IKVM avec ApacheFOP
    java.io.File fo = new java.io.File(FichierResultatFO);
    java.io.File pdf = new java.io.File(FichierPDFGenere);
    org.apache.fop.apps.FopFactory fopFactory = org.apache.fop.apps.FopFactory.newInstance();
    org.apache.fop.apps.FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
    java.io.OutputStream sortie = new java.io.BufferedOutputStream(new java.io.FileOutputStream(pdf));
    org.apache.fop.apps.Fop fop = fopFactory.newFop("application/pdf", foUserAgent, sortie);
    javax.xml.transform.TransformerFactory factory = javax.xml.transform.TransformerFactory.newInstance();
    javax.xml.transform.Transformer transformer = factory.newTransformer();
    javax.xml.transform.Source src = new javax.xml.transform.stream.StreamSource(fo);
    javax.xml.transform.Result res = new javax.xml.transform.sax.SAXResult(fop.getDefaultHandler());
    transformer.transform(src, res);
    sortie.close();
}

 

Résultat

Une fois que les premiers résultats fonctionnels ont été atteint, la curiosité pousse à aller voir combien tout ça prend en mémoire. Il y a en effet finalement 13 librairies à mettre en place pour faire fonctionner le tout, pour un total de 36 Mo. Vu que toutes les classes de Java ont été reproduites en IKVM, c’est un peu logique, mais ce qui est intéressant, c’est que la CLR, comme pour des librairies .NET standards, ne charge que ce dont elle a besoin, à savoir en gros 5 Mo, et prend quelques dizaines de Mo pour un gros PDF, mais cette partie de mémoire est considérée comme ponctuelle, et sera libérée lors du prochain passage du garbage collector.

Bref, ça marche ! Honnêtement, je ne me suis pas penché sur IKVM pour vous indiquer clairement comment, mais je préfère avoir un produit qui fonctionne sans savoir pourquoi que l’inverse.

[Edit]

Je reprends les commentaires manuellement depuis mon ancienne plateforme de blog, depuis laquelle je migre les articles pour celle-ci.

mrique :

Merci de bloguer ça, c’est juste un retour pour te dire qu’on va s’en servir et que ton investissement de bloggueur n’est pas une perdu au fond de l’océan du web : y’a des vrais gens qui sont contents à l’autre bout ;-)
by

mrique :

PS : le luxe ce serait qu’on puisse télécharger les assemblages !!!!! (je sais j’abuse)

Alexandre :

Effectivement ça aurait été parfait avec les assemblages ^^

Sinon comment faire avec les nouvelles versions de FOP et d’IKVM ??

JP Gouigoux :

En fait, il y a eu tellement de versions mineures que c’est difficile de refaire les lignes de commandes et les références pour tout. Et pour ce qui est du téléchargement, c’est rendu encore plus complexe par le fait que le tout pèse quelques dizaines de Mo… Si vous me remettez un commentaire avec les versions exactes, je peux vous reposter les lignes de commandes, et les libs.

Alexandre :

Les dernières versions sont FOP 1.0 et IKVM 0.46.0.1.
Mon principal problème est qu’il n’y a plus IKVM.OpenJDK.ClassLibrary.dll , sinon changer les versions des librairies je peux faire ^^

JP Gouigoux :

Je viens de télécharger FOP 1.0 et IKVM 0.46.0.1 pour voir, et effectivement ClassLibrary.dll n’existe plus. Par contre, en enlevant purement et simplement la référence sur la première ligne de commande (celle pour xml-api), ça passe très bien. J’imagine que ikvmc doit prendre des références par défaut sur tout ce qui est classes standards de Java, et j’ai l’impression que celles-ci ont été découpées en plusieurs librairies. Ce qui est une bonne idée car ClassLibrary était énorme, si je me rappelle bien.

Je te laisse faire le test sur les autres, n’hésite pas à demander si tu tombes sur des cas où ça ne passe pas. J’essaierai d’aider.

Alexandre :

OK j’ai réussi à générer les nouveaux assemblages, mais pas mal de Warning…
Par contre aucun fichier IKVM.ClassPath.dll
J’ai alors importer toutes les dll de IKVM/bin et celles générées.
J’ai repris le code d’exemple, et la j’ai une exception levée à cette ligne :
Dim fopFactory As org.apache.fop.apps.FopFactory = org.apache.fop.apps.FopFactory.newInstance()
qui est : L’exception System.TypeInitializationException n’a pas été gérée.Une exception a été levée par l’initialiseur de type pour ‘org.apache.xmlgraphics.image.loader.ImageManager’.

Une idée ?
Merci bien !

JP Gouigoux :

Est-ce que la librairie xmlgraphics recompilée par IKVM est bien présente au même endroit que la lib FOP ?

Alexandre :

Oui tout est dans le même dossier

JP Gouigoux :

Vous pouvez recopier le code que vous utilisez ?

Alexandre :

Dim Source As New System.Xml.XmlDocument
Source.Load(« D:\Documents\test.xml »)
Dim FichierXSLFO As String = « D:\Documents\test_xsl.xsl »
Dim FichierResultatPDF As String = « D:\Desktop » & « /test_IKVM.pdf »

‘ Récupération du fichier XSL
Dim Moteur As System.Xml.Xsl.XslCompiledTransform = New System.Xml.Xsl.XslCompiledTransform()
Moteur.Load(FichierXSLFO)

‘ Application de la transformation
Dim FichierResultatFO As String = System.IO.Path.GetTempFileName()
Dim Scribe As New System.Xml.XmlTextWriter(FichierResultatFO, System.Text.Encoding.UTF8)

‘Moteur.Transform(Source.CreateNavigator(), Scribe)
Scribe.Close()

‘ Création du PDF par interop IKVM avec ApacheFOP
Dim fo As New java.io.File(FichierResultatFO)
Dim pdf As New java.io.File(FichierResultatPDF)
Dim fopFactory As org.apache.fop.apps.FopFactory = org.apache.fop.apps.FopFactory.newInstance()
Dim foUserAgent As org.apache.fop.apps.FOUserAgent = fopFactory.newFOUserAgent()
Dim sortie As java.io.OutputStream = New java.io.BufferedOutputStream(New java.io.FileOutputStream(pdf))
Dim fop As org.apache.fop.apps.Fop = fopFactory.newFop(« application/pdf », foUserAgent, sortie)
Dim factory As javax.xml.transform.TransformerFactory = javax.xml.transform.TransformerFactory.newInstance()
Dim transformer As javax.xml.transform.Transformer = factory.newTransformer()
Dim src As javax.xml.transform.Source = New javax.xml.transform.stream.StreamSource(fo)
Dim res As javax.xml.transform.Result = New javax.xml.transform.sax.SAXResult(fop.getDefaultHandler())
transformer.transform(src, res)
sortie.close()

JP Gouigoux :

Oups, j’ai répondu à mon message précédent au lieu de celui-ci.

Je viens de me rendre compte qu’il y avait aussi des fichiers supplémentaires dans \lib qu’il n’y avait pas avant: xalan, xml-apis-ext. Je pense qu’il va falloir compiler tout ça aussi pour ne plus avoir de classe manquante. Je te laisse ajouter les références une par une. Je pense que c’est la bonne voie de reprendre toutes les erreurs IKVMC0100 depuis les premières références, et de rajouter tout ce qu’il faut pour qu’il n’y ait plus de warning. Malheureusement, ça prend pas mal de temps, et je dois arrêter là.

Préviens moi de ton avancement. J’essaierai de jeter un oeil à nouveau à un autre moment de libre…

JP Gouigoux :

Je vais essayer de faire le test. Je ne vous promets pas de date, parce que j’ai deux projets en cours en ce moment qui me prennent pas mal de temps.

JP Gouigoux :

Désolé pour le temps de réaction…

Je viens de faire le test ce soir, et j’ai aussi ce genre d’exception, mais sur des autres classes. Du coup, j’ai regardé plus précisément les logs, et certains des warnings sont en fait des erreurs qui arriveront à la runtime. D’après ce que je comprends, si IKVM ne trouve pas les références qui lui sont nécessaires pour transformer le bytecode en IL, il émet du code IL qui va envoyer une ClassNotFoundException.

Du coup, je pense que l’approche doit consister à ajouter explicitement toutes les dépendances nécessaires, et en reprenant tout depuis la première librairie, sinon on reproduira le problème plus loin.

J’ai réussi à améliorer les résultats sur fop.jar : en stockant les logs de ikvmc, je suis passé d’un demi-mega à seulement 16 Ko d’avertissements. Mais je suis maintenant coincé sur les dépendances que j’ai créées avant et qui doivent elles aussi avoir des classes qui étaient absentes.

Je ne vais pas avoir le temps ce soir de continuer, mais je pense que le plus simple est peut être de passer ikvmc en warnaserror quand c’est possible. Quand ce n’est pas le cas, il faut éplucher les logs pour être sûr qu’on n’a pas oublié de références. Et du coup, tout reprendre depuis le début en vérifiant à chaque fois qu’on a tout…

About JP Gouigoux

Jean-Philippe Gouigoux est Architecte Logiciel, MVP Connected Systems Developer. Il intervient régulièrement à l'Université de Bretagne Sud ainsi qu'à l'Agile Tour. Plus de détails sur la page "Curriculum Vitae" de ce blog.
This entry was posted in .NET and tagged , . Bookmark the permalink.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha Captcha Reload