最近在学习使用Django搭建网站,在用户个人资料界面,想要上传用户头像,并把所上传的图像重设大小,再保存到用户的头像属性中。遇到了一些问题,花了一晚上时间终于解决了。
主要是针对forms.py中的这个更新头像的表单类,我这里的名字是 ProfileHeadUpdateForm
class ProfileHeadUpdateForm(forms.ModelForm):
我在models.py中定义了头像字段名为 head_image
head_image = models.ImageField(verbose_name='头像', upload_to='head', default="head/default.png")
在forms.py的class ProfileHeadUpdateForm中,定义了 clean_head_image 函数:
def clean_head_image(self):
先获取用户所上传的图片:
image = self.cleaned_data.get("head_image")
如果用户选择了图片,这里image是个InMemoryUploadedFile类(只要图片大小小于2.5M);如果没有选择图片,image是个str,就是default对应的图片的路径。所以这里判断一下用户有没有选择图片:
if type(image) == str: raise forms.ValidationError("请选择一张图片")
之后就处理这个InMemoryUploadedFile类,它就是那个图片,只不过它存在于服务器内存中,而不是服务器硬盘上。但是PIL的Image.open的参数是(fp, mode="r"),PIL中对其解释如下:
"""
:param fp: A filename (string), pathlib.Path object or a file object. The file object must implement :py:meth:`~file.read`,
"""
也就是说,fp可以是个字符串,即文件路径,从硬盘中读取数据,也可以是个file对象。而InMemoryUploadedFile的父类实现了跟file一样的方法(它的所有父类全部都在django中定义,所以它本质上应该不是个file object,但是如果实现了与file object一样的方法,之后在PIL中使用它应该是可以的,因为python不管你是不是file object还是别的,它只在意你有没有这个方法。如果我没理解错的话)。因此fp参数也可以是这个InMemoryUploadedFile类的实例。下面用PIL读取image,并进行重设大小:
from PIL import Image img = Image.open(image) img = img.resize((200, 200), Image.ANTIALIAS)
之后就遇到了最重要的问题。因为forms中的clean方法必须返回跟image类型一样的实例,问题就在于我虽然把image的图像改了,现在它是PIL的某个类的实例,我如何才能把img转化成InMemoryUploadedFile类的实例呢?
最终我决定重新生成一个InMemoryUploadedFile类的实例。它需要的参数是:
def __init__(self, file, field_name, name, content_type, size, charset, content_type_extra=None):
其中field_name, name, content_type, charset, content_type_extra 可以通过image的属性获取,但是file和size就有点问题了。现在问题变成,如何用PIL将自己的图片实例转化为file object,并获取其size。
看了一下PIL的save方法:
def save(self, fp, format=None, **params): """ :param fp: A filename (string), pathlib.Path object or file object. :param format: Optional format override. If omitted, the format to use is determined from the filename extension. If a file object was used instead of a filename, this parameter should always be used.
"""
PIL可以把图片保存在file object(即参数fp所指的file object)中,也可以保存在硬盘上。这取决与fp的类型。但是如果要保存在file object中,format参数是必须给定的。但是format从哪里获得呢?从image的content_type属性就可以获得。先在内存生成一个file object(因为图片是二进制文件,所以不能用StringIO):
from io import BytesIO img_io = BytesIO()
再用PIL把img保存到这个对象里:
img.save(img_io, format=image.content_type.split('/')[-1].upper(), quality='keep')
最后只差size了,看了一下BytesIO并没有提供方法来获取这个图像的size。查了一些方法,最终还是用了最简单的sys.getsizeof()方法:
from sys import getsizeof size = getsizeof(img_io)
最后生成一个InMemoryUploadedFile类的实例并返回它:
image = InMemoryUploadedFile(img_io, image.field_name, image.name, image.content_type, size, image.charset, image.content_type_extra) return image
总结一下代码:
from io import BytesIO from sys import getsizeof from django import forms from django.contrib.auth.forms import UserCreationForm, UsernameField from .models import User from PIL import Image from django.core.files.uploadedfile import InMemoryUploadedFile class ProfileHeadUpdateForm(forms.ModelForm): class Meta: model = User fields = ("head_image",) def clean_head_image(self): image = self.cleaned_data.get("head_image") if type(image) == str: raise forms.ValidationError("请选择一张图片") img = Image.open(image).resize((200, 200), Image.ANTIALIAS) img_io = BytesIO() img.save(img_io, format=image.content_type.split('/')[-1].upper(), quality='keep') image = InMemoryUploadedFile(img_io, image.field_name, image.name, image.content_type, getsizeof(img_io), image.charset, image.content_type_extra) return image