[FIX] report: correct page numbering
[odoo/odoo.git] / openerp / report / render / rml2pdf / trml2pdf.py
index 0067bf3..00b119a 100644 (file)
@@ -37,6 +37,7 @@ from openerp.tools.safe_eval import safe_eval as eval
 from reportlab.lib.units import inch,cm,mm
 from openerp.tools.misc import file_open
 from reportlab.pdfbase import pdfmetrics
+from reportlab.lib.pagesizes import A4, letter
 
 try:
     from cStringIO import StringIO
@@ -44,8 +45,23 @@ try:
 except ImportError:
     from StringIO import StringIO
 
+_logger = logging.getLogger(__name__)
+
 encoding = 'utf-8'
 
+def select_fontname(fontname, default_fontname):
+    if fontname not in pdfmetrics.getRegisteredFontNames()\
+         or fontname not in pdfmetrics.standardFonts:
+        # let reportlab attempt to find it
+        try:
+            pdfmetrics.getFont(fontname)
+        except Exception:
+            _logger.warning('Could not locate font %s, substituting default: %s',
+                fontname, default_fontname)
+            fontname = default_fontname
+    return fontname
+
+
 def _open_image(filename, path=None):
     """Attempt to open a binary file and return the descriptor
     """
@@ -69,61 +85,45 @@ def _open_image(filename, path=None):
 class NumberedCanvas(canvas.Canvas):
     def __init__(self, *args, **kwargs):
         canvas.Canvas.__init__(self, *args, **kwargs)
-        self._codes = []
-        self._flag=False
-        self._pageCount=0
-        self._currentPage =0
-        self._pageCounter=0
-        self.pages={}
+        self._saved_page_states = []
 
     def showPage(self):
-        self._currentPage +=1
-        if not self._flag:
-           self._pageCount += 1
-        else:
-            self.pages.update({self._currentPage:self._pageCount})
-        self._codes.append({'code': self._code, 'stack': self._codeStack})
+        self._saved_page_states.append(dict(self.__dict__))
         self._startPage()
-        self._flag=False
-
-    def pageCount(self):
-        if self.pages.get(self._pageCounter,False):
-            self._pageNumber=0
-        self._pageCounter +=1
-        key=self._pageCounter
-        if not self.pages.get(key,False):
-            while not self.pages.get(key,False):
-                key = key + 1
+
+    def save(self):
+        """add page info to each page (page x of y)"""
+        for state in self._saved_page_states:
+            self.__dict__.update(state)
+            self.draw_page_number()
+            canvas.Canvas.showPage(self)
+        canvas.Canvas.save(self)
+
+    def draw_page_number(self):
+        page_count = len(self._saved_page_states)
         self.setFont("Helvetica", 8)
         self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40),
-            "Page %(this)i of %(total)i" % {
-               'this': self._pageNumber+1,
-               'total': self.pages.get(key,False),
+            " %(this)i / %(total)i" % {
+               'this': self._pageNumber,
+               'total': page_count,
             }
         )
 
-    def save(self):
-        """add page info to each page (page x of y)"""
-        # reset page counter
-        self._pageNumber = 0
-        for code in self._codes:
-            self._code = code['code']
-            self._codeStack = code['stack']
-            self.pageCount()
-            canvas.Canvas.showPage(self)
-#        self.restoreState()
-        self._doc.SaveToFile(self._filename, self)
 
 class PageCount(platypus.Flowable):
+    def __init__(self, story_count=0):
+        platypus.Flowable.__init__(self)
+        self.story_count = story_count
+
     def draw(self):
-        self.canv.beginForm("pageCount")
+        self.canv.beginForm("pageCount%d" % self.story_count)
         self.canv.setFont("Helvetica", utils.unit_get(str(8)))
         self.canv.drawString(0, 0, str(self.canv.getPageNumber()))
         self.canv.endForm()
 
 class PageReset(platypus.Flowable):
     def draw(self):
