|
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() |