Génération d'impressions de pages Web


Rédigé le : 27/04/2006
Auteur : Teddy Doré
Pré-requis techniques : HTML / XML / XSL
Application : .NET / Java
Sujets abordés : PDF / FOP / nFOP / XSL

I.Pourquoi ça n’imprime pas (bien) ?
II.Plan de bataille
III.Premiers contacts
IV.Les PDF et la mode
V.Harder, better, faster, stronger
VI.Conclusions
VII.Références

I. Pourquoi ça n’imprime pas (bien) ?

Les impressions dans les applications web (intranet ou internet) sont souvent problématiques. Une page HTML basique offre peu de possibilités pour nos chères imprimantes.
Certes il est toujours possible d’agrémenter notre bien aimée page d’un lien vers un media alternatif, de ruser dans les déclarations de styles CSS voire même de combiner les deux.
Cependant, ces solutions (non exhaustives) apportent avec elles pas mal d’inconvénients : une forte dépendance au navigateur qui oblige à prendre en compte des considérations pas toujours évidentes (les marges d’impression côté client et les entêtes d’impression via IE pour n’en nommer que deux), des développements supplémentaires pour ajuster chaque page à notre bonne vieille fichue feuille de papier, bon courage si vous avez une cinquantaine d’entités...

Pour éviter ce genre de drame, j’ai personnellement opté pour une génération PDF qui permettra l’impression générique des pages Web affichées.
En plus au passage, l’archivage en sera simplifié.

Armé de ma paresse légendaire, j’ai commencé à rechercher une solution libre (les cordons de la bourse budgétaire du projet étant scellés) qui génèrerait un joli PDF pimpant à partir de ma copine la page Web (en l’occurrence dans un contexte .NET, mais peu importe).
Déception : je n’ai trouvé aucun code libre simple et efficace sur le sujet. Les solutions payantes étant réellement hors de prix.
Rendons néanmoins à Google ce qui lui revient, quelques codes sources proposent une réalisation de la chose. Mais aucun qui ne nécessiterait pas une modification profonde des pages Web.
Seule piste viable à mon goût, l’utilisation de FOP(1) (du moins de son frère jumeau .NET : nFOP(2)). Il en existe d’autres, que je n’ai pas testé. Demandez à Google, il est très serviable.

II. Plan de bataille

FOP / nFOP étant, pour vulgariser la chose, des « boîtes noires » générant des flux/fichiers PDF à partir d’ordres XSL.

Pour générer un document PDF via FOP ou nFOP, il nous faut produire un fichier XML-FO de « pilotage de génération ».
Ca tombe bien, notre source HTML n’est rien de plus qu’un flux XML déguisé.
Reste à traduire ce cachottier de flux en ordres XML-FO.

Heureusement pour nous, la DTD des fichiers XML-FO est très similaire, pour ne pas dire calquée, à la grammaire HTML.
Nous n’avons qu’à parser notre HTML XMLisé avec une feuille XSL de transcodage qui nous produira notre désiré XML-FO.

HTML >> Flux XML >> XSL de transcodage >> XML-FO >> FOP/nFOP >> PDF

III. Premiers contacts

Prenons une page HTML simple :

<HTML>
<HEAD>
<TITLE>Mon site</TITLE>
</HEAD>
<BODY>
<H1>Mon titre</H1>
<P>plop plop <B>plip</B> plop</P>
<A HREF=”index.html”>Retour</A>
<HR/>
<P CLASS=«maClasse»>blah blah</P>
</BODY>
</HTML>

Le principe est de détecter toute balise à reproduire dans le fichier XML-FO et à traiter son contenu de manière intelligente.
Pour les balises de type container c’est relativement simple :

  <xsl:template match="H1">
    <fo:block font-size= «16.0pt»>
      <xsl:apply-templates></xsl:apply-templates>
    </fo:block>
  </xsl:template>

Ici la balise HTML H1 sera remplacé par un fo:block (bloc de texte dans le fichier PDF final) avec une police de caractère de 16pt.
Ci-dessous les autres templates pour parser intégralement notre exemple :

<xsl:template match="BODY" >
	<fo:block><xsl:apply-templates /></fo:block>
</xsl:template>

<xsl:template match="P">
	<fo:block><xsl:apply-templates/></fo:block>
</xsl:template>

<xsl:template match="P[@CLASS='maClasse']">
	<fo:block color="red"><xsl:apply-templates/></fo:block>
</xsl:template>

<xsl:template match="HR" >
	<fo:block><fo:leader /></fo:block>
</xsl:template>

<xsl:template match="B">
	<fo:inline font-weight="bold"><xsl:apply-templates/></fo:inline>
</xsl:template>

<xsl:template match="A">
	<xsl:choose>
		<xsl:when test="@HREF">
			<fo:basic-link external-destination="{@HREF}" >
				<xsl:value-of select="."/>
			</fo:basic-link>
		</xsl:when>	
		<xsl:otherwise>
			<fo:inline>
				<xsl:apply-templates/>
			</fo:inline>
		</xsl:otherwise>
	</xsl:choose>
</xsl:template>

Le fichier XML-FO résultant ressemblerait à ceci :

<fo:block>
<fo:block font-size= «16.0pt»>Mon titre</fo:block>
<fo:block>plop plop <fo:inline font-weight=«bold»>plip</fo:inline> plop<fo:block>
<fo:basic-link external-destination="index.html" >Retour</fo:basic-link>
<fo:leader />
<fo:block color="red">blah blah</fo:block>
</fo:block>

