Sat, 02 Jan 2010 15:11:35 +0000
First commit after changing to Python 3.1.
# -*- coding: utf-8 -*- # Copyright (c) 2007 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing an exporter for PDF. """ # This code is a port of the C++ code found in SciTE 1.74 # Original code: Copyright 1998-2006 by Neil Hodgson <neilh@scintilla.org> from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.Qsci import QsciScintilla from .ExporterBase import ExporterBase import Preferences PDF_FONT_DEFAULT = 1 # Helvetica PDF_FONTSIZE_DEFAULT = 10 PDF_SPACING_DEFAULT = 1.2 PDF_MARGIN_DEFAULT = 72 # 1.0" PDF_ENCODING = "WinAnsiEncoding" PDFfontNames = [ "Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique", "Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique", "Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic" ] PDFfontAscenders = [629, 718, 699] PDFfontDescenders = [157, 207, 217] PDFfontWidths = [600, 0, 0] PDFpageSizes = { # name : (height, width) "Letter" : (792, 612), "A4" : (842, 595), } class PDFStyle(object): """ Simple class to store the values of a PDF style. """ def __init__(self): """ Constructor """ self.fore = "" self.font = 0 class PDFObjectTracker(object): """ Class to conveniently handle the tracking of PDF objects so that the cross-reference table can be built (PDF1.4Ref(p39)) All writes to the file are passed through a PDFObjectTracker object. """ def __init__(self, file): """ Constructor @param file file object open for writing (file) """ self.file = file self.offsetList = [] self.index = 1 def write(self, objectData): """ Public method to write the data to the file. @param objectData data to be written (integer or string) """ if isinstance(objectData, int): self.file.write("%d" % objectData) else: self.file.write(objectData) def add(self, objectData): """ Public method to add a new object. @param objectData data to be added (integer or string) @return object number assigned to the supplied data (integer) """ self.offsetList.append(self.file.tell()) self.write(self.index) self.write(" 0 obj\n") self.write(objectData) self.write("endobj\n") ind = self.index self.index += 1 return ind def xref(self): """ Public method to build the xref table. @return file offset of the xref table (integer) """ xrefStart = self.file.tell() self.write("xref\n0 ") self.write(self.index) # a xref entry *must* be 20 bytes long (PDF1.4Ref(p64)) # so extra space added; also the first entry is special self.write("\n0000000000 65535 f \n") ind = 0 while ind < len(self.offsetList): self.write("%010d 00000 n \n" % self.offsetList[ind]) ind += 1 return xrefStart class PDFRender(object): """ Class to manage line and page rendering. Apart from startPDF, endPDF everything goes in via add() and nextLine() so that line formatting and pagination can be done properly. """ def __init__(self): """ Constructor """ self.pageStarted = False self.firstLine = False self.pageCount = 0 self.pageData = "" self.style = {} self.segStyle = "" self.segment = "" self.pageMargins = { "left" : 72, "right" : 72, "top" : 72, "bottom" : 72, } self.fontSize = 0 self.fontSet = 0 self.leading = 0.0 self.pageWidth = 0 self.pageHeight = 0 self.pageContentStart = 0 self.xPos = 0.0 self.yPos = 0.0 self.justWhiteSpace = False self.oT = None def fontToPoints(self, thousandths): """ Public method to convert the font size to points. @return point size of the font (integer) """ return self.fontSize * thousandths / 1000.0 def setStyle(self, style_): """ Public method to set a style. @param style_ style to be set (integer) @return the PDF string to set the given style (string) """ styleNext = style_ if style_ == -1: styleNext = self.styleCurrent buf = "" if styleNext != self.styleCurrent or style_ == -1: if self.style[self.styleCurrent].font != self.style[styleNext].font or \ style_ == -1: buf += "/F%d %d Tf " % (self.style[styleNext].font + 1, self.fontSize) if self.style[self.styleCurrent].fore != self.style[styleNext].fore or \ style_ == -1: buf += "%srg " % self.style[styleNext].fore return buf def startPDF(self): """ Public method to start the PDF document. """ if self.fontSize <= 0: self.fontSize = PDF_FONTSIZE_DEFAULT # leading is the term for distance between lines self.leading = self.fontSize * PDF_SPACING_DEFAULT # sanity check for page size and margins pageWidthMin = int(self.leading) + \ self.pageMargins["left"] + self.pageMargins["right"] if self.pageWidth < pageWidthMin: self.pageWidth = pageWidthMin pageHeightMin = int(self.leading) + \ self.pageMargins["top"] + self.pageMargins["bottom"] if self.pageHeight < pageHeightMin: self.pageHeight = pageHeightMin # start to write PDF file here (PDF1.4Ref(p63)) # ASCII>127 characters to indicate binary-possible stream self.oT.write("%PDF-1.3\n%�쏢\n") self.styleCurrent = QsciScintilla.STYLE_DEFAULT # build objects for font resources; note that font objects are # *expected* to start from index 1 since they are the first objects # to be inserted (PDF1.4Ref(p317)) for i in range(4): buffer = \ "<</Type/Font/Subtype/Type1/Name/F%d/BaseFont/%s/Encoding/%s>>\n" % \ (i + 1, PDFfontNames[self.fontSet * 4 + i], PDF_ENCODING) self.oT.add(buffer) self.pageContentStart = self.oT.index def endPDF(self): """ Public method to end the PDF document. """ if self.pageStarted: # flush buffers self.endPage() # refer to all used or unused fonts for simplicity resourceRef = self.oT.add( "<</ProcSet[/PDF/Text]\n/Font<</F1 1 0 R/F2 2 0 R/F3 3 0 R/F4 4 0 R>> >>\n") # create all the page objects (PDF1.4Ref(p88)) # forward reference pages object; calculate its object number pageObjectStart = self.oT.index pagesRef = pageObjectStart + self.pageCount for i in range(self.pageCount): buffer = "<</Type/Page/Parent %d 0 R\n" \ "/MediaBox[ 0 0 %d %d]\n" \ "/Contents %d 0 R\n" \ "/Resources %d 0 R\n>>\n" % \ (pagesRef, self.pageWidth, self.pageHeight, self.pageContentStart + i, resourceRef) self.oT.add(buffer) # create page tree object (PDF1.4Ref(p86)) self.pageData = "<</Type/Pages/Kids[\n" for i in range(self.pageCount): self.pageData += "%d 0 R\n" % (pageObjectStart + i) self.pageData += "]/Count %d\n>>\n" % self.pageCount self.oT.add(self.pageData) # create catalog object (PDF1.4Ref(p83)) buffer = "<</Type/Catalog/Pages %d 0 R >>\n" % pagesRef catalogRef = self.oT.add(buffer) # append the cross reference table (PDF1.4Ref(p64)) xref = self.oT.xref() # end the file with the trailer (PDF1.4Ref(p67)) buffer = "trailer\n<< /Size %d /Root %d 0 R\n>>\nstartxref\n%d\n%%%%EOF\n" % \ (self.oT.index, catalogRef, xref) self.oT.write(buffer) def add(self, ch, style_): """ Public method to add a character to the page. @param ch character to add (string) @param style_ number of the style of the character (integer) """ if not self.pageStarted: self.startPage() # get glyph width (TODO future non-monospace handling) glyphWidth = self.fontToPoints(PDFfontWidths[self.fontSet]) self.xPos += glyphWidth # if cannot fit into a line, flush, wrap to next line if self.xPos > self.pageWidth - self.pageMargins["right"]: self.nextLine() self.xPos += glyphWidth # if different style, then change to style if style_ != self.styleCurrent: self.flushSegment() # output code (if needed) for new style self.segStyle = self.setStyle(style_) self.stylePrev = self.styleCurrent self.styleCurrent = style_ # escape these characters if ch == ')' or ch == '(' or ch == '\\': self.segment += '\\' if ch != ' ': self.justWhiteSpace = False self.segment += ch # add to segment data def flushSegment(self): """ Public method to flush a segment of data. """ if len(self.segment) > 0: if self.justWhiteSpace: # optimise self.styleCurrent = self.stylePrev else: self.pageData += self.segStyle self.pageData += "(%s)Tj\n" % self.segment self.segment = "" self.segStyle = "" self.justWhiteSpace = True def startPage(self): """ Public method to start a new page. """ self.pageStarted = True self.firstLine = True self.pageCount += 1 fontAscender = self.fontToPoints(PDFfontAscenders[self.fontSet]) self.yPos = self.pageHeight - self.pageMargins["top"] - fontAscender # start a new page buffer = "BT 1 0 0 1 %d %d Tm\n" % (self.pageMargins["left"], int(self.yPos)) # force setting of initial font, colour self.segStyle = self.setStyle(-1) buffer += self.segStyle self.pageData = buffer self.xPos = self.pageMargins["left"] self.segment = "" self.flushSegment() def endPage(self): """ Public method to end a page. """ self.pageStarted = False self.flushSegment() # build actual text object; +3 is for "ET\n" # PDF1.4Ref(p38) EOL marker preceding endstream not counted textObj = "<</Length %d>>\nstream\n%sET\nendstream\n" % \ (len(self.pageData) - 1 + 3, self.pageData) self.oT.add(textObj) def nextLine(self): """ Public method to start a new line. """ if not self.pageStarted: self.startPage() self.xPos = self.pageMargins["left"] self.flushSegment() # PDF follows cartesian coords, subtract -> down self.yPos -= self.leading fontDescender = self.fontToPoints(PDFfontDescenders[self.fontSet]) if self.yPos < self.pageMargins["bottom"] + fontDescender: self.endPage() self.startPage() return if self.firstLine: # avoid breakage due to locale setting f = int(self.leading * 10 + 0.5) buffer = "0 -%d.%d TD\n" % (f // 10, f % 10) self.firstLine = False else: buffer = "T*\n" self.pageData += buffer class ExporterPDF(ExporterBase): """ Class implementing an exporter for PDF. """ def __init__(self, editor, parent = None): """ Constructor @param editor reference to the editor object (QScintilla.Editor.Editor) @param parent parent object of the exporter (QObject) """ ExporterBase.__init__(self, editor, parent) def __getPDFRGB(self, color): """ Private method to convert a color object to the correct PDF color. @param color color object to convert (QColor) @return PDF color description (string) """ pdfColor = "" for component in [color.red(), color.green(), color.blue()]: c = (component * 1000 + 127) // 255 if c == 0 or c == 1000: pdfColor += "%d " % (c // 1000) else: pdfColor += "0.%03d " % c return pdfColor def exportSource(self): """ Public method performing the export. """ self.pr = PDFRender() filename = self._getFileName(self.trUtf8("PDF Files (*.pdf)")) if not filename: return try: QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() self.editor.recolor(0, -1) lex = self.editor.getLexer() tabSize = Preferences.getEditor("TabWidth") if tabSize == 0: tabSize = 4 # get magnification value to add to default screen font size self.pr.fontSize = Preferences.getEditorExporter("PDF/Magnification") # set font family according to face name fontName = Preferences.getEditorExporter("PDF/Font") self.pr.fontSet = PDF_FONT_DEFAULT if fontName == "Courier": self.pr.fontSet = 0 elif fontName == "Helvetica": self.pr.fontSet = 1 elif fontName == "Times": self.pr.fontSet = 2 # page size: height, width, pageSize = Preferences.getEditorExporter("PDF/PageSize") try: pageDimensions = PDFpageSizes[pageSize] except KeyError: pageDimensions = PDFpageSizes["A4"] self.pr.pageHeight = pageDimensions[0] self.pr.pageWidth = pageDimensions[1] # page margins: left, right, top, bottom # < 0 to use PDF default values val = Preferences.getEditorExporter("PDF/MarginLeft") if val < 0: self.pr.pageMargins["left"] = PDF_MARGIN_DEFAULT else: self.pr.pageMargins["left"] = val val = Preferences.getEditorExporter("PDF/MarginRight") if val < 0: self.pr.pageMargins["right"] = PDF_MARGIN_DEFAULT else: self.pr.pageMargins["right"] = val val = Preferences.getEditorExporter("PDF/MarginTop") if val < 0: self.pr.pageMargins["top"] = PDF_MARGIN_DEFAULT else: self.pr.pageMargins["top"] = val val = Preferences.getEditorExporter("PDF/MarginBottom") if val < 0: self.pr.pageMargins["bottom"] = PDF_MARGIN_DEFAULT else: self.pr.pageMargins["bottom"] = val # collect all styles available for that 'language' # or the default style if no language is available... if lex: istyle = 0 while istyle <= QsciScintilla.STYLE_MAX: if (istyle <= QsciScintilla.STYLE_DEFAULT or \ istyle > QsciScintilla.STYLE_LASTPREDEFINED): if lex.description(istyle) or \ istyle == QsciScintilla.STYLE_DEFAULT: style = PDFStyle() font = lex.font(istyle) if font.italic(): style.font |= 2 if font.bold(): style.font |= 1 colour = lex.color(istyle) style.fore = self.__getPDFRGB(colour) self.pr.style[istyle] = style # grab font size from default style if istyle == QsciScintilla.STYLE_DEFAULT: fontSize = QFontInfo(font).pointSize() if fontSize > 0: self.pr.fontSize += fontSize else: self.pr.fontSize = PDF_FONTSIZE_DEFAULT istyle += 1 else: style = PDFStyle() font = Preferences.getEditorOtherFonts("DefaultFont") if font.italic(): style.font |= 2 if font.bold(): style.font |= 1 colour = self.editor.color() style.fore = self.__getPDFRGB(colour) self.pr.style[0] = style self.pr.style[QsciScintilla.STYLE_DEFAULT] = style fontSize = QFontInfo(font).pointSize() if fontSize > 0: self.pr.fontSize += fontSize else: self.pr.fontSize = PDF_FONTSIZE_DEFAULT try: f = open(filename, "w") # initialise PDF rendering ot = PDFObjectTracker(f) self.pr.oT = ot self.pr.startPDF() # do here all the writing lengthDoc = self.editor.length() if lengthDoc == 0: self.pr.nextLine() # enable zero length docs else: pos = 0 column = 0 utf8 = self.editor.isUtf8() utf8Ch = "" utf8Len = 0 while pos < lengthDoc: ch = self.editor.rawCharAt(pos) style = self.editor.styleAt(pos) if ch == '\t': # expand tabs ts = tabSize - (column % tabSize) column += ts self.pr.add(' ' * ts, style) elif ch == '\r' or ch == '\n': if ch == '\r' and self.editor.rawCharAt(pos + 1) == '\n': pos += 1 # close and begin a newline... self.pr.nextLine() column = 0 else: # write the character normally... if ord(ch) > 127 and utf8: utf8Ch += ch if utf8Len == 0: if (ord(utf8Ch[0]) & 0xF0) == 0xF0: utf8Len = 4 elif (ord(utf8Ch[0]) & 0xE0) == 0xE0: utf8Len = 3 elif (ord(utf8Ch[0]) & 0xC0) == 0xC0: utf8Len = 2 column -= 1 # will be incremented again later elif len(utf8Ch) == utf8Len: # convert utf-8 character to win ansi using cp1250 ch = utf8Ch.decode('utf8')\ .encode('cp1250', 'replace') self.pr.add(ch, style) utf8Ch = "" utf8Len = 0 else: column -= 1 # will be incremented again later else: self.pr.add(ch, style) column += 1 pos += 1 # write required stuff and close the PDF file self.pr.endPDF() f.close() except IOError as err: QApplication.restoreOverrideCursor() QMessageBox.critical(self.editor, self.trUtf8("Export source"), self.trUtf8( """<p>The source could not be exported to <b>{0}</b>.</p>""" """<p>Reason: {1}</p>""")\ .format(filename, str(err)), QMessageBox.StandardButtons(\ QMessageBox.Ok)) finally: QApplication.restoreOverrideCursor()