我想要编写一个图片管理程序,那么通常会将文件保存在磁盘,数据库字段只保存文件的地址。Django 为此有个专门的 ImageField,不仅仅提供了保存地址,还有获取图片地址、宽高等功能,为此需要好好看看其内部机制。
ImageField 提供的功能
首先查看官网文档,其继承自 FileField,有 FileField 的所有属性和方法,有几点需要关注:
- 通过
upload_to
初始化参数可以定制上传路径。 - 通过
storage
初始化参数可以定制储存后端。 - 访问模型的 FileField 实际是在使用一个 FieldFile 实例,有
name
、path
、url
和size
等属性,但是不可以使用标准的 python 文件 api 读写。 - 这些属性都需要调用储存引擎方法获得,而不是储存在数据库中。
再看 ImageField,他相对 FileField 有额外的 height
和 width
属性。同样不是储存在数据库中,而是通过 Pillow 打开图片获得的,这就带来了性能和查询问题。
文档中还介绍了 ImageField 有验证输入文件是否为图片的功能,目前看来也是通过 Pillow 打开文件来获取的,支持的格式也没有详细说明,需要看看源码。
最后还有两个 ImageField 独有的参数:height_field
和 width_field
,根据说明是“每次保存模型实例时将自动填充图像的高度/宽度”,但又说“ImageField
实例在数据库中创建为 varchar
列”,只看说明完全搞不懂这两个参数在干什么。
ImageField 支持的格式
文档中未载明,但通过搜索发现,其提供了 get_available_image_extensions()
方法,可以获取到支持的扩展名列表:
>>> from django.core.validators import get_available_image_extensions
>>> get_available_image_extensions()
['blp', 'bmp', 'dib', 'bufr', 'cur', 'pcx', 'dcx', 'dds', 'ps', 'eps', 'fit', 'fits', 'fli', 'flc', 'ftc', 'ftu', 'gbr', 'gif', 'grib', 'h5', 'hdf', 'png', 'apng', 'jp2', 'j2k', 'jpc', 'jpf', 'jpx', 'j2c', 'icns', 'ico', 'im', 'iim', 'jfif', 'jpe', 'jpg', 'jpeg', 'mpg', 'mpeg', 'tif', 'tiff', 'mpo', 'msp', 'palm', 'pcd', 'pdf', 'pxr', 'pbm', 'pgm', 'ppm', 'pnm', 'psd', 'qoi', 'bw', 'rgb', 'rgba', 'sgi', 'ras', 'tga', 'icb', 'vda', 'vst', 'webp', 'wmf', 'emf', 'xbm', 'xpm']
Django 通过 Pillow 来验证图片,所以这就是 Pillow 默认支持的图片类型,可以看到常用的 bmp、jpg、png、gif 和 webp 都已经包含在内。但是目前苹果常用的 heic 格式和先进 CDN 支持的 avif 格式都没有支持。
目前看可能需要手动注册 pillow-avif-plugin 插件和 pillow-heif 插件,但这两种格式可能有专利和授权问题需要考虑,暂时先放着不管了。
ImageField 的宽和高
需要记住一点,一个 Field 对应数据库中一列,因此 ImageField 在数据库中仅保存了地址。宽高这些属性都是通过 Pillow 打开文件后临时读取的。这面临两个问题,一是性能,二是查询。
第二点尤为致命,无法在数据库中直接查询在很多时候会很麻烦。经过阅读源码和一些搜索,可以发现 Django 的 ImageField 已经考虑到了这点,设置 height_field
和 width_field
参数后会自动将高度和宽度填入指定的字段中。
来一个例子,有模型:
from django.db import models
class MyModel(models.Model):
img_width = models.IntegerField()
img_height = models.IntegerField()
image = models.ImageField(
upload_to='images/',
height_field='img_height',
width_field='img_width',
)
height_field
和 width_field
分别指定了高度和宽度的字段名称为 img_height
和 img_width
,因此在保存时会自动填入以上字段。
在查看源码时发现,ImageField 的宽高属性时是通过信号系统来添加到实例的,也就是在实例初始化完成后,再更新属性。这给了我一个思路,对于图片的 exif 信息等需要额外附加的信息,都可以通过信号系统在保存后或保存时添加。
Django 当前支持了 JSONField,因此可以更简单的保存查询这些额外信息。