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