Django - 保存图像的多个版本
Django - Save multiple versions of an Image
我的应用程序需要保存上传图像的多个版本。一张高质量图像和另一张仅供缩略图使用的图像(低质量)。
目前这在大部分时间都有效,但有时保存方法会失败,我的所有缩略图图像都会被删除,尤其是当我在表单 remove_cover 复选框中使用时
raise ValueError("The '%s' attribute has no file associated with it."
% self.field.name) app | ValueError: The
'postcover_tn' attribute has no file associated with it.
-> 在此处查看完整跟踪:https://pastebin.com/hgieMGet
models.py
class Post(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField()
content = models.TextField(blank=False)
postcover = models.ImageField(
verbose_name="Post Cover",
blank=True,
null=True,
upload_to=image_uploads,
)
postcover_tn = models.ImageField(
verbose_name="Post Cover Thumbnail",
blank=True,
null=True,
upload_to=image_uploads,
)
published_date = models.DateTimeField(auto_now_add=True, null=True)
def save(self, *args, **kwargs):
super(Post, self).save(*args, **kwargs)
if self.postcover:
if not (self.postcover_tn and os.path.exists(self.postcover_tn.path)):
image = Image.open(self.postcover)
outputIoStream = BytesIO()
baseheight = 500
hpercent = baseheight / image.size[1]
wsize = int(image.size[0] * hpercent)
imageTemproaryResized = image.resize((wsize, baseheight))
imageTemproaryResized.save(outputIoStream, format='PNG')
outputIoStream.seek(0)
self.postcover = InMemoryUploadedFile(outputIoStream, 'ImageField',
"%s.png" % self.postcover.name.split('.')[0], 'image/png',
sys.getsizeof(outputIoStream), None)
image = Image.open(self.postcover)
outputIoStream = BytesIO()
baseheight = 175
hpercent = baseheight / image.size[1]
wsize = int(image.size[0] * hpercent)
imageTemproaryResized = image.resize((wsize, baseheight))
imageTemproaryResized.save(outputIoStream, format='PNG')
outputIoStream.seek(0)
self.postcover_tn = InMemoryUploadedFile(outputIoStream, 'ImageField',
"%s.png" % self.postcover.name.split('.')[0], 'image/png',
sys.getsizeof(outputIoStream), None)
elif self.postcover_tn:
self.postcover_tn.delete()
super(Post, self).save(*args, **kwargs)
看来我也无法正确解析:
- self.postcover_tn.delete() -> class 'InMemoryUploadedFile'
的未解析属性引用 'delete'
- self.postcover_tn.path -> class 'InMemoryUploadedFile'
的未解析属性引用 'path'
forms.py:
def save(self, commit=True):
instance = super(PostForm, self).save(commit=False)
if self.cleaned_data.get('remove_cover'):
try:
os.unlink(instance.postcover.path)
except OSError:
pass
instance.postcover = None
if commit:
instance.save()
return instance
也许如果我们从另一个角度看问题,我们可以开箱即用地解决它。 signals
在处理图像(添加、更新和删除)方面非常方便,下面是我如何设法解决您的问题:
在models.py
中:
# from django.template.defaultfilters import slugify
class Post(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField()
# slug = models.SlugField('slug', max_length=255,
# unique=True, null=True, blank=True,
# help_text='If blank, the slug will be generated automatically from the given title.'
# )
content = models.TextField(blank=False)
# ------------------------------------------------------------------------------------
# rename images with the current post id/pk (which is UUID) and keep the extension
# for cover thumbnail we append "_thumbnail" to the name
# e.g:
# img/posts/77b122a3d241461b80c51adc41d719fb.jpg
# img/posts/77b122a3d241461b80c51adc41d719fb_thumbnail.jpg
def upload_cover(instance, filename):
ext = filename.split('.')[-1]
filename = '{}.{}'.format(instance.id, ext)
path = 'img/posts/'
return '{}{}'.format(path, filename)
postcover = models.ImageField('Post Cover',
upload_to=upload_cover, # callback function
null=True, blank=True,
help_text=_('Upload Post Cover.')
)
def upload_thumbnail(instance, filename):
ext = filename.split('.')[-1]
filename = '{}_thumbnail.{}'.format(instance.id, ext)
path = 'img/posts/'
return '{}{}'.format(path, filename)
postcover_tn = models.ImageField('Post Cover Thumbnail',
upload_to=upload_thumbnail, # callback function
null=True, blank=True,
help_text=_('Upload Post Cover Thumbnail.')
)
# ------------------------------------------------------------------------------------
published_date = models.DateTimeField(auto_now_add=True, null=True)
def save(self, *args, **kwargs):
# i moved the logic to signals
# if not self.slug:
# self.slug = slugify(self.title)
super(Post, self).save(*args, **kwargs)
创建新文件并重命名 signals.py
(接近 models.py
):
import io
import sys
from PIL import Image
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.dispatch import receiver
from django.db.models.signals import pre_save, pre_delete
from .models import Post
# DRY
def image_resized(image, h):
name = image.name
_image = Image.open(image)
content_type = Image.MIME[_image.format]
r = h / _image.size[1] # ratio
w = int(_image.size[0] * r)
imageTemproaryResized = _image.resize((w, h))
file = io.BytesIO()
imageTemproaryResized.save(file, _image.format)
file.seek(0)
size = sys.getsizeof(file)
return file, name, content_type, size
@receiver(pre_save, sender=Post, dispatch_uid='post.save_image')
def save_image(sender, instance, **kwargs):
# add image (cover | thumbnail)
if instance._state.adding:
# postcover
file, name, content_type, size = image_resized(instance.postcover, 500)
instance.postcover = InMemoryUploadedFile(file, 'ImageField', name, content_type, size, None)
# postcover_tn
file, name, content_type, size = image_resized(instance.postcover_tn, 175)
instance.postcover_tn = InMemoryUploadedFile(file, 'ImageField', name, content_type, size, None)
# update image (cover | thumbnail)
if not instance._state.adding:
# we have 2 cases:
# - replace old with new
# - delete old (when 'clear' checkbox is checked)
# postcover
old = sender.objects.get(pk=instance.pk).postcover
new = instance.postcover
if (old and not new) or (old and new and old.url != new.url):
old.delete(save=False)
# postcover_tn
old = sender.objects.get(pk=instance.pk).postcover_tn
new = instance.postcover_tn
if (old and not new) or (old and new and old.url != new.url):
old.delete(save=False)
@receiver(pre_delete, sender=Post, dispatch_uid='post.delete_image')
def delete_image(sender, instance, **kwargs):
s = sender.objects.get(pk=instance.pk)
if (not s.postcover or s.postcover is not None) and (not s.postcover_tn or s.postcover_tn is not None):
s.postcover.delete(False)
s.postcover_tn.delete(False)
在apps.py
中:
我们需要在 apps.py
中注册信号,因为我们使用装饰器 @receiver
:
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class BlogConfig(AppConfig): # change to the name of your app
name = 'blog' # and here
verbose_name = _('Blog Entries')
def ready(self):
from . import signals
这是post
管理区的第一个屏幕截图
由于缩略图是从 post 封面生成的,作为 UI/UX 良好做法,不需要显示 post 封面缩略图的第二个输入文件(我保留了第二个图像字段只读 admin.py
).
下面是我上传图片后的第二张截图
PS:屏幕截图是从我正在开发的另一个应用程序中截取的,所以变化不大,在你的情况下你应该看到
- Post 封面 改为特色图片
Currently: img/posts/8b0be417db564c53ad06cb493029e2ca.jpg
(参见 models.py
中的 upload_cover()
)而不是 Currently: img/blog/posts/featured/8b0be417db564c53ad06cb493029e2ca.jpg
在admin.py
# "img/posts/default.jpg" and "img/posts/default_thumbnail.jpg" are placeholders
# grab to 2 image placeholders from internet and put them under "/static" folder
def get_post_cover(obj):
src = obj.postcover.url if obj.postcover and \
hasattr(obj.postcover, 'url') else os.path.join(
settings.STATIC_URL, 'img/posts/default.jpg')
return mark_safe('<img src="{}" height="500" style="border:1px solid #ccc">'.format(src))
get_post_cover.short_description = ''
get_post_cover.allow_tags = True
def get_post_cover_thumbnail(obj):
src = obj.postcover_tn.url if obj.postcover_tn and \
hasattr(obj.postcover_tn, 'url') else os.path.join(
settings.STATIC_URL, 'img/posts/default_thumbnail.jpg')
return mark_safe('<img src="{}" height="175" style="border:1px solid #ccc">'.format(src))
get_post_cover_thumbnail.short_description = ''
get_post_cover_thumbnail.allow_tags = True
class PostAdmin(admin.ModelAdmin):
list_display = ('title', .. )
fields = (
'author', 'title', 'content',
get_post_cover, get_post_cover_thumbnail, 'postcover',
)
readonly_fields = (get_post_cover, get_post_cover_thumbnail)
[..]
最后,您不需要 forms.py
中的 save()
函数中的任何 delete 逻辑
我的应用程序需要保存上传图像的多个版本。一张高质量图像和另一张仅供缩略图使用的图像(低质量)。 目前这在大部分时间都有效,但有时保存方法会失败,我的所有缩略图图像都会被删除,尤其是当我在表单 remove_cover 复选框中使用时
raise ValueError("The '%s' attribute has no file associated with it." % self.field.name) app | ValueError: The 'postcover_tn' attribute has no file associated with it.
-> 在此处查看完整跟踪:https://pastebin.com/hgieMGet
models.py
class Post(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField()
content = models.TextField(blank=False)
postcover = models.ImageField(
verbose_name="Post Cover",
blank=True,
null=True,
upload_to=image_uploads,
)
postcover_tn = models.ImageField(
verbose_name="Post Cover Thumbnail",
blank=True,
null=True,
upload_to=image_uploads,
)
published_date = models.DateTimeField(auto_now_add=True, null=True)
def save(self, *args, **kwargs):
super(Post, self).save(*args, **kwargs)
if self.postcover:
if not (self.postcover_tn and os.path.exists(self.postcover_tn.path)):
image = Image.open(self.postcover)
outputIoStream = BytesIO()
baseheight = 500
hpercent = baseheight / image.size[1]
wsize = int(image.size[0] * hpercent)
imageTemproaryResized = image.resize((wsize, baseheight))
imageTemproaryResized.save(outputIoStream, format='PNG')
outputIoStream.seek(0)
self.postcover = InMemoryUploadedFile(outputIoStream, 'ImageField',
"%s.png" % self.postcover.name.split('.')[0], 'image/png',
sys.getsizeof(outputIoStream), None)
image = Image.open(self.postcover)
outputIoStream = BytesIO()
baseheight = 175
hpercent = baseheight / image.size[1]
wsize = int(image.size[0] * hpercent)
imageTemproaryResized = image.resize((wsize, baseheight))
imageTemproaryResized.save(outputIoStream, format='PNG')
outputIoStream.seek(0)
self.postcover_tn = InMemoryUploadedFile(outputIoStream, 'ImageField',
"%s.png" % self.postcover.name.split('.')[0], 'image/png',
sys.getsizeof(outputIoStream), None)
elif self.postcover_tn:
self.postcover_tn.delete()
super(Post, self).save(*args, **kwargs)
看来我也无法正确解析:
- self.postcover_tn.delete() -> class 'InMemoryUploadedFile' 的未解析属性引用 'delete'
- self.postcover_tn.path -> class 'InMemoryUploadedFile' 的未解析属性引用 'path'
forms.py:
def save(self, commit=True):
instance = super(PostForm, self).save(commit=False)
if self.cleaned_data.get('remove_cover'):
try:
os.unlink(instance.postcover.path)
except OSError:
pass
instance.postcover = None
if commit:
instance.save()
return instance
也许如果我们从另一个角度看问题,我们可以开箱即用地解决它。 signals
在处理图像(添加、更新和删除)方面非常方便,下面是我如何设法解决您的问题:
在models.py
中:
# from django.template.defaultfilters import slugify
class Post(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField()
# slug = models.SlugField('slug', max_length=255,
# unique=True, null=True, blank=True,
# help_text='If blank, the slug will be generated automatically from the given title.'
# )
content = models.TextField(blank=False)
# ------------------------------------------------------------------------------------
# rename images with the current post id/pk (which is UUID) and keep the extension
# for cover thumbnail we append "_thumbnail" to the name
# e.g:
# img/posts/77b122a3d241461b80c51adc41d719fb.jpg
# img/posts/77b122a3d241461b80c51adc41d719fb_thumbnail.jpg
def upload_cover(instance, filename):
ext = filename.split('.')[-1]
filename = '{}.{}'.format(instance.id, ext)
path = 'img/posts/'
return '{}{}'.format(path, filename)
postcover = models.ImageField('Post Cover',
upload_to=upload_cover, # callback function
null=True, blank=True,
help_text=_('Upload Post Cover.')
)
def upload_thumbnail(instance, filename):
ext = filename.split('.')[-1]
filename = '{}_thumbnail.{}'.format(instance.id, ext)
path = 'img/posts/'
return '{}{}'.format(path, filename)
postcover_tn = models.ImageField('Post Cover Thumbnail',
upload_to=upload_thumbnail, # callback function
null=True, blank=True,
help_text=_('Upload Post Cover Thumbnail.')
)
# ------------------------------------------------------------------------------------
published_date = models.DateTimeField(auto_now_add=True, null=True)
def save(self, *args, **kwargs):
# i moved the logic to signals
# if not self.slug:
# self.slug = slugify(self.title)
super(Post, self).save(*args, **kwargs)
创建新文件并重命名 signals.py
(接近 models.py
):
import io
import sys
from PIL import Image
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.dispatch import receiver
from django.db.models.signals import pre_save, pre_delete
from .models import Post
# DRY
def image_resized(image, h):
name = image.name
_image = Image.open(image)
content_type = Image.MIME[_image.format]
r = h / _image.size[1] # ratio
w = int(_image.size[0] * r)
imageTemproaryResized = _image.resize((w, h))
file = io.BytesIO()
imageTemproaryResized.save(file, _image.format)
file.seek(0)
size = sys.getsizeof(file)
return file, name, content_type, size
@receiver(pre_save, sender=Post, dispatch_uid='post.save_image')
def save_image(sender, instance, **kwargs):
# add image (cover | thumbnail)
if instance._state.adding:
# postcover
file, name, content_type, size = image_resized(instance.postcover, 500)
instance.postcover = InMemoryUploadedFile(file, 'ImageField', name, content_type, size, None)
# postcover_tn
file, name, content_type, size = image_resized(instance.postcover_tn, 175)
instance.postcover_tn = InMemoryUploadedFile(file, 'ImageField', name, content_type, size, None)
# update image (cover | thumbnail)
if not instance._state.adding:
# we have 2 cases:
# - replace old with new
# - delete old (when 'clear' checkbox is checked)
# postcover
old = sender.objects.get(pk=instance.pk).postcover
new = instance.postcover
if (old and not new) or (old and new and old.url != new.url):
old.delete(save=False)
# postcover_tn
old = sender.objects.get(pk=instance.pk).postcover_tn
new = instance.postcover_tn
if (old and not new) or (old and new and old.url != new.url):
old.delete(save=False)
@receiver(pre_delete, sender=Post, dispatch_uid='post.delete_image')
def delete_image(sender, instance, **kwargs):
s = sender.objects.get(pk=instance.pk)
if (not s.postcover or s.postcover is not None) and (not s.postcover_tn or s.postcover_tn is not None):
s.postcover.delete(False)
s.postcover_tn.delete(False)
在apps.py
中:
我们需要在 apps.py
中注册信号,因为我们使用装饰器 @receiver
:
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class BlogConfig(AppConfig): # change to the name of your app
name = 'blog' # and here
verbose_name = _('Blog Entries')
def ready(self):
from . import signals
这是post
管理区的第一个屏幕截图
由于缩略图是从 post 封面生成的,作为 UI/UX 良好做法,不需要显示 post 封面缩略图的第二个输入文件(我保留了第二个图像字段只读 admin.py
).
下面是我上传图片后的第二张截图
PS:屏幕截图是从我正在开发的另一个应用程序中截取的,所以变化不大,在你的情况下你应该看到
- Post 封面 改为特色图片
Currently: img/posts/8b0be417db564c53ad06cb493029e2ca.jpg
(参见models.py
中的upload_cover()
)而不是Currently: img/blog/posts/featured/8b0be417db564c53ad06cb493029e2ca.jpg
在admin.py
# "img/posts/default.jpg" and "img/posts/default_thumbnail.jpg" are placeholders
# grab to 2 image placeholders from internet and put them under "/static" folder
def get_post_cover(obj):
src = obj.postcover.url if obj.postcover and \
hasattr(obj.postcover, 'url') else os.path.join(
settings.STATIC_URL, 'img/posts/default.jpg')
return mark_safe('<img src="{}" height="500" style="border:1px solid #ccc">'.format(src))
get_post_cover.short_description = ''
get_post_cover.allow_tags = True
def get_post_cover_thumbnail(obj):
src = obj.postcover_tn.url if obj.postcover_tn and \
hasattr(obj.postcover_tn, 'url') else os.path.join(
settings.STATIC_URL, 'img/posts/default_thumbnail.jpg')
return mark_safe('<img src="{}" height="175" style="border:1px solid #ccc">'.format(src))
get_post_cover_thumbnail.short_description = ''
get_post_cover_thumbnail.allow_tags = True
class PostAdmin(admin.ModelAdmin):
list_display = ('title', .. )
fields = (
'author', 'title', 'content',
get_post_cover, get_post_cover_thumbnail, 'postcover',
)
readonly_fields = (get_post_cover, get_post_cover_thumbnail)
[..]
最后,您不需要 forms.py
save()
函数中的任何 delete 逻辑