Ticket #4176: latex.2.py

File latex.2.py, 9.7 kB (added by yvesf, 4 months ago)

v3

Line 
1# -*- coding: utf-8 -*-
2## This file is part of Gajim.
3##
4## Gajim is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published
6## by the Free Software Foundation; version 3 only.
7##
8## Gajim is distributed in the hope that it will be useful,
9## but WITHOUT ANY WARRANTY; without even the implied warranty of
10## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11## GNU General Public License for more details.
12##
13## You should have received a copy of the GNU General Public License
14## along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
15##
16
17'''
18LaTeX Plugin.
19
20based on Acronym Plugin by Mateusz Biliński
21:author: Yves Fischer <yvesf@xapek.org>
22:since: 17th June 2008
23:copyright: Copyright (2008) Yves Fischer
24:license: GPL
25'''
26
27
28from threading import Thread
29import os,gtk,gobject,tempfile,random,subprocess
30
31from plugins import GajimPlugin
32from plugins.helpers import log, log_calls
33
34
35gtk.gdk.threads_init() ##for gtk.gdk.thread_[enter|leave]()
36
37
38#following constants are taken from Pidgin Latex Plugin
39def latex_template(code):
40        return """\\documentclass[12pt]{article}
41\\usepackage[dvips]{graphicx}
42\\usepackage{amsmath}
43\\usepackage{amssymb}
44\\pagestyle{empty}
45\\begin{document}
46\\begin{gather*}
47%s
48\\end{gather*}
49\\end{document}""" % (code)
50
51
52
53"""
54Yes, this is simply a copy/paste of KopeteTex blacklist.
55But too bad in LaTeX and system security to verify all
56of this
57"""
58BLACKLIST=["\def","\\let","\\futurelet","\\newcommand","\\renewcomment",
59           "\\else","\\fi","\\write","\\input","\\include","\\chardef","\\catcode",
60           "\\makeatletter","\\noexpand","\\toksdef","\\every","\\errhelp",
61           "\\errorstopmode","\\scrollmode","\\nonstopmode","\\batchmode",
62           "\\read","\\csname","\\newhelp","\\relax","\\afterground","\\afterassignment",
63           "\\expandafter","\\noexpand","\\special","\\command","\\loop","\\repeat",
64           "\\toks","\\output","\\line","\\mathcode","\\name","\\item","\\section",
65           "\\mbox","\\DeclareRobustCommand"
66]
67
68
69
70
71"""
72
73"""
74class LatexRenderer(Thread):
75        def __init__(self, iter_start, iter_end, buffer, widget):
76                Thread.__init__(self)
77
78                self.code = iter_start.get_text(iter_end)
79                self.mark_name = "LatexRendererMark%s" % (random.randint(0,1000).__str__())
80                self.mark = buffer.create_mark(self.mark_name, iter_start, True)
81
82                self.buffer = buffer
83                self.widget = widget
84
85                #delete code and show message "processing"
86                self.buffer.delete(iter_start, iter_end)
87                #iter_start.forward_char()
88                self.buffer.insert(iter_start, "Processing LaTeX")
89
90                self.start() #start background processing
91
92        def run(self):
93#               gtk.gdk.threads_enter() #its nearly non-sense using this in a full thread
94                #should be fine-grained later
95                try:
96                        if self.check_code():
97                                self.show_image()
98                        else:
99                                self.show_error("There are bad commands!")
100                except:
101                        pass
102                finally:
103        #               gtk.gdk.threads_leave()
104                        self.buffer.delete_mark(self.mark)
105
106        """
107        String -> TextBuffer
108        """
109        def show_error(self, message):
110                gtk.gdk.threads_enter()
111                iter_mark = self.buffer.get_iter_at_mark(self.mark)
112                iter_end = iter_mark.copy().forward_search("Processing LaTeX", gtk.TEXT_SEARCH_TEXT_ONLY)[1]
113                self.buffer.delete(iter_mark, iter_end)
114
115
116                pixbuf = self.widget.render_icon(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON)
117                self.buffer.insert_pixbuf(iter_end, pixbuf)
118                self.buffer.insert(iter_end, message)
119                gtk.gdk.threads_leave()
120
121        """
122        Latex -> PNG -> TextBuffer
123        """
124        @log_calls("LatexRenderer.show_image()")
125        def show_image(self):
126                fn = "gajim-latex-" + random.randint(0,1000).__str__()
127                log.debug("Generate %s.dvi" % os.path.join(tempfile.gettempdir(), fn))
128                try:
129                        p_latex = subprocess.Popen(("latex -jobname %s -output-format dvi" % fn).split(" "), stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=tempfile.gettempdir())
130                        log.debug("latex process spawned, write latex code to sdtin")
131                        log.debug(latex_template(self.code[2:len(self.code)-2]))
132                        p_latex.stdin.write(latex_template(self.code[2:len(self.code)-2]))
133                        p_latex.stdin.close()
134                        log.debug("Wait for latex-process to finish")
135                        p_latex.wait()
136                        if p_latex.returncode != 0:
137                                err = p_latex.stdout.read()
138                                raise OSError("Error in latex code: " + err)
139                except OSError, e:
140                        self.show_error("latex error: " + e.message + "\n" + "===ORIGINAL CODE====\n" + self.code[2:len(self.code)-1])
141                        log.debug("latex error: " + e.message)
142                        log.debug("latex code: " + self.code[2:len(self.code)-2])
143                        return False
144                finally:
145                        os.remove(os.path.join(tempfile.gettempdir(), fn + ".aux"))
146                        os.remove(os.path.join(tempfile.gettempdir(), fn + ".log"))
147
148                log.debug("DVI OK")
149                log.debug("Generate %s.png from DVI using dvipng" % os.path.join(tempfile.gettempdir(), fn))
150                try:
151                        p_dvipng = subprocess.Popen(("dvipng -T tight -x 1200 -z 9 -bg transparent -o %s.png %s.dvi" % (fn, fn)).split(" "),stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,cwd=tempfile.gettempdir())
152                        p_dvipng.stdin.close() #i have nothing to say
153                        log.debug("wait for dvipng-processs to finish")
154                        p_dvipng.wait()
155                        if p_dvipng.returncode != 0:
156                                err = p_dvipng.stdout.read() + p_dvipng.stderr.read()
157                                raise OSError("Error in dvipng dvi->png conversion subprocess: " + err)
158                except OSError, e:
159                        self.show_error("Can't execute dvipng: " + e.message)
160                        log.debug("Can't execute dvipng: " + e.message)
161                        return False
162                finally:
163                        os.remove(os.path.join(tempfile.gettempdir(), fn + ".dvi"))
164                log.debug("PNG OK")
165                log.debug("Loading PNG %s" % os.path.join(tempfile.gettempdir(),fn+".png"))
166                try:
167                        gtk.gdk.threads_enter()
168                        pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(tempfile.gettempdir(),fn+".png"))
169                        log.debug("png loaded")
170                        iter_mark = self.buffer.get_iter_at_mark(self.mark)
171                        iter_end = iter_mark.copy().forward_search("Processing LaTeX", gtk.TEXT_SEARCH_TEXT_ONLY)[1]
172                        log.debug("Delete old Text")
173                        self.buffer.delete(iter_mark, iter_end)
174                        log.debug("Insert pixbuf")
175                        self.buffer.insert_pixbuf(iter_end, pixbuf)
176                        log.debug("OK _ DONE")
177                except gobject.GError:
178                        self.show_error("Cant open %s.png for reading" % os.path.join(tempfile.gettempdir(),fn))
179                        log.debug("Cant open %s.png for reading" % os.path.join(tempfile.gettempdir(),fn))
180                finally:
181                        gtk.gdk.threads_leave()
182                        os.remove(os.path.join(tempfile.gettempdir(), fn + ".png"))
183               
184
185        def check_code(self):
186                for bad_cmd in BLACKLIST:
187                        if self.code.find(bad_cmd) != -1:
188                                print "Found bad command %s" % bad_cmd
189                                return False
190                return True
191
192
193
194class LatexPluginConfiguration(gtk.Window):
195        def __init__(self):
196                gtk.Window.__init__(self)
197                self.set_title("Latex Plugin Configuration")
198               
199                self.pane = gtk.VBox()
200                self.add(self.pane)
201
202                self.btn_test = gtk.Button("Test Latex Configuration")
203                self.btn_test.connect("clicked", self.start_test)
204                self.pane.pack_start(self.btn_test,expand=False)
205
206                self.lbl_messages = gtk.Label("Results:\n")
207                self.lbl_messages.set_line_wrap(True)
208                self.pane.pack_start(self.lbl_messages)
209
210        def log(self, message):
211                self.lbl_messages.set_text(self.lbl_messages.get_text() + "\n" + message)
212
213       
214        """
215        performs very simple checks (check if executable is in PATH)
216        """
217        def start_test(self,widget):
218                log = self.log
219                from os import system
220                log("Start Test")
221                log("Test Latex Binary")
222                ret = system("latex -version")
223                if ret != 0:
224                        log("No LaTeX binary found in PATH")
225                else:
226                        log("OK")
227                log("Test dvipng")
228                ret = system("dvipng --version")
229                if ret != 0:
230                        log("No dvipng binary found in PATH")
231                else:
232                        log("OK")
233
234        def run(self,parent):
235                self.present()
236                self.show_all()
237
238
239
240
241
242class LatexPlugin(GajimPlugin):
243        name = u'Latex Plugin'
244        short_name = u'latex'
245        version = u'0.1'
246        description = u'''Invoke Latex to render $$foobar$$ sourrounded Latex equations. Needs latex and dvipng'''
247        authors = [u'Yves Fischer <yvesf@xapek.org>']
248        homepage = u'http://xapek.org'
249
250
251        def init(self):
252                self.config_dialog = LatexPluginConfiguration()
253               
254                self.gui_extension_points = {
255                        'chat_control_base' : (self.connect_with_chat_control_base,
256                                                                   self.disconnect_from_chat_control_base)
257                }
258
259        """
260        start rendering if clicked on a link
261        """
262        def textview_event_after(self, tag, widget, event, iter):
263                if tag.get_property("name") != "latex" or event.type != gtk.gdk.BUTTON_PRESS:
264                        return
265                dollar_start, iter_start = iter.backward_search("$$", gtk.TEXT_SEARCH_TEXT_ONLY)
266                iter_end, dollar_end = iter.forward_search("$$", gtk.TEXT_SEARCH_TEXT_ONLY)
267                LatexRenderer(dollar_start, dollar_end, widget.get_buffer(), widget)
268
269        """
270        called when conversation text widget changes
271        """
272        def textbuffer_live_latex_expander(self, tb):
273                def split_list(list):
274                        newlist = []
275                        for i in range(0,len(list)-1,2):
276                                newlist.append( [ list[i], list[i+1], ] )
277                        return newlist
278
279                assert isinstance(tb,gtk.TextBuffer)
280                start_iter = tb.get_start_iter()
281                points = []
282                tuple_found = start_iter.forward_search("$$", gtk.TEXT_SEARCH_TEXT_ONLY)
283                while tuple_found != None:
284                        points.append(tuple_found)
285                        tuple_found = tuple_found[1].forward_search("$$", gtk.TEXT_SEARCH_TEXT_ONLY)
286
287                for pair in split_list(points):
288                        tb.apply_tag_by_name("latex", pair[0][1], pair[1][0])
289
290        def connect_with_chat_control_base(self, chat_control):
291                d = {}
292                tv = chat_control.conv_textview.tv
293                tb = tv.get_buffer()
294
295                self.latex_tag = gtk.TextTag("latex")
296                self.latex_tag.set_property('foreground', "blue")
297                self.latex_tag.set_property('underline', "single")
298                d['tag_id'] = self.latex_tag.connect('event', self.textview_event_after)               
299                tb.get_tag_table().add(self.latex_tag)
300
301                d['h_id'] = tb.connect('changed', self.textbuffer_live_latex_expander)
302                chat_control.latexs_expander_plugin_data = d
303
304                return True
305
306        def disconnect_from_chat_control_base(self, chat_control):
307                d = chat_control.latexs_expander_plugin_data
308                #tv = chat_control.msg_textview
309                tv = chat_control.conv_textview.tv
310
311                tv.get_buffer().disconnect(d['h_id'])
312                self.latex_tag.disconnect(d['tag_id'])