-        self.canv._pageNumber = 0
+        self.canv._doPageReset = True
 
 class _rml_styles(object,):
     def __init__(self, nodes, localcontext):
@@ -140,9 +140,11 @@ class _rml_styles(object,):
             for style in node.findall('paraStyle'):
                 sname = style.get('name')
                 self.styles[sname] = self._para_style_update(style)
-
-                self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
-
+                if self.default_style.has_key(sname):
+                    for key, value in self.styles[sname].items():                    
+                        setattr(self.default_style[sname], key, value)
+                else:
+                    self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
             for variable in node.findall('initialize'):
                 for name in variable.findall('name'):
                     self.names[ name.get('id')] = name.get('value')
@@ -152,7 +154,12 @@ class _rml_styles(object,):
         for attr in ['textColor', 'backColor', 'bulletColor', 'borderColor']:
             if node.get(attr):
                 data[attr] = color.get(node.get(attr))
-        for attr in ['fontName', 'bulletFontName', 'bulletText']:
+        for attr in ['bulletFontName', 'fontName']:
+            if node.get(attr):
+                fontname= select_fontname(node.get(attr), None)
+                if fontname is not None:
+                    data['fontName'] = fontname
+        for attr in ['bulletText']:
             if node.get(attr):
                 data[attr] = node.get(attr)
         for attr in ['fontSize', 'leftIndent', 'rightIndent', 'spaceBefore', 'spaceAfter',
@@ -215,7 +222,7 @@ class _rml_styles(object,):
             if sname in self.styles_obj:
                 style = self.styles_obj[sname]
             else:
-                sys.stderr.write('Warning: style not found, %s - setting default!\n' % (node.get('style'),) )
+                _logger.debug('Warning: style not found, %s - setting default!', node.get('style'))
         if not style:
             style = self.default_style['Normal']
         para_update = self._para_style_update(node)
@@ -226,7 +233,11 @@ class _rml_styles(object,):
         return style
 
 class _rml_doc(object):
-    def __init__(self, node, localcontext, images={}, path='.', title=None):
+    def __init__(self, node, localcontext=None, images=None, path='.', title=None):
+        if images is None:
+            images = {}
+        if localcontext is None:
+            localcontext = {}
         self.localcontext = localcontext
         self.etree = node
         self.filename = self.etree.get('filename')
@@ -240,16 +251,30 @@ class _rml_doc(object):
         from reportlab.pdfbase.ttfonts import TTFont
 
         for node in els:
+
             for font in node.findall('registerFont'):
                 name = font.get('fontName').encode('ascii')
                 fname = font.get('fontFile').encode('ascii')
                 if name not in pdfmetrics._fonts:
                     pdfmetrics.registerFont(TTFont(name, fname))
+                #by default, we map the fontName to each style (bold, italic, bold and italic), so that 
+                #if there isn't any font defined for one of these style (via a font family), the system
+                #will fallback on the normal font.
                 addMapping(name, 0, 0, name)    #normal
                 addMapping(name, 0, 1, name)    #italic
                 addMapping(name, 1, 0, name)    #bold
                 addMapping(name, 1, 1, name)    #italic and bold
 
+            #if registerFontFamily is defined, we register the mapping of the fontName to use for each style.
+            for font_family in node.findall('registerFontFamily'):
+                family_name = font_family.get('normal').encode('ascii')
+                if font_family.get('italic'):
+                    addMapping(family_name, 0, 1, font_family.get('italic').encode('ascii'))
+                if font_family.get('bold'):
+                    addMapping(family_name, 1, 0, font_family.get('bold').encode('ascii'))
+                if font_family.get('boldItalic'):
+                    addMapping(family_name, 1, 1, font_family.get('boldItalic').encode('ascii'))
+
     def setTTFontMapping(self,face, fontname, filename, mode='all'):
         from reportlab.lib.fonts import addMapping
         from reportlab.pdfbase import pdfmetrics
@@ -257,18 +282,18 @@ class _rml_doc(object):
 
         if fontname not in pdfmetrics._fonts:
             pdfmetrics.registerFont(TTFont(fontname, filename))
-        if (mode == 'all'):
+        if mode == 'all':
             addMapping(face, 0, 0, fontname)    #normal
             addMapping(face, 0, 1, fontname)    #italic
             addMapping(face, 1, 0, fontname)    #bold
             addMapping(face, 1, 1, fontname)    #italic and bold
         elif (mode== 'normal') or (mode == 'regular'):
             addMapping(face, 0, 0, fontname)    #normal
-        elif (mode == 'italic'):
+        elif mode == 'italic':
             addMapping(face, 0, 1, fontname)    #italic
-        elif (mode == 'bold'):
+        elif mode == 'bold':
             addMapping(face, 1, 0, fontname)    #bold
-        elif (mode == 'bolditalic'):
+        elif mode == 'bolditalic':
             addMapping(face, 1, 1, fontname)    #italic and bold
 
     def _textual_image(self, node):
@@ -311,7 +336,9 @@ class _rml_doc(object):
             self.canvas.save()
 
 class _rml_canvas(object):
-    def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images={}, path='.', title=None):
+    def __init__(self, canvas, localcontext, doc_tmpl=None, doc=None, images=None, path='.', title=None):
+        if images is None:
+            images = {}
         self.localcontext = localcontext
         self.canvas = canvas
         self.styles = doc.styles
