From 8bff21d54b85bcfbe77a540993e3abe07fe840db Mon Sep 17 00:00:00 2001
From: Yann Leboulanger <asterix@lagaule.org>
Date: Mon, 30 Apr 2012 21:08:45 +0200
Subject: [PATCH] add latex plugin

---
 latex/__init__.py      |   1 +
 latex/config_dialog.ui |  89 +++++++++
 latex/latex.py         | 399 +++++++++++++++++++++++++++++++++++++++++
 latex/manifest.ini     |   8 +
 4 files changed, 497 insertions(+)
 create mode 100644 latex/__init__.py
 create mode 100644 latex/config_dialog.ui
 create mode 100644 latex/latex.py
 create mode 100644 latex/manifest.ini

diff --git a/latex/__init__.py b/latex/__init__.py
new file mode 100644
index 00000000..081433d1
--- /dev/null
+++ b/latex/__init__.py
@@ -0,0 +1 @@
+from latex import LatexPlugin
diff --git a/latex/config_dialog.ui b/latex/config_dialog.ui
new file mode 100644
index 00000000..631a74bc
--- /dev/null
+++ b/latex/config_dialog.ui
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+<interface>
+  <requires lib="gtk+" version="2.16"/>
+  <!-- interface-naming-policy toplevel-contextual -->
+  <object class="GtkTextBuffer" id="textbuffer1">
+    <property name="text">Plug-in decription should be displayed here. This text will be erased during PluginsWindow initialization.</property>
+  </object>
+  <object class="GtkWindow" id="window1">
+    <child>
+      <object class="GtkVBox" id="vbox1">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkHBox" id="hbox111">
+            <property name="visible">True</property>
+            <property name="border_width">3</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkLabel" id="label1">
+                <property name="visible">True</property>
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">PNG dpi:</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="png_dpi_label">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="invisible_char">&#x25CF;</property>
+                <signal name="changed" handler="on_png_dpi_label_changed"/>
+              </object>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkHBox" id="hbox1">
+            <property name="visible">True</property>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <object class="GtkButton" id="test_button">
+                <property name="label" translatable="yes">Test Latex Configuration</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <signal name="clicked" handler="on_test_button_clicked"/>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="result_label">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Result:
+</property>
+          </object>
+          <packing>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/latex/latex.py b/latex/latex.py
new file mode 100644
index 00000000..1a9cad03
--- /dev/null
+++ b/latex/latex.py
@@ -0,0 +1,399 @@
+# -*- coding: utf-8 -*-
+#
+## plugins/latex/latex.py
+##
+## Copyright (C) 2010-2011 Yves Fischer <yvesf AT xapek.org>
+## Copyright (C) 2011 Yann Leboulanger <asterix AT lagaule.org>
+##
+## This file is part of Gajim.
+##
+## Gajim is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## Gajim is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
+##
+
+
+from threading import Thread
+import os
+import gtk
+import gobject
+from tempfile import mkstemp, mkdtemp
+import random
+from subprocess import Popen, PIPE
+
+from common import gajim
+from common import helpers
+from plugins import GajimPlugin
+from plugins.helpers import log, log_calls
+from plugins.gui import GajimPluginConfigDialog
+
+gtk.gdk.threads_init() # for gtk.gdk.thread_[enter|leave]()
+
+def latex_template(code):
+    return '''\\documentclass[12pt]{article}
+\\usepackage[dvips]{graphicx}
+\\usepackage{amsmath}
+\\usepackage{amssymb}
+\\pagestyle{empty}
+\\begin{document}
+\\begin{large}
+\\begin{gather*}
+%s
+\\end{gather*}
+\\end{large}
+\\end{document}''' % (code)
+
+def write_latex(filename, str_):
+    texstr = latex_template(str_)
+
+    file_ = open(filename, "w+")
+    file_.write(texstr)
+    file_.flush()
+    file_.close()
+
+def popen_nt_friendly(command, directory):
+    if os.name == 'nt':
+        # CREATE_NO_WINDOW
+        return Popen(command, creationflags=0x08000000, cwd=directory,
+            stdout=PIPE)
+    else:
+        return Popen(command, cwd=directory, stdout=PIPE)
+
+def try_run(argv, directory):
+    try:
+        p = popen_nt_friendly(argv, directory)
+        out = p.communicate()[0]
+        log.info(out)
+        return p.wait()
+    except Exception, e:
+        return _('Error executing "%(command)s": %(error)s') % {
+            'command': " ".join(argv),
+            'error': helpers.decode_string(str(e))}
+
+BLACKLIST = ['\def', '\\let', '\\futurelet', '\\newcommand', '\\renewcomment',
+   '\\else', '\\fi', '\\write', '\\input', '\\include', '\\chardef',
+   '\\catcode', '\\makeatletter', '\\noexpand', '\\toksdef', '\\every',
+   '\\errhelp', '\\errorstopmode', '\\scrollmode', '\\nonstopmode',
+   '\\batchmode', '\\read', '\\csname', '\\newhelp', '\\relax', '\\afterground',
+   '\\afterassignment', '\\expandafter', '\\noexpand', '\\special', '\\command',
+   '\\loop', '\\repeat', '\\toks', '\\output', '\\line', '\\mathcode', '\\name',
+   '\\item', '\\section', '\\mbox', '\\DeclareRobustCommand', '\\[', '\\]'
+]
+
+
+class LatexRenderer(Thread):
+    def __init__(self, iter_start, iter_end, buffer_, widget, png_dpi):
+        Thread.__init__(self)
+
+        self.code = iter_start.get_text(iter_end)
+        self.mark_name = 'LatexRendererMark%s' % str(random.randint(0,1000))
+        self.mark = buffer_.create_mark(self.mark_name, iter_start, True)
+
+        self.buffer_ = buffer_
+        self.widget = widget
+        self.png_dpi = png_dpi
+
+        # delete code and show message 'processing'
+        self.buffer_.delete(iter_start, iter_end)
+        # iter_start.forward_char()
+        self.buffer_.insert(iter_start, _('Processing LaTeX'))
+
+        self.start() # start background processing
+
+    def run(self):
+        try:
+            if self.check_code():
+                self.show_image()
+            else:
+                self.show_error(_('There are bad commands!'))
+        except:
+            pass
+        finally:
+            self.buffer_.delete_mark(self.mark)
+
+    def show_error(self, message):
+        """
+        String -> TextBuffer
+        """
+        gtk.gdk.threads_enter()
+        iter_mark = self.buffer_.get_iter_at_mark(self.mark)
+        iter_end = iter_mark.copy().forward_search(_('Processing LaTeX'),
+            gtk.TEXT_SEARCH_TEXT_ONLY)[1]
+        self.buffer_.delete(iter_mark, iter_end)
+
+        pixbuf = self.widget.render_icon(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON)
+        self.buffer_.insert_pixbuf(iter_end, pixbuf)
+        self.buffer_.insert(iter_end, message)
+        gtk.gdk.threads_leave()
+
+    @log_calls('LatexRenderer')
+    def show_image(self):
+        """
+        Latex -> PNG -> TextBuffer
+        """
+
+        def fg_str(fmt):
+            try:
+                return [{'hex' : '+level-colors', 'tex' : '-fg'}[fmt],
+                    gajim.interface.get_fg_color(fmt)]
+            except KeyError:
+                # interface may not be available when we test latex at startup
+                return []
+            except AttributeError:
+                # interface may not be available when we test latext at startup
+                return {'hex': ['+level-colors', '0x000000'],
+                    'tex': ['-fg', 'rgb 0.0 0.0 0.0']}[fmt]
+
+        try:
+            tmpdir = mkdtemp(prefix='gajim_tex')
+            tmppng = mkstemp(prefix='gajim_tex', suffix='.png')[1]
+        except Exception:
+            msg = 'Could not create temporary files for Latex plugin'
+            log.debug(msg)
+            self.show_error(_('latex error: %s\n===ORIGINAL CODE====\n%s') % (
+                msg, self.code[2:len(self.code)-2]))
+            return False
+
+        tmpfile = os.path.join(tmpdir, 'gajim_tex')
+
+        # build latex string
+        write_latex(tmpfile + '.tex', self.code[2:len(self.code)-2])
+
+        # convert TeX to dvi
+        exitcode = try_run(['latex', '--interaction=nonstopmode',
+            tmpfile + '.tex'], tmpdir)
+
+        if exitcode == 0:
+            # convert dvi to png
+            log.debug('DVI OK')
+            exitcode = try_run(['dvipng'] + fg_str('tex') + ['-T', 'tight',
+                '-D', self.png_dpi, tmpfile + '.dvi', '-o', tmpfile + '.png'],
+                tmpdir)
+
+            if exitcode:
+                # dvipng failed, try convert
+                log.debug('dvipng failed, try convert')
+                exitcode = try_run(['convert'] + fg_str('hex') + ['-trim',
+                    '-density', self.png_dpi, tmpfile + '.dvi',
+                    tmpfile + '.png'], tmpdir)
+
+        # remove temp files created by us and TeX
+        extensions = ['.tex', '.log', '.aux', '.dvi']
+        for ext in extensions:
+            try:
+                os.remove(tmpfile + ext)
+            except Exception:
+                pass
+
+        if exitcode == 0:
+            log.debug('PNG OK')
+            os.rename(tmpfile + '.png', tmppng)
+        else:
+            log.debug('PNG FAILED')
+            os.remove(tmppng)
+            os.rmdir(tmpdir)
+            self.show_error(_('Convertion to image failed\n===ORIGINAL CODE===='
+                '\n%s') % self.code[2:len(self.code)-2])
+            return False
+
+        log.debug('Loading PNG %s' % tmppng)
+        try:
+            gtk.gdk.threads_enter()
+            pixbuf = gtk.gdk.pixbuf_new_from_file(tmppng)
+            log.debug('png loaded')
+            iter_mark = self.buffer_.get_iter_at_mark(self.mark)
+            iter_end = iter_mark.copy().forward_search('Processing LaTeX',
+                gtk.TEXT_SEARCH_TEXT_ONLY)[1]
+            log.debug('Delete old Text')
+            self.buffer_.delete(iter_mark, iter_end)
+            log.debug('Insert pixbuf')
+            self.buffer_.insert_pixbuf(iter_end, pixbuf)
+        except gobject.GError:
+            self.show_error(_('Cannot open %s for reading') % tmppng)
+            log.debug('Cant open %s for reading' % tmppng)
+        finally:
+            gtk.gdk.threads_leave()
+            os.remove(tmppng)
+
+    def check_code(self):
+        for bad_cmd in BLACKLIST:
+            if self.code.find(bad_cmd) != -1:
+                # Found bad command
+                return False
+        return True
+
+class LatexPluginConfiguration(GajimPluginConfigDialog):
+    def init(self):
+        self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
+            'config_dialog.ui')
+        self.xml = gtk.Builder()
+        self.xml.set_translation_domain('gajim_plugins')
+        self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, ['vbox1'])
+        hbox = self.xml.get_object('vbox1')
+        self.child.pack_start(hbox)
+        self.result_label = self.xml.get_object('result_label')
+
+        self.xml.connect_signals(self)
+
+    def on_run(self):
+        widget = self.xml.get_object('png_dpi_label')
+        widget.set_text(str(self.plugin.config['png_dpi']))
+
+    def show_result(self, msg):
+        self.result_label.set_text(self.result_label.get_text() + '\n' + msg)
+
+    def on_test_button_clicked(self,widget):
+        """
+        performs very simple checks (check if executable is in PATH)
+        """
+        self.show_result(_('Test Latex Binary'))
+        exitcode = try_run(['latex', '-version'], None)
+        if exitcode != 0:
+            self.show_result(_('  No LaTeX binary found in PATH'))
+        else:
+            self.show_result(_('  OK'))
+
+        self.show_result(_('Test dvipng'))
+        exitcode = try_run(['dvipng', '--version'], None)
+        if exitcode != 0:
+            self.show_result(_('  No dvipng binary found in PATH'))
+        else:
+            self.show_result(_('  OK'))
+
+        self.show_result(_('Test ImageMagick'))
+        exitcode = try_run(['convert', '-version'], None)
+        if exitcode != 0:
+            self.show_result(_('  No convert binary found in PATH'))
+        else:
+            self.show_result(_('  OK'))
+
+    def on_png_dpi_label_changed(self, label):
+        self.plugin.config['png_dpi'] = label.get_text()
+
+class LatexPlugin(GajimPlugin):
+    def init(self):
+        self.description = _('Invoke Latex to render $$foobar$$ sourrounded ' \
+            'Latex equations. Needs latex and dvipng or ImageMagick.')
+        self.config_dialog = LatexPluginConfiguration(self)
+        self.config_default_values = {'png_dpi': ('108', '')}
+
+        self.gui_extension_points = {
+            'chat_control_base': (self.connect_with_chat_control_base,
+                self.disconnect_from_chat_control_base)
+        }
+        self.test_activatable()
+
+    def test_activatable(self):
+        """
+        performs very simple checks (check if executable is in PATH)
+        """
+        self.available_text = ''
+        exitcode = try_run(['latex', '-version'], None)
+        if exitcode != 0:
+            latex_available = False
+        else:
+            latex_available = True
+
+        exitcode = try_run(['dvipng', '--version'], None)
+        if exitcode != 0:
+            dvipng_available = False
+        else:
+            dvipng_available = True
+
+        exitcode = try_run(['convert', '-version'], None)
+        if exitcode != 0:
+            imagemagick_available = False
+        else:
+            imagemagick_available = True
+
+        pkgs = ''
+
+        if not latex_available:
+            if os.name == 'nt':
+                pkgs = 'MikTex'
+            else:
+                pkgs = 'texlive-latex-base'
+            self.available_text = _('LaTeX is not available')
+            self.activatable = False
+        if not dvipng_available and not imagemagick_available:
+            if os.name == 'nt':
+                if not pkgs:
+                    pkgs = 'MikTex'
+            else:
+                if pkgs:
+                    pkgs += _(' and ')
+                pkgs += '%s or %s' % ('dvipng', 'ImageMagick')
+            if self.available_text:
+                self.available_text += ' and '
+            self.available_text += _('dvipng and Imagemagick are not available')
+
+        if self.available_text:
+            self.activatable = False
+            self.available_text += _('. Install %s') % pkgs
+
+    def textview_event_after(self, tag, widget, event, iter):
+        """
+        start rendering if clicked on a link
+        """
+        if tag.get_property('name') != 'latex' or \
+        event.type != gtk.gdk.BUTTON_PRESS:            return
+        dollar_start, iter_start = iter.backward_search('$$',
+            gtk.TEXT_SEARCH_TEXT_ONLY)
+        iter_end, dollar_end = iter.forward_search('$$',
+            gtk.TEXT_SEARCH_TEXT_ONLY)
+        LatexRenderer(dollar_start, dollar_end, widget.get_buffer(), widget,
+            self.config['png_dpi'])
+
+    def textbuffer_live_latex_expander(self, tb):
+        """
+        called when conversation text widget changes
+        """
+        def split_list(list):
+            newlist = []
+            for i in range(0, len(list)-1, 2):
+                newlist.append( [ list[i], list[i+1], ] )
+            return newlist
+
+        assert isinstance(tb, gtk.TextBuffer)
+        start_iter = tb.get_start_iter()
+        points = []
+        tuple_found = start_iter.forward_search('$$', gtk.TEXT_SEARCH_TEXT_ONLY)
+        while tuple_found != None:
+            points.append(tuple_found)
+            tuple_found = tuple_found[1].forward_search('$$',
+                gtk.TEXT_SEARCH_TEXT_ONLY)
+
+        for pair in split_list(points):
+            tb.apply_tag_by_name('latex', pair[0][1], pair[1][0])
+
+    def connect_with_chat_control_base(self, chat_control):
+        d = {}
+        tv = chat_control.conv_textview.tv
+        tb = tv.get_buffer()
+
+        self.latex_tag = gtk.TextTag('latex')
+        self.latex_tag.set_property('foreground', 'blue')
+        self.latex_tag.set_property('underline', 'single')
+        d['tag_id'] = self.latex_tag.connect('event', self.textview_event_after)
+        tb.get_tag_table().add(self.latex_tag)
+
+        d['h_id'] = tb.connect('changed', self.textbuffer_live_latex_expander)
+        chat_control.latexs_expander_plugin_data = d
+
+        return True
+
+    def disconnect_from_chat_control_base(self, chat_control):
+        d = chat_control.latexs_expander_plugin_data
+        tv = chat_control.conv_textview.tv
+
+        tv.get_buffer().disconnect(d['h_id'])
+        self.latex_tag.disconnect(d['tag_id'])
+
diff --git a/latex/manifest.ini b/latex/manifest.ini
new file mode 100644
index 00000000..a9b24e08
--- /dev/null
+++ b/latex/manifest.ini
@@ -0,0 +1,8 @@
+[info]
+name: Latex
+short_name: latex
+version: 0.1
+description: render received latex code
+authors: Yves Fischer <yvesf@xapek.org>
+ Yann Leboulanger <asterix@lagaule.org>
+homepage: http://www.gajim.org/
-- 
GitLab