QScintilla/Exporters/ExporterPDF.py

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

eric ide

mercurial