Accueil » Html2Pdf partie 2 : avec ReportLab

Html2Pdf partie 2 : avec ReportLab

Document Actions

Par macadames le 30/03/2005 12:19

Là c'est carrément un nouveau sujet qui démarre et tous les commentaires sont bienvenus. Utilisez le forum.

Catégories : Zope

Avant tout, il est préférable que PIL soit installé.
Pour Windows : http://www.zope.org/Members/SmileyChris/howto/pil_for_windows

installer Reportlab pour zope 2.7.x

Télécharger à cette adresse : http://reportlab.org/

décompacter

déplacer le dossier reportlab dans votre zope_folder/bin/Lib/site-packages ou dans votre python_folder/site-packages si Python est installé à part ...

créer dans ce dossier un fichier reportlabs.pth qui contient juste le texte reportlab (path relatif)

Installer Tiny Rml2PDF et RML pages templates

Télécharger trml2pdf à cette adresse : http://openreport.org/index.py/static/page/resources

Décompacter et déplacer uniquement le sous-dossier trml2pdf (dans un autre du même nom petit piège) dans votre instance_zope/Products

Télécharger RMLPage Template à cette adresse : http://www.zope.org/Members/mkerrin/RMLPageTemplate

Décompacter et placer le dossier dans instance_zope/Products

Redémarrer Zope

Vérifier que tout va bien dans le control panel > Products

Attention, surement à cause du piège plus haut, il y a parfois une erreur dans le produit RMLPageTemplate, et dans ce cas vous voyez que ce produit ne s'est pas installé correctement, ouvrez le fichier votre_instance_zope/Products/RMLPageTemplate/RMLPageTemplate.py, et changez la ligne :


from trml2pdf.trml2pdf import parseString

par :


from Products.trml2pdf import parseString

Voilà maintenant on est prêt à travailler, dans la ZMI on peut créer ses propres RMLPagesTemplates qui permettent de faire du RML dynamique et du pdf à la volée dans plone par simple appel de l'url du pageTemplate à la manière d'un template d'affichage classique de Plone.

Notre premier RMLPageTemplate

On va prendre l'exemple 1, le classique "Hello World", fourni avec trml2pdf en lui ajoutant la syntaxe nécessaire pour faire du tal/metal (attributs xmlns à ne pas oublier) et en remplaçant le "Hello World" par le titre du document en cours, et tout ça dans un document PDF fabriqué à la volée. Créez un RML Page Template dans votre dossier portal_skins/Custom avec le code suivant :


<!DOCTYPE document SYSTEM "rml_1_0.dtd">
<document filename="example_1.pdf"
          xmlns:tal="http://xml.zope.org/namespaces/tal"
          xmlns:metal="http://xml.zope.org/namespaces/metal">

<stylesheet>
</stylesheet>
<pageDrawing>
         
<drawCentredString x="2.1in" y="0.8in">
                   
<span tal:content="here/title_or_id">Hello World.</span>
         
</drawCentredString>
</pageDrawing>
</document>

Sauvegardez avec le nom testRML1

Tapez l'adresse de n'importe quel document de votre site Plone en ajoutant /testRML1 derrière, par exemple : http://monsitePlone/mondocument/testRML1

Et voilà le titre de votre document qui s'affiche au milieu d'une page PDF

Donc en suivant le guide du langage RML à cette adresse, http://www.reportlab.com/docs/RML_UserGuide_1_0.pdf, on va pouvoir générer du pdf dynamique à partir de formulaires ou d'extraction de bases de données aussi facilement qu'avec un Page Template classique, et avec toutes les fonctions avancées de ReportLab (des codes barres, des filigrannes, tout ce qu'on veut ça c'est vraiment génial).

Traduire du html

Là c'est déjà plus complexe, vu que le rml doit être parfaitement structuré (xml) et qu'en plus il refuse l'imbrication de deux tags identiques, donc même avec un parser xsl (voir exemple ici : http://openreport.org/files/xsl:rml/html2rml/) si le document n'est pas structuré pour une traduction rml ça risque de planter. La solution serait de travailler avec un éditeur wysiwyg Xml plutôt qu'avec un éditeur html.

On va quand-même essayer une méthode qui traduit les documents xhtml bien structurés (pas de tags identiques imbriqués, et éviter le texte hors d'un tag p pre table ou autre, ...). Ce travail demande à être amélioré alors ne vous gênez surtout pas, c'est du vite fait et en plus je ne suis pas un Pythonistas, je prends l'ascenceur des zopeurs.

D'abord créer un Rml PageTemplate dans Custom appelé par exemple testRml


<!DOCTYPE document SYSTEM "rml_1_0.dtd">
<document filename="testrml_2.pdf"
          xmlns:tal="http://xml.zope.org/namespaces/tal"
          xmlns:metal="http://xml.zope.org/namespaces/metal">

<template pageSize="(21cm, 29.7cm)"
          leftMargin="1.5cm"
          rightMargin="1.5cm"
          topMargin="2cm"
          bottomMargin="2cm"
          title="Macadames - test RML 1"
          author="jean-mat@macadames.com"
          showBoundary="0"
          allowSplitting="20"
          >

    
<pageTemplate id="main">
          
<pageGraphics>
           
<!-- par exemple votre logo ici -->
           
<image x="1.5cm" y="26.5cm" width="18cm" height="1.7cm" file="http://macadames.com/logoAfpy.jpg"/>
          
</pageGraphics>
          
<frame id="columnOne" x1="1.5cm" y1="4cm" width="18cm"
              
height="21.7cm"/>
    
</pageTemplate>


</template>
<stylesheet>
    
<paraStyle name="titleBox"
     fontName="Helvetica-Bold"
     fontSize="16"
     />

     
<paraStyle name="descriptionBox"
     fontName="Times-Bold"
     fontSize="11"
     spaceBefore="0.4cm"
     />

     
<paraStyle name="descriptionBoxBR"
     fontName="Times-Bold"
     fontSize="11"
     spaceBefore="0cm"
     />

     
<paraStyle name="espaceIntro"
     fontName="Times-Bold"
     fontSize="11"
     spaceBefore="1cm"
     />

     
<paraStyle name="normal"
     fontName="Times-Roman"
     fontSize="10"
     spaceBefore="0.4cm"
     />

     
<paraStyle name="normalBR"
     fontName="Times-Roman"
     fontSize="10"
     spaceBefore="0cm"
     />

     
<paraStyle name="ploneCode"
     fontName="Courier"
     fontSize="10"

     textColor="blue"
     spaceBefore="0.4cm"
     />

     
<paraStyle name="title1"
     fontName="Helvetica-Bold"
     fontSize="16"
     textcolor="crimson"
     spaceBefore="0.8cm"
     />

     
<paraStyle name="title2"
     fontName="Helvetica-Bold"
     fontSize="15"
     textColor="blue"
     spaceBefore="0.6cm"
     />

     
<paraStyle name="title3"
     fontName="Helvetica-Bold"
     fontSize="14"
     textColor="blue"
     spaceBefore="0.5cm"
     />

     
<paraStyle name="title4"
     fontName="Helvetica"
     fontSize="13"
     textColor="olivedrab"
     spaceBefore="0.4cm"
     />

     
<paraStyle name="title5"
     fontName="Helvetica"
     fontSize="12"
     spaceBefore="0.3cm"
     />

     
<paraStyle name="title6"
     fontName="Helvetica"
     fontSize="12"
     spaceBefore="0.2cm"
     />

     
<paraStyle name="title7"
     fontName="Helvetica"
     fontSize="11"
     spaceBefore="0.2cm"
     />

    
<blockTableStyle id="ploneTable">
      
<blockFont name="Helvetica" size="10"/>
      
<blockAlignment value="LEFT"/>
      
<blockValign value="TOP"/>
      
<blockLeftPadding lenght="0.5cm"/>
    
</blockTableStyle>
</stylesheet>

<story>

     
<condPageBreak height="144"/>
     
<para style="titleBox">
     
<span tal:replace="python:here.title_or_id()" />
     
</para>
     
<para style="descriptionBox">
         
<span tal:replace="structure python:here.xhtml2rml(here.Description(),'descriptionBox')" />
     
</para>
    
<para style="espaceIntro">
    
</para>
    
<para style="normal"
          tal:define="len_text python:len(here.text)">

        
<span tal:replace="structure python:here.xhtml2rml(here.text,'normal')"
              
tal:condition="len_text" />
    
</para> 


</story>

</document>

Le template fait appel à une External Method appelée xhtml2rml, donc on crée une external Method à la racine du site Plone ou de tout autre portail (ça devrait marcher avec n'importe quel CMS basé sur Zope si on modifie une ligne ou 2).



# External method xhtml2rml
# transforme du xhtml en rml pour faire du trml2pdf 
# si document d'origine structuré dans ce but ça passe
# sinon ça casse
# pas de tags identiques imbriqués "<p>etc<p>etc</p></p>" sinon plantage 
# éviter les tags table ou img à l'intérieur des p
# éviter les div ou les span
# les balises style ne sont pas prises en charge
# utiliser <font face="Helvetica" plutôt que style="font-family: helvetica"
# mais attention avec les familles de police reportlab connait les familles pdf et pas les autres
# éviter du texte en dehors des tags (<p></p> <pre></pre><code></code><table></table>)
# les ul ol ne sont pas traités par trml2pdf pour l'instant


# Python imports
from StringIO import StringIO
# avec module re on arrive pas à traiter les regexp trop complexes
import pre as re
import urlparse
import AccessControl

# CMF imports
from Products.CMFCore.utils import getToolByName


def xhtml2rml(self,chaineHtml,chaineStyle):

 
obj_url = self.absolute_url()

 
# Method to replace link by new one
 
def replace_url(match):
       
"""Compute local url
       """

       
url = str(match.group('url'))
       
if match.group('protocol') is not None:
           
url = '%s%s' % (match.group('protocol'), url)
       
else:
         
try:
            
url=urlparse.urljoin (obj_url, url)
         
except:
            
pass 
       
return 'src="%s"' % url 
       
return match.group(0) 



 
# Method to replace html entities by Iso char
 
def replace_entites(match):

       
entite = str(match.group(1))

       
# dictionnaire de correspondance entite html caractère accentué
       
# il y a surement une classe existante mais j'ai pas cherché
       
# ne sont pas traités ici les & # xxx ; 
       
dEnt = {"agrave":"à", "Agrave":"À", "aacute":"á", "Aacute":"Á", "acirc":"â", "Acirc":"Â",\
 
"atilde":"ã", "Atilde":"Ã", "auml":"ä", "Auml":"Ä", "aring":"å", "Aring":"Å", "aelig":"æ", "AElig":"Æ",\
 
"egrave":"è", "Egrave":"È", "eacute":"é", "Eacute":"É", "ecirc":"ê", "Ecirc":"Ê", "euml":"ë", "Euml":"Ë",\
 
"igrave":"ì", "Igrave":"Ì", "iacute":"í", "Iacute":"Í", "icirc":"î", "Icirc":"Î", "iuml":"ï", "Iuml":"Ï",\
 
"ograve":"ò", "Ograve":"Ò", "oacute":"ó", "Oacute":"Ó", "ocirc":"ô", "Ocirc":"Ô", "ouml":"ö", "Ouml":"Ö",\
 
"oslash":"ø", "Oslash":"Ø", "oelig":"oe", "OElig":"OE", "ugrave":"ù", "Ugrave":"Ù", "uacute":"ú",\
 
"Uacute":"Ú", "ucirc":"û", "Ucirc":"Û", "uuml":"ü", "Uuml":"Ü", "ntilde":"ñ", "Ntilde":"Ñ", "ccedil":"ç",\
 
"Ccedil":"Ç", "yacute":"ý", "Yacute":"Ý", "szlig":"ß", "laquo":"«", "raqo":"»", "para":"§", "copy":"©",\
 
"nbsp":" ", "quot": "\"", "amp":"&", "lt":"<", "gt":">" }
       
if dEnt.has_key(entite):
         
return dEnt[entite]
       
else:
         
return '&' + entite + ';'
       
return match.group(0) 

 
# Method to replace & by & amp ;
 
def replace_ecom(match):

       
Letter = ''
       
if match.group(1) is not None: 
        
Letter= str(match.group(1))
        
return '&' + Letter
       
else:
        
return '&' +'amp;'
       
return match.group(0) 


 
# Method to replace tag by new one
 
def replace_tag(match):

       
debutTag = str(match.group('debutTag'))
       
finTag = str(match.group('finTag'))
       
attrTag=''
       
contentTag=''

       
if match.group('attrTag') is not None:
         
attrTag = str(match.group('attrTag'))
       
if match.group('contentTag') is not None:
         
contentTag = str(match.group('contentTag'))

       
# pour traduire les entites html sauf dans champ pre et code 
       
# (affichés en cdata) sinon ça plante le parser xml
       
reEntites = re.compile('&([a-zA-Z]+);')
  

       
if debutTag.lower()=='p':
          
contentTag = reEntites.sub(replace_entites, contentTag)
          
debutTag='para' + ' style="' + chaineStyle + '"'
          
finTag='</para>'
          
return '\r\n' + finTag + '\r\n' + '<'+ debutTag + attrTag +'>\r\n'+ contentTag + '\r\n'

       
if debutTag.lower() in ('pre','code'):
          
# transformer en xpre et mettre en CDATA sinon marche pas
          
# ne pas traduire les entités
          
debutTag='xpre'
          
finTag='</xpre>'
          
attrTag = ' style="ploneCode"'
          
entete ='\r\n</para>\r\n'
          
fin='\r\n<para' + ' style="' + chaineStyle + 'BR">\r\n'
          
return entete + '<'+ debutTag + attrTag +'>\r\n <![CDATA[ \r\n'+ contentTag + '\r\n ]]'\
 
+ '> \r\n' + finTag + '\r\n' + fin    

       
elif match.group('hNumber') is not None:
          
contentTag = reEntites.sub(replace_entites, contentTag)
          
debutTag='para'
          
finTag='</para>'
          
attrTag = ' style="title' + str(match.group('hNumber')) + '"'
          
return '\r\n' + finTag + '\r\n' + '<'+ debutTag + attrTag +'>\r\n'+ contentTag + '\r\n'

       
elif debutTag.lower()=='table':
          
contentTag = reEntites.sub(replace_entites, contentTag)
          
debutTag='blockTable'
          
finTag='</blockTable>'
          
attrTag = ' style="ploneTable"'
          
entete ='\r\n</para>\r\n'
          
fin='\r\n<para' + ' style="' + chaineStyle + '">\r\n'
          
return entete + '<'+ debutTag + attrTag +'>\r\n'+ contentTag + '\r\n' + finTag + fin      
       
       
# la chaine renvoyée remplace toute l'expression
       
return match.group(0) 

 
# Method to replace tag Img by new one
 
def replace_tag_img(match):
        
       
newWidth=''
       
newHeight=''
       
if match.group('contentTagImg') is not None:
         
contentTagImg = str(match.group('contentTagImg'))
         
# extraire source
         
reSrc = re.compile ('src\s*=\s*([\'\"])([^\"\']*)\\1', re.IGNORECASE)
         
matchs = reSrc.search( contentTagImg )
         
srcImg = '%s' % matchs.group(2)
         
# extraire largeur et la diviser par 1.43 rapport point pixel en 96dpi
         
reWidth = re.compile ('width\s*=\s*([\'\"])([0-9]+)(px)?\\1', re.IGNORECASE)
         
matchs = reWidth.search( contentTagImg )
         
if matchs is not None:
           
newWidth= str(int(int(matchs.group(2))/1.43))
 
         
# extraire hauteur et la diviser par 1.43 rapport point pixel en 96dpi
         
reHeight = re.compile ('height\s*=\s*([\'\"])([0-9]+)(px)?\\1', re.IGNORECASE)
         
matchs = reHeight.search( contentTagImg )
         
if matchs is not None:
           
newHeight = str(int(int(matchs.group(2))/1.43))

       
if newWidth and newHeight:
           
tailleImage = ' width="' + newWidth + '" height="' + newHeight + '"'
       
else:
           
# si on met pas de taille à l'image la mise en page risque de partir en live mais ça marche
           
tailleImage =''
       

       
entete = '</para>\r\n'
       
fin='\r\n<para' + ' style="' + chaineStyle + '">\r\n'
       
tagImg='\r\n<illustration' + tailleImage + '>\r\n<image file="' + srcImg + '" x="0" y="0"'\
 
+ tailleImage + ' />\r\n</illustration>\r\n'
       

       
return entete + tagImg + fin       
       
# la chaine renvoyée remplace toute l'expression
       
return match.group(0) 


 
# Method to replace tag Br by new one
 
def replace_tag_br(match):

       
entete = '</para>\r\n'
       
fin='<para' + ' style="' + chaineStyle + 'BR">\r\n'      

       
return entete + fin       




 
chaineRml = chaineHtml



 
abs_url = re.compile('src\s*=\s*([\'\"])(?P<protocol>(ht|f)tps?)?(?P<url>[^\"\']*)\\1', re.IGNORECASE)
 
chaineRml = abs_url.sub(replace_url, chaineRml) 


 
reTag  = re.compile('<(?P<debutTag>(p|pre|code|table|h(?P<hNumber>[1-7])))(?P<attrTag>\s+[^>]*\s*)?>\
(?P<contentTag>([^<]|<(?!/\\1))*)(?P<finTag></(\\1)\s*>)'
, re.IGNORECASE |re.DOTALL)
 
chaineRml = reTag.sub(replace_tag, chaineRml) 


 
reTagImg = re.compile('<img(?P<contentTagImg>[^>]*)/?\s*>', re.IGNORECASE |re.DOTALL)
 
chaineRml = reTagImg.sub(replace_tag_img, chaineRml) 

 
reTagBr = re.compile('<br[^>]*/?\s*>', re.IGNORECASE |re.DOTALL)
 
chaineRml = reTagBr.sub(replace_tag_br, chaineRml) 


 
# on enlève les a href qui ne servent à rien avec ReportLab
 
# on pourrait faire un findall et faire par exemple un glossaire des liens à la fin
 
# on enlève les div - les traiter mériterait une autre usine à gaz faite différemment
 
# on enlève les span - on aurait pu les traiter, analyser la balise style et euh ...
 
reTagInutiles = re.compile('<a\s[^>]*>|</a\s*>|</?span[^>]*>|</?div[^>]*>', re.IGNORECASE |re.DOTALL)
 
chaineRml = reTagInutiles.sub('', chaineRml) 


 
# on traduit en UTF-8
 
errors="strict"
 
default="utf-8"
 
try:
    
prop   = getToolByName(self, "portal_properties")
    
encodage = prop.site_properties.getProperty("default_charset", default)
 
except:
    
encodage = default
 
 
if encodage.lower() in ("utf-8", "utf8"):
    
chaineRml=unicode(chaineRml, "utf-8", errors)
 
else:
    
chaineRml=unicode(chaineRml, encodage, errors).encode("utf-8", errors)


 
# pour tous les & qui restent faut remplacer par & amp ; 
 
# pour compatibilité xml
 
reEcom = re.compile('&([^a-zA-Z])|&\s')
 
chaineRml = reEcom.sub(replace_ecom, chaineRml) 
       

 
return chaineRml




Et maintenant pour avoir le pdf de votre fichier HTML il suffit de taper l'url avec "/testRml1" derrière

Le RmlTemplate proposé ne marche qu'avec un contenu disposant d'un attribut "text" comme un document Plone standard par exemple, pour que ça marche avec n'importe quoi il suffit de modifier ce template (voir exemple html2pdf avec htmldoc c'est le même genre de chose à faire) Pour voir ce que donne ce document traduit en pdf avec cette méthode, cliquez sur cette adresse : http://macadames.com/temporaire/html2pdf2/rmlAfpy Pour voir le fichier xml résultant du RML Page Template il suffit d'ajouter "/testRml1/view_rml" derrière l'url du document. Donc : http://macadames.com/temporaire/html2pdf2/rmlAfpy/view_rml

Bon ensuite il faut faire les portal_actions et les portal_actions_icons (voir exemple avec htmldoc)

Il faudrait également mettre un pied de page avec le numéro de page et la référence du document, à partir du guide trml2pdf

Faire un Ebook ?

On voit que pour traduire n'importe quelle page html, lorsqu'on est pas super calé en python et en xml, c'est plus simple avec htmldoc, même si le résultat n'est pas parfait.

De plus, ReportLab ne dit rien sur l'insertion de liens dans un document, donc on va utiliser htmldoc, et ce sera la troisième et dernière partie du feuilleton commencé par Dams en 2004 sur zopeur.org.

Chapeau...

Posté par gillou le 03/04/2005 16:10
...pour cet excellent tutoriel...

Il ne nous reste plus qu'à apprendre le RML et les onglets "PDF" vont murir sur les sites "Zope powered".

Une petite précision pour la fin de l'exposé : l'absence de possibilité de liens n'est pas une limitation du toolkit Reportlab (qui fournit une API à cet effet) mais une limitation du RML, langage de rendu pour les media papier pour lequel l'intérêt des hyperliens est peu évident.

cfr. CMFReportTool

Posté par mmmmhhh le 31/03/2006 13:16
CMFReportTool est un outil basé sur Reportlab également et générant du PDF depuis un fichier .pml (même chose en gros que le RML). Il a été dernièrement remis dans le collective de Plone par Godefroid Chapelle, nous l'utilisons ici sur 3 applications et il semble tenir la route. Si TinyRML semble correctement supporté, qu'en est-il de RMLPageTemplate? Je vois que la dernière release date d'un an et demi, notre soucis majeur étant de ne pas avoir à redévelopper une série de chose sur nos applications, nous nous demandons quelle option choisir : RMLPageTemplate (http://www.zope.org/Members/mkerrin/RMLPageTemplate) ou CMFReportTool (http://svn.plone.org/view/collective/CMFReportTool/trunk/)???

Des avis?

Gauthier.