BLOG
Enjoy when you can, and endure when you must.
APR 09, 2015/Django
让 ImageField 更懂我的心

Django 的 ORM 提供了一个 ImageField 为我们在图片的存取上带来了极大的便利。不过在 Web 中,我们经常有这样的一个需求,就是不同的页面可能呈现不同尺寸的图片,例如一个图片浏览器通常会在列表页中显示方形的缩略图而在详情页中才展现完整尺寸的图片,为了页面显示的美观,我们必须在满足不同比例显示的同时保证美观,也就是不能让图片变形。一种处理方式就是在图片上传的时候就自动生成所需的缩略图,然后根据需求调用不同的图片达到所需的效果。要让 ImageField 更懂我们的心,看来需求对它动动手脚。

要想定制它,首先需要了解 ImageField 是如何工作的。通过阅读官方文档,可以快速的知晓,当我们在对 ImageField 做操作时,实际的奥秘来源于 ImageFieldFile。之后再阅读一下源码,基本来说就大概知道其工作原理了,比如为什么能够通过 model.image_field.url 来获得基于 MEDIA_URL 的链接。我们现在希望的是能自动保存多张图片,那一定是在 save 上做手脚,因此接下来的一切就好办了,先看代码:

class MultiSizeImageFieldFile(ImageFieldFile):
    def __init__(self, instance, field, name):
        self.config = field.config or {}
        super(MultiSizeImageFieldFile, self).__init__(instance, field, name)
    def save(self, name, content, save=True):
        # name为原文件名,与upload_to等无关
        super(MultiSizeImageFieldFile, self).save(name, content, save)
        dimensions = self.config or {}
        if not type(dimensions) == dict:
            raise ImproperlyConfigured("The configuration of dimensions for the uploading image must be dict.")
        # 保存各尺寸图片
        status, data = ImageIOTools.parse(self.file)
        image = data if status else None
        if dimensions:
            base_path, filename = os.path.split(self.name)
            for dim in dimensions.values():
                if not type(dim) == dict:
                    continue
                try:
                    width, height = dim['size']
                    action, path = dim['action'], dim['dir']
                    #quality = dim['quality']
                except Exception as e:
                    continue
                if action == 'crop':
                    # 按所需大小剪裁
                    manipulated = ImageTrimTools.auto_crop(image, width, height)
                elif action == 'scale':
                    # 缩放图片并保持原比例
                    manipulated = ImageTrimTools.scale(image, width, height)
                else:
                    continue
                path = os.path.join(self.storage.location, base_path, path)
                ImageIOTools.save(manipulated, path, filename)
    save.alters_data = True

MultiSizeImageFieldFile 自然是从 ImageFieldFile 继承而来,实现功能所需的就是重写 __init__() 以及 save() 方法,具体来看:

重写 __init__() 是为了在初始化时增加一个 config 变量,这是我们的配置项,以告诉程序需要自动保存哪些尺寸的图片,其 config  为一个字典,包含如下内容:

{
    'thumbnail': {
        'action': 'crop',
        'size': (400, 400),
        'dir': 'thumbnails',
        'quality': 100
    },
    'normal': {
        'action': 'scale',
        'size': (1200, 0),
        'dir': 's',
        'quality': 100
    },
}

它定义了两种尺寸,thumbnail 和 normal,thumbnail 是裁剪一张 400 x 400 的方形图用于缩略图展示,而 normal 则是将原图以原比例缩放到 width 为 1200 以防止原图太大而导致大量占用带宽且加载缓慢。

saev () 则真正在 model 存储的时候在保存原图的同时完成图片的剪裁或缩放并存到指定的目录中。其中用到了 ImageTrimTools,是我之前所写的图片处理方法,可以参考我的 github。

为了在获取时能方便的得到不同尺寸的图片,我还在 MultiSizeImageFieldFile 中增加了以下方法:

class MultiSizeImageFieldFile(ImageFieldFile):

    ...
    
    def _get_dimensions_path(self):
        if not hasattr(self, '_dimensions_path'):
            self._dimensions_path = {}
            for key, value in self.config.items():
                base_path, filename = os.path.split(self.name)
                self._dimensions_path[key] = self.storage.path(os.path.join(base_path, value['dir'], filename))
        return self._dimensions_path
    dimensions_path = property(_get_dimensions_path)
    def _get_dimensions_url(self):
        if not hasattr(self, '_dimensions_url'):
            self._dimensions_url = {}
            for key, value in self.config.items():
                base_path, filename = os.path.split(self.name)
                self._dimensions_url[key] = self.storage.url(os.path.join(base_path, value['dir'], filename))
        return self._dimensions_url
    dimensions_url = property(_get_dimensions_url)
    
    ...

这样就可以通过 model.image_field.dimensions_url.get('normal') 来获取对应尺寸图片的链接。

接下来还有一步没做,就是要告诉 ImageField 使用 MultiSizeImageFieldFile 作为代理。我的做法如下:

class MultiSizeImageField(ImageField):
    """
    A customed ImageField for saving different sizes of the image automatically.
    """
    attr_class = MultiSizeImageFieldFile
    def __init__(self, verbose_name=None, name=None, config=None, **kwargs):
        self.config = config
        width_field = height_field = None
        super(MultiSizeImageField, self).__init__(verbose_name, name, width_field,
                                                  height_field, **kwargs)

是不是挺简单的呢。其实,很多时候,我们只需要开动脑筋,发挥创造力,就能达到一些想要的效果。不过真要了解其工作原理,还可以仔细阅读一下官方文档中关于 ImageField、FiledFile 和 FileStorage 相关的介绍。

COMMENTS
27/03From killgodnes

perfect

25/05From Junn

UI整体风格更简洁了, 也清爽. 但还是可以提点想法: 整体页面布局和版式, 少了一些流畅, 带着些许凌乱, 蓝色与其他颜色的对比比较强烈. 一些小问题: 比如, 页面头部的不整齐(或许是浏览器兼容

25/05From jwfy

留个脚印

18/05From hu

太棒了!

LEAVE COMMNT