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)