@@ -320,7 +347,6 @@ class _rml_canvas(object):
         self.images = images
         self.path = path
         self.title = title
-        self._logger = logging.getLogger('report.rml.canvas')
         if self.title:
             self.canvas.setTitle(self.title)
 
@@ -335,7 +361,7 @@ class _rml_canvas(object):
             if n.tag == 'pageCount':
                 if x or y:
                     self.canvas.translate(x,y)
-                self.canvas.doForm('pageCount')
+                self.canvas.doForm('pageCount%s' % (self.canvas._storyCount,))
                 if x or y:
                     self.canvas.translate(-x,-y)
             if n.tag == 'pageNumber':
@@ -405,7 +431,7 @@ class _rml_canvas(object):
         self.canvas.circle(x_cen=utils.unit_get(node.get('x')), y_cen=utils.unit_get(node.get('y')), r=utils.unit_get(node.get('radius')), **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
 
     def _place(self, node):
-        flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title).render(node)
+        flows = _rml_flowable(self.doc, self.localcontext, images=self.images, path=self.path, title=self.title, canvas=self.canvas).render(node)
         infos = utils.attr_get(node, ['x','y','width','height'])
 
         infos['y']+=infos['height']
@@ -416,7 +442,7 @@ class _rml_canvas(object):
                 flow.drawOn(self.canvas,infos['x'],infos['y'])
                 infos['height']-=h
             else:
-                raise ValueError, "Not enough space"
+                raise ValueError("Not enough space")
 
     def _line_mode(self, node):
         ljoin = {'round':1, 'mitered':0, 'bevelled':2}
@@ -444,7 +470,7 @@ class _rml_canvas(object):
         if not nfile:
             if node.get('name'):
                 image_data = self.images[node.get('name')]
-                self._logger.debug("Image %s used", node.get('name'))
+                _logger.debug("Image %s used", node.get('name'))
                 s = StringIO(image_data)
             else:
                 newtext = node.text
@@ -458,7 +484,7 @@ class _rml_canvas(object):
                 if image_data:
                     s = StringIO(image_data)
                 else:
-                    self._logger.debug("No image data!")
+                    _logger.debug("No image data!")
                     return False
         else:
             if nfile in self.images:
@@ -471,17 +497,17 @@ class _rml_canvas(object):
                 if up and up.scheme:
                     # RFC: do we really want to open external URLs?
                     # Are we safe from cross-site scripting or attacks?
-                    self._logger.debug("Retrieve image from %s", nfile)
+                    _logger.debug("Retrieve image from %s", nfile)
                     u = urllib.urlopen(str(nfile))
                     s = StringIO(u.read())
                 else:
