[preview] Refactor image loading

- Make Pillow the fallback loader
- Simplify loading of images
- Use Gtk.Image instead of Gajims TextViewImage
This commit is contained in:
Philipp Hörist
2017-11-20 19:45:30 +01:00
parent 3722070eb3
commit cca0fa0b2b

View File

@@ -32,16 +32,17 @@ from gajim.common import configpaths
from gajim import dialogs from gajim import dialogs
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.helpers import log_calls from gajim.plugins.helpers import log_calls
from gajim.conversation_textview import TextViewImage
from url_image_preview.http_functions import get_http_head, get_http_file from url_image_preview.http_functions import get_http_head, get_http_file
from url_image_preview.config_dialog import UrlImagePreviewConfigDialog from url_image_preview.config_dialog import UrlImagePreviewConfigDialog
log = logging.getLogger('gajim.plugin_system.preview') log = logging.getLogger('gajim.plugin_system.preview')
PILLOW_AVAILABLE = True
try: try:
from PIL import Image from PIL import Image
except: except:
log.debug('Pillow not available') log.debug('Pillow not available')
PILLOW_AVAILABLE = False
try: try:
if os.name == 'nt': if os.name == 'nt':
@@ -222,7 +223,7 @@ class Base(object):
with open(filepath, 'rb') as f: with open(filepath, 'rb') as f:
mem = f.read() mem = f.read()
app.thread_interface( app.thread_interface(
self._save_thumbnail, [thumbpath, (mem, '')], self._save_thumbnail, [thumbpath, mem],
self._update_img, [real_text, repl_start, self._update_img, [real_text, repl_start,
repl_end, filepath, encrypted]) repl_end, filepath, encrypted])
@@ -249,62 +250,48 @@ class Base(object):
self._check_mime_size, [real_text, repl_start, repl_end, self._check_mime_size, [real_text, repl_start, repl_end,
filepaths, key, iv, encrypted]) filepaths, key, iv, encrypted])
def _save_thumbnail(self, thumbpath, tuple_arg): def _save_thumbnail(self, thumbpath, mem):
mem, alt = tuple_arg
size = self.plugin.config['PREVIEW_SIZE'] size = self.plugin.config['PREVIEW_SIZE']
use_Gtk = False
output = None
try:
output = BytesIO()
im = Image.open(BytesIO(mem))
im.thumbnail((size, size), Image.ANTIALIAS)
im.save(output, "jpeg", quality=100, optimize=True)
mem = output.getvalue()
output.close()
except Exception as e:
if output:
output.close()
log.info("Failed to load image using pillow, "
"falling back to gdk pixbuf.")
log.debug(e)
use_Gtk = True
if use_Gtk:
log.info("Pillow not available or file corrupt, "
"trying to load using gdk pixbuf.")
try: try:
loader = GdkPixbuf.PixbufLoader() loader = GdkPixbuf.PixbufLoader()
loader.write(mem) loader.write(mem)
loader.close() loader.close()
pixbuf = loader.get_pixbuf() pixbuf = loader.get_pixbuf()
pixbuf, w, h = self._get_pixbuf_of_size(pixbuf, size) except GLib.GError as error:
log.info('Failed to load image using Gdk.Pixbuf')
log.debug(error)
ok, mem = pixbuf.save_to_bufferv("jpeg", ["quality"], ["100"]) if not PILLOW_AVAILABLE:
except Exception as e: log.info('Pillow not available')
log.info("Failed to load image using gdk pixbuf, " return
"ignoring image.") # Try Pillow
log.debug(e) image = Image.open(BytesIO(mem)).convert("RGBA")
return ('', '') array = GLib.Bytes.new(image.tobytes())
width, height = image.size
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
array, GdkPixbuf.Colorspace.RGB, True,
8, width, height, width * 4)
thumbnail = pixbuf.scale_simple(
size, size, GdkPixbuf.InterpType.BILINEAR)
try: try:
self._create_path(os.path.dirname(thumbpath)) self._create_path(os.path.dirname(thumbpath))
self._write_file(thumbpath, mem) thumbnail.savev(thumbpath, 'png', [], [])
except Exception as e: except Exception as error:
dialogs.ErrorDialog( dialogs.ErrorDialog(
_('Could not save file'), _('Could not save file'),
_('Exception raised while saving thumbnail ' _('Exception raised while saving thumbnail '
'for image file (see error log for more ' 'for image file (see error log for more '
'information)'), 'information)'),
transient_for=app.app.get_active_window()) transient_for=app.app.get_active_window())
log.error(str(e)) log.error(error)
return (mem, alt) return
return thumbnail
def _load_thumbnail(self, thumbpath): def _load_thumbnail(self, thumbpath):
with open(thumbpath, 'rb') as f: return GdkPixbuf.Pixbuf.new_from_file(thumbpath)
mem = f.read()
f.closed
return (mem, '')
def _write_file(self, path, data): def _write_file(self, path, data):
log.info("Writing '%s' of size %d...", path, len(data)) log.info("Writing '%s' of size %d...", path, len(data))
@@ -316,59 +303,44 @@ class Base(object):
log.error("Failed to write file '%s'!", path) log.error("Failed to write file '%s'!", path)
raise raise
def _update_img(self, tuple_arg, url, repl_start, repl_end, def _update_img(self, pixbuf, url, repl_start, repl_end,
filepath, encrypted): filepath, encrypted):
mem, alt = tuple_arg if pixbuf is None:
if mem: # If image could not be downloaded, URL is already displayed
try: log.error('Could not download image for URL: %s', url)
return
urlparts = urlparse(url) urlparts = urlparse(url)
filename = os.path.basename(urlparts.path) filename = os.path.basename(urlparts.path)
eb = Gtk.EventBox() event_box = Gtk.EventBox()
eb.connect('button-press-event', self.on_button_press_event, event_box.connect('button-press-event', self.on_button_press_event,
filepath, filename, url, encrypted) filepath, filename, url, encrypted)
eb.connect('enter-notify-event', self.on_enter_event) event_box.connect('enter-notify-event', self.on_enter_event)
eb.connect('leave-notify-event', self.on_leave_event) event_box.connect('leave-notify-event', self.on_leave_event)
# this is threadsafe
# (Gtk textview is NOT threadsafe by itself!!)
def add_to_textview(): def add_to_textview():
try: # textview closed in the meantime etc. try:
at_end = self.textview.at_the_end() at_end = self.textview.at_the_end()
buffer_ = repl_start.get_buffer() buffer_ = repl_start.get_buffer()
iter_ = buffer_.get_iter_at_mark(repl_start) iter_ = buffer_.get_iter_at_mark(repl_start)
# buffer_.insert(iter_, "\n")
anchor = buffer_.create_child_anchor(iter_) anchor = buffer_.create_child_anchor(iter_)
# Use url as tooltip for image image = Gtk.Image.new_from_pixbuf(pixbuf)
img = TextViewImage(anchor, url) event_box.add(image)
loader = GdkPixbuf.PixbufLoader() event_box.show_all()
loader.write(mem) self.textview.tv.add_child_at_anchor(event_box, anchor)
loader.close()
pixbuf = loader.get_pixbuf()
img.set_from_pixbuf(pixbuf)
eb.add(img)
eb.show_all()
self.textview.tv.add_child_at_anchor(eb, anchor)
buffer_.delete(iter_, buffer_.delete(iter_,
buffer_.get_iter_at_mark(repl_end)) buffer_.get_iter_at_mark(repl_end))
if at_end: if at_end:
GLib.idle_add(self.textview.scroll_to_end_iter) GLib.idle_add(self.textview.scroll_to_end_iter)
except Exception as ex: except Exception as ex:
log.warn("Exception while loading %s: %s", url, ex) log.exception("Exception while loading %s: %s", url, ex)
return False return False
# add to mainloop --> make call threadsafe # add to mainloop --> make call threadsafe
GLib.idle_add(add_to_textview) GLib.idle_add(add_to_textview)
except Exception:
# URL is already displayed
log.error('Could not display image for URL: %s', url)
raise
else:
# If image could not be downloaded, URL is already displayed
log.error('Could not download image for URL: %s -- %s',
url, alt)
def _check_mime_size(self, tuple_arg, def _check_mime_size(self, tuple_arg,
url, repl_start, repl_end, filepaths, url, repl_start, repl_end, filepaths,
@@ -428,7 +400,7 @@ class Base(object):
log.error(str(e)) log.error(str(e))
# Create thumbnail, write it to harddisk and return it # Create thumbnail, write it to harddisk and return it
return self._save_thumbnail(thumbpath, (mem, alt)) return self._save_thumbnail(thumbpath, mem)
def _create_path(self, folder): def _create_path(self, folder):
if os.path.exists(folder): if os.path.exists(folder):
@@ -450,26 +422,6 @@ class Base(object):
backend=be).decryptor() backend=be).decryptor()
return decryptor.update(data) + decryptor.finalize() return decryptor.update(data) + decryptor.finalize()
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)
image_width = pixbuf.get_width()
image_height = pixbuf.get_height()
if image_width > image_height:
if image_width > size:
image_height = int(size / float(image_width) * image_height)
image_width = int(size)
else:
if image_height > size:
image_width = int(size / float(image_height) * image_width)
image_height = int(size)
crop_pixbuf = pixbuf.scale_simple(image_width, image_height,
GdkPixbuf.InterpType.BILINEAR)
return (crop_pixbuf, image_width, image_height)
def make_rightclick_menu(self, event, data): def make_rightclick_menu(self, event, data):
xml = Gtk.Builder() xml = Gtk.Builder()
xml.set_translation_domain('gajim_plugins') xml.set_translation_domain('gajim_plugins')