"Fossies" - the Fresh Open Source Software Archive

Member "xhtml2pdf-0.2.2/xhtml2pdf/paragraph.py" (16 Apr 2018, 16142 Bytes) of package /linux/www/xhtml2pdf-0.2.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "paragraph.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 0.1b2_vs_0.2b.

    1 #!/bin/env/python
    2 # -*- coding: utf-8 -*-
    3 
    4 # Copyright 2010 Dirk Holtwick, holtwick.it
    5 #
    6 # Licensed under the Apache License, Version 2.0 (the "License");
    7 # you may not use this file except in compliance with the License.
    8 # You may obtain a copy of the License at
    9 #
   10 #     http://www.apache.org/licenses/LICENSE-2.0
   11 #
   12 # Unless required by applicable law or agreed to in writing, software
   13 # distributed under the License is distributed on an "AS IS" BASIS,
   14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   15 # See the License for the specific language governing permissions and
   16 # limitations under the License.
   17 
   18 """
   19 A paragraph class to be used with ReportLab Platypus.
   20 
   21 TODO
   22 ====
   23 
   24 - Bullets
   25 - Weblinks and internal links
   26 - Borders and margins (Box)
   27 - Underline, Background, Strike
   28 - Images
   29 - Hyphenation
   30 + Alignment
   31 + Breakline, empty lines
   32 + TextIndent
   33 - Sub and super
   34 
   35 """
   36 import copy
   37 import logging
   38 import re
   39 
   40 import six
   41 from reportlab.lib.colors import Color
   42 from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT, TA_RIGHT
   43 from reportlab.pdfbase.pdfmetrics import stringWidth
   44 from reportlab.platypus.flowables import Flowable
   45 
   46 logger = logging.getLogger("xhtml2pdf")
   47 
   48 
   49 class Style(dict):
   50     """
   51     Style.
   52 
   53     Single place for style definitions: Paragraphs and Fragments. The
   54     naming follows the convention of CSS written in camelCase letters.
   55     """
   56 
   57     DEFAULT = {
   58         "textAlign": TA_LEFT,
   59         "textIndent": 0.0,
   60         "width": None,
   61         "height": None,
   62         "fontName": "Times-Roman",
   63         "fontSize": 10.0,
   64         "color": Color(0, 0, 0),
   65         "lineHeight": 1.5,
   66         "lineHeightAbsolute": None,
   67         "pdfLineSpacing": 0,
   68         "link": None,
   69     }
   70 
   71     def __init__(self, **kw):
   72         self.update(self.DEFAULT)
   73         self.update(kw)
   74         self.spaceBefore = 0
   75         self.spaceAfter = 0
   76         self.keepWithNext = False
   77 
   78 
   79 class Box(dict):
   80     """
   81     Box.
   82 
   83     Handles the following styles:
   84 
   85         backgroundColor, backgroundImage
   86         paddingLeft, paddingRight, paddingTop, paddingBottom
   87         marginLeft, marginRight, marginTop, marginBottom
   88         borderLeftColor, borderLeftWidth, borderLeftStyle
   89         borderRightColor, borderRightWidth, borderRightStyle
   90         borderTopColor, borderTopWidth, borderTopStyle
   91         borderBottomColor, borderBottomWidth, borderBottomStyle
   92 
   93     Not used in inline Elements:
   94 
   95         paddingTop, paddingBottom
   96         marginTop, marginBottom
   97 
   98     """
   99 
  100     name = "box"
  101 
  102     def drawBox(self, canvas, x, y, w, h):
  103         canvas.saveState()
  104 
  105         # Background
  106         bg = self.get("backgroundColor", None)
  107         if bg is not None:
  108             # draw a filled rectangle (with no stroke) using bg color
  109             canvas.setFillColor(bg)
  110             canvas.rect(x, y, w, h, fill=1, stroke=0)
  111 
  112         # Borders
  113         def _drawBorderLine(bstyle, width, color, x1, y1, x2, y2):
  114             # We need width and border style to be able to draw a border
  115             if width and bstyle:
  116                 # If no color for border is given, the text color is used (like defined by W3C)
  117                 if color is None:
  118                     color = self.get("textColor", Color(0, 0, 0))
  119                 if color is not None:
  120                     canvas.setStrokeColor(color)
  121                     canvas.setLineWidth(width)
  122                     canvas.line(x1, y1, x2, y2)
  123 
  124         _drawBorderLine(self.get("borderLeftStyle", None),
  125                         self.get("borderLeftWidth", None),
  126                         self.get("borderLeftColor", None),
  127                         x, y, x, y + h)
  128         _drawBorderLine(self.get("borderRightStyle", None),
  129                         self.get("borderRightWidth", None),
  130                         self.get("borderRightColor", None),
  131                         x + w, y, x + w, y + h)
  132         _drawBorderLine(self.get("borderTopStyle", None),
  133                         self.get("borderTopWidth", None),
  134                         self.get("borderTopColor", None),
  135                         x, y + h, x + w, y + h)
  136         _drawBorderLine(self.get("borderBottomStyle", None),
  137                         self.get("borderBottomWidth", None),
  138                         self.get("borderBottomColor", None),
  139                         x, y, x + w, y)
  140 
  141         canvas.restoreState()
  142 
  143 
  144 class Fragment(Box):
  145     """
  146     Fragment.
  147 
  148     text:       String containing text
  149     fontName:
  150     fontSize:
  151     width:      Width of string
  152     height:     Height of string
  153     """
  154 
  155     name = "fragment"
  156     isSoft = False
  157     isText = False
  158     isLF = False
  159 
  160 
  161     def calc(self):
  162         self["width"] = 0
  163 
  164 
  165 class Word(Fragment):
  166     """
  167     A single word.
  168     """
  169 
  170     name = "word"
  171     isText = True
  172 
  173     def calc(self):
  174         """
  175         XXX Cache stringWith if not accelerated?!
  176         """
  177         self["width"] = stringWidth(self["text"], self["fontName"], self["fontSize"])
  178 
  179 
  180 class Space(Fragment):
  181     """
  182     A space between fragments that is the usual place for line breaking.
  183     """
  184 
  185     name = "space"
  186     isSoft = True
  187 
  188     def calc(self):
  189         self["width"] = stringWidth(" ", self["fontName"], self["fontSize"])
  190 
  191 
  192 class LineBreak(Fragment):
  193     """
  194     Line break.
  195     """
  196 
  197     name = "br"
  198     isSoft = True
  199     isLF = True
  200 
  201     pass
  202 
  203 
  204 class BoxBegin(Fragment):
  205     name = "begin"
  206 
  207     def calc(self):
  208         self["width"] = self.get("marginLeft", 0) + self.get("paddingLeft", 0) # + border if border
  209 
  210     def draw(self, canvas, y):
  211         # if not self["length"]:
  212         x = self.get("marginLeft", 0) + self["x"]
  213         w = self["length"] + self.get("paddingRight", 0)
  214         h = self["fontSize"]
  215         self.drawBox(canvas, x, y, w, h)
  216 
  217 
  218 class BoxEnd(Fragment):
  219     name = "end"
  220 
  221     def calc(self):
  222         self["width"] = self.get("marginRight", 0) + self.get("paddingRight", 0) # + border
  223 
  224 
  225 class Image(Fragment):
  226     name = "image"
  227 
  228     pass
  229 
  230 
  231 class Line(list):
  232     """
  233     Container for line fragments.
  234     """
  235 
  236     LINEHEIGHT = 1.0
  237 
  238     def __init__(self, style):
  239         self.width = 0
  240         self.height = 0
  241         self.isLast = False
  242         self.style = style
  243         self.boxStack = []
  244         list.__init__(self)
  245 
  246     def doAlignment(self, width, alignment):
  247         # Apply alignment
  248         if alignment != TA_LEFT:
  249             lineWidth = self[- 1]["x"] + self[- 1]["width"]
  250             emptySpace = width - lineWidth
  251             if alignment == TA_RIGHT:
  252                 for frag in self:
  253                     frag["x"] += emptySpace
  254             elif alignment == TA_CENTER:
  255                 for frag in self:
  256                     frag["x"] += emptySpace / 2.0
  257             elif alignment == TA_JUSTIFY and not self.isLast: # XXX last line before split
  258                 delta = emptySpace / (len(self) - 1)
  259                 for i, frag in enumerate(self):
  260                     frag["x"] += i * delta
  261 
  262         # Boxes
  263         for frag in self:
  264             x = frag["x"] + frag["width"]
  265             if isinstance(frag, BoxBegin):
  266                 self.boxStack.append(frag)
  267             elif isinstance(frag, BoxEnd):
  268                 if self.boxStack:
  269                     frag = self.boxStack.pop()
  270                     frag["length"] = x - frag["x"]
  271 
  272         # Handle the rest
  273         for frag in self.boxStack:
  274             frag["length"] = x - frag["x"]
  275 
  276     def doLayout(self, width):
  277         """
  278         Align words in previous line.
  279         """
  280 
  281         # Calculate dimensions
  282         self.width = width
  283 
  284         font_sizes = [0] + [frag.get("fontSize", 0) for frag in self]
  285         self.fontSize = max(font_sizes)
  286         self.height = self.lineHeight = max(frag * self.LINEHEIGHT for frag in font_sizes)
  287 
  288         # Apply line height
  289         y = (self.lineHeight - self.fontSize) # / 2
  290         for frag in self:
  291             frag["y"] = y
  292 
  293         return self.height
  294 
  295     def dumpFragments(self):
  296         logger.debug("Line")
  297         logger.debug(40 * "-")
  298         for frag in self:
  299             logger.debug("%s", frag.get("text", frag.name.upper()))
  300 
  301 
  302 class Text(list):
  303     """
  304     Container for text fragments.
  305 
  306     Helper functions for splitting text into lines and calculating sizes
  307     and positions.
  308     """
  309 
  310     def __init__(self, data=None, style=None):
  311         # Mutable arguments are a shit idea
  312         if data is None:
  313             data = []
  314 
  315         self.lines = []
  316         self.width = 0
  317         self.height = 0
  318         self.maxWidth = 0
  319         self.maxHeight = 0
  320         self.style = style
  321         list.__init__(self, data)
  322 
  323     def calc(self):
  324         """
  325         Calculate sizes of fragments.
  326         """
  327         for word in self:
  328             word.calc()
  329 
  330     def splitIntoLines(self, maxWidth, maxHeight, splitted=False):
  331         """
  332         Split text into lines and calculate X positions. If we need more
  333         space in height than available we return the rest of the text
  334         """
  335         self.lines = []
  336         self.height = 0
  337         self.maxWidth = self.width = maxWidth
  338         self.maxHeight = maxHeight
  339         boxStack = []
  340 
  341         style = self.style
  342         x = 0
  343 
  344         # Start with indent in first line of text
  345         if not splitted:
  346             x = style["textIndent"]
  347 
  348         lenText = len(self)
  349         pos = 0
  350         while pos < lenText:
  351 
  352             # Reset values for new line
  353             posBegin = pos
  354             line = Line(style)
  355 
  356             # Update boxes for next line
  357             for box in copy.copy(boxStack):
  358                 box["x"] = 0
  359                 line.append(BoxBegin(box))
  360 
  361             while pos < lenText:
  362 
  363                 # Get fragment, its width and set X
  364                 frag = self[pos]
  365                 fragWidth = frag["width"]
  366                 frag["x"] = x
  367                 pos += 1
  368 
  369                 # Keep in mind boxes for next lines
  370                 if isinstance(frag, BoxBegin):
  371                     boxStack.append(frag)
  372                 elif isinstance(frag, BoxEnd):
  373                     boxStack.pop()
  374 
  375                 # If space or linebreak handle special way
  376                 if frag.isSoft:
  377                     if frag.isLF:
  378                         line.append(frag)
  379                         break
  380                         # First element of line should not be a space
  381                     if x == 0:
  382                         continue
  383                         # Keep in mind last possible line break
  384 
  385                 # The elements exceed the current line
  386                 elif fragWidth + x > maxWidth:
  387                     break
  388 
  389                 # Add fragment to line and update x
  390                 x += fragWidth
  391                 line.append(frag)
  392 
  393             # Remove trailing white spaces
  394             while line and line[-1].name in ("space", "br"):
  395                 line.pop()
  396 
  397             # Add line to list
  398             line.dumpFragments()
  399             # if line:
  400             self.height += line.doLayout(self.width)
  401             self.lines.append(line)
  402 
  403             # If not enough space for current line force to split
  404             if self.height > maxHeight:
  405                 return posBegin
  406 
  407             # Reset variables
  408             x = 0
  409 
  410         # Apply alignment
  411         self.lines[- 1].isLast = True
  412         for line in self.lines:
  413             line.doAlignment(maxWidth, style["textAlign"])
  414 
  415         return None
  416 
  417     def dumpLines(self):
  418         """
  419         For debugging dump all line and their content
  420         """
  421         for i, line in enumerate(self.lines):
  422             logger.debug("Line %d:", i)
  423             logger.debug(line.dumpFragments())
  424 
  425 
  426 class Paragraph(Flowable):
  427     """
  428     A simple Paragraph class respecting alignment.
  429 
  430     Does text without tags.
  431 
  432     Respects only the following global style attributes:
  433     fontName, fontSize, leading, firstLineIndent, leftIndent,
  434     rightIndent, textColor, alignment.
  435     (spaceBefore, spaceAfter are handled by the Platypus framework.)
  436 
  437     """
  438     def __init__(self, text, style, debug=False, splitted=False, **kwDict):
  439 
  440         Flowable.__init__(self)
  441 
  442         self.text = text
  443         self.text.calc()
  444         self.style = style
  445         self.text.style = style
  446 
  447         self.debug = debug
  448         self.splitted = splitted
  449 
  450         # More attributes
  451         for k, v in six.iteritems(kwDict):
  452             setattr(self, k, v)
  453 
  454         # set later...
  455         self.splitIndex = None
  456 
  457     # overwritten methods from Flowable class
  458     def wrap(self, availWidth, availHeight):
  459         """
  460         Determine the rectangle this paragraph really needs.
  461         """
  462 
  463         # memorize available space
  464         self.avWidth = availWidth
  465         self.avHeight = availHeight
  466 
  467         logger.debug("*** wrap (%f, %f)", availWidth, availHeight)
  468 
  469         if not self.text:
  470             logger.debug("*** wrap (%f, %f) needed", 0, 0)
  471             return 0, 0
  472 
  473         # Split lines
  474         width = availWidth
  475         self.splitIndex = self.text.splitIntoLines(width, availHeight)
  476 
  477         self.width, self.height = availWidth, self.text.height
  478 
  479         logger.debug("*** wrap (%f, %f) needed, splitIndex %r", self.width, self.height, self.splitIndex)
  480 
  481         return self.width, self.height
  482 
  483     def split(self, availWidth, availHeight):
  484         """
  485         Split ourselves in two paragraphs.
  486         """
  487 
  488         logger.debug("*** split (%f, %f)", availWidth, availHeight)
  489 
  490         splitted = []
  491         if self.splitIndex:
  492             text1 = self.text[:self.splitIndex]
  493             text2 = self.text[self.splitIndex:]
  494             p1 = Paragraph(Text(text1), self.style, debug=self.debug)
  495             p2 = Paragraph(Text(text2), self.style, debug=self.debug, splitted=True)
  496             splitted = [p1, p2]
  497 
  498             logger.debug("*** text1 %s / text %s", len(text1), len(text2))
  499 
  500         logger.debug('*** return %s', self.splitted)
  501 
  502         return splitted
  503 
  504     def draw(self):
  505         """
  506         Render the content of the paragraph.
  507         """
  508 
  509         logger.debug("*** draw")
  510 
  511         if not self.text:
  512             return
  513 
  514         canvas = self.canv
  515         style = self.style
  516 
  517         canvas.saveState()
  518 
  519         # Draw box arround paragraph for debugging
  520         if self.debug:
  521             bw = 0.5
  522             bc = Color(1, 1, 0)
  523             bg = Color(0.9, 0.9, 0.9)
  524             canvas.setStrokeColor(bc)
  525             canvas.setLineWidth(bw)
  526             canvas.setFillColor(bg)
  527             canvas.rect(
  528                 style.leftIndent,
  529                 0,
  530                 self.width,
  531                 self.height,
  532                 fill=1,
  533                 stroke=1)
  534 
  535         y = 0
  536         dy = self.height
  537         for line in self.text.lines:
  538             y += line.height
  539             for frag in line:
  540 
  541                 # Box
  542                 if hasattr(frag, "draw"):
  543                     frag.draw(canvas, dy - y)
  544 
  545                 # Text
  546                 if frag.get("text", ""):
  547                     canvas.setFont(frag["fontName"], frag["fontSize"])
  548                     canvas.setFillColor(frag.get("color", style["color"]))
  549                     canvas.drawString(frag["x"], dy - y + frag["y"], frag["text"])
  550 
  551                 # XXX LINK
  552                 link = frag.get("link", None)
  553                 if link:
  554                     _scheme_re = re.compile('^[a-zA-Z][-+a-zA-Z0-9]+$')
  555                     x, y, w, h = frag["x"], dy - y, frag["width"], frag["fontSize"]
  556                     rect = (x, y, w, h)
  557                     if isinstance(link, six.text_type):
  558                         link = link.encode('utf8')
  559                     parts = link.split(':', 1)
  560                     scheme = len(parts) == 2 and parts[0].lower() or ''
  561                     if _scheme_re.match(scheme) and scheme != 'document':
  562                         kind = scheme.lower() == 'pdf' and 'GoToR' or 'URI'
  563                         if kind == 'GoToR':
  564                             link = parts[1]
  565 
  566                         canvas.linkURL(link, rect, relative=1, kind=kind)
  567                     else:
  568                         if link[0] == '#':
  569                             link = link[1:]
  570                             scheme = ''
  571                         canvas.linkRect("", scheme != 'document' and link or parts[1], rect, relative=1)
  572 
  573         canvas.restoreState()