-                    self._logger.debug("Open image file %s ", nfile)
+                    _logger.debug("Open image file %s ", nfile)
                     s = _open_image(nfile, path=self.path)
         try:
             img = ImageReader(s)
             (sx,sy) = img.getSize()
-            self._logger.debug("Image is %dx%d", sx, sy)
-            args = { 'x': 0.0, 'y': 0.0 }
+            _logger.debug("Image is %dx%d", sx, sy)
+            args = { 'x': 0.0, 'y': 0.0, 'mask': 'auto'}
             for tag in ('width','height','x','y'):
                 if node.get(tag):
                     args[tag] = utils.unit_get(node.get(tag))
@@ -525,17 +551,7 @@ class _rml_canvas(object):
         self.canvas.drawPath(self.path, **utils.attr_get(node, [], {'fill':'bool','stroke':'bool'}))
 
     def setFont(self, node):
-        fontname = node.get('name')
-        if fontname not in pdfmetrics.getRegisteredFontNames()\
-             or fontname not in pdfmetrics.standardFonts:
-                # let reportlab attempt to find it
-                try:
-                    pdfmetrics.getFont(fontname)
-                except Exception:
-                    logging.getLogger('report.fonts').debug('Could not locate font %s, substituting default: %s',
-                                 fontname,
-                                 self.canvas._fontname)
-                    fontname = self.canvas._fontname
+        fontname = select_fontname(node.get('name'), self.canvas._fontname)
         return self.canvas.setFont(fontname, utils.unit_get(node.get('size')))
 
     def render(self, node):
@@ -564,7 +580,9 @@ class _rml_canvas(object):
                 tags[n.tag](n)
 
 class _rml_draw(object):
-    def __init__(self, localcontext ,node, styles, images={}, path='.', title=None):
+    def __init__(self, localcontext, node, styles, images=None, path='.', title=None):
+        if images is None:
+            images = {}
         self.localcontext = localcontext
         self.node = node
         self.styles = styles
@@ -588,20 +606,34 @@ class _rml_Illustration(platypus.flowables.Flowable):
         self.height = utils.unit_get(node.get('height'))
         self.self2 = self2
     def wrap(self, *args):
-        return (self.width, self.height)
+        return self.width, self.height
     def draw(self):
         drw = _rml_draw(self.localcontext ,self.node,self.styles, images=self.self2.images, path=self.self2.path, title=self.self2.title)
         drw.render(self.canv, None)
 
+# Workaround for issue #15: https://bitbucket.org/rptlab/reportlab/issue/15/infinite-pages-produced-when-splitting 
+original_pto_split = platypus.flowables.PTOContainer.split
+def split(self, availWidth, availHeight):
+    res = original_pto_split(self, availWidth, availHeight)
+    if len(res) > 2 and len(self._content) > 0:
+        header = self._content[0]._ptoinfo.header
+        trailer = self._content[0]._ptoinfo.trailer
+        if isinstance(res[-2], platypus.flowables.UseUpSpace) and len(header + trailer) == len(res[:-2]):
+            return []
+    return res
+platypus.flowables.PTOContainer.split = split
+
 class _rml_flowable(object):
-    def __init__(self, doc, localcontext, images=None, path='.', title=None):
+    def __init__(self, doc, localcontext, images=None, path='.', title=None, canvas=None):
+        if images is None:
+            images = {}
         self.localcontext = localcontext
         self.doc = doc
         self.styles = doc.styles
-        self.images = images or {}
+        self.images = images
         self.path = path
         self.title = title
-        self._logger = logging.getLogger('report.rml.flowable')
+        self.canvas = canvas
 
     def _textual(self, node):
         rc1 = utils._process_text(self, node.text or '')
@@ -610,11 +642,13 @@ class _rml_flowable(object):
             for key in txt_n.attrib.keys():
                 if key in ('rml_except', 'rml_loop', 'rml_tag'):
                     del txt_n.attrib[key]
-            if True or not self._textual(n).isspace():
-                if not n.tag == 'bullet':
+            if not n.tag == 'bullet':
+                if n.tag == 'pageNumber': 
+                    txt_n.text = self.canvas and str(self.canvas.getPageNumber()) or ''
+                else:
                     txt_n.text = utils.xml2str(self._textual(n))
-                txt_n.tail = n.tail and utils._process_text(self, n.tail.replace('\n','')) or ''
-                rc1 += etree.tostring(txt_n)
+            txt_n.tail = n.tail and utils.xml2str(utils._process_text(self, n.tail.replace('\n',''))) or ''
+            rc1 += etree.tostring(txt_n)
         return rc1
 
     def _table(self, node):
@@ -742,7 +776,7 @@ class _rml_flowable(object):
                 from reportlab.graphics.barcode import createBarcodeDrawing
 
             except ImportError:
-                self._logger.warning("Cannot use barcode renderers:", exc_info=True)
+                _logger.warning("Cannot use barcode renderers:", exc_info=True)
                 return None
             args = utils.attr_get(node, [], {'ratio':'float','xdim':'unit','height':'unit','checksum':'int','quiet':'int','width':'unit','stop':'bool','bearers':'int','barWidth':'float','barHeight':'float'})
             codes = {
@@ -790,10 +824,10 @@ class _rml_flowable(object):
             if not node.get('file'):
                 if node.get('name'):
                     if node.get('name') in self.doc.images:
-                        self._logger.debug("Image %s read ", node.get('name'))
+                        _logger.debug("Image %s read ", node.get('name'))
                         image_data = self.doc.images[node.get('name')].read()
                     else:
-                        self._logger.warning("Image %s not defined", node.get('name'))
+                        _logger.warning("Image %s not defined", node.get('name'))
                         return False
                 else:
                     import base64
@@ -802,11 +836,11 @@ class _rml_flowable(object):
                         newtext = utils._process_text(self, node.text or '')
                     image_data = base64.decodestring(newtext)
                 if not image_data:
-                    self._logger.debug("No inline image data")
+                    _logger.debug("No inline image data")
                     return False
                 image = StringIO(image_data)
             else:
-                self._logger.debug("Image get from file %s", node.get('file'))
+                _logger.debug("Image get from file %s", node.get('file'))
                 image = _open_image(node.get('file'), path=self.doc.path)
             return platypus.Image(image, mask=(250,255,250,255,250,255), **(utils.attr_get(node, ['width','height'])))
         elif node.tag=='spacer':
@@ -868,8 +902,15 @@ class EndFrameFlowable(ActionFlowable):
         ActionFlowable.__init__(self,('frameEnd',resume))
 
 class TinyDocTemplate(platypus.BaseDocTemplate):
+
+    def beforeDocument(self):
+        # Store some useful value directly inside canvas, so it's available
+        # on flowable drawing (needed for proper PageCount handling)
+        self.canv._doPageReset = False
+        self.canv._storyCount = 0
+
     def ___handle_pageBegin(self):
-        self.page = self.page + 1
+        self.page += 1
         self.pageTemplate.beforeDrawPage(self.canv,self)
         self.pageTemplate.checkPageSize(self.canv,self)
         self.pageTemplate.onPage(self.canv,self)
@@ -883,15 +924,29 @@ class TinyDocTemplate(platypus.BaseDocTemplate):
                 self.frame = f
                 break
         self.handle_frameBegin()
-    def afterFlowable(self, flowable):
-        if isinstance(flowable, PageReset):
-            self.canv._pageCount=self.page
-            self.page=0
-            self.canv._flag=True
+
+    def afterPage(self):
+        if self.canv._doPageReset:
+            # Following a <pageReset/> tag:
+            # - we reset page number to 0
+            # - we add  an new PageCount flowable (relative to the current
+            #   story number), but not for NumeredCanvas at is handle page
+            #   count itself)
+            # NOTE: _rml_template render() method add a PageReset flowable at end
+            #   of each story, so we're sure to pass here at least once per story.
+            if not isinstance(self.canv, NumberedCanvas):
+                self.handle_flowable([ PageCount(story_count=self.canv._storyCount) ])
+            self.canv._pageCount = self.page
+            self.page = 0
+            self.canv._flag = True
             self.canv._pageNumber = 0
+            self.canv._doPageReset = False
+            self.canv._storyCount += 1
 
 class _rml_template(object):
-    def __init__(self, localcontext, out, node, doc, images={}, path='.', title=None):
+    def __init__(self, localcontext, out, node, doc, images=None, path='.', title=None):
+        if images is None:
+            images = {}
         if not localcontext:
             localcontext={'internal_header':True}
         self.localcontext = localcontext
@@ -899,9 +954,13 @@ class _rml_template(object):
         self.path = path
         self.title = title
 
-        if not node.get('pageSize'):
-            pageSize = (utils.unit_get('21cm'), utils.unit_get('29.7cm'))
-        else:
+        pagesize_map = {'a4': A4,
+                    'us_letter': letter
+                    }
+        pageSize = A4
+        if self.localcontext.get('company'):
+            pageSize = pagesize_map.get(self.localcontext.get('company').paper_format, A4)
+        if node.get('pageSize'):
             ps = map(lambda x:x.strip(), node.get('pageSize').replace(')', '').replace('(', '').split(','))
             pageSize = ( utils.unit_get(ps[0]),utils.unit_get(ps[1]) )
 
@@ -936,28 +995,26 @@ class _rml_template(object):
         if self.localcontext and not self.localcontext.get('internal_header',False):
             del self.localcontext['internal_header']
         fis = []
-        r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title)
+        r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title, canvas=None)
         story_cnt = 0
         for node_story in node_stories:
             if story_cnt > 0:
                 fis.append(platypus.PageBreak())
             fis += r.render(node_story)
-            # Reset Page Number with new story tag
-            fis.append(PageReset())
             story_cnt += 1
-        if self.localcontext and self.localcontext.get('internal_header',False):
-            self.doc_tmpl.afterFlowable(fis)
-            self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
-        else:
-            fis.append(PageCount())
-            self.doc_tmpl.build(fis)
+        try:
+            if self.localcontext and self.localcontext.get('internal_header',False):
+                self.doc_tmpl.afterFlowable(fis)
+                self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
+            else:
+                self.doc_tmpl.build(fis)
+        except platypus.doctemplate.LayoutError, e:
+            e.name = 'Print Error'
+            e.value = 'The document you are trying to print contains a table row that does not fit on one page. Please try to split it in smaller rows or contact your administrator.'
+            raise
 
-def parseNode(rml, localcontext=None,fout=None, images=None, path='.',title=None):
+def parseNode(rml, localcontext=None, fout=None, images=None, path='.', title=None):
     node = etree.XML(rml)
-    if localcontext is None:
-        localcontext = {}
-    if images is None:
-        images = {}
     r = _rml_doc(node, localcontext, images, path, title=title)
     #try to override some font mappings
     try:
@@ -967,13 +1024,13 @@ def parseNode(rml, localcontext=None,fout=None, images=None, path='.',title=None
         # means there is no custom fonts mapping in this system.
         pass
     except Exception:
-        logging.getLogger('report').warning('Cannot set font mapping', exc_info=True)
+        _logger.warning('Cannot set font mapping', exc_info=True)
         pass
     fp = StringIO()
     r.render(fp)
     return fp.getvalue()
 
-def parseString(rml, localcontext = {},fout=None, images={}, path='.',title=None):
+def parseString(rml, localcontext=None, fout=None, images=None, path='.', title=None):
     node = etree.XML(rml)
     r = _rml_doc(node, localcontext, images, path, title=title)
 
@@ -1008,3 +1065,5 @@ if __name__=="__main__":
         print 'Usage: trml2pdf input.rml >output.pdf'
         print 'Try \'trml2pdf --help\' for more information.'
 
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: