| 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 | ''' |
|---|
| 18 | LaTeX Plugin. |
|---|
| 19 | |
|---|
| 20 | based 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 | |
|---|
| 28 | from threading import Thread |
|---|
| 29 | import os,gtk,gobject,tempfile,random,subprocess |
|---|
| 30 | |
|---|
| 31 | from plugins import GajimPlugin |
|---|
| 32 | from plugins.helpers import log, log_calls |
|---|
| 33 | |
|---|
| 34 | |
|---|
| 35 | gtk.gdk.threads_init() ##for gtk.gdk.thread_[enter|leave]() |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | #following constants are taken from Pidgin Latex Plugin |
|---|
| 39 | def 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 | """ |
|---|
| 54 | Yes, this is simply a copy/paste of KopeteTex blacklist. |
|---|
| 55 | But too bad in LaTeX and system security to verify all |
|---|
| 56 | of this |
|---|
| 57 | """ |
|---|
| 58 | BLACKLIST=["\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 | """ |
|---|
| 74 | class 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 | |
|---|
| 194 | class 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 | |
|---|
| 242 | class 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']) |
|---|