diff --git a/httpupload/httpupload.py b/httpupload/httpupload.py index a9bdfeba9bc90f0f4afffc1951b549ec9166cf26..3f82e34ebd1ac42f5cbe956be4c3487e7e55ab6f 100644 --- a/httpupload/httpupload.py +++ b/httpupload/httpupload.py @@ -4,20 +4,11 @@ from gi.repository import GObject, Gtk import os import time -import base64 -import tempfile from urllib.request import Request, urlopen -from urllib.parse import quote as urlquote import mimetypes # better use the magic packet, but that's not a standard lib import gtkgui_helpers +import logging from queue import Queue -try: - from PIL import Image - pil_available = True -except: - pil_available = False -from io import BytesIO -import base64 import binascii from common import gajim @@ -25,10 +16,11 @@ from common import ged import chat_control from plugins import GajimPlugin from plugins.helpers import log_calls -import logging from dialogs import FileChooserDialog, ImageChooserDialog, ErrorDialog import nbxmpp +from .thumbnail import thumbnail + log = logging.getLogger('gajim.plugin_system.httpupload') if os.name != 'nt': @@ -52,8 +44,6 @@ TAGSIZE = 16 jid_to_servers = {} iq_ids_to_callbacks = {} last_info_query = {} -max_thumbnail_size = 2048 -max_thumbnail_dimension = 160 class HttpuploadPlugin(GajimPlugin): @@ -125,10 +115,11 @@ class HttpuploadPlugin(GajimPlugin): #pass # query info at most every 60 seconds in case something goes wrong - if (not chat_control.account in last_info_query or \ - last_info_query[chat_control.account] + 60 < time.time()) and \ - not gajim.get_jid_from_account(chat_control.account) in jid_to_servers and \ - gajim.account_is_connected(chat_control.account): + if ((not chat_control.account in last_info_query or + last_info_query[chat_control.account] + 60 < time.time()) + and not gajim.get_jid_from_account(chat_control.account) in jid_to_servers + and gajim.account_is_connected(chat_control.account) + ): log.info("Account %s: Using dicovery to find jid of httpupload component" % chat_control.account) id_ = gajim.get_an_id() iq = nbxmpp.Iq( @@ -289,11 +280,11 @@ class Base(object): progress_window.close_dialog() error = stanza.getTag("error") if error and error.getTag("text"): - ErrorDialog(_('Could not request upload slot'), + ErrorDialog(_('Could not request upload slot'), _('Got unexpected response from server: %s') % str(error.getTagData("text")), transient_for=self.chat_control.parent_win.window) else: - ErrorDialog(_('Could not request upload slot'), + ErrorDialog(_('Could not request upload slot'), _('Got unexpected response from server (protocol mismatch??)'), transient_for=self.chat_control.parent_win.window) return @@ -313,7 +304,7 @@ class Base(object): except: log.error("Could not open file") progress_window.close_dialog() - ErrorDialog(_('Could not open file'), + ErrorDialog(_('Could not open file'), _('Exception raised while opening file (see error log for more information)'), transient_for=self.chat_control.parent_win.window) raise # fill error log with useful information @@ -323,7 +314,7 @@ class Base(object): if not put or not get: log.error("got unexpected stanza: " + str(stanza)) progress_window.close_dialog() - ErrorDialog(_('Could not request upload slot'), + ErrorDialog(_('Could not request upload slot'), _('Got unexpected response from server (protocol mismatch??)'), transient_for=self.chat_control.parent_win.window) return @@ -335,69 +326,17 @@ class Base(object): log.info("Upload completed successfully") xhtml = None is_image = mime_type.split('/', 1)[0] == 'image' - if (not isinstance(self.chat_control, chat_control.ChatControl) or not self.chat_control.gpg_is_active) and \ - self.dialog_type == 'image' and is_image and not self.encrypted_upload: - + if ((not isinstance(self.chat_control, chat_control.ChatControl) + or not self.chat_control.gpg_is_active) + and self.dialog_type == 'image' + and is_image + and not self.encrypted_upload + ): progress_messages.put(_('Calculating (possible) image thumbnail...')) - thumb = None - quality_steps = (100, 80, 60, 50, 40, 35, 30, 25, 23, 20, 18, 15, 13, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) - with open(path_to_file, 'rb') as content_file: - thumb = urlquote(base64.standard_b64encode(content_file.read()), '') - if thumb and len(thumb) < max_thumbnail_size: - quality = 100 - log.info("Image small enough (%d bytes), not resampling" % len(thumb)) - elif pil_available: - log.info("PIL available, using it for image downsampling") - try: - for quality in quality_steps: - thumb = Image.open(path_to_file) - thumb.thumbnail((max_thumbnail_dimension, max_thumbnail_dimension), Image.ANTIALIAS) - output = BytesIO() - thumb.save(output, format='JPEG', quality=quality, optimize=True) - thumb = output.getvalue() - output.close() - thumb = urlquote(base64.standard_b64encode(thumb), '') - log.debug("pil thumbnail jpeg quality %d produces an image of size %d..." % (quality, len(thumb))) - if len(thumb) < max_thumbnail_size: - break - except: - thumb = None - else: - thumb = None - if not thumb: - log.info("PIL not available, using GTK for image downsampling") - temp_file = None - try: - with open(path_to_file, 'rb') as content_file: - thumb = content_file.read() - loader = Gtk.gdk.PixbufLoader() - loader.write(thumb) - loader.close() - pixbuf = loader.get_pixbuf() - scaled_pb = self.get_pixbuf_of_size(pixbuf, max_thumbnail_dimension) - handle, temp_file = tempfile.mkstemp(suffix='.jpeg', prefix='gajim_httpupload_scaled_tmp', dir=gajim.TMP) - log.debug("Saving temporary jpeg image to '%s'..." % temp_file) - os.close(handle) - for quality in quality_steps: - scaled_pb.save(temp_file, "jpeg", {"quality": str(quality)}) - with open(temp_file, 'rb') as content_file: - thumb = content_file.read() - thumb = urlquote(base64.standard_b64encode(thumb), '') - log.debug("gtk thumbnail jpeg quality %d produces an image of size %d..." % (quality, len(thumb))) - if len(thumb) < max_thumbnail_size: - break - except: - thumb = None - finally: - if temp_file: - os.unlink(temp_file) + thumb = thumbnail(path_to_file) if thumb: - if len(thumb) > max_thumbnail_size: - log.info("Couldn't compress image enough, not sending any thumbnail") - else: - log.info("Using thumbnail jpeg quality %d (image size: %d bytes)" % (quality, len(thumb))) - xhtml = '<body><br/><a href="%s"> <img alt="%s" src="data:image/png;base64,%s"/> </a></body>' % \ - (get.getData(), get.getData(), thumb) + xhtml = '<body><br/><a href="%s"><img alt="%s" src="data:image/jpeg;base64,%s"/></a></body>' % \ + (get.getData(), get.getData(), thumb) progress_window.close_dialog() id_ = gajim.get_an_id() def add_oob_tag(): @@ -414,7 +353,7 @@ class Base(object): ErrorDialog(_('Could not upload file'), _('Got unexpected http response code from server: ') + str(response_code), transient_for=self.chat_control.parent_win.window) - + def uploader(): progress_messages.put(_('Uploading file via HTTP...')) try: @@ -495,26 +434,6 @@ class Base(object): self.dialog_type = 'image' self.dlg = ImageChooserDialog(on_response_ok=self.on_file_dialog_ok, on_response_cancel=None) - 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 scaled_pixbuf - 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, - Gtk.gdk.INTERP_BILINEAR) - return crop_pixbuf - class StreamFileWithProgress: def __init__(self, path, mode, callback=None, diff --git a/httpupload/thumbnail.py b/httpupload/thumbnail.py new file mode 100644 index 0000000000000000000000000000000000000000..b55660d5e6e19c5b9ac251dd8f5f0ac4ab13c0e5 --- /dev/null +++ b/httpupload/thumbnail.py @@ -0,0 +1,90 @@ +from gi.repository import GdkPixbuf +import base64 +from io import BytesIO +import os +import sys +import logging +from urllib.parse import quote as urlquote +try: + from PIL import Image + pil_available = True +except: + pil_available = False + +log = logging.getLogger('gajim.plugin_system.httpupload.thumbnail') + +def scale_down_to(pixbuf, size): + # Creates a pixbuf that fits in the specified square of sizexsize + # while preserving the aspect ratio + # Returns scaled_pixbuf + 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 + + +max_thumbnail_size = 2048 +max_thumbnail_dimension = 160 +base64_size_factor = 4/3 +def thumbnail(path_to_file): + """ + Generates a JPEG thumbnail and base64-encodes, ensuring that the encoded + size is less than max_thumbnail_size bytes. If this is not possible, returns + None. + """ + thumb = None + quality_steps = (100, 80, 60, 50, 40, 35, 30, 25, 23, 20, 18, 15, 13, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) + # If the whole file is small enough, we'll just use that as a thumbnail + # without downsampling. + if os.path.getsize(path_to_file) * base64_size_factor < max_thumbnail_size: + with open(path_to_file, 'rb') as content_file: + thumb = urlquote(base64.standard_b64encode(content_file.read()), '') + log.info("Image small enough (%d bytes), not resampling" % len(thumb)) + return thumb + elif pil_available: + log.info("PIL available, using it for image downsampling") + try: + for quality in quality_steps: + thumb = Image.open(path_to_file) + thumb.thumbnail((max_thumbnail_dimension, max_thumbnail_dimension), Image.ANTIALIAS) + output = BytesIO() + thumb.save(output, format='JPEG', quality=quality, optimize=True) + thumb = output.getvalue() + output.close() + thumb = urlquote(base64.standard_b64encode(thumb), '') + log.debug("pil thumbnail jpeg quality %d produces an image of size %d...", quality, len(thumb)) + if len(thumb) < max_thumbnail_size: + log.debug("Size is acceptable.") + return thumb + except: + log.info("Exception occurred during PIL downsampling", exc_info=sys.exc_info()) + thumb = None + # If we haven't returned by now we couldn't use PIL for one reason or + # another, so let's pass on to GdkPixbuf + log.info("using GdkPixBuf for image downsampling") + temp_file = None + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(path_to_file) + scaled_pb = scale_down_to(pixbuf, max_thumbnail_dimension) + for quality in quality_steps: + success, thumb_raw = scaled_pb.save_to_bufferv("jpeg", ["quality"], [str(quality)]) + log.debug("gdkpixbuf thumbnail jpeg quality %d produces an image of size %d...", + quality, + len(thumb_raw) * base64_size_factor) + if len(thumb_raw) * base64_size_factor < max_thumbnail_size: + log.debug("Size is acceptable.") + return urlquote(base64.standard_b64encode(thumb_raw)) + except: + log.info("Exception occurred during GdkPixbuf downsampling, not providing thumbnail", exc_info=sys.exc_info()) + return None + log.info("No acceptably small thumbnail was generated.")