src/eric7/QScintilla/Exporters/ExporterPDF.py

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

eric ide

mercurial