"Fossies" - the Fresh Open Source Software Archive

Member "xhtml2pdf-0.2.5/xhtml2pdf/paragraph.py" (25 Sep 2020, 16141 Bytes) of package /linux/www/xhtml2pdf-0.2.5.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 latest Fossies "Diffs" side-by-side code changes report: 0.2.4_vs_0.2.5.

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