--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/QScintilla/Exporters/ExporterPDF.py Sat May 15 18:45:04 2021 +0200 @@ -0,0 +1,634 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2007 - 2021 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 PyQt5.QtGui import QFontInfo +from PyQt5.Qsci import QsciScintilla + +from E5Gui import E5MessageBox +from E5Gui.E5OverrideCursor import E5OverrideCursor + +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: + """ + Simple class to store the values of a PDF style. + """ + def __init__(self): + """ + Constructor + """ + self.fore = "" + self.font = 0 + + +class PDFObjectTracker: + """ + 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("{0:d}".format(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("{0:010d} 00000 n \n".format(self.offsetList[ind])) + ind += 1 + return xrefStart + + +class PDFRender: + """ + 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. + + @param thousandths font size (integer) + @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{0:d} {1:d} Tf ".format( + self.style[styleNext].font + 1, self.fontSize) + if ( + (self.style[self.styleCurrent].fore != + self.style[styleNext].fore) or + style_ == -1 + ): + buf += "{0}rg ".format(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{0:d}/BaseFont/{1}/" + "Encoding/{2}>>\n" + ).format( + 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 {0:d} 0 R\n" + "/MediaBox[ 0 0 {1:d} {2:d}]\n" + "/Contents {3:d} 0 R\n" + "/Resources {4:d} 0 R\n>>\n" + ).format( + 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 += "{0:d} 0 R\n".format(pageObjectStart + i) + self.pageData += "]/Count {0:d}\n>>\n".format(self.pageCount) + self.oT.add(self.pageData) + + # create catalog object (PDF1.4Ref(p83)) + buffer = "<</Type/Catalog/Pages {0:d} 0 R >>\n".format(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 {0:d} /Root {1:d} 0 R\n>>\nstartxref\n{2:d}\n" + "%%EOF\n" + ).format(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 in (')', '(', '\\'): + 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 += "({0})Tj\n".format(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 {0:d} {1:d} Tm\n".format( + 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 {0:d}>>\nstream\n{1}ET\nendstream\n".format( + 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 -{0:d}.{1:d} TD\n".format(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 in (0, 1000): + pdfColor += "{0:d} ".format(c // 1000) + else: + pdfColor += "0.{0:03d} ".format(c) + return pdfColor + + def exportSource(self): + """ + Public method performing the export. + """ + self.pr = PDFRender() + + filename = self._getFileName(self.tr("PDF Files (*.pdf)")) + if not filename: + return + + self.editor.recolor(0, -1) + lex = self.editor.getLexer() + + tabSize = self.editor.getEditorConfig("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 + + # get substyles + subs_start, subs_count = self.editor.getSubStyleRange( + istyle) + for subs_idx in range(subs_count): + style = PDFStyle() + font = lex.font(subs_start + subs_idx) + if font.italic(): + style.font |= 2 + if font.bold(): + style.font |= 1 + + colour = lex.color(subs_start + subs_idx) + style.fore = self.__getPDFRGB(colour) + # styleAt returns negative numbers for substyles + self.pr.style[subs_idx - subs_start] = 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 + + with E5OverrideCursor(), open(filename, "w", encoding="cp1250", + errors="backslashreplace") as f: + # save file in win ansi using cp1250 + try: + # 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 = b"" + utf8Len = 0 + + while pos < lengthDoc: + ch = self.editor.byteAt(pos) + style = self.editor.styleAt(pos) + + if ch == b'\t': + # expand tabs + ts = tabSize - (column % tabSize) + column += ts + self.pr.add(' ' * ts, style) + elif ch in (b'\r', b'\n'): + if ( + ch == b'\r' and + self.editor.byteAt(pos + 1) == b'\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 (utf8Ch[0] & 0xF0) == 0xF0: + utf8Len = 4 + elif (utf8Ch[0] & 0xE0) == 0xE0: + utf8Len = 3 + elif (utf8Ch[0] & 0xC0) == 0xC0: + utf8Len = 2 + column -= 1 + # will be incremented again later + elif len(utf8Ch) == utf8Len: + ch = utf8Ch.decode('utf8') + self.pr.add(ch, style) + utf8Ch = b"" + utf8Len = 0 + else: + column -= 1 + # will be incremented again later + else: + self.pr.add(ch.decode(), style) + column += 1 + + pos += 1 + + # write required stuff and close the PDF file + self.pr.endPDF() + except OSError as err: + E5MessageBox.critical( + self.editor, + self.tr("Export source"), + self.tr( + """<p>The source could not be exported to""" + """ <b>{0}</b>.</p><p>Reason: {1}</p>""") + .format(filename, str(err)))