Je ne vous cacherai pas que ce n’est pas suffisant pour que FOP/nFOP comprenne ce que vous tentez de lui demander. Il faut aussi paramétrer les marges, la taille du document, bref la mise en page. Tout ceci pouvant être intégré dans notre XSL de transcodage.
Jetez un oeil dans les références de ce document (5) pour vous aider dans cette pénible tâche.
Voila c’est génial on sait comment générer un fichier XML-FO.
So what ?

Et bien il vous suffit maintenant de développer un brin de code instanciant votre moteur FOP ou nFOP et lui donner à manger notre joli bébé FO qui deviendra un grand et fort fichier PDF.

IV. Les PDF et la mode

Etant un fanatique de la séparation HTML / CSS, je me suis posé la question de l’externalisation des propriétés stylistiques de notre fichier XSL de transcodage.
En effet, une fois que vous aurez réalisé le transcodage pour toutes les balises HTML qui vous intéressent, il serait dommage d’avoir à dupliquer ce fichier simplement parce que certaines pages doivent avoir leurs balises H1 en bleu et d’autre en rouge.

Il était une fois dans le monde merveilleux de XSL, ce que les stylistes appelaient les groupes d’attributs (xsl:attribute-set). Ils peuvent être utilisés comme des classes CSS et pire encore, on pourrait envisager de les générer à partir de nos classes CSS directement, chose que je n’ai pas encore tenté je l’avoue. Frappez moi.

Reprenons l’exemple de la balise H1 :

<H1>Mon titre</H1>

Le code de transcodage XSL correspondant deviendrait :

<xsl:template match="H1" >
	<fo:block xsl:use-attribute-sets="baliseH1">
		<xsl:apply-templates />
	</fo:block>
</xsl:template>

Et la fameuse définition du groupe d’attribut serait :

<xsl:attribute-set name="baliseH1" >
	<xsl:attribute name="color">#FF0000</xsl:attribute>
	<xsl:attribute name="font-size">16.0pt</xsl:attribute>
</xsl:attribute-set>

Je vous laisse imaginer la suite : une série de groupes d’attributs stockés dans un fichier XSL externe et appelée selon la détection ou non de telle ou telle feuille de styles.

V. Harder, better, faster, stronger

Pour aller plus loin techniquement voici l’XSL de transcodage pour les tableaux HTML, du moins ma version imparfaite et subjective.
A noter que l’XML-FO, pour la génération de tableaux, nécessite l’énumération des colonnes du tableau avant la production du tableau : si vous avez un tableau HTML de trois colonnes, il vous faudra déclarer trois fois fo:table-column avant de commencer à décrire celui-ci. La portion de code qui suit fait ce traitement automatiquement en calculant le nombre de colonnes à partir du nombre de TD et de TH présents dans le premier TR du tableau.

<xsl:template name="insertTableColumn" >
	<xsl:param name="cpt"/>
	<xsl:param name="nb"/>
	<xsl:choose>
		<xsl:when test=".//TR[1]/TD[$cpt]/@WIDTH">
			<fo:table-column column-width="{.//TR[1]/TD[$cpt]/@WIDTH}" />
		</xsl:when>
		<xsl:when test=".//TR[1]/TH[$cpt]/@WIDTH">
			<fo:table-column column-width="{.//TR[1]/TH[$cpt]/@WIDTH}" />
		</xsl:when>
		<xsl:otherwise>
			<fo:table-column />
		</xsl:otherwise>
	</xsl:choose>
	<xsl:if test="$nb > $cpt"> 
		<xsl:call-template name="insertTableColumn"> 
			<xsl:with-param name="cpt" select="$cpt + 1"/>
			<xsl:with-param name="nb" select="$nb"/> 
		</xsl:call-template> 
	</xsl:if>
</xsl:template>


<xsl:template match="TABLE">
	<fo:table xsl:use-attribute-sets="table" >
		<xsl:call-template name="insertTableColumn">
			<xsl:with-param name="cpt">1</xsl:with-param>  
			<xsl:with-param name="nb" select="(count(TR[1]/TD)+sum(TR[1]/TD/@COLSPAN)-count(TR[1]/TD[@COLSPAN]))+(count(TR[1]/TH)+sum(TR[1]/TH/@COLSPAN)-count(TR[1]/TH[@COLSPAN]))" />
		</xsl:call-template>
		<fo:table-body><xsl:apply-templates/></fo:table-body>
	</fo:table>
</xsl:template>

<xsl:template match="TR">
	<fo:table-row xsl:use-attribute-sets="tr">
		<xsl:apply-templates/>
	</fo:table-row>
</xsl:template>

<xsl:template match="TD">
	<fo:table-cell number-columns-spanned="{@COLSPAN}" width="{@WIDTH}" xsl:use-attribute-sets="td">
		<fo:block><xsl:apply-templates/></fo:block>
	</fo:table-cell>
</xsl:template>

VI. Conclusions

Ce document n’a pas vocation d’être exhaustif ou parfait. Il reflète la mise en œuvre d’une solution technique adaptée à un projet réel.
En espérant que mes recherches puissent servir à d’autres, vous pouvez toujours me contacter par mail ou passer par Ecranbleu.org.

Teddy Doré.

VII. Références

(1) FOP : http://xmlgraphics.apache.org/fop/
(2) nFOP : http://sourceforge.net/projects/nfop/
(3) XSL-FO Comparatifs : http://zoonek2.free.fr/UNIX/44_XSL-FO/XSL-FO.html
(4) XSL-FO Reference : http://zvon.org/xxl/xslfoReference/Output/index.html
(5) XSL-FO Mise en page : http://www.xml-campus.ch/xmlcampus/articles/prod/fr/didactique024006_xslfo_fr.htm