ImprovedImageField
В процессе работы над одним проектом я в очередной раз столкнулся с тем, что джанговский ImageField меня ну никак не устраивает — в нём отсутствует возможность ресайза при аплоаде, некак загружать картинки в разные директории1, кроме как по дате, ну и динамически назвать файлик2 тоже нельзя.
В результате поисков готового решения для копипаста не нашлось, хотя два готовых поля с различной функциональностью было. Первое имеет страшный код с жуткими комментариями3, с готовой функциональностью, закрывающей большую часть ТЗ и неработающее в современной Джанге. :) Второе — умеет заливать файлики по динамически изменяющемуся пути, но зато больше ничего не умеет.
Совмещение этих двух подходов много времени не заняло — на самом деле написание этого поста заняло куда больше времени, чем прошло от идеи и начала поисков до рабочей реализации (код поля приведен в конце поста).
Разбор полётов
В функциях fit и rename, занимающихся соответственно ресайзом и
переименованием, объяснять в общем-то нечего — всё сказано в
докстрингах. :)
Сам класс начинается с изменения init‘а, для получения параметров
ресайза. Тут же, кстати, кроется небольшой недостаток — сейчас
невозможно не указывать upload_to в параметрах классу, но я пока не
придумал, как проверить существование функции для динамической
установки пути upload_to.
Кстати, где-то не так давно прочитал, что использование
contribute_to_class сейчас не приветствуется, но я для себя
альтернатив в документации не нашёл — потому все три рабочие функции
подключаю именно с помощью contribute_to_class.
Первая — _upload_to, проверяет на существование у модели метода
determine_FIELD_upload_to (который из параметров принимает только self),
и при наличии такого — вызывает его для установки upload_to (при
отсутствии остаётся рабочим параметр, переданный в конструктор класса).
Дальше тоже нету ничего военного — _resize, после проверки на
заполненность поля исправляет его размер (сразу после сохранения4).
Ну и “переименовывалка” — при заполненности поля и присутствии у
модели метода determine_FIELD_filename переименовывает в соответствии с
ним сущестсвующий файл (а вот это происходит как раз перед
сохранением5).
Использовать всё это очень просто, вот пример готовой модели:
class UserProfile(models.Model):
user = models.ForeignKey(User)
avatar = ImprovedImageField(max_height=100, max_width=100, blank=True,
upload_to='_')
def determine_avatar_upload_to(self):
return self.user.username
def determine_avatar_filename(self):
return 'avatar'
Теперь при загрузке аватаров для пользователя ‘piranha’ они будут помещаться в
каталог {{ settings.MEDIA_ROOT }}/piranha/avatar.jpg6.
Код самого поля:
import os
import shutil
import Image
from django.db.models import ImageField, signals
from django.dispatch import dispatcher
from django.conf import settings
def fit(file_path, max_width, max_height):
"""Resize file (located on file path) to maximum dimensions proportionally.
At least one of max_width and max_height must be not None."""
if not (max_width or max_height):
# Can't resize
return
img = Image.open(file_path)
w, h = img.size
w = int(max_width or w)
h = int(max_height or h)
img.thumbnail((w, h), Image.ANTIALIAS)
img.save(file_path)
def rename(old_path, new_name):
"""
old_name is relative to MEDIA_ROOT
new_name is just base name, without extension
"""
def fp(path):
return os.path.join(settings.MEDIA_ROOT, path)
if not os.path.isfile(fp(old_path)):
return old_path
path = os.path.dirname(old_path)
ext = os.path.splitext(old_path)[1]
# django wants to have '/' in path
new_path = '/'.join([path, new_name + ext])
if new_path != old_path:
try:
shutil.move(fp(old_path), fp(new_path))
except IOError:
return old_path
return new_path
class ImprovedImageField(ImageField):
"""Allows model instance to specify following parameters dynamically:
— upload_to, specify following method for model:
def determine_FIELD_upload_to(self):
return 'avatars/%d' % self.user.username
— filename (relative to upload_to, without extension):
def determine_FIELD_filename(self):
return self.pk
Additionally field supports automatic resizing, if at least one of
max_width and max_height supplied.
Current flaws: upload_to must be specified as parameter (even if
custom method exist, although parameter can carry useless value).
Based on:
http://code.djangoproject.com/wiki/CustomUploadAndFilters
http://scottbarnham.com/blog/2007/07/31/uploading-images-to-a-dynamic-path-with-django/
Copyright (c) 2008 Alexander Solovyov under new BSD License.
"""
def __init__(self, max_width=None, max_height=None, **kwargs):
self.max_width, self.max_height = max_width, max_height
super(ImprovedImageField, self).__init__(**kwargs)
def db_type(self):
"""Required by Django for ORM."""
return 'varchar(100)'
def contribute_to_class(self, cls, name):
"""Hook up events so we can access the instance."""
super(ImprovedImageField, self).contribute_to_class(cls, name)
dispatcher.connect(self._upload_to, signals.post_init, sender=cls)
dispatcher.connect(self._resize, signals.post_save, sender=cls)
dispatcher.connect(self._rename, signals.pre_save, sender=cls)
def _upload_to(self, instance=None):
"""Get dynamic upload_to value from the model instance."""
if hasattr(instance, 'determine_%s_upload_to' % self.attname):
self.upload_to = getattr(instance, 'determine_%s_upload_to' % self.attname)()
def _resize(self, instance):
if getattr(instance, self.attname):
real_path = os.path.join(settings.MEDIA_ROOT, getattr(instance, self.attname))
if os.path.isfile(real_path):
fit(real_path, self.max_width, self.max_height)
def _rename(self, instance):
if hasattr(instance, 'determine_%s_filename' % self.attname) and getattr(instance, self.attname):
filename = getattr(instance, 'determine_%s_filename' % self.attname)()
new_path = rename(getattr(instance, self.attname), filename)
setattr(instance, self.attname, new_path)
UPD. Наконец-то пофиксил траблу с get_FIELD_filename.
А если этого не сделать, то директория быстро загрязнится кучей файлов, что в будущем вполне может привести к заметному увеличению времени поиска файлов системой.
Это в первоначальные планы не входило, но позже обрело своё место под солнцем. :)
В стиле a=2+3 # Вычисляем 2+3.
А точнее, после вызова сигнала post_save у нашей модели.
На этот раз — сигнала pre_save.
Естественно, расширение зависит от типа файла. :)
Comments
А что будет если в _rename я захочу использовать поле id, которые появится после
INSERT?Собственно, сам, когда столкнулся с необходимости переименования и изменения размера(и ещё создания превьюх), сделал универсальное переименование в уникальный хеш(на практике выяснилось, что “красивое” имя файла совсем не важно). А, как оказалось, масштабирование и создания маленьких образов лучше сделать сразу в
_save_FIELD_file, чтобы диск не насиловать много:)А ещё надо добавить опцию для непропорционального тумбнэйла, что бы квадратные тумбы получать, но пр и этом они не были сжатыми..
вот что то типа такого.
Спасибо, попробую этот код. У меня иначе сделано - я накладывааю полученный thumbnail на чёрный фон (центрируя по вертикали/горизонтали).
Вот кстати интересный вариант - накладывать на прозрачный фон, центрируя по вертикали и горизонтали. Но с jpg это не катит, да и с png (долбаный IE!) тоже.
У меня фон именно чёрный поэтому с прозрачностью заморачиваться не приходится. Может как вариант в settings прописывать цвет фона (это на правах бреда)?
Может. Типа, в зависимости от дизайна сайта. С другой стороны, если дизайн меняется, наступает полное и абсолютное зло.
У нас в рабочем проекте цсс-ные учаснеги как-то достигли центрирования неквадратных аватаров в квадратном участочке. Так что может это лучший вариант? :)
Согласен, что при смене дизайна наступает конец :) Но в данном случае нет необходимости.
А я как раз удалил. Имхо кроп - это зло. :)
А вот насчёт красивого имени не совсем согласен. Если есть необходимость выводить аватар пользователя, то чтобы не лезть дополнительным запросом в таблицу с профайлом, можно аватару присваивать ID пользователя в качестве имени файла. У меня так реализовано :)
PS. Хотя можно сделать любимый contribute_to_class и хранить тот же хеш в таблице User.
Хмм… похоже, будет неприятно. ;-)
Ну… Неважно для работоспособности? Это да. Неважно для пользователей? А вот это смотря каких - мне, например, приятно, что я могу вспомнить схему написания какого-то урла и без проблем найти то, что хочется. А не как в ЖЖ - через уникальные айдишники. Собственно, для этого human-readable урлы и придумали, и красивый путь к картинкам - всего лишь ещё одно его проявление. ;-)
Хмм… Кстати да, я просто привязался к событиям вокруг этого метода, а можно было бы просто его самого заменить… Надо поразмышлять.
Ответь себе на вопрос, как часто ты вбиваешь в адресную строку url картинки?:)
Тут тебя ожидает подлянка небольшая. ;-) Я - нередко по памяти урлы набираю, в том числе и картинки. :)
Ну надо признать, что это не очень распространенный use case….
Ну, я про подлянку писал. Естественно, я никак не являюсь репрезентативной выборкой.
Но, если есть возможность, средства и желание - почему нет? =) Кстати, ещё при чётко понятном сразу названии нету проблем с накапливанием старых аватаров - они просто заменяются новым.
Блин, что то не интуитивный у тебя интерфейс в блогах.. Хотел просто откомментить в блогах, а получилось кому-то ответил
У каждого сообщения есть ссылка - ответить. Логично, что это ответ на сообщение. А ссылка комментирования поста - находится под постом. Может разве что её налево передвинуть…
У меня создание превью попроще происходит. А у тебя это всё более “по уму” :)
У меня нету создания превью. =) Тамбнейлами у меня занимается sorl-thumbnail, а тут я просто делаю картинку меньшего размера.
Что-то не работает ссылка на sort-thumbnail, говорит 404.
Что-то меня заглючило. sorl-thumbnail, пофиксил.
а мы взяли sorl-thumbnail и довели до ума. тоже довольно удобно получилось.
Это разные вещи. Я тоже использую изменённый
sorl-thumbnail. И я об этом уже писал, в комментарии выше.два момента:
get_FIELD_filename пересекается с методом, который добавляет в модель FileField (от которого унаследован ImageField), что не очень хорошо
инициализация upload_to в post_init не будет работать для довольно простого случая, когда вышеприведенный UserProfile создается без инициализации user в конструкторе:
по-моему, можно не извращаться с upload_to, если потом все равно делается переименование в pre_save. надо только rename научить между каталогами двигать :)
Хм. Реальный втык. Переименую. :-)
В принципе да, просто они у меня лично в разных местах выросли. :) Прикол в том, что
upload_toвполне может быть полезен без переименования. Можно, конечно, в методе переименования это всё делать, но как-то лениво проверять… :)Comment form for «ImprovedImageField»