/ Python / 47浏览

探索 Django 的 ImageField

我想要编写一个图片管理程序,那么通常会将文件保存在磁盘,数据库字段只保存文件的地址。Django 为此有个专门的 ImageField,不仅仅提供了保存地址,还有获取图片地址、宽高等功能,为此需要好好看看其内部机制。

ImageField 提供的功能

首先查看官网文档,其继承自 FileField,有 FileField 的所有属性和方法,有几点需要关注:

  1. 通过 upload_to 初始化参数可以定制上传路径。
  2. 通过 storage 初始化参数可以定制储存后端。
  3. 访问模型的 FileField 实际是在使用一个 FieldFile 实例,有 namepathurl size 等属性,但是可以使用标准的 python 文件 api 读写。
  4. 这些属性都需要调用储存引擎方法获得,而不是储存在数据库中。

再看 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,因此可以更简单的保存查询这些额外信息。