# HG changeset patch # User Goffi # Date 1572377919 -3600 # Node ID 2cc2f65379f725cddba3e397884c7372e55c89ad # Parent f8e3789912d01d419e225a0f15a547e9acc238c0 core: added imageCheck and imageResize methods: imageCheck will give a report on image, notably it will tell if it's too big and needs to be resized before a transfer. imageResize will create a new image with the requested size and return a path to it. diff -r f8e3789912d0 -r 2cc2f65379f7 sat/bridge/bridge_constructor/bridge_template.ini --- a/sat/bridge/bridge_constructor/bridge_template.ini Tue Oct 29 20:24:29 2019 +0100 +++ b/sat/bridge/bridge_constructor/bridge_template.ini Tue Oct 29 20:38:39 2019 +0100 @@ -909,3 +909,24 @@ sig_out=a{ss} doc=Get a dict to short name => whole namespaces doc_return=namespaces mapping + +[imageCheck] +type=method +category=core +sig_in=s +sig_out=s +doc=Analyze an image a return a report +doc_return=serialized report + +[imageResize] +async= +type=method +category=core +sig_in=sii +sig_out=s +doc=Create a new image with desired size +doc_param_0=image_path: path of the image to resize +doc_param_1=width: width of the new image +doc_param_2=height: height of the new image +doc_return=path of the new image with desired size + the image must be deleted once not needed anymore diff -r f8e3789912d0 -r 2cc2f65379f7 sat/bridge/dbus_bridge.py --- a/sat/bridge/dbus_bridge.py Tue Oct 29 20:24:29 2019 +0100 +++ b/sat/bridge/dbus_bridge.py Tue Oct 29 20:38:39 2019 +0100 @@ -385,6 +385,18 @@ return self._callback("historyGet", str(from_jid), str(to_jid), limit, between, filters, str(profile), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, + in_signature='s', out_signature='s', + async_callbacks=None) + def imageCheck(self, arg_0): + return self._callback("imageCheck", str(arg_0)) + + @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, + in_signature='sii', out_signature='s', + async_callbacks=('callback', 'errback')) + def imageResize(self, image_path, width, height, callback=None, errback=None): + return self._callback("imageResize", str(image_path), width, height, callback=callback, errback=errback) + + @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='b', async_callbacks=None) def isConnected(self, profile_key="@DEFAULT@"): diff -r f8e3789912d0 -r 2cc2f65379f7 sat/core/constants.py --- a/sat/core/constants.py Tue Oct 29 20:24:29 2019 +0100 +++ b/sat/core/constants.py Tue Oct 29 20:38:39 2019 +0100 @@ -361,6 +361,13 @@ # internationalisation DEFAULT_LOCALE = "en_GB" + # Contexts + # context indicate how things are done (e.g. to who/what a file is transfered) + # this is useful to make some decision (e.g. image size) + + # something is done in instant messaging + CONTEXT_CHAT = "CONTEXT_CHAT" + ## Misc ## SAVEFILE_DATABASE = APP_NAME_FILE + ".db" IQ_SET = '/iq[@type="set"]' diff -r f8e3789912d0 -r 2cc2f65379f7 sat/core/sat_main.py --- a/sat/core/sat_main.py Tue Oct 29 20:24:29 2019 +0100 +++ b/sat/core/sat_main.py Tue Oct 29 20:38:39 2019 +0100 @@ -40,8 +40,10 @@ from sat.memory import encryption from sat.tools import async_trigger as trigger from sat.tools import utils +from sat.tools import images from sat.tools.common import dynamic_import from sat.tools.common import regex +from sat.tools.common import data_format from sat.stdui import ui_contact_list, ui_profile_manager import sat.plugins @@ -161,6 +163,8 @@ self.bridge.register_method("loadParamsTemplate", self.memory.load_xml) self.bridge.register_method("sessionInfosGet", self.getSessionInfos) self.bridge.register_method("namespacesGet", self.getNamespaces) + self.bridge.register_method("imageCheck", self._imageCheck) + self.bridge.register_method("imageResize", self._imageResize) self.memory.initialized.addCallback(self._postMemoryInit) @@ -632,6 +636,17 @@ } return defer.succeed(data) + # images + + def _imageCheck(self, path): + report = images.checkImage(self, path) + return data_format.serialise(report) + + def _imageResize(self, path, width, height): + d = images.resizeImage(path, (width, height)) + d.addCallback(lambda new_image_path: str(new_image_path)) + return d + # local dirs def getLocalPath(self, client, dir_name, *extra_path, **kwargs): diff -r f8e3789912d0 -r 2cc2f65379f7 sat/tools/images.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/tools/images.py Tue Oct 29 20:38:39 2019 +0100 @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# SAT: a jabber client +# Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Methods to manipulate images""" + +import tempfile +from PIL import Image +from sat.core.constants import Const as C +from pathlib import Path +from twisted.internet import threads + + +def checkImage(host, path, context=C.CONTEXT_CHAT): + """Analyze image and return a report + + report will indicate if image is too large, and the recommended new size if this is + the case + @param host: SàT instance + @param path(str, pathlib.Path): image to open + @param context(str): context in which the image is transfered + @return dict: report on image, with following keys: + - too_large: true if image is oversized + - recommended_size: if too_large is True, recommended size to use + """ + # TODO: context is not used yet + report = {} + image = Image.open(path) + max_size = tuple(host.memory.getConfig(None, "image_max", (1200, 720))) + if image.size > max_size: + report['too_large'] = True + if image.size[0] > max_size[0]: + factor = max_size[0] / image.size[0] + if image.size[1] * factor > max_size[1]: + factor = max_size[1] / image.size[1] + else: + factor = max_size[1] / image.size[1] + report['recommended_size'] = [image.width*factor, image.height*factor] + else: + report['too_large'] = False + + return report + + +def _resizeImageBlocking(image_path, new_size): + im_path = Path(image_path) + im = Image.open(im_path) + resized = im.resize(new_size, Image.LANCZOS) + with tempfile.NamedTemporaryFile(suffix=im_path.suffix, delete=False) as f: + resized.save(f, format=im.format) + return Path(f.name) + + +def resizeImage(image_path, new_size): + """Resize an image to a new temporary file, and return it path + + @param image_path(str, Path): path of the original image + @param new_size(tuple[int, int]): size to use for new image + @return (Path): path of the resized file. The image at this path must be deleted + after use + """ + return threads.deferToThread(_resizeImageBlocking, image_path, new_size) diff -r f8e3789912d0 -r 2cc2f65379f7 sat_frontends/bridge/dbus_bridge.py --- a/sat_frontends/bridge/dbus_bridge.py Tue Oct 29 20:24:29 2019 +0100 +++ b/sat_frontends/bridge/dbus_bridge.py Tue Oct 29 20:38:39 2019 +0100 @@ -469,6 +469,29 @@ error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.historyGet(from_jid, to_jid, limit, between, filters, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + def imageCheck(self, arg_0, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.imageCheck(arg_0, **kwargs)) + + def imageResize(self, image_path, width, height, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.imageResize(image_path, width, height, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + def isConnected(self, profile_key="@DEFAULT@", callback=None, errback=None): if callback is None: error_handler = None @@ -1085,6 +1108,22 @@ self.db_core_iface.historyGet(from_jid, to_jid, limit, between, filters, profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) return fut + def imageCheck(self, arg_0): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.imageCheck(arg_0, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def imageResize(self, image_path, width, height): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.imageResize(image_path, width, height, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + def isConnected(self, profile_key="@DEFAULT@"): loop = asyncio.get_running_loop() fut = loop.create_future() diff -r f8e3789912d0 -r 2cc2f65379f7 sat_frontends/bridge/pb.py --- a/sat_frontends/bridge/pb.py Tue Oct 29 20:24:29 2019 +0100 +++ b/sat_frontends/bridge/pb.py Tue Oct 29 20:38:39 2019 +0100 @@ -375,6 +375,22 @@ errback = self._generic_errback d.addErrback(errback) + def imageCheck(self, arg_0, callback=None, errback=None): + d = self.root.callRemote("imageCheck", arg_0) + if callback is not None: + d.addCallback(callback) + if errback is None: + errback = self._generic_errback + d.addErrback(errback) + + def imageResize(self, image_path, width, height, callback=None, errback=None): + d = self.root.callRemote("imageResize", image_path, width, height) + if callback is not None: + d.addCallback(callback) + if errback is None: + errback = self._generic_errback + d.addErrback(errback) + def isConnected(self, profile_key="@DEFAULT@", callback=None, errback=None): d = self.root.callRemote("isConnected", profile_key) if callback is not None: @@ -773,6 +789,16 @@ d.addErrback(self._errback) return d.asFuture(asyncio.get_event_loop()) + def imageCheck(self, arg_0): + d = self.root.callRemote("imageCheck", arg_0) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def imageResize(self, image_path, width, height): + d = self.root.callRemote("imageResize", image_path, width, height) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + def isConnected(self, profile_key="@DEFAULT@"): d = self.root.callRemote("isConnected", profile_key) d.addErrback(self._errback)