Python, Django: Автоматический ресайз загружаемых изображений

VitaliyRodnenko, 29.04.2009

На данный момент мы занимаемся разработкой городской социальной сети. Не «убийцы» Вконтакте и Одноклассников, ее задачи будут в корне иными. В качестве программной основы был выбран Django, написанный на языке Python.

Разрабатывая интерфейс управления личной информацией необходимо было решить задачу: при загрузке пользователем собственной фотографии, ее размер должен быть не более некоторых значений, определенных настройками системы, если фото больше, то необходимо произвести ресайз с сохранением пропорций.

В этой статья я представлю наше решение поставленной задачи. Возможно, оно окажется не оптимальным, поэтому с удовольствием выслушаю ваши комментарии!

Django, ресайз изображений

Функция ресайза изображения. Автоматически изменяет размер, сохраняя пропорции:

def imageResize(data, output_size):
  """
  Resize image for thumbnails and preview
  data — image for resize
  output_size — turple, contains width and height of output image, for example (200, 500)
  """

  from PIL import Image

  image = Image.open(data)
  m_width = float(output_size[0])
  m_height = float(output_size[1])
  if image.mode not in ('L', 'RGB'):
    image = image.convert('RGB')
  w_k = image.size[0]/m_width
  h_k = image.size[1]/m_height
  if output_size < image.size:
    if w_k > h_k:
      new_size = (m_width, image.size[1]/w_k)
    else:
      new_size = (image.size[0]/h_k, m_height)
  else:
    new_size = image.size
  return image.resize(new_size,Image.ANTIALIAS)

Например, при максимально возможных размерах изображения 200×500 px, загружая изображения на 1024×768 px, новые изображения будут — 200×150 px.

Далее перекрываем стандартный джанговский models.ImageField, расширяя метод save_form_data, в котором и будем производить ресайз:

class AvatarImageField(models.ImageField):
  def save_form_data(self, instance, data):
    from StringIO import StringIO
    from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile

    if data and isinstance(data, UploadedFile):
      image = imageResize(data, settings.AVATAR_SIZE)
      new_image = StringIO()
      image.save(new_image, 'JPEG', quality=85)
      data = SimpleUploadedFile(data.name, new_image.getvalue(), data.content_type)

      # Удаление старого файла
      previous = u'%s%s' % (settings.MEDIA_ROOT, instance.avatar)
      if os.path.isfile(previous):
        os.remove(previous)
      # -
    super(AvatarImageField, self).save_form_data(instance, data)

Определяем функцию, которая будет возвращать путь к директории сохранения файла, а так же имя файла. Она будет возвращать нужный путь для upload_to файлового поля. В нашем случае имя файла генерируется некоторой хэш-функцией:

  def make_upload_path(instance, filename, prefix = False):
    # Переопределение имени загружаемого файла.
    from utils.hashfunc import get_hash

    filename = 'a_' + get_hash('md5') + '.jpg'
    return u"%s/%s" % (settings.AVATAR_UPLOAD_DIR, filename)

И, собственно, сама модель данных, поле avatar определяется нашим перекрытым классом AvatarImageField, следовательно, наследует всю его функциональность:

class Avatars(models.Model):
  user = models.ForeignKey(User, unique=True)
  avatar = AvatarImageField(
    upload_to=make_upload_path,
    null = False,
    blank = False,
    )

Готово. Теперь все загружаемые изображение, превышающие определенные в системе размеры будут ресайзиться, сохраняя пропорции.

С удовольствием готов ответить на вопросы, а так же выслушать конструктивную критику этого метода!

Говорим спасибо

Выражаю благодарность Ивану Сагалаеву за его ресурс о программировании в целом и в частности на Python. На этапе ознакомления с Django почти с не покидали его страницы.

Подписаться на обновления блога

Вам понравился наш блог, хотите следить за обновлениями? Подпишитесь на RSS рассылку или рассылку по электронной почте. Так же вы можете следить за нами в Twitter.

Категории: Django, Python | Комментировать

Комментарии (12)

  1. excieve / 29.04.2009 в 04:09

    ИМХО, не очень правильный подход. А если в будующем будет нужно изменить этот размер? Мне кажется, что лучше сохранять в оригинальном размере, а при доступе к изображению в шаблоне — применять фильтр или тэг, естественно, кешируя результат. Что дает большую гибкость как в настройке, так и в использовании — можно легко изпользовать сколько угодно вариантов размеров изображения. Такой подход уже реализован в sorl-thumbnail кажется. Однако, если изменения размеров не планируется в принципе или нужно только изначально ограничить размер неким максимальным значением, то это, конечно, подходящее решение.

  2. maxcentry / 29.04.2009 в 10:26

    Не знаю на сколько это просто в реализации на Python, но избыточное (больше чем это нужно для реального изображения) использование фильтров по типу Sharpen и затем уменьшение изображения позволит добиться большей резкости в уменьшенном варианте изображения. Степень избыточности можно подобрать опытным путем.

  3. Skaizer / 29.04.2009 в 13:36

    excieve, да, действительно, не подумал о ситуациях, когда в будущем размеры необходимо будет изменить. На момент написания этого скрипта реализация с помощью шаблонного тега или фильтра казалась несколько странной. Спасибо за обоснование! Минус конечно в том, что придется хранить тонны изображений, которые возможно окажутся не нужными. А пользователи любя загрузить картинки, снятые на >5 мегапиксельные камеры…

    maxcentry, благодарю, и правда изображения стали смотреться по четче. в Python это реализуется достаточно просто:

    from PIL import ImageEnhance
    sharpen = ImageEnhance.Sharpness(image)
    image = sharpen.enhance(15.0)
  4. woodpeaker / 29.04.2009 в 18:13

    А почему вы не учитываете нагрузки на сервер при ресайзинге?

    У меня была задача — налету собирать из кусочков PNG board, накладывать слои и менять размер картинки по запросу, но данная задача так грузила сервер, что я от такого отказался…

    Точнее не то чтобы отказался, а дополнил: при первом запросе картинки размер ресайзится и ложится в «кэш-папку», далее отдается как статика.

    По вашему получается, что на одну картинку надо около 5-6 сек проца(40-50%) + памяти (на глаз не помню сколько). + Если запросов будет больше чем 1 в две сек ваш сервак будет испытывать некие трудности, вплоть до зависания.

    Так, что самый оптимальный вариант (+ подстраховка). Сохранять оригинальную картинку + сразу ресайзить. Есди надо будет другие размеры, то с оригинала можно будет сделать новые варианты.

    А с ресайзом налету — не вариант, будет сильно тупить.

  5. woodpeaker / 29.04.2009 в 18:15

    поправка: не в вашем случае 5-6 сек на проц… это в моем случае было:-) В Вашем будет около 1-2 сек, взамисимости от общей загрузки

  6. rootart / 30.04.2009 в 00:15

    Ну по перше я не думаю що зміна розмірів картинки методами PIL вимагає дуже багато ресурсів (звісно все залежить від потужностей). Крім того якщо вже на те пішло то потрібно ставити певного роду чергу обробки у випадку великої завантаженості. Особисто я для себе використовував схожі методи описані тут http://www.djangosnippets.org/snippets/20/ .

    Відносно ресайза на ходу то можна застосувати тільки потрібно правильно кешувати результат
    http://www.djangosnippets.org/snippets/192/

  7. Skaizer / 07.05.2009 в 13:53

    Попробовал для ресайзов использовать sorl-thumbnail (code.google.com/p/sorl-thumbnail/), действительно мощный модуль, но возникли вопросы:

    1. Можно ли использовать ресайз изображений sorl-thumbnail на уровне загрузки файла изображения. Т.е. я понимаю, что sorl-thumbnail создаст ресайз при первой попытке открыть миниатюру, но мне бы так же хотелось ресайзить оригинальное изображение, например до некоторого максимального значения, определенного в настройках системы. Чтобы не хранить 5,6,7 и т.д. мегапиксельные фото, я хочу хранить оригинал, например в разрешении не превышающем 1600х1200, а из него потом делать различные сумбнейлы. На данный момент я произвожу этот ресайз своими средствами, возможно это можно организовать через sorl-thumbnail ?
    2. В проекте несколько приложений, в которых должен использоваться sorl-thumbnail, например персональный профиль пользователя, где юзер может закачать аватар и фотогалерея, где так же необходимо создавать несколько превьюшек загружаемых изображений. Но настройки в sorl-thumbnail в settings.py ведь будут распространяться для всех приложений, использующих sorl-thumbnail. Файловая организация проекта такова, чтоизображения различных приложения хранятся в различных директориях. Thumbnail’ы должны сохраняться в поддиреториях директории приложения. Если мы зададим префикс THUMBNAIL_PREFIX или директории THUMBNAIL_BASEDIR, THUMBNAIL_SUBDIR то они будут одинаковы для всего проекта. Можно ли сделать их локальными для каждого из приложений, где необходим sorl-thumbnail?

    Офф. документацию почитал, возможно упустил важные моменты. Уважаемые посетители нашего блога, если у вас имеются мысли по этому поводу, поделитесь, пожалуйста.

  8. Никита / 03.09.2009 в 18:05

    Я использую немного другой подход, хотя, что-то подобное тоже писал давно:

    Собственно, в модели есть картинка, одна основная (абстрактная, get_thumbnail_url) функция создания тумбочек и несколько используемых (get_small, get_big, …) на сайте.

    get_thumbnail_url проверяет есть ли изображение, потом проверяет есть ли тумбочка, если нет, то строит путь os.makedirs и создает уменьшенную копию, возвращает путь.

    Кстати, для уменьшения использую стандартную функция image.thumbnail((width,height), ANTIALIAS), где один из параметров может быть *.

    
    class Photo(models.Model):
       adv = models.ForeignKey(Adv, verbose_name=u"объявление")
       photo = models.ImageField(verbose_name=u"фото",  upload_to=u"photos/%Y/%m/%d")
       
       def get_thumbnail_url(self, width, height):
          """Возвращает URL уменьшенной копии фотографии"""
          if not os.path.exists(self.photo.path):  return None
          thumb_url = None
          photo = str(self.photo)
          full_path_to_file = "%s/%s" % (MEDIA_ROOT, photo)
          filename = self.photo.path[self.photo.path.rfind("/")+1:]
          upload_to = self.photo.path.replace(MEDIA_ROOT+"/", "").replace("/"+filename,"")
          path_to_thumbnail_dir = "%s/thumbnails/%s/%sx%s" % (MEDIA_ROOT, upload_to, str(width), str(height))
          if not os.path.exists(path_to_thumbnail_dir): os.makedirs(path_to_thumbnail_dir)
          path_to_thumbnail = "%s/%s" % (path_to_thumbnail_dir, filename)
          if not os.path.exists(path_to_thumbnail):
             try:
                image = open_image(self.photo.path)
                image.thumbnail((width,height), ANTIALIAS)
                image.save(path_to_thumbnail, image.format, quality=95)
             except:
                return None
          return "%sthumbnails/%s/%sx%s/%s" % (MEDIA_URL, upload_to, str(width), str(height), filename)
    
       def get_small(self):
          if not self.get_thumbnail_url(100, 75):
             return "%simages/no-photo.jpg" % MEDIA_URL
          return self.get_thumbnail_url(100, 75)
       
       def get_medium(self):
          return self.get_thumbnail_url(180, "*")
       
       def get_big(self):
          return self.get_thumbnail_url(400, "*")
  9. Skaizer / 05.09.2009 в 13:52

    Спасибо за метод, интересно! Я все-таки стал использовать code.google.com/p/sorl-thumbnail/ для своих проектов, возможности библиотеки очень богатые. К сожалению, где-то приходится подпиливать своими патчами, но в целом она умеет все, что мне нужно при работе с изображениями и превьюшками.

  10. trilby / 10.02.2010 в 10:40

    Skaizer: ну как, не нашли способ ресайзить оригинальную загруженную картинку до приемлемых размеров? то, что я ищу :)

  11. Gordio / 04.11.2011 в 03:22

    Предпочитаю и вам рекомендую попробовать https://github.com/SmileyChris/easy-thumbnails , в нем и загружаемый файл можно подготовить и тумбочки в кеш складывать.

  12. Владимир / 02.06.2012 в 07:11

    Моя реализация состоит в том чтобы хранить оригиналы в GridFS (MongoDB). Во первых мы решаем проблему с масштабируемостью ресурса и хранением ненужных изображений. При первом обращении к оригиналу или Thumbnail’у мы достаем из базы изображение, ресайзится при нужде и сохраняется в кэше. Для ресайза используется pillow. Если пользователь удаляет пост, то удаляется и оригинал, и чистится кэш.

Оставить комментарий

480×60
480×60