Html2Pdf partie 2 : avec ReportLab
Par macadames le 30/03/2005 12:19
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.
cfr. CMFReportTool
Des avis?
Gauthier.










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.
Réponses à ce commentaire