eric6/QScintilla/Exporters/ExporterPDF.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing an exporter for PDF.
8 """
9
10 from __future__ import unicode_literals
11
12 # This code is a port of the C++ code found in SciTE 1.74
13 # Original code: Copyright 1998-2006 by Neil Hodgson <neilh@scintilla.org>
14
15 from PyQt5.QtCore import Qt
16 from PyQt5.QtGui import QCursor, QFontInfo
17 from PyQt5.QtWidgets import QApplication
18 from PyQt5.Qsci import QsciScintilla
19
20 from E5Gui import E5MessageBox
21
22 from .ExporterBase import ExporterBase
23
24 import Preferences
25
26 PDF_FONT_DEFAULT = 1 # Helvetica
27 PDF_FONTSIZE_DEFAULT = 10
28 PDF_SPACING_DEFAULT = 1.2
29 PDF_MARGIN_DEFAULT = 72 # 1.0"
30 PDF_ENCODING = "WinAnsiEncoding"
31
32 PDFfontNames = [
33 "Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique",
34 "Helvetica", "Helvetica-Bold", "Helvetica-Oblique",
35 "Helvetica-BoldOblique", "Times-Roman", "Times-Bold", "Times-Italic",
36 "Times-BoldItalic"
37 ]
38 PDFfontAscenders = [629, 718, 699]
39 PDFfontDescenders = [157, 207, 217]
40 PDFfontWidths = [600, 0, 0]
41
42 PDFpageSizes = {
43 # name : (height, width)
44 "Letter": (792, 612),
45 "A4": (842, 595),
46 }
47
48
49 class PDFStyle(object):
50 """
51 Simple class to store the values of a PDF style.
52 """
53 def __init__(self):
54 """
55 Constructor
56 """
57 self.fore = ""
58 self.font = 0
59
60
61 class PDFObjectTracker(object):
62 """
63 Class to conveniently handle the tracking of PDF objects so that the
64 cross-reference table can be built (PDF1.4Ref(p39)).
65
66 All writes to the file are passed through a PDFObjectTracker object.
67 """
68 def __init__(self, file):
69 """
70 Constructor
71
72 @param file file object open for writing (file)
73 """
74 self.file = file
75 self.offsetList = []
76 self.index = 1
77
78 def write(self, objectData):
79 """
80 Public method to write the data to the file.
81
82 @param objectData data to be written (integer or string)
83 """
84 if isinstance(objectData, int):
85 self.file.write("{0:d}".format(objectData))
86 else:
87 self.file.write(objectData)
88
89 def add(self, objectData):
90 """
91 Public method to add a new object.
92
93 @param objectData data to be added (integer or string)
94 @return object number assigned to the supplied data (integer)
95 """
96 self.offsetList.append(self.file.tell())
97 self.write(self.index)
98 self.write(" 0 obj\n")
99 self.write(objectData)
100 self.write("endobj\n")
101 ind = self.index
102 self.index += 1
103 return ind
104
105 def xref(self):
106 """
107 Public method to build the xref table.
108
109 @return file offset of the xref table (integer)
110 """
111 xrefStart = self.file.tell()
112 self.write("xref\n0 ")
113 self.write(self.index)
114 # a xref entry *must* be 20 bytes long (PDF1.4Ref(p64))
115 # so extra space added; also the first entry is special
116 self.write("\n0000000000 65535 f \n")
117 ind = 0
118 while ind < len(self.offsetList):
119 self.write("{0:010d} 00000 n \n".format(self.offsetList[ind]))
120 ind += 1
121 return xrefStart
122
123
124 class PDFRender(object):
125 """
126 Class to manage line and page rendering.
127
128 Apart from startPDF, endPDF everything goes in via add() and nextLine()
129 so that line formatting and pagination can be done properly.
130 """
131 def __init__(self):
132 """
133 Constructor
134 """
135 self.pageStarted = False
136 self.firstLine = False
137 self.pageCount = 0
138 self.pageData = ""
139 self.style = {}
140 self.segStyle = ""
141 self.segment = ""
142 self.pageMargins = {
143 "left": 72,
144 "right": 72,
145 "top": 72,
146 "bottom": 72,
147 }
148 self.fontSize = 0
149 self.fontSet = 0
150 self.leading = 0.0
151 self.pageWidth = 0
152 self.pageHeight = 0
153 self.pageContentStart = 0
154 self.xPos = 0.0
155 self.yPos = 0.0
156 self.justWhiteSpace = False
157 self.oT = None
158
159 def fontToPoints(self, thousandths):
160 """
161 Public method to convert the font size to points.
162
163 @param thousandths font size (integer)
164 @return point size of the font (integer)
165 """
166 return self.fontSize * thousandths / 1000.0
167
168 def setStyle(self, style_):
169 """
170 Public method to set a style.
171
172 @param style_ style to be set (integer)
173 @return the PDF string to set the given style (string)
174 """
175 styleNext = style_
176 if style_ == -1:
177 styleNext = self.styleCurrent
178
179 buf = ""
180 if styleNext != self.styleCurrent or style_ == -1:
181 if self.style[self.styleCurrent].font != \
182 self.style[styleNext].font or style_ == -1:
183 buf += "/F{0:d} {1:d} Tf ".format(
184 self.style[styleNext].font + 1, self.fontSize)
185 if self.style[self.styleCurrent].fore != \
186 self.style[styleNext].fore or style_ == -1:
187 buf += "{0}rg ".format(self.style[styleNext].fore)
188 return buf
189
190 def startPDF(self):
191 """
192 Public method to start the PDF document.
193 """
194 if self.fontSize <= 0:
195 self.fontSize = PDF_FONTSIZE_DEFAULT
196
197 # leading is the term for distance between lines
198 self.leading = self.fontSize * PDF_SPACING_DEFAULT
199
200 # sanity check for page size and margins
201 pageWidthMin = int(self.leading) + \
202 self.pageMargins["left"] + self.pageMargins["right"]
203 if self.pageWidth < pageWidthMin:
204 self.pageWidth = pageWidthMin
205 pageHeightMin = int(self.leading) + \
206 self.pageMargins["top"] + self.pageMargins["bottom"]
207 if self.pageHeight < pageHeightMin:
208 self.pageHeight = pageHeightMin
209
210 # start to write PDF file here (PDF1.4Ref(p63))
211 # ASCII>127 characters to indicate binary-possible stream
212 self.oT.write("%PDF-1.3\n%�쏢\n")
213 self.styleCurrent = QsciScintilla.STYLE_DEFAULT
214
215 # build objects for font resources; note that font objects are
216 # *expected* to start from index 1 since they are the first objects
217 # to be inserted (PDF1.4Ref(p317))
218 for i in range(4):
219 buffer = \
220 "<</Type/Font/Subtype/Type1/Name/F{0:d}/BaseFont/{1}/" \
221 "Encoding/{2}>>\n".format(
222 i + 1, PDFfontNames[self.fontSet * 4 + i], PDF_ENCODING)
223 self.oT.add(buffer)
224
225 self.pageContentStart = self.oT.index
226
227 def endPDF(self):
228 """
229 Public method to end the PDF document.
230 """
231 if self.pageStarted:
232 # flush buffers
233 self.endPage()
234
235 # refer to all used or unused fonts for simplicity
236 resourceRef = self.oT.add(
237 "<</ProcSet[/PDF/Text]\n/Font<</F1 1 0 R/F2 2 0 R/F3 3 0 R/"
238 "F4 4 0 R>> >>\n")
239
240 # create all the page objects (PDF1.4Ref(p88))
241 # forward reference pages object; calculate its object number
242 pageObjectStart = self.oT.index
243 pagesRef = pageObjectStart + self.pageCount
244 for i in range(self.pageCount):
245 buffer = "<</Type/Page/Parent {0:d} 0 R\n" \
246 "/MediaBox[ 0 0 {1:d} {2:d}]\n" \
247 "/Contents {3:d} 0 R\n" \
248 "/Resources {4:d} 0 R\n>>\n".format(
249 pagesRef, self.pageWidth, self.pageHeight,
250 self.pageContentStart + i, resourceRef)
251 self.oT.add(buffer)
252
253 # create page tree object (PDF1.4Ref(p86))
254 self.pageData = "<</Type/Pages/Kids[\n"
255 for i in range(self.pageCount):
256 self.pageData += "{0:d} 0 R\n".format(pageObjectStart + i)
257 self.pageData += "]/Count {0:d}\n>>\n".format(self.pageCount)
258 self.oT.add(self.pageData)
259
260 # create catalog object (PDF1.4Ref(p83))
261 buffer = "<</Type/Catalog/Pages {0:d} 0 R >>\n".format(pagesRef)
262 catalogRef = self.oT.add(buffer)
263
264 # append the cross reference table (PDF1.4Ref(p64))
265 xref = self.oT.xref()
266
267 # end the file with the trailer (PDF1.4Ref(p67))
268 buffer = \
269 "trailer\n<< /Size {0:d} /Root {1:d} 0 R\n>>\nstartxref\n{2:d}\n" \
270 "%%EOF\n".format(self.oT.index, catalogRef, xref)
271 self.oT.write(buffer)
272
273 def add(self, ch, style_):
274 """
275 Public method to add a character to the page.
276
277 @param ch character to add (string)
278 @param style_ number of the style of the character (integer)
279 """
280 if not self.pageStarted:
281 self.startPage()
282
283 # get glyph width (TODO future non-monospace handling)
284 glyphWidth = self.fontToPoints(PDFfontWidths[self.fontSet])
285 self.xPos += glyphWidth
286
287 # if cannot fit into a line, flush, wrap to next line
288 if self.xPos > self.pageWidth - self.pageMargins["right"]:
289 self.nextLine()
290 self.xPos += glyphWidth
291
292 # if different style, then change to style
293 if style_ != self.styleCurrent:
294 self.flushSegment()
295 # output code (if needed) for new style
296 self.segStyle = self.setStyle(style_)
297 self.stylePrev = self.styleCurrent
298 self.styleCurrent = style_
299
300 # escape these characters
301 if ch == ')' or ch == '(' or ch == '\\':
302 self.segment += '\\'
303 if ch != ' ':
304 self.justWhiteSpace = False
305 self.segment += ch # add to segment data
306
307 def flushSegment(self):
308 """
309 Public method to flush a segment of data.
310 """
311 if len(self.segment) > 0:
312 if self.justWhiteSpace: # optimise
313 self.styleCurrent = self.stylePrev
314 else:
315 self.pageData += self.segStyle
316 self.pageData += "({0})Tj\n".format(self.segment)
317 self.segment = ""
318 self.segStyle = ""
319 self.justWhiteSpace = True
320
321 def startPage(self):
322 """
323 Public method to start a new page.
324 """
325 self.pageStarted = True
326 self.firstLine = True
327 self.pageCount += 1
328 fontAscender = self.fontToPoints(PDFfontAscenders[self.fontSet])
329 self.yPos = self.pageHeight - self.pageMargins["top"] - fontAscender
330
331 # start a new page
332 buffer = "BT 1 0 0 1 {0:d} {1:d} Tm\n".format(
333 self.pageMargins["left"], int(self.yPos))
334
335 # force setting of initial font, colour
336 self.segStyle = self.setStyle(-1)
337 buffer += self.segStyle
338 self.pageData = buffer
339 self.xPos = self.pageMargins["left"]
340 self.segment = ""
341 self.flushSegment()
342
343 def endPage(self):
344 """
345 Public method to end a page.
346 """
347 self.pageStarted = False
348 self.flushSegment()
349
350 # build actual text object; +3 is for "ET\n"
351 # PDF1.4Ref(p38) EOL marker preceding endstream not counted
352 textObj = "<</Length {0:d}>>\nstream\n{1}ET\nendstream\n".format(
353 len(self.pageData) - 1 + 3, self.pageData)
354 self.oT.add(textObj)
355
356 def nextLine(self):
357 """
358 Public method to start a new line.
359 """
360 if not self.pageStarted:
361 self.startPage()
362
363 self.xPos = self.pageMargins["left"]
364 self.flushSegment()
365
366 # PDF follows cartesian coords, subtract -> down
367 self.yPos -= self.leading
368 fontDescender = self.fontToPoints(PDFfontDescenders[self.fontSet])
369 if self.yPos < self.pageMargins["bottom"] + fontDescender:
370 self.endPage()
371 self.startPage()
372 return
373
374 if self.firstLine:
375 # avoid breakage due to locale setting
376 f = int(self.leading * 10 + 0.5)
377 buffer = "0 -{0:d}.{1:d} TD\n".format(f // 10, f % 10)
378 self.firstLine = False
379 else:
380 buffer = "T*\n"
381 self.pageData += buffer
382
383
384 class ExporterPDF(ExporterBase):
385 """
386 Class implementing an exporter for PDF.
387 """
388 def __init__(self, editor, parent=None):
389 """
390 Constructor
391
392 @param editor reference to the editor object (QScintilla.Editor.Editor)
393 @param parent parent object of the exporter (QObject)
394 """
395 ExporterBase.__init__(self, editor, parent)
396
397 def __getPDFRGB(self, color):
398 """
399 Private method to convert a color object to the correct PDF color.
400
401 @param color color object to convert (QColor)
402 @return PDF color description (string)
403 """
404 pdfColor = ""
405 for component in [color.red(), color.green(), color.blue()]:
406 c = (component * 1000 + 127) // 255
407 if c == 0 or c == 1000:
408 pdfColor += "{0:d} ".format(c // 1000)
409 else:
410 pdfColor += "0.{0:03d} ".format(c)
411 return pdfColor
412
413 def exportSource(self):
414 """
415 Public method performing the export.
416 """
417 self.pr = PDFRender()
418
419 filename = self._getFileName(self.tr("PDF Files (*.pdf)"))
420 if not filename:
421 return
422
423 try:
424 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
425 QApplication.processEvents()
426
427 self.editor.recolor(0, -1)
428 lex = self.editor.getLexer()
429
430 tabSize = self.editor.getEditorConfig("TabWidth")
431 if tabSize == 0:
432 tabSize = 4
433
434 # get magnification value to add to default screen font size
435 self.pr.fontSize = Preferences.getEditorExporter(
436 "PDF/Magnification")
437
438 # set font family according to face name
439 fontName = Preferences.getEditorExporter("PDF/Font")
440 self.pr.fontSet = PDF_FONT_DEFAULT
441 if fontName == "Courier":
442 self.pr.fontSet = 0
443 elif fontName == "Helvetica":
444 self.pr.fontSet = 1
445 elif fontName == "Times":
446 self.pr.fontSet = 2
447
448 # page size: height, width,
449 pageSize = Preferences.getEditorExporter("PDF/PageSize")
450 try:
451 pageDimensions = PDFpageSizes[pageSize]
452 except KeyError:
453 pageDimensions = PDFpageSizes["A4"]
454 self.pr.pageHeight = pageDimensions[0]
455 self.pr.pageWidth = pageDimensions[1]
456
457 # page margins: left, right, top, bottom
458 # < 0 to use PDF default values
459 val = Preferences.getEditorExporter("PDF/MarginLeft")
460 if val < 0:
461 self.pr.pageMargins["left"] = PDF_MARGIN_DEFAULT
462 else:
463 self.pr.pageMargins["left"] = val
464 val = Preferences.getEditorExporter("PDF/MarginRight")
465 if val < 0:
466 self.pr.pageMargins["right"] = PDF_MARGIN_DEFAULT
467 else:
468 self.pr.pageMargins["right"] = val
469 val = Preferences.getEditorExporter("PDF/MarginTop")
470 if val < 0:
471 self.pr.pageMargins["top"] = PDF_MARGIN_DEFAULT
472 else:
473 self.pr.pageMargins["top"] = val
474 val = Preferences.getEditorExporter("PDF/MarginBottom")
475 if val < 0:
476 self.pr.pageMargins["bottom"] = PDF_MARGIN_DEFAULT
477 else:
478 self.pr.pageMargins["bottom"] = val
479
480 # collect all styles available for that 'language'
481 # or the default style if no language is available...
482 if lex:
483 istyle = 0
484 while istyle <= QsciScintilla.STYLE_MAX:
485 if (istyle <= QsciScintilla.STYLE_DEFAULT or
486 istyle > QsciScintilla.STYLE_LASTPREDEFINED):
487 if lex.description(istyle) or \
488 istyle == QsciScintilla.STYLE_DEFAULT:
489 style = PDFStyle()
490
491 font = lex.font(istyle)
492 if font.italic():
493 style.font |= 2
494 if font.bold():
495 style.font |= 1
496
497 colour = lex.color(istyle)
498 style.fore = self.__getPDFRGB(colour)
499 self.pr.style[istyle] = style
500
501 # grab font size from default style
502 if istyle == QsciScintilla.STYLE_DEFAULT:
503 fontSize = QFontInfo(font).pointSize()
504 if fontSize > 0:
505 self.pr.fontSize += fontSize
506 else:
507 self.pr.fontSize = PDF_FONTSIZE_DEFAULT
508
509 istyle += 1
510 else:
511 style = PDFStyle()
512
513 font = Preferences.getEditorOtherFonts("DefaultFont")
514 if font.italic():
515 style.font |= 2
516 if font.bold():
517 style.font |= 1
518
519 colour = self.editor.color()
520 style.fore = self.__getPDFRGB(colour)
521 self.pr.style[0] = style
522 self.pr.style[QsciScintilla.STYLE_DEFAULT] = style
523
524 fontSize = QFontInfo(font).pointSize()
525 if fontSize > 0:
526 self.pr.fontSize += fontSize
527 else:
528 self.pr.fontSize = PDF_FONTSIZE_DEFAULT
529
530 try:
531 # save file in win ansi using cp1250
532 f = open(filename, "w", encoding="cp1250",
533 errors="backslashreplace")
534
535 # initialise PDF rendering
536 ot = PDFObjectTracker(f)
537 self.pr.oT = ot
538 self.pr.startPDF()
539
540 # do here all the writing
541 lengthDoc = self.editor.length()
542
543 if lengthDoc == 0:
544 self.pr.nextLine() # enable zero length docs
545 else:
546 pos = 0
547 column = 0
548 utf8 = self.editor.isUtf8()
549 utf8Ch = b""
550 utf8Len = 0
551
552 while pos < lengthDoc:
553 ch = self.editor.byteAt(pos)
554 style = self.editor.styleAt(pos)
555
556 if ch == b'\t':
557 # expand tabs
558 ts = tabSize - (column % tabSize)
559 column += ts
560 self.pr.add(' ' * ts, style)
561 elif ch == b'\r' or ch == b'\n':
562 if ch == b'\r' and \
563 self.editor.byteAt(pos + 1) == b'\n':
564 pos += 1
565 # close and begin a newline...
566 self.pr.nextLine()
567 column = 0
568 else:
569 # write the character normally...
570 if ord(ch) > 127 and utf8:
571 utf8Ch += ch
572 if utf8Len == 0:
573 if (utf8Ch[0] & 0xF0) == 0xF0:
574 utf8Len = 4
575 elif (utf8Ch[0] & 0xE0) == 0xE0:
576 utf8Len = 3
577 elif (utf8Ch[0] & 0xC0) == 0xC0:
578 utf8Len = 2
579 column -= 1
580 # will be incremented again later
581 elif len(utf8Ch) == utf8Len:
582 ch = utf8Ch.decode('utf8')
583 self.pr.add(ch, style)
584 utf8Ch = b""
585 utf8Len = 0
586 else:
587 column -= 1
588 # will be incremented again later
589 else:
590 self.pr.add(ch.decode(), style)
591 column += 1
592
593 pos += 1
594
595 # write required stuff and close the PDF file
596 self.pr.endPDF()
597 f.close()
598 except IOError as err:
599 QApplication.restoreOverrideCursor()
600 E5MessageBox.critical(
601 self.editor,
602 self.tr("Export source"),
603 self.tr(
604 """<p>The source could not be exported to"""
605 """ <b>{0}</b>.</p><p>Reason: {1}</p>""")
606 .format(filename, str(err)))
607 finally:
608 QApplication.restoreOverrideCursor()

eric ide

mercurial