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.
This commit is contained in:
Goffi 2019-10-29 20:38:39 +01:00
commit 93a4e488ea
7 changed files with 197 additions and 0 deletions

View file

@ -909,3 +909,24 @@ sig_in=
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

View file

@ -384,6 +384,18 @@ class DbusObject(dbus.service.Object):
def historyGet(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@", callback=None, errback=None):
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)

View file

@ -361,6 +361,13 @@ class Const(object):
# 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"]'

View file

@ -40,8 +40,10 @@ from sat.memory import cache
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 @@ class SAT(service.Service):
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 @@ class SAT(service.Service):
}
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):

77
sat/tools/images.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
"""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)

View file

@ -469,6 +469,29 @@ class Bridge:
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 @@ class AIOBridge(Bridge):
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()

View file

@ -375,6 +375,22 @@ class Bridge(object):
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 @@ class AIOBridge(Bridge):
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)