#!/bin/env/python
# -*- coding: utf-8 -*-
 
# Copyright 2010 Dirk Holtwick, holtwick.it
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
 
"""
A paragraph class to be used with ReportLab Platypus.
 
TODO
====
 
- Bullets
- Weblinks and internal links
- Borders and margins (Box)
- Underline, Background, Strike
- Images
- Hyphenation
+ Alignment
+ Breakline, empty lines
+ TextIndent
- Sub and super
 
"""
 
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT, TA_RIGHT
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.platypus.flowables import Flowable
from reportlab.lib.colors import Color
 
class Style(dict):
 
    """
    Style.
 
    Single place for style definitions: Paragraphs and Fragments. The
    naming follows the convention of CSS written in camelCase letters.
    """
 
    DEFAULT = {
        "textAlign": TA_LEFT,
        "textIndent": 0.0,
        "width": None,
        "height": None,
        "fontName": "Times-Roman",
        "fontSize": 10.0,
        "color": Color(0, 0, 0),
        "lineHeight": 1.5,
        "lineHeightAbsolute": None,
        "pdfLineSpacing": 0,
        "link": None,
        }
 
    def __init__(self, **kw):
        self.update(self.DEFAULT)
        self.update(kw)
        self.spaceBefore = 0
        self.spaceAfter = 0
        self.keepWithNext = False
 
class Box(dict):
 
    """
    Box.
 
    Handles the following styles:
 
        backgroundColor, backgroundImage
        paddingLeft, paddingRight, paddingTop, paddingBottom
        marginLeft, marginRight, marginTop, marginBottom
        borderLeftColor, borderLeftWidth, borderLeftStyle
        borderRightColor, borderRightWidth, borderRightStyle
        borderTopColor, borderTopWidth, borderTopStyle
        borderBottomColor, borderBottomWidth, borderBottomStyle
 
    Not used in inline Elements:
 
        paddingTop, paddingBottom
        marginTop, marginBottom
 
    """
 
    name = "box"
 
    def drawBox(self, canvas, x, y, w, h):
        canvas.saveState()
 
        # Background
        bg = self.get("backgroundColor", None)
        if bg is not None:
            # draw a filled rectangle (with no stroke) using bg color
            canvas.setFillColor(bg)
            canvas.rect(x, y, w, h, fill=1, stroke=0)
 
        # Borders
        def _drawBorderLine(bstyle, width, color, x1, y1, x2, y2):
            # We need width and border style to be able to draw a border
            if width and bstyle:
                # If no color for border is given, the text color is used (like defined by W3C)
                if color is None:
                    color = self.get("textColor", Color(0, 0, 0))
                # print "Border", bstyle, width, color
                if color is not None:
                    canvas.setStrokeColor(color)
                    canvas.setLineWidth(width)
                    canvas.line(x1, y1, x2, y2)
 
        _drawBorderLine(self.get("borderLeftStyle", None),
            self.get("borderLeftWidth", None),
            self.get("borderLeftColor", None),
            x, y, x, y + h)
        _drawBorderLine(self.get("borderRightStyle", None),
            self.get("borderRightWidth", None),
            self.get("borderRightColor", None),
            x + w, y, x + w, y + h)
        _drawBorderLine(self.get("borderTopStyle", None),
            self.get("borderTopWidth", None),
            self.get("borderTopColor", None),
            x, y + h, x + w, y + h)
        _drawBorderLine(self.get("borderBottomStyle", None),
            self.get("borderBottomWidth", None),
            self.get("borderBottomColor", None),
            x, y, x + w, y)
 
        canvas.restoreState()
 
class Fragment(Box):
 
    """
    Fragment.
 
    text:       String containing text
    fontName:
    fontSize:
    width:      Width of string
    height:     Height of string
    """
 
    name = "fragment"
    isSoft = False
    isText = False
    isLF = False
 
    def calc(self):
        self["width"] = 0
 
class Word(Fragment):
 
    " A single word. "
 
    name = "word"
    isText = True
 
    def calc(self):
        """
        XXX Cache stringWith if not accelerated?!
        """
        self["width"] = stringWidth(self["text"], self["fontName"], self["fontSize"])
 
class Space(Fragment):
 
    """
    A space between fragments that is the usual place for line breaking.
    """
 
    name = "space"
    isSoft = True
 
    def calc(self):
        self["width"] = stringWidth(" ", self["fontName"], self["fontSize"])
 
class LineBreak(Fragment):
    " Line break. "
 
    name = "br"
    isSoft = True
    isLF = True
 
    pass
 
class BoxBegin(Fragment):
 
    name = "begin"
 
    def calc(self):
        self["width"] = self.get("marginLeft", 0) + self.get("paddingLeft", 0) # + border if border
 
    def draw(self, canvas, y):
        # if not self["length"]:
        x = self.get("marginLeft", 0) + self["x"]
        w = self["length"] + self.get("paddingRight", 0)
        h = self["fontSize"]
        self.drawBox(canvas, x, y, w, h)
 
class BoxEnd(Fragment):
 
    name = "end"
 
    def calc(self):
        self["width"] = self.get("marginRight", 0) + self.get("paddingRight", 0) # + border
 
class Image(Fragment):
 
    name = "image"
 
    pass
 
class Line(list):
 
    """
    Container for line fragments.
    """
 
    LINEHEIGHT = 1.0
 
    def __init__(self, style):
        self.width = 0
        self.height = 0
        self.isLast = False
        self.style = style
        self.boxStack = []
        list.__init__(self)
 
    def doAlignment(self, width, alignment):
        # Apply alignment
        if alignment != TA_LEFT:
            lineWidth = self[ - 1]["x"] + self[ - 1]["width"]
            emptySpace = width - lineWidth
            if alignment == TA_RIGHT:
                for j, frag in enumerate(self):
                    frag["x"] += emptySpace
            elif alignment == TA_CENTER:
                for j, frag in enumerate(self):
                    frag["x"] += emptySpace / 2.0
            elif alignment == TA_JUSTIFY and not self.isLast: # XXX last line before split
                delta = emptySpace / (len(self) - 1)
                for j, frag in enumerate(self):
                    frag["x"] += j * delta
 
        # Boxes
        for frag in self:
            x = frag["x"] + frag["width"]
            # print "***", x, frag["x"]
            if isinstance(frag, BoxBegin):
                self.boxStack.append(frag)
            elif isinstance(frag, BoxEnd):
                if self.boxStack:
                    frag = self.boxStack.pop()
                    frag["length"] = x - frag["x"]
 
        # Handle the rest
        for frag in self.boxStack:
            print "***", x, frag["x"]
            frag["length"] = x - frag["x"]
 
    def doLayout(self, width):
        "Align words in previous line."
 
        # Calculate dimensions
        self.width = width
        self.height = self.lineHeight = max(frag.get("fontSize" , 0) * self.LINEHEIGHT for frag in self)
 
        # Apply line height
        self.fontSize = max(frag.get("fontSize" , 0) for frag in self)
        y = (self.lineHeight - self.fontSize) # / 2
        for frag in self:
            frag["y"] = y
 
        return self.height
 
    def dumpFragments(self):
        print "Line", 40*"-"
        for frag in self:
            print "%s" % frag.get("text", frag.name.upper()),
        print
 
class Group(list):
    pass
 
class Text(list):
 
    """
    Container for text fragments.
 
    Helper functions for splitting text into lines and calculating sizes
    and positions.
    """
 
    def __init__(self, data=[], style=None):
        #self.groups = []
        self.lines = []
        self.width = 0
        self.height = 0
        self.maxWidth = 0
        self.maxHeight = 0
        self.style = style
        list.__init__(self, data)
 
    def calc(self):
        """
        Calculate sizes of fragments.
        """
        #pos = 0
        #while
        #whi
        #group = Group()
        #gWidth = 0
        #for frag in self:
        #    width = frag.calc()
        #    if frag.isSoft:
        #        group.width = gWidth
        #        self.groups.append(group)
        #        self.
        #        gWidth += width
        [word.calc() for word in self]
 
    def splitIntoLines(self, maxWidth, maxHeight, splitted=False):
        """
        Split text into lines and calculate X positions. If we need more
        space in height than available we return the rest of the text
        """
        self.lines = []
        self.height = 0
        self.maxWidth = self.width = maxWidth
        self.maxHeight = maxHeight
        boxStack = []
 
        style = self.style
        x = 0
 
        # Start with indent in first line of text
        if not splitted:
            x = style["textIndent"]
 
        lenText = len(self)
        pos = 0
        while pos < lenText:
 
            # Reset values for new line
            posBegin = pos
            posSpace = pos
            line = Line(style)
 
            # Update boxes for next line
            for box in copy.copy(boxStack):
                box["x"] = 0
                line.append(BoxBegin(box))
 
            while pos < lenText:
 
                # Get fragment, its width and set X
                frag = self[pos]
                fragWidth = frag["width"]
                frag["x"] = x
                pos += 1
 
                # Keep in mind boxes for next lines
                if isinstance(frag, BoxBegin):
                    boxStack.append(frag)
                elif isinstance(frag, BoxEnd):
                    boxStack.pop()
 
                # If space or linebreak handle special way
                if frag.isSoft:
                    if frag.isLF:
                        posSpace = pos
                        line.append(frag)
                        break
                    # First element of line should not be a space
                    if x == 0:
                        continue
                    # Keep in mind last possible line break
                    posSpace = pos - 1
 
                # The elements exceed the current line
                elif (fragWidth + x > maxWidth):
                    break
 
                # Add fragment to line and update x
                x += fragWidth
                line.append(frag)
 
            # Remove until last soft item
            #if (posSpace < pos) and (posSpace > posBegin):
            #    print "Remove", line[::-(posSpace - posBegin)]
            #    del line[::-(posSpace - posBegin)]
            #    pos = posSpace
 
            # Remove trailing white spaces
            while line and line[-1].name in ("space", "br"):
                # print "Pop",
                line.pop()
 
            # Add line to list
            line.dumpFragments()
            # if line:
            self.height += line.doLayout(self.width)
            self.lines.append(line)
 
            # If not enough space for current line force to split
            if self.height > maxHeight:
                return posBegin
 
            # Reset variables
            x = 0
 
        # Apply alignment
        self.lines[ - 1].isLast = True
        [line.doAlignment(maxWidth, style["textAlign"]) for line in self.lines]
 
        return None
 
    def dumpLines(self):
        """
        For debugging dump all line and their content
        """
        i = 0
        for line in self.lines:
            print "Line %d:" % i,
            line.dumpFragments()
            i += 1
 
class Paragraph(Flowable):
    """A simple Paragraph class respecting alignment.
 
    Does text without tags.
 
    Respects only the following global style attributes:
    fontName, fontSize, leading, firstLineIndent, leftIndent,
    rightIndent, textColor, alignment.
    (spaceBefore, spaceAfter are handled by the Platypus framework.)
 
    """
 
    def __init__(self, text, style, debug=False, splitted=False, **kwDict):
 
        Flowable.__init__(self)
        # self._showBoundary = True
 
        self.text = text
        self.text.calc()
        self.style = style
        self.text.style = style
 
        self.debug = debug
        self.splitted = splitted
 
        # More attributes
        for k, v in kwDict.items():
            setattr(self, k, v)
 
        # set later...
        self.splitIndex = None
 
    # overwritten methods from Flowable class
 
    def wrap(self, availWidth, availHeight):
        "Determine the rectangle this paragraph really needs."
 
        # memorize available space
        self.avWidth = availWidth
        self.avHeight = availHeight
 
        if self.debug:
            print "*** wrap (%f, %f)" % (availWidth, availHeight)
 
        if not self.text:
            if self.debug:
                print "*** wrap (%f, %f) needed" % (0, 0)
            return 0, 0
 
        style = self.style
 
        # Split lines
        width = availWidth # - style.leftIndent - style.rightIndent
        self.splitIndex = self.text.splitIntoLines(width, availHeight)
 
        self.width, self.height = availWidth, self.text.height
 
        if self.debug:
            print "*** wrap (%f, %f) needed, splitIndex %r" % (self.width, self.height, self.splitIndex)
 
        return self.width, self.height
 
    #def visitFirstParagraph(self, para):
    #    return para
 
    #def visitOtherParagraph(self, para):
    #    return para
 
    def split(self, availWidth, availHeight):
        "Split ourself in two paragraphs."
 
        if self.debug:
            print "*** split (%f, %f)" % (availWidth, availHeight)
 
        splitted = []
        if self.splitIndex:
            text1 = self.text[:self.splitIndex]
            text2 = self.text[self.splitIndex:]
            p1 = Paragraph(Text(text1), self.style, debug=self.debug)
            p2 = Paragraph(Text(text2), self.style, debug=self.debug, splitted=True)
            splitted = [p1, p2]
 
            if self.debug:
                print "*** text1 %s / text %s" % (len(text1), len(text2))
 
        if self.debug:
            print '*** return %s' % self.splitted
 
        return splitted
 
 
    def draw(self):
        "Render the content of the paragraph."
 
        if self.debug:
            print "*** draw"
 
        if not self.text:
            return
 
        canvas = self.canv
        style = self.style
 
        canvas.saveState()
 
        # Draw box arround paragraph for debugging
        if self.debug:
            bw = 0.5
            bc = Color(1, 1, 0)
            bg = Color(0.9, 0.9, 0.9)
            canvas.setStrokeColor(bc)
            canvas.setLineWidth(bw)
            canvas.setFillColor(bg)
            canvas.rect(
                style.leftIndent,
                0,
                self.width,
                self.height,
                fill=1,
                stroke=1)
 
        y = 0
        dy = self.height
        for line in self.text.lines:
            y += line.height
            for frag in line:
 
                # Box
                if hasattr(frag, "draw"):
                    frag.draw(canvas, dy - y)
 
                # Text
                if frag.get("text", ""):
                    canvas.setFont(frag["fontName"], frag["fontSize"])
                    canvas.setFillColor(frag.get("color", style["color"]))
                    canvas.drawString(frag["x"], dy - y + frag["y"], frag["text"])
 
                # XXX LINK
                link = frag.get("link", None)
                if link:
                    _scheme_re = re.compile('^[a-zA-Z][-+a-zA-Z0-9]+$')
                    x, y, w, h = frag["x"], dy - y, frag["width"], frag["fontSize"]
                    rect = (x, y, w, h)
                    if isinstance(link, unicode):
                        link = link.encode('utf8')
                    parts = link.split(':', 1)
                    scheme = len(parts) == 2 and parts[0].lower() or ''
                    if _scheme_re.match(scheme) and scheme != 'document':
                        kind = scheme.lower() == 'pdf' and 'GoToR' or 'URI'
                        if kind == 'GoToR': link = parts[1]
                        # tx._canvas.linkURL(link, rect, relative=1, kind=kind)
                        canvas.linkURL(link, rect, relative=1, kind=kind)
                    else:
                        if link[0] == '#':
                            link = link[1:]
                            scheme = ''
                        canvas.linkRect("", scheme != 'document' and link or parts[1], rect, relative=1)
 
        canvas.restoreState()
 
if __name__ == "__main__":
    from reportlab.platypus import SimpleDocTemplate
    from reportlab.lib.styles import *
    from reportlab.rl_config import *
    from reportlab.lib.units import *
 
    import os
    import copy
    import re
    import pprint
 
    styles = getSampleStyleSheet()
 
    ALIGNMENTS = (TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY)
 
    TEXT = """
    Lörem ipsum dolor sit amet, consectetur adipisicing elit,
    sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
    Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
    ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit
    in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
    Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
    officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet,
    consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore
    et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
    ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
    dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
    nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt
    in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum
    dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
    incididunt ut labore et dolore magna aliqua.
    """.strip()
 
    def textGenerator(data, fn, fs):
        i = 1
        for word in re.split('\s+', data):
            if word:
                yield Word(
                    text="[%d|%s]" % (i, word),
                    fontName=fn,
                    fontSize=fs
                    )
                yield Space(
                    fontName=fn,
                    fontSize=fs
                    )
 
    def createText(data, fn, fs):
        text = Text(list(textGenerator(data, fn, fs)))
        return text
 
    def makeBorder(width, style="solid", color=Color(1, 0, 0)):
        return dict(
            borderLeftColor=color,
            borderLeftWidth=width,
            borderLeftStyle=style,
            borderRightColor=color,
            borderRightWidth=width,
            borderRightStyle=style,
            borderTopColor=color,
            borderTopWidth=width,
            borderTopStyle=style,
            borderBottomColor=color,
            borderBottomWidth=width,
            borderBottomStyle=style
            )
 
    def test():
        doc = SimpleDocTemplate("test.pdf")
        story = []
 
        style = Style(fontName="Helvetica", textIndent=24.0)
        fn = style["fontName"]
        fs = style["fontSize"]
        sampleText1 = createText(TEXT[:100], fn, fs)
        sampleText2 = createText(TEXT[100:], fn, fs)
 
        text = Text(sampleText1 + [
            Space(
                fontName=fn,
                fontSize=fs),
            Word(
                text="TrennbarTrennbar",
                pairs=[("Trenn-", "barTrennbar")],
                fontName=fn,
                fontSize=fs),
            Space(
                fontName=fn,
                fontSize=fs),
            Word(
                text="Normal",
                color=Color(1, 0, 0),
                fontName=fn,
                fontSize=fs),
            Space(
                fontName=fn,
                fontSize=fs),
            Word(
                text="gGrößer",
                fontName=fn,
                fontSize=fs * 1.5),
            Space(
                fontName=fn,
                fontSize=fs),
            Word(
                text="Bold",
                fontName="Times-Bold",
                fontSize=fs),
            Space(
                fontName=fn,
                fontSize=fs),
            Word(
                text="jItalic",
                fontName="Times-Italic",
                fontSize=fs),
            Space(
                fontName=fn,
                fontSize=fs),
 
            # <span style="border: 1px solid red;">ipsum <span style="border: 1px solid green; padding: 4px; padding-left: 20px; background: yellow; margin-bottom: 8px; margin-left: 10px;">
            # Lo<font size="12pt">re</font>m</span> <span style="background:blue; height: 30px;">ipsum</span> Lorem</span>
 
            BoxBegin(
                fontName=fn,
                fontSize=fs,
                **makeBorder(0.5, "solid", Color(0, 1, 0))),
            Word(
                text="Lorem",
                fontName="Times-Bold",
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName="Times-Bold",
                fontSize=fs),
            Space(
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Space(
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Space(
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Space(
                fontName=fn,
                fontSize=fs),
            BoxBegin(
                fontName=fn,
                fontSize=fs,
                backgroundColor=Color(1, 1, 0),
                **makeBorder(1, "solid", Color(1, 0, 0))),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            BoxEnd(),
            Space(
                fontName=fn,
                fontSize=fs),
            Word(
                text="Lorem",
                fontName=fn,
                fontSize=fs),
            Space(
                fontName=fn,
                fontSize=fs),
            BoxEnd(),
 
            LineBreak(
                fontName=fn,
                fontSize=fs),
            LineBreak(
                fontName=fn,
                fontSize=fs),
            ] + sampleText2)
 
        story.append(Paragraph(
            copy.copy(text),
            style,
            debug=0))
 
        for i in range(10):
            style = copy.deepcopy(style)
            style["textAlign"] = ALIGNMENTS[i % 4]
            text = createText(("(%d) " % i) + TEXT, fn, fs)
            story.append(Paragraph(
                copy.copy(text),
                style,
                debug=0))
        doc.build(story)
 
    test()
    os.system("start test.pdf")
 
    # createText(TEXT, styles["Normal"].fontName, styles["Normal"].fontSize)