Exemple #1
0
class PDFRenderer(object):

    ONE_INCH = 1.0 * inch
    MIN_HEIGHT_TABLE_AND_CHART = 4 * inch

    _fontManager = None

    outputFile = None
    reportLabPaperSize = (0, 0)
    _includeSplunkLogo = True
    _title = ""
    _story = []
    _runningAsScript = False

    _style = STYLES["Normal"]
    CENTER_STYLE = copy.deepcopy(STYLES['Normal'])
    TITLE_STYLE = copy.deepcopy(STYLES["Normal"])
    BULLET_STYLE = STYLES["Bullet"]
    _bulletStyle = STYLES["Bullet"]
    _tableTitleStyle = STYLES["Title"]
    _listTitleStyle = STYLES["Bullet"]
    _hardWrapStyle = copy.deepcopy(STYLES["Normal"])
    _hardWrapStyle.wordWrap = "CJK"
    _TABLE_COL_LEFT_PADDING = 2
    _TABLE_COL_RIGHT_PADDING = 2
    _MARGINS = [inch, inch, inch, inch]

    def __init__(self,
                 title,
                 outputFile,
                 paperSize,
                 timestamp="",
                 includeSplunkLogo=None,
                 cidFontList=None):
        """ outputFile can either be a filename or a file-like object """
        self.outputFile = outputFile
        self.paperSize = paperSize
        self.reportLabPaperSize = PAPERSIZES[
            self.paperSize]['reportLabPaperSize']
        self.logoTransformSize = PAPERSIZES[
            self.paperSize]['logoTransformSize']
        self._log("outputFile: " + str(self.outputFile))
        self._log("reportLabPaperSize: " + str(self.reportLabPaperSize))
        self._title = title
        self._timestamp = timestamp
        if includeSplunkLogo != None:
            self._includeSplunkLogo = includeSplunkLogo
        logger.debug("pdf-init pdfrenderer include-splunk-logo=%s" %
                     self._includeSplunkLogo)

        self._fontManager = FontManager(cidFontList=cidFontList)

        self.TITLE_STYLE.fontSize = 14
        self.TITLE_STYLE.leading = 16
        self.CENTER_STYLE.alignment = reportlab.lib.enums.TA_CENTER

        # TODO: need a better way to determine max cell height
        #       225 ~= margins + footer height + a few lines for header row
        self.maxTableCellHeight = self.reportLabPaperSize[1] - 225
        return

    def conditionalPageBreak(self):
        self._story.append(CondPageBreak(self.MIN_HEIGHT_TABLE_AND_CHART))

    def spaceBetween(self, space=0.5 * inch):
        self._story.append(EnsureSpaceBetween(space))

    def renderText(self, text, style=None, escapeText=True):
        if style is None:
            style = self._style

        if escapeText:
            readyText = su.escape(text)
        else:
            readyText = text

        logger.debug("renderText readyText='%s'" % readyText)
        self._story.append(
            Paragraph(self._fontManager.encodeTextForParagraph(readyText),
                      style))

    def renderBulletText(self, text, bullet='-', style=None):
        if style is None:
            style = self._bulletStyle
        self._story.append(
            Paragraph(self._fontManager.encodeTextForParagraph(
                su.escape(text)),
                      style,
                      bulletText=bullet))

    def renderHtml(self, text):
        if text is None:
            return

        def multiple_replacer(*key_values):
            replace_dict = dict(key_values)
            replacement_function = lambda match: replace_dict[match.group(0)]
            pattern = re.compile(
                "|".join([re.escape(k) for k, v in key_values]), re.M)
            return lambda string: pattern.sub(replacement_function, string)

        def multiple_replace(string, *key_values):
            return multiple_replacer(*key_values)(string)

        # reportlab supports a set of text manipulation tags
        #  transform those HTML tags that aren't supported into reportlab
        #  supported tags
        lineBreakingTagReplacements = (u"<li>", u"<li><br/>"), (
            u"<h1>", u"<h1><font size='24'><br/><br/>"
        ), (u"</h1>", u"</font><br/></h1>"), (
            u"<h2>", u"<h2><font size='20'><br/><br/>"
        ), (u"</h2>", u"</font><br/></h2>"), (
            u"<h3>", u"<h3><font size='18'><br/><br/>"
        ), (u"</h3>", u"</font><br/></h3>"), (
            u"<h4>",
            u"<h4><font size='14'><br/>"), (u"</h4>", u"</font><br/></h4>"), (
                u"<h5>", u"<h5><font size='12'><br/>"
            ), (u"</h5>", u"</font><br/></h5>"), (u"<h6>", u"<h6><br/>"), (
                u"</h6>", u"<br/></h6>"), (u"<h7>", u"<h7><br/>"), (
                    u"</h7>", u"<br/></h7>"), (u"<h8>", u"<h8><br/>"), (
                        u"</h8>", u"<br/></h8>"), (u"<h9>", u"<h9><br/>"), (
                            u"</h9>", u"<br/></h9>"), (
                                u"<h10>",
                                u"<h10><br/>"), (u"</h10>", u"<br/></h10>"), (
                                    u"<br>", u"<br/>"), (u"<p>", u"<p><br/>")

        repText = multiple_replace(text, *lineBreakingTagReplacements)

        # need to remove some elements
        #  any elements that make references to external things -- don't want reportlab to try to resolve links
        #  reportlab doesn't like the title attribute
        removeElements = [
            '(<img[^>]*>)', '(</img>)', '(title="[^"]*")', '(<a[^>]*>)',
            '(</a>)'
        ]

        repText = re.sub('|'.join(removeElements), '', repText)
        logger.debug("renderHtml text='%s' repText='%s'" % (text, repText))

        self.renderText(repText, escapeText=False)

    def renderTextNoFormatting(self, text):
        self._story.append(TableText(text, fontManager=self._fontManager))

    def renderListItem(self, text, sequencerNum=None, style=None):
        if style is None:
            style = self._listTitleStyle
        if sequencerNum != None:
            text = "<seq id=" + str(sequencerNum) + "/>" + text
        self._story.append(
            Paragraph(
                self._fontManager.encodeTextForParagraph(su.escape(text)),
                style))

    def renderTable(self,
                    data,
                    title=None,
                    headerRow=None,
                    columnWidths=[],
                    columnHardWraps=[],
                    columnVAlignments=[],
                    displayLineNumbers=False):
        """ data should be a 2-D list of embedded lists e.g. [[a,b],[c,d],[e,f]]
            if headerRow is specified, then that row will be repeated at the top of each page if the table spans multiple pages,
            columnWidths ([int]) specifies the width of each column, if a column is not specified it will be sized automatically,
            columnHardWraps ([bool]) specifies whether or not to hard wrap a column, if a column is not specified it will be wrapped softly
            columnVAlignments (['TOP','MIDDLE','BOTTOM']) specifies vertical alignment of cells in a column, if not specified will be aligned at BOTTOM
            displayLineNumbers (bool) specifies whether or not to show line numbers
        """

        # handle title and header
        if title != None:
            self.renderText(title, style=self._tableTitleStyle)
        if headerRow != None:
            data.insert(0, headerRow)
            logger.debug("renderTable> headerRow: " + str(headerRow))

        # handle row numbers
        if displayLineNumbers:
            for index, row in enumerate(data):
                if index == 0 and headerRow != None:
                    row.insert(0, "")
                else:
                    rowNumber = index
                    if headerRow == None:
                        rowNumber = rowNumber + 1
                    row.insert(0, str(rowNumber))

        numDataCols = 0

        # iterate over the data in order to wrap each cell in a Paragraph flowable with a style
        numberCells = [
        ]  # an array of tuples identifying cells that are numbers
        cellWidthsByCol = []
        styledData = []
        for rowIdx, row in enumerate(data):
            styledRow = []

            for cellNum, cell in enumerate(row):
                # set the style based on columnHardWraps[cellNum]
                style = self._style
                if len(columnHardWraps) > cellNum:
                    if columnHardWraps[cellNum]:
                        style = self._hardWrapStyle

                cellFlowable = None
                if "##__SPARKLINE__##" in str(cell):
                    # build sparkline and insert into row
                    cellFlowable = Sparkline(str(cell))
                    styledRow.append(cellFlowable)
                else:
                    cellFlowable = TableText(
                        str(cell),
                        fontManager=self._fontManager,
                        maxCellHeight=self.maxTableCellHeight)
                    styledRow.append(cellFlowable)
                    if cellFlowable.isNumeric():
                        numberCells.append((cellNum, rowIdx))

                # build up matrix of cell widths by column
                if rowIdx == 0:
                    cellWidthsByCol.append([])
                cellWidthsByCol[cellNum].append(cellFlowable.width)

            numDataCols = len(styledRow)
            styledData.append(styledRow)

        columnWidths = self.determineColumnWidths(
            cellWidthsByCol,
            tableWidth=self.reportLabPaperSize[0] - self._MARGINS[0] -
            self._MARGINS[2],
            columnPadding=self._TABLE_COL_LEFT_PADDING +
            self._TABLE_COL_RIGHT_PADDING)

        # create the necessary table style commands to handle vertical alignment setting
        tableStyleCommands = []
        if columnVAlignments is not None:
            for i, valign in enumerate(columnVAlignments):
                tableStyleCommands.append(('VALIGN', (i, 0), (i, -1), valign))

        for numberCell in numberCells:
            tableStyleCommands.append(
                ('ALIGN', numberCell, numberCell, 'RIGHT'))

        # line to the right of all columns
        tableStyleCommands.append(
            ('LINEAFTER', (0, 0), (-2, -1), 0.25, colors.lightgrey))

        firstDataRow = 0
        if headerRow != None:
            tableStyleCommands.append(
                ('LINEBELOW', (0, 0), (-1, 0), 1, colors.black))
            firstDataRow = 1

        # lines to the bottom and to the right of each cell
        tableStyleCommands.append(
            ('LINEBELOW', (0, firstDataRow), (-1, -2), 0.25, colors.lightgrey))

        # tighten up the columns
        tableStyleCommands.append(
            ('LEFTPADDING', (0, 0), (-1, -1), self._TABLE_COL_LEFT_PADDING))
        tableStyleCommands.append(
            ('RIGHTPADDING', (0, 0), (-1, -1), self._TABLE_COL_RIGHT_PADDING))

        # create the Table flowable and insert into story
        table = Table(styledData,
                      repeatRows=(headerRow != None),
                      colWidths=columnWidths)
        table.setStyle(TableStyle(tableStyleCommands))
        self._story.append(table)

    def determineColumnWidths(self, cellWidthsByCol, tableWidth,
                              columnPadding):
        columnSizer = ColumnSizer(cellWidthsByCol, tableWidth, columnPadding)
        return columnSizer.getWidths()

    def renderSvgString(self, svgString, title=None):
        svgImageFlowable = pdfgen_svg.getSvgImageFromString(
            svgString, self._fontManager)
        if svgImageFlowable is None:
            self._log("renderSvg> svgImageFlowable for " + svgString +
                      " is invalid")
        else:
            if title != None:
                self.renderText(title, style=self._tableTitleStyle)
            self._story.append(svgImageFlowable)

    def save(self):
        #        self._log("starting save", logLevel='info')
        doc = PDFGenDocTemplate(self.outputFile,
                                pagesize=self.reportLabPaperSize)
        doc.setTitle(self._title)
        doc.splunkPaperSize = self.paperSize
        doc.setTimestamp(self._timestamp)
        doc.setFontManager(self._fontManager)
        if self._includeSplunkLogo:
            doc.setLogoSvgString(
                _splunkLogoSvg.replace("***logoTransformSize***",
                                       str(self.logoTransformSize)))
        self._log("Doc pageSize: " + str(getattr(doc, "pagesize")))

        for flowable in self._story:
            flowable.hAlign = 'CENTER'
#        self._log("before doc.build", logLevel='info')
        doc.build(self._story, onFirstPage=_footer, onLaterPages=_footer)
#        self._log("after doc.build", logLevel='info')
#        if len(wrapTimes) > 1:
#            self._log("wrap time stats; min=%s, max=%s, agg=%s, avg=%s, num=%s" % _getStats(wrapTimes), logLevel='info')
#        if len(stringWidthTimes) > 1:
#            self._log("width time stats; min=%s, max=%s, agg=%s, avg=%s, num=%s" % _getStats(stringWidthTimes), logLevel='info')
#        self._log("font manager cache length=%s" % len(self._fontManager._textWidthCache), logLevel='info')
#        if len(drawTimes) > 1:
#            self._log("draw time stats; min=%s, max=%s, agg=%s, avg=%s, num=%s" % _getStats(drawTimes), logLevel='info')

    def _log(self, msg, logLevel='debug'):
        if self._runningAsScript:
            print logLevel + " : " + msg
            return

        if logLevel == 'debug':
            logger.debug(msg)
        elif logLevel == 'info':
            logger.info(msg)
        elif logLevel == 'warning':
            logger.warning(msg)
        elif logLevel == 'error':
            logger.error(msg)