|
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 RTF. |
|
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 import time |
|
14 |
|
15 from PyQt6.QtGui import QFontInfo |
|
16 from PyQt6.Qsci import QsciScintilla |
|
17 |
|
18 from EricWidgets import EricMessageBox |
|
19 from EricGui.EricOverrideCursor import EricOverrideCursor |
|
20 |
|
21 from .ExporterBase import ExporterBase |
|
22 |
|
23 import Preferences |
|
24 |
|
25 |
|
26 class ExporterRTF(ExporterBase): |
|
27 """ |
|
28 Class implementing an exporter for RTF. |
|
29 """ |
|
30 RTF_HEADEROPEN = "{\\rtf1\\ansi\\deff0\\deftab720" |
|
31 RTF_HEADERCLOSE = "\n" |
|
32 RTF_FONTDEFOPEN = "{\\fonttbl" |
|
33 RTF_FONTDEF = "{{\\f{0:d}\\fnil\\fcharset{1:d} {2};}}" |
|
34 RTF_FONTDEFCLOSE = "}" |
|
35 RTF_COLORDEFOPEN = "{\\colortbl" |
|
36 RTF_COLORDEF = "\\red{0:d}\\green{1:d}\\blue{2:d};" |
|
37 RTF_COLORDEFCLOSE = "}" |
|
38 RTF_INFOOPEN = "{\\info " |
|
39 RTF_INFOCLOSE = "}" |
|
40 RTF_COMMENT = "{\\comment Generated by eric's RTF export filter.}" |
|
41 # to be used by strftime |
|
42 RTF_CREATED = r"{\creatim\yr%Y\mo%m\dy%d\hr%H\min%M\sec%S}" |
|
43 RTF_BODYOPEN = "" |
|
44 RTF_BODYCLOSE = "}" |
|
45 |
|
46 RTF_SETFONTFACE = "\\f" |
|
47 RTF_SETFONTSIZE = "\\fs" |
|
48 RTF_SETCOLOR = "\\cf" |
|
49 RTF_SETBACKGROUND = "\\highlight" |
|
50 RTF_BOLD_ON = "\\b" |
|
51 RTF_BOLD_OFF = "\\b0" |
|
52 RTF_ITALIC_ON = "\\i" |
|
53 RTF_ITALIC_OFF = "\\i0" |
|
54 |
|
55 RTF_EOLN = "\\line\n" |
|
56 RTF_TAB = "\\tab " |
|
57 |
|
58 RTF_COLOR = "#000000" |
|
59 |
|
60 def __init__(self, editor, parent=None): |
|
61 """ |
|
62 Constructor |
|
63 |
|
64 @param editor reference to the editor object (QScintilla.Editor.Editor) |
|
65 @param parent parent object of the exporter (QObject) |
|
66 """ |
|
67 ExporterBase.__init__(self, editor, parent) |
|
68 |
|
69 def __GetRTFNextControl(self, pos, style): |
|
70 """ |
|
71 Private method to extract the next RTF control word from style. |
|
72 |
|
73 @param pos position to start search (integer) |
|
74 @param style style definition to search in (string) |
|
75 @return tuple of new start position and control word found |
|
76 (integer, string) |
|
77 """ |
|
78 # \f0\fs20\cf0\highlight0\b0\i0 |
|
79 if pos >= len(style): |
|
80 return pos, "" |
|
81 |
|
82 oldpos = pos |
|
83 pos += 1 # implicit skip over leading '\' |
|
84 while pos < len(style) and style[pos] != '\\': |
|
85 pos += 1 |
|
86 return pos, style[oldpos:pos] |
|
87 |
|
88 def __GetRTFStyleChange(self, last, current): |
|
89 """ |
|
90 Private method to extract control words that are different between two |
|
91 styles. |
|
92 |
|
93 @param last least recently used style (string) |
|
94 @param current current style (string) |
|
95 @return string containing the delta between these styles (string) |
|
96 """ |
|
97 # \f0\fs20\cf0\highlight0\b0\i0 |
|
98 lastPos = 0 |
|
99 currentPos = 0 |
|
100 delta = '' |
|
101 i = 0 |
|
102 while i < 6: |
|
103 lastPos, lastControl = self.__GetRTFNextControl(lastPos, last) |
|
104 currentPos, currentControl = self.__GetRTFNextControl(currentPos, |
|
105 current) |
|
106 if lastControl != currentControl: |
|
107 delta += currentControl |
|
108 i += 1 |
|
109 if delta != '': |
|
110 delta += ' ' |
|
111 return delta |
|
112 |
|
113 def exportSource(self): |
|
114 """ |
|
115 Public method performing the export. |
|
116 """ |
|
117 filename = self._getFileName(self.tr("RTF Files (*.rtf)")) |
|
118 if not filename: |
|
119 return |
|
120 |
|
121 self.editor.recolor(0, -1) |
|
122 tabs = Preferences.getEditorExporter("RTF/UseTabs") |
|
123 tabSize = self.editor.getEditorConfig("TabWidth") |
|
124 if tabSize == 0: |
|
125 tabSize = 4 |
|
126 |
|
127 with EricOverrideCursor(), open(filename, "w", encoding="utf-8") as f: |
|
128 try: |
|
129 styles, fontsize = self.__prepareStyles(f) |
|
130 |
|
131 lastStyle = ( |
|
132 self.RTF_SETFONTFACE + "0" + |
|
133 self.RTF_SETFONTSIZE + "{0:d}".format(fontsize) + |
|
134 self.RTF_SETCOLOR + "0" + |
|
135 self.RTF_SETBACKGROUND + "1" + |
|
136 self.RTF_BOLD_OFF + |
|
137 self.RTF_ITALIC_OFF |
|
138 ) |
|
139 |
|
140 lengthDoc = self.editor.length() |
|
141 prevCR = False |
|
142 column = 0 |
|
143 pos = 0 |
|
144 deltaStyle = "" |
|
145 styleCurrent = -1 |
|
146 utf8 = self.editor.isUtf8() |
|
147 utf8Ch = b"" |
|
148 utf8Len = 0 |
|
149 |
|
150 while pos < lengthDoc: |
|
151 ch = self.editor.byteAt(pos) |
|
152 style = self.editor.styleAt(pos) |
|
153 if style != styleCurrent: |
|
154 deltaStyle = self.__GetRTFStyleChange( |
|
155 lastStyle, styles[style]) |
|
156 if deltaStyle: |
|
157 f.write(deltaStyle) |
|
158 styleCurrent = style |
|
159 lastStyle = styles[style] |
|
160 |
|
161 if ch == b'{': |
|
162 f.write('\\{') |
|
163 elif ch == b'}': |
|
164 f.write('\\}') |
|
165 elif ch == b'\\': |
|
166 f.write('\\\\') |
|
167 elif ch == b'\t': |
|
168 if tabs: |
|
169 f.write(self.RTF_TAB) |
|
170 else: |
|
171 ts = tabSize - (column % tabSize) |
|
172 f.write(' ' * ts) |
|
173 column += ts - 1 |
|
174 elif ch == b'\n': |
|
175 if not prevCR: |
|
176 f.write(self.RTF_EOLN) |
|
177 column -= 1 |
|
178 elif ch == b'\r': |
|
179 f.write(self.RTF_EOLN) |
|
180 column -= 1 |
|
181 else: |
|
182 if ord(ch) > 0x7F and utf8: |
|
183 utf8Ch += ch |
|
184 if utf8Len == 0: |
|
185 if (utf8Ch[0] & 0xF0) == 0xF0: |
|
186 utf8Len = 4 |
|
187 elif (utf8Ch[0] & 0xE0) == 0xE0: |
|
188 utf8Len = 3 |
|
189 elif (utf8Ch[0] & 0xC0) == 0xC0: |
|
190 utf8Len = 2 |
|
191 column -= 1 |
|
192 # will be incremented again later |
|
193 elif len(utf8Ch) == utf8Len: |
|
194 ch = utf8Ch.decode('utf8') |
|
195 if ord(ch) <= 0xff: |
|
196 f.write("\\'{0:x}".format(ord(ch))) |
|
197 else: |
|
198 f.write("\\u{0:d}\\'{1:x}".format( |
|
199 ord(ch), ord(ch) & 0xFF)) |
|
200 utf8Ch = b"" |
|
201 utf8Len = 0 |
|
202 else: |
|
203 column -= 1 |
|
204 # will be incremented again later |
|
205 else: |
|
206 f.write(ch.decode()) |
|
207 |
|
208 column += 1 |
|
209 prevCR = ch == b'\r' |
|
210 pos += 1 |
|
211 |
|
212 f.write(self.RTF_BODYCLOSE) |
|
213 except OSError as err: |
|
214 EricMessageBox.critical( |
|
215 self.editor, |
|
216 self.tr("Export source"), |
|
217 self.tr( |
|
218 """<p>The source could not be exported to""" |
|
219 """ <b>{0}</b>.</p><p>Reason: {1}</p>""") |
|
220 .format(filename, str(err))) |
|
221 |
|
222 def __prepareStyles(self, f): |
|
223 """ |
|
224 Private method to generate and store the different styles. |
|
225 |
|
226 @param f filepointer to the open RTF |
|
227 @type object |
|
228 @return styles, fontsize |
|
229 @rtype dict, int |
|
230 """ |
|
231 styles = {} |
|
232 fonts = {} |
|
233 colors = {} |
|
234 lastStyle = "" |
|
235 |
|
236 lex = self.editor.getLexer() |
|
237 |
|
238 wysiwyg = Preferences.getEditorExporter("RTF/WYSIWYG") |
|
239 if wysiwyg: |
|
240 if lex: |
|
241 defaultFont = lex.font(QsciScintilla.STYLE_DEFAULT) |
|
242 else: |
|
243 defaultFont = Preferences.getEditorOtherFonts("DefaultFont") |
|
244 else: |
|
245 defaultFont = Preferences.getEditorExporter("RTF/Font") |
|
246 fontface = defaultFont.family() |
|
247 fontsize = QFontInfo(defaultFont).pointSize() << 1 |
|
248 if fontsize == 0: |
|
249 fontsize = 10 << 1 |
|
250 characterset = QsciScintilla.SC_CHARSET_DEFAULT |
|
251 |
|
252 if lex: |
|
253 fgColour = lex.color(QsciScintilla.STYLE_DEFAULT) |
|
254 bgColour = lex.paper(QsciScintilla.STYLE_DEFAULT) |
|
255 else: |
|
256 fgColour = self.editor.color() |
|
257 bgColour = self.editor.paper() |
|
258 |
|
259 f.write(self.RTF_HEADEROPEN + self.RTF_FONTDEFOPEN) |
|
260 fonts[0] = fontface |
|
261 fontCount = 1 |
|
262 f.write(self.RTF_FONTDEF.format(0, characterset, fontface)) |
|
263 colors[0] = fgColour |
|
264 colors[1] = bgColour |
|
265 colorCount = 2 |
|
266 |
|
267 if lex: |
|
268 istyle = 0 |
|
269 while istyle <= QsciScintilla.STYLE_MAX: |
|
270 if ( |
|
271 istyle < QsciScintilla.STYLE_DEFAULT or |
|
272 istyle > QsciScintilla.STYLE_LASTPREDEFINED |
|
273 ): |
|
274 if lex.description(istyle): |
|
275 font = lex.font(istyle) |
|
276 lastStyle = self.RTF_SETFONTFACE |
|
277 if wysiwyg: |
|
278 fontKey = None |
|
279 for key, value in fonts.items(): |
|
280 if value.lower() == font.family().lower(): |
|
281 fontKey = key |
|
282 break |
|
283 else: |
|
284 fonts[fontCount] = font.family() |
|
285 f.write(self.RTF_FONTDEF.format( |
|
286 fontCount, characterset, |
|
287 font.family())) |
|
288 fontKey = fontCount |
|
289 fontCount += 1 |
|
290 |
|
291 lastStyle += "{0:d}".format(fontKey) |
|
292 else: |
|
293 lastStyle += "0" |
|
294 |
|
295 lastStyle += self.RTF_SETFONTSIZE |
|
296 if wysiwyg and QFontInfo(font).pointSize(): |
|
297 lastStyle += ( |
|
298 "{0:d}".format( |
|
299 QFontInfo(font).pointSize() << 1) |
|
300 ) |
|
301 else: |
|
302 lastStyle += "{0:d}".format(fontsize) |
|
303 |
|
304 sColour = lex.color(istyle) |
|
305 sColourKey = None |
|
306 for key, value in colors.items(): |
|
307 if value == sColour: |
|
308 sColourKey = key |
|
309 break |
|
310 else: |
|
311 colors[colorCount] = sColour |
|
312 sColourKey = colorCount |
|
313 colorCount += 1 |
|
314 lastStyle += ( |
|
315 self.RTF_SETCOLOR + |
|
316 "{0:d}".format(sColourKey) |
|
317 ) |
|
318 |
|
319 sColour = lex.paper(istyle) |
|
320 sColourKey = None |
|
321 for key, value in colors.items(): |
|
322 if value == sColour: |
|
323 sColourKey = key |
|
324 break |
|
325 else: |
|
326 colors[colorCount] = sColour |
|
327 sColourKey = colorCount |
|
328 colorCount += 1 |
|
329 |
|
330 lastStyle += ( |
|
331 self.RTF_SETBACKGROUND + |
|
332 "{0:d}".format(sColourKey) |
|
333 ) |
|
334 |
|
335 if font.bold(): |
|
336 lastStyle += self.RTF_BOLD_ON |
|
337 else: |
|
338 lastStyle += self.RTF_BOLD_OFF |
|
339 if font.italic(): |
|
340 lastStyle += self.RTF_ITALIC_ON |
|
341 else: |
|
342 lastStyle += self.RTF_ITALIC_OFF |
|
343 styles[istyle] = lastStyle |
|
344 |
|
345 # get substyles |
|
346 subs_start, subs_count = self.editor.getSubStyleRange( |
|
347 istyle) |
|
348 for subs_idx in range(subs_count): |
|
349 font = lex.font(subs_start + subs_idx) |
|
350 lastStyle = self.RTF_SETFONTFACE |
|
351 if wysiwyg: |
|
352 fontKey = None |
|
353 for key, value in fonts.items(): |
|
354 if value.lower() == font.family().lower(): |
|
355 fontKey = key |
|
356 break |
|
357 else: |
|
358 fonts[fontCount] = font.family() |
|
359 f.write(self.RTF_FONTDEF.format( |
|
360 fontCount, characterset, |
|
361 font.family())) |
|
362 fontKey = fontCount |
|
363 fontCount += 1 |
|
364 |
|
365 lastStyle += "{0:d}".format(fontKey) |
|
366 else: |
|
367 lastStyle += "0" |
|
368 |
|
369 lastStyle += self.RTF_SETFONTSIZE |
|
370 if wysiwyg and QFontInfo(font).pointSize(): |
|
371 lastStyle += ( |
|
372 "{0:d}".format( |
|
373 QFontInfo(font).pointSize() << 1) |
|
374 ) |
|
375 else: |
|
376 lastStyle += "{0:d}".format(fontsize) |
|
377 |
|
378 sColour = lex.color(subs_start + subs_idx) |
|
379 sColourKey = None |
|
380 for key, value in colors.items(): |
|
381 if value == sColour: |
|
382 sColourKey = key |
|
383 break |
|
384 else: |
|
385 colors[colorCount] = sColour |
|
386 sColourKey = colorCount |
|
387 colorCount += 1 |
|
388 lastStyle += ( |
|
389 self.RTF_SETCOLOR + |
|
390 "{0:d}".format(sColourKey) |
|
391 ) |
|
392 |
|
393 sColour = lex.paper(subs_start + subs_idx) |
|
394 sColourKey = None |
|
395 for key, value in colors.items(): |
|
396 if value == sColour: |
|
397 sColourKey = key |
|
398 break |
|
399 else: |
|
400 colors[colorCount] = sColour |
|
401 sColourKey = colorCount |
|
402 colorCount += 1 |
|
403 |
|
404 lastStyle += ( |
|
405 self.RTF_SETBACKGROUND + |
|
406 "{0:d}".format(sColourKey) |
|
407 ) |
|
408 |
|
409 if font.bold(): |
|
410 lastStyle += self.RTF_BOLD_ON |
|
411 else: |
|
412 lastStyle += self.RTF_BOLD_OFF |
|
413 if font.italic(): |
|
414 lastStyle += self.RTF_ITALIC_ON |
|
415 else: |
|
416 lastStyle += self.RTF_ITALIC_OFF |
|
417 styles[subs_idx - subs_start] = lastStyle |
|
418 |
|
419 else: |
|
420 styles[istyle] = ( |
|
421 self.RTF_SETFONTFACE + "0" + |
|
422 self.RTF_SETFONTSIZE + |
|
423 "{0:d}".format(fontsize) + |
|
424 self.RTF_SETCOLOR + "0" + |
|
425 self.RTF_SETBACKGROUND + "1" + |
|
426 self.RTF_BOLD_OFF + |
|
427 self.RTF_ITALIC_OFF |
|
428 ) |
|
429 |
|
430 istyle += 1 |
|
431 else: |
|
432 styles[0] = ( |
|
433 self.RTF_SETFONTFACE + "0" + |
|
434 self.RTF_SETFONTSIZE + |
|
435 "{0:d}".format(fontsize) + |
|
436 self.RTF_SETCOLOR + "0" + |
|
437 self.RTF_SETBACKGROUND + "1" + |
|
438 self.RTF_BOLD_OFF + |
|
439 self.RTF_ITALIC_OFF |
|
440 ) |
|
441 |
|
442 f.write(self.RTF_FONTDEFCLOSE + self.RTF_COLORDEFOPEN) |
|
443 for value in colors.values(): |
|
444 f.write(self.RTF_COLORDEF.format( |
|
445 value.red(), value.green(), value.blue())) |
|
446 f.write(self.RTF_COLORDEFCLOSE) |
|
447 f.write(self.RTF_INFOOPEN + self.RTF_COMMENT) |
|
448 f.write(time.strftime(self.RTF_CREATED)) |
|
449 f.write(self.RTF_INFOCLOSE) |
|
450 f.write(self.RTF_HEADERCLOSE + |
|
451 self.RTF_BODYOPEN + self.RTF_SETFONTFACE + "0" + |
|
452 self.RTF_SETFONTSIZE + "{0:d}".format(fontsize) + |
|
453 self.RTF_SETCOLOR + "0 ") |
|
454 |
|
455 return styles, fontsize |