@MaxR

Как в TornadoWeb (Python3) обрабатывать присланные картинки?

Добрый день,

есть тестовый сайт на python3 + tornado(4.3).
Кейс:
пользователь загружает аватарку большого размера, мне ее надо ужать до стандартного размера и показать пользователю для следующего шага (выбор участка изображения как фотографии).

Подскажите, пожалуйста, как организовать обработку фотографии так, чтобы не блокировать другие запросы пользователей?

P.S.
Для себя рассматривал следующие варианты:
  1. мультипроцессинг (вынести обработку в отдельный процесс, но тут сложности с синхронизацией вывода пользователю)
  2. асинхронная задача в Celery. Вопрос накладных расходов и вообще - красиво ли это?
  • Вопрос задан
  • 664 просмотра
Пригласить эксперта
Ответы на вопрос 2
@7j7
Часть прямо из проекта !!!

__author__ = 'vadim7j7'
# ----------------------------------------------------------------------------------------------------------------------
import multiprocessing
from os.path import join, isfile
from os import unlink
from io import BytesIO

import tornado.gen
from concurrent.futures import ThreadPoolExecutor
from tornado.concurrent import run_on_executor
from PIL import Image

from app import BaseHandler
from app.modules.helpers import gen_ran_name, crop_img
from app.sql_raw_methods.User import get_user_info, update_user_photo


# ----------------------------------------------------------------------------------------------------------------------
class UpFile(BaseHandler):
    executor = ThreadPoolExecutor(max_workers=multiprocessing.cpu_count())

    def __init__(self, application, request, **kwargs):
        super().__init__(application, request, **kwargs)
        self.path_constants = {
            '1': self.settings['tmp_path'],
            '2': self.settings['tmp_path'],
            '3': self.settings['user_avatars']}

    @tornado.gen.coroutine
    def post(self):
        self._data['dateType'] = 'json'
        if self.request.files['file']:
            if len(self.request.files['file'][0]['body']) <= 12000000:
                try:
                    thumbnail = yield self.make_thumbnail(self.request.files['file'][0]['body'])
                except IOError as er:
                    self._data['status'] = 500
                else:
                    self._data['body'] = thumbnail
                    if self.get_argument("dataTypeObject") == '3':
                        yield self.rewrite_user_avatar(thumbnail)

        self.render(None)

    def delete(self):
        self._data['dateType'] = 'json'
        name = self.get_argument('file', None)
        from_remove = self.get_argument('from_remove', 'tmp')
        if name is not None:
            patch_file_tmp = join(self.settings['static_path'], from_remove, name)
            if isfile(patch_file_tmp):
                unlink(patch_file_tmp)
                self._data['body'] = 'Ok'
            else:
                self._data['status'] = 404
                self._data['body'] = 'Not file is tmp'
        else:
            self._data['status'] = 301
            self._data['body'] = 'Not correct request'

        self.render(None)

    @run_on_executor
    def make_thumbnail(self, content):
        im = Image.open(BytesIO(content))
        path_out = self.path_constants[self.get_argument('dataTypeObject')]

        with BytesIO() as output:
            new_name = '%s.JPEG' % gen_ran_name()
            patch_file_tmp = join(path_out, new_name)

            if self.get_argument("dataTypeObject") == '3':
                src_width, src_height = im.size
                im = crop_img(im, src_width, src_height, 128, 128)

            im.convert('RGB').save(patch_file_tmp, 'JPEG', quality=100, optimize=True)
            del im

            return new_name

    @tornado.gen.coroutine
    def rewrite_user_avatar(self, photo):
        cursor = yield self.db.execute(get_user_info(self.current_user['id'], 'photo'))
        user = cursor.fetchone()

        if user[0]:
            old_avatar_path = join(self.path_constants.get('3'), user[0])
            if isfile(old_avatar_path):
                unlink(old_avatar_path)
            yield self.db.execute(update_user_photo(self.current_user['id'], photo))


# ----------------------------------------------------------------------------------------------------------------------

def crop_img(original_img, src_width, src_height, max_width, max_height):
    if max_width <= 0:
        max_width = max_height * src_width / src_height

    if max_height <= 0:
        max_height = max_width * src_height / src_width

    src_ratio = float(src_width) / float(src_height)
    dst_width, dst_height = max_width, max_height
    dst_ratio = float(dst_width) / float(dst_height)

    if dst_ratio < src_ratio:
        crop_height = src_height
        crop_width = crop_height * dst_ratio
        x_offset = float(src_width - crop_width) / 2
        y_offset = 0
    else:
        crop_width = src_width
        crop_height = crop_width / dst_ratio
        x_offset = 0
        y_offset = float(src_height - crop_height) / 3

    preview_img = original_img.crop((int(x_offset), int(y_offset),
                                     int(x_offset + int(crop_width)),
                                     int(y_offset) + int(crop_height))).resize((int(dst_width), int(dst_height)), 1)

    return preview_img
Ответ написан
@bromzh
Drugs-driven development
Уменьшай на клиенте, например. www.jqueryrain.com/demo/jquery-crop-image-plugin
Ну а вообще, торнадо же асинхронный, просто делай обработчик асинхронным. Для этого достаточно обернуть обработчик в декоратор, чтобы сделать из него корутину. Причём подойдёт декоратор из питона 3.4. И скорее всего даже новый синтаксис async/await тоже заработает, если настроить торнадо на IOLoop из asyncio (который входит в библиотеку для питонов >= 3.4 ).

www.tornadoweb.org/en/stable/guide/coroutines.html
Там пример кода есть. Вот нужно строку response = await http_client.fetch(url) заменить на обрезку файла. Чтобы заработало асинхронно, нужно сделать обрезку файла в виде корутины.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы