Skip to content
Snippets Groups Projects
Commit 64e5f3f7 authored by Yann Leboulanger's avatar Yann Leboulanger
Browse files

[url_image_preview] [Arune]:

 * Content-type and content-length are checked for every URL in MUCs and regular chats
 * Plugin config now have a setting for max image download size (which is checked to content-length)
 * If content-type match and content-length is smaller then setting the image will be downloaded and displayed
 * If image can be loaded, the URL is no longer displayed (except as a tooltip on the image)
 * Image is clickable (opens browser with URL)
 * Images are displayed using XHTML
 * URL is displayed until image is displayed
Fixes #102, #105
parent a62c8ef5
No related branches found
No related tags found
No related merge requests found
<?xml version="1.0"?>
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="2.16"/>
<!-- interface-naming-policy toplevel-contextual -->
<object class="GtkListStore" id="liststore1">
<columns>
<!-- column-name Text -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">256 KiB</col>
</row>
<row>
<col id="0" translatable="yes">512 KiB</col>
</row>
<row>
<col id="0" translatable="yes">1 MiB</col>
</row>
<row>
<col id="0" translatable="yes">5 MiB</col>
</row>
<row>
<col id="0" translatable="yes">10 MiB</col>
</row>
</data>
</object>
<object class="GtkWindow" id="window1">
<property name="can_focus">False</property>
<child>
<object class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">9</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkHBox" id="hbox2">
<object class="GtkFrame" id="frame1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkLabel" id="preview_size_lebel">
<property name="width_request">133</property>
<object class="GtkTable" id="table1">
<property name="visible">True</property>
<property name="xalign">0.029999999329447746</property>
<property name="label" translatable="yes">Preview size</property>
<property name="ellipsize">start</property>
<property name="single_line_mode">True</property>
<property name="track_visited_links">False</property>
<property name="can_focus">False</property>
<property name="n_rows">2</property>
<property name="n_columns">2</property>
<child>
<object class="GtkSpinButton" id="preview_size">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
<property name="width_chars">6</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
<signal name="value-changed" handler="preview_size_value_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="y_options"/>
</packing>
</child>
<child>
<object class="GtkComboBox" id="max_size_combobox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">liststore1</property>
<signal name="changed" handler="max_size_value_changed" swapped="no"/>
<child>
<object class="GtkCellRendererText" id="cellrenderertext1"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="y_options">GTK_EXPAND</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="max_size_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xpad">13</property>
<property name="label" translatable="yes">Accept files smaller then</property>
<property name="track_visited_links">False</property>
</object>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="y_options">GTK_EXPAND</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="preview_size_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xpad">12</property>
<property name="label" translatable="yes">Preview size</property>
<property name="track_visited_links">False</property>
</object>
<packing>
<property name="y_options">GTK_EXPAND</property>
</packing>
</child>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="preview_size">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_tooltip">True</property>
<property name="tooltip_text" translatable="yes">Preview size(10-512)</property>
<property name="invisible_char">&#x25CF;</property>
<property name="width_chars">6</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
<signal name="value_changed" handler="preview_size_value_changed"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="padding">6</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
......
[info]
name: Url image preview
short_name: url_image_preview
version: 0.5.4
version: 0.6.0
description: Url image preview in chatbox.<br/>
Based on patch in <a href="http://trac.gajim.org/attachment/ticket/5300">ticket #5300</a>.
authors = Denis Fomin <fominde@gmail.com>
Yann Leboulanger <asterix@lagaule.org>
Anders Sandblad <runeson@gmail.com>
homepage = http://trac-plugins.gajim.org/wiki/UrlImagePreviewPlugin
min_gajim_version: 0.16
max_gajim_version: 0.16.9
......@@ -3,16 +3,18 @@
import gtk
import re
import os
import urllib2
import base64
from common import gajim
from common import helpers
from plugins import GajimPlugin
from plugins.helpers import log_calls
from plugins.helpers import log_calls, log
from plugins.gui import GajimPluginConfigDialog
from conversation_textview import TextViewImage
EXTENSIONS = ('.png','.jpg','.jpeg','.gif','.raw','.svg')
ACCEPTED_MIME_TYPES = ('image/png','image/jpeg','image/gif','image/raw',
'image/svg+xml')
class UrlImagePreviewPlugin(GajimPlugin):
@log_calls('UrlImagePreviewPlugin')
......@@ -24,7 +26,8 @@ class UrlImagePreviewPlugin(GajimPlugin):
'print_special_text': (self.print_special_text,
self.print_special_text1),}
self.config_default_values = {
'PREVIEW_SIZE': (150, 'Preview size(10-512)'),}
'PREVIEW_SIZE': (150, 'Preview size(10-512)'),
'MAX_FILE_SIZE': (524288, 'Max file size for image preview')}
self.chat_control = None
self.controls = []
......@@ -47,7 +50,7 @@ class UrlImagePreviewPlugin(GajimPlugin):
if control.chat_control.conv_textview != tv:
continue
control.print_special_text(special_text, other_tags, graphics=True,
iter_=None)
iter_=iter_)
def print_special_text1(self, chat_control, special_text, other_tags=None,
graphics=True, iter_=None):
......@@ -69,48 +72,167 @@ class Base(object):
# remove qip bbcode
special_text = special_text.rsplit('[/img]')[0]
name, extension = os.path.splitext(special_text)
if extension.lower() not in EXTENSIONS:
return
if not special_text.startswith('http://') and \
special_text.startswith('www.'):
if special_text.startswith('www.'):
special_text = 'http://' + special_text
if not special_text.startswith('ftp://') and \
special_text.startswith('ftp.'):
if special_text.startswith('ftp.'):
special_text = 'ftp://' + special_text
# show pics preview
buffer_ = self.textview.tv.get_buffer()
iter_ = buffer_.get_end_iter()
mark = buffer_.create_mark(None, iter_, True)
# start downloading image
gajim.thread_interface(helpers.download_image, [
self.textview.account, {'src': special_text}], self._update_img,
[mark])
def _update_img(self, (mem, alt), mark):
if not iter_:
iter_ = buffer_.get_end_iter()
# Detect XHTML-IM link
ttt = buffer_.get_tag_table()
tags_ = [(ttt.lookup(t) if isinstance(t, str) else t) for t in other_tags]
for t in tags_:
is_xhtml_link = getattr(t, 'href', None)
if is_xhtml_link:
break
# Show URL, until image is loaded (if ever)
repl_start = buffer_.create_mark(None, iter_, True)
buffer_.insert_with_tags(iter_, special_text, \
*[(ttt.lookup(t) if isinstance(t, str) else t) for t in ["url"]])
repl_end = buffer_.create_mark(None, iter_, True)
# First get the http head request with does not fetch data, just headers
gajim.thread_interface(self._get_http_head, [self.textview.account,
special_text], self._check_mime_size, [special_text, repl_start, repl_end])
# Don't print the URL in the message window (in the calling function)
self.textview.plugin_modified = True
def _check_mime_size(self, (file_mime, file_size), url, repl_start, repl_end):
# Check if mime type is acceptable
if file_mime.lower() not in ACCEPTED_MIME_TYPES:
# URL is already displayed
log.info('Not accepted mime type for URL: %s' % url)
return
# Check if file size is acceptable
if file_size > self.plugin.config['MAX_FILE_SIZE'] or file_size == 0:
log.info('File size to big or unknown for URL: %s' % url)
# URL is already displayed
return
# Start downloading image
gajim.thread_interface(helpers.download_image, [ self.textview.account, {
'src': url, 'max_size':self.plugin.config['MAX_FILE_SIZE'] } ],
self._update_img, [url, file_mime, repl_start, repl_end])
def _update_img(self, (mem, alt), url, file_mime, repl_start, repl_end):
if mem:
try:
loader = gtk.gdk.PixbufLoader()
loader.write(mem)
loader.close()
pixbuf = loader.get_pixbuf()
pixbuf, w, h = self.get_pixbuf_of_size(pixbuf,
self.plugin.config['PREVIEW_SIZE'])
buffer_ = mark.get_buffer()
end_iter = buffer_.get_iter_at_mark(mark)
anchor = buffer_.create_child_anchor(end_iter)
img = TextViewImage(anchor, alt)
img.set_from_pixbuf(pixbuf)
img.show()
self.textview.tv.add_child_at_anchor(img, anchor)
w, h = self.get_thumb_size(pixbuf, self.plugin.config['PREVIEW_SIZE'])
imgb64 = base64.b64encode(mem)
xhtml = '<body xmlns=\'http://www.w3.org/1999/xhtml\'><br/>'
xhtml += '<a href="%s">' % url
xhtml += '<img src="data:%s;base64,%s" width="%s" height="%s"/> ' % \
(file_mime, imgb64, w, h)
xhtml += '</a></body>'
buffer_ = repl_start.get_buffer()
iter_ = buffer_.get_iter_at_mark(repl_start)
buffer_.delete(iter_, buffer_.get_iter_at_mark(repl_end))
self.textview.tv.display_html(xhtml.encode('utf-8'), self.textview.tv,
self.textview, iter_=iter_)
except Exception:
# URL is already displayed
log.error('Could not display image for URL: %s' % url)
else:
# If image could not be downloaded, URL is already displayed
log.error('Could not download image for URL: %s' % url)
def _get_http_head (self, account, url):
# Check if proxy is used
proxy = helpers.get_proxy_info(account)
if proxy and proxy['type'] in ('http', 'socks5'):
return self._get_http_head_proxy(url, proxy)
return self._get_http_head_direct(url)
def _get_http_head_direct (self, url):
log.debug('Get head request direct for URL: %s' % url)
try:
req = urllib2.Request(url)
req.get_method = lambda : 'HEAD'
req.add_header('User-Agent', 'Gajim %s' % gajim.version)
f = urllib2.urlopen(req)
except Exception, ex:
log.debug('Could not get head response for URL: %s' % url)
return ('', 0)
url_headers = f.info()
ctype = ''
ctype_list = url_headers.getheaders('Content-Type')
if ctype_list:
ctype = ctype_list[0]
clen_list = url_headers.getheaders('Content-Length')
clen = 0
if clen_list:
try:
clen = int(clen_list[0])
except ValueError:
pass
return (ctype, clen)
def _get_http_head_proxy(self, url, proxy):
log.debug('Get head request with proxy for URL: %s' % url)
if not gajim.HAVE_PYCURL:
log.error('PYCURL not installed')
return ('', 0)
import pycurl
from cStringIO import StringIO
headers = ''
try:
b = StringIO()
c = pycurl.Curl()
c.setopt(pycurl.URL, url.encode('utf-8'))
c.setopt(pycurl.FOLLOWLOCATION, 1)
c.setopt(pycurl.CONNECTTIMEOUT, 5)
# Make a HEAD request:
c.setopt(pycurl.CUSTOMREQUEST, 'HEAD')
c.setopt(pycurl.NOBODY, 1)
c.setopt(pycurl.HEADER, 1)
c.setopt(pycurl.TIMEOUT, 10)
c.setopt(pycurl.MAXFILESIZE, 2000000)
c.setopt(pycurl.WRITEFUNCTION, b.write)
c.setopt(pycurl.USERAGENT, 'Gajim ' + gajim.version)
def get_pixbuf_of_size(self, pixbuf, size):
# Creates a pixbuf that fits in the specified square of sizexsize
# while preserving the aspect ratio
# Returns tuple: (scaled_pixbuf, actual_width, actual_height)
# set proxy
c.setopt(pycurl.PROXY, proxy['host'].encode('utf-8'))
c.setopt(pycurl.PROXYPORT, proxy['port'])
if proxy['useauth']:
c.setopt(pycurl.PROXYUSERPWD, proxy['user'].encode('utf-8')\
+ ':' + proxy['pass'].encode('utf-8'))
c.setopt(pycurl.PROXYAUTH, pycurl.HTTPAUTH_ANY)
if proxy['type'] == 'http':
c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_HTTP)
elif proxy['type'] == 'socks5':
c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5)
x = c.perform()
c.close()
headers = b.getvalue()
except pycurl.error, ex:
log.debug('Could not get head response for URL: %s' % url)
return ('', 0)
ctype = ''
searchObj = re.search( r'^Content-Type: (.*)$', headers, re.M|re.I)
if searchObj:
ctype = searchObj.group(1).strip()
clen = 0
searchObj = re.search( r'^Content-Length: (.*)$', headers, re.M|re.I)
if searchObj:
try:
clen = int(searchObj.group(1).strip())
except ValueError:
pass
return (ctype, clen)
def get_thumb_size(self, pixbuf, size):
image_width = pixbuf.get_width()
image_height = pixbuf.get_height()
......@@ -123,24 +245,25 @@ class Base(object):
image_width = int(size / float(image_height) * image_width)
image_height = int(size)
crop_pixbuf = pixbuf.scale_simple(image_width, image_height,
gtk.gdk.INTERP_BILINEAR)
return (crop_pixbuf, image_width, image_height)
return (image_width, image_height)
def disconnect_from_chat_control(self):
pass
class UrlImagePreviewPluginConfigDialog(GajimPluginConfigDialog):
max_file_size = [262144, 524288, 1048576, 5242880, 10485760]
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'])
self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, [
'vbox1', 'liststore1'])
self.preview_size_spinbutton = self.xml.get_object('preview_size')
self.preview_size_spinbutton.get_adjustment().set_all(20, 10, 512, 1,
10, 0)
self.max_size_combobox = self.xml.get_object('max_size_combobox')
vbox = self.xml.get_object('vbox1')
self.child.pack_start(vbox)
......@@ -149,7 +272,16 @@ class UrlImagePreviewPluginConfigDialog(GajimPluginConfigDialog):
def on_run(self):
self.preview_size_spinbutton.set_value(self.plugin.config[
'PREVIEW_SIZE'])
value = self.plugin.config['MAX_FILE_SIZE']
if value:
self.max_size_combobox.set_active(self.max_file_size.index(value))
else:
self.max_size_combobox.set_active(-1)
def preview_size_value_changed(self, spinbutton):
self.plugin.config['PREVIEW_SIZE'] = spinbutton.get_value()
def max_size_value_changed(self, widget):
self.plugin.config['MAX_FILE_SIZE'] = self.max_file_size[
self.max_size_combobox.get_active()]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment