Bootstrap

Django上传图片和访问自动生成缩略图

需求

        django使用默认的 ImageFieldFile 字段,可以上传和访问图片,但是访问的图片是原图,对于有限带宽的服务器网络压力很大,因此,我们上传的图片资源,访问的时候,字段访问缩略图,则可以为服务器节省大量资源。

需求:访问图片连接的时候,可以访问原图,也可以访问缩略图,同时支持缩放的缩略图

方案

缩略图生成是通过 django-imagekit这个库自动生成

官方文档:Installation — ImageKit 3.2.6 documentation 

 官方使用

当然,我们可以根据文档直接使用

在model中直接定义使用

from django.db import models
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill

class Profile(models.Model):
    avatar = models.ImageField(upload_to='avatars')
    avatar_thumbnail = ImageSpecField(source='avatar',
                                      processors=[ResizeToFill(100, 50)],
                                      format='JPEG',
                                      options={'quality': 60})

profile = Profile.objects.all()[0]
print profile.avatar_thumbnail.url    # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
print profile.avatar_thumbnail.width  # > 100

正如你可能知道的那样,ImageSpecFields 的工作方式与 Django 的非常相似 图像字段。不同之处在于它们是由 ImageKit 基于您给出的说明。在上面的示例中,头像 缩略图是头像图像的调整大小版本,保存为 JPEG 和 质量 60.

但是,有时您不需要保留原始图像(头像 上面的例子);当用户上传图像时,您只想对其进行处理 并保存结果。在这些情况下,您可以使用以下类:ProcessedImageField

from django.db import models
from imagekit.models import ProcessedImageField

class Profile(models.Model):
    avatar_thumbnail = ProcessedImageField(upload_to='avatars',
                                           processors=[ResizeToFill(100, 50)],
                                           format='JPEG',
                                           options={'quality': 60})

profile = Profile.objects.all()[0]
print profile.avatar_thumbnail.url    # > /media/avatars/MY-avatar.jpg
print profile.avatar_thumbnail.width  # > 100

但是,上面的两种,都没有完美符合自己的需求,因此,决定自定义封装实现

自定义封装

1.自定义模型字段,并重新url方法

class ProcessedImageFieldFile(ImageFieldFile):
    def save(self, name, content, save=True):
        filename, ext = os.path.splitext(name)
        spec = self.field.get_spec(source=content)
        ext = suggest_extension(name, spec.format)
        new_name = '%s%s' % (filename, ext)
        content = generate(spec)
        return super().save(new_name, content, save)

    def delete(self, save=True):
        # Clear the image dimensions cache
        if hasattr(self, "_dimensions_cache"):
            del self._dimensions_cache
        name = self.name
        try:
            for i in self.field.scales:
                self.name = f"{name.split('.')[0]}_{i}.jpg"
                super().delete(False)
        except Exception as e:
            pass
        self.name = name
        super().delete(save)

    @property
    def url(self):
        url: str = super().url
        if url.endswith('.png'):
            return url.replace('.png', '_1.jpg')
        return url


class ProcessedImageField(models.ImageField, SpecHostField):
    """
    ProcessedImageField is an ImageField that runs processors on the uploaded
    image *before* saving it to storage. This is in contrast to specs, which
    maintain the original. Useful for coercing fileformats or keeping images
    within a reasonable size.

    """
    attr_class = ProcessedImageFieldFile

    def __init__(self, processors=None, format=None, options=None, scales=None,
                 verbose_name=None, name=None, width_field=None, height_field=None,
                 autoconvert=None, spec=None, spec_id=None, **kwargs):
        """
        The ProcessedImageField constructor accepts all of the arguments that
        the :class:`django.db.models.ImageField` constructor accepts, as well
        as the ``processors``, ``format``, and ``options`` arguments of
        :class:`imagekit.models.ImageSpecField`.

        """
        # if spec is not provided then autoconvert will be True by default
        if spec is None and autoconvert is None:
            autoconvert = True

        self.scales = scales if scales is not None else [1]

        SpecHost.__init__(self, processors=processors, format=format,
                          options=options, autoconvert=autoconvert, spec=spec,
                          spec_id=spec_id)
        models.ImageField.__init__(self, verbose_name, name, width_field,
                                   height_field, **kwargs)

    def contribute_to_class(self, cls, name):
        self._set_spec_id(cls, name)
        return super().contribute_to_class(cls, name)

class UserInfo(AbstractUser):
    class GenderChoices(models.IntegerChoices):
        UNKNOWN = 0, _("保密")
        MALE = 1, _("男")
        FEMALE = 2, _("女")

    avatar = ProcessedImageField(verbose_name="用户头像", null=True, blank=True,
                                 upload_to=upload_directory_path,
                                 processors=[ResizeToFill(512, 512)],  # 默认存储像素大小
                                 scales=[1, 2, 3, 4],  # 缩略图可缩小倍数,
                                 format='png')

    nickname = models.CharField(verbose_name="昵称", max_length=150, blank=True)
    gender = models.IntegerField(choices=GenderChoices.choices, default=GenderChoices.UNKNOWN, verbose_name="性别")

2,在模型中使用自定义的图片字段

class UserInfo(AbstractUser):
    class GenderChoices(models.IntegerChoices):
        UNKNOWN = 0, _("保密")
        MALE = 1, _("男")
        FEMALE = 2, _("女")

    avatar = ProcessedImageField(verbose_name="用户头像", null=True, blank=True,
                                 upload_to=upload_directory_path,
                                 processors=[ResizeToFill(512, 512)],  # 默认存储像素大小
                                 scales=[1, 2, 3, 4],  # 缩略图可缩小倍数,
                                 format='png')

    nickname = models.CharField(verbose_name="昵称", max_length=150, blank=True)
    gender = models.IntegerField(choices=GenderChoices.choices, default=GenderChoices.UNKNOWN, verbose_name="性别")

默认存储的是png的图片,等访问的时候,自动生成缩略图

3,重写静态资源访问

from django.conf import settings
from django.contrib import admin
from django.urls import path, include, re_path

from common.celery.flower import CeleryFlowerView
from common.core.utils import auto_register_app_url
from common.utils.media import serve  # 重新这个方法

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/system/', include('system.urls')),
    re_path(r'api/flower/(?P<path>.*)', CeleryFlowerView.as_view(), name='flower-view'),
    # media路径配置 开发环境可以启动下面配置,正式环境需要让nginx读取资源,无需进行转发
    re_path('^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
]

auto_register_app_url(urlpatterns)

server方法如下:

import mimetypes
import posixpath
from pathlib import Path

from django.apps import apps
from django.http import FileResponse, Http404, HttpResponseNotModified
from django.utils._os import safe_join
from django.utils.http import http_date
from django.utils.translation import gettext as _
from django.views.static import directory_index, was_modified_since

from common.fields.image import ProcessedImageField, get_thumbnail

def source_name(generator, index):
    source_filename = getattr(generator.source, 'name', None)
    ext = suggest_extension(source_filename or '', generator.format)
    return f"{os.path.splitext(source_filename)[0]}_{index}{ext}"


def get_thumbnail(source, index, force=False):
    scales = source.field.scales
    # spec = ImageSpec(source)
    spec = source.field.get_spec(source=source)
    width = spec.processors[0].width
    height = spec.processors[0].height
    spec.format = 'JPEG'
    spec.options = {'quality': 90}
    if index not in scales:
        index = scales[-1]
    spec.processors = [ResizeToFill(int(width / index), int(height / index))]
    file = ImageCacheFile(spec, name=source_name(spec, index))
    file.generate(force=force)
    return file.name

def get_media_path(path):
    path_list = path.split('/')
    if len(path_list) == 4:
        pic_names = path_list[3].split('_')
        if len(pic_names) != 2:
            return
        model = apps.get_model(path_list[0], path_list[1])
        field = ''
        for i in model._meta.fields:
            if isinstance(i, ProcessedImageField):
                field = i.name
                break
        if field:
            obj = model.objects.filter(pk=path_list[2]).first()
            if obj:
                pic = getattr(obj, field)
                index = pic_names[1].split('.')
                if pic and len(index) > 0:
                    return get_thumbnail(pic, int(index[0]))


def serve(request, path, document_root=None, show_indexes=False):
    path = posixpath.normpath(path).lstrip("/")
    fullpath = Path(safe_join(document_root, path))
    if fullpath.is_dir():
        if show_indexes:
            return directory_index(path, fullpath)
        raise Http404(_("Directory indexes are not allowed here."))
    if not fullpath.exists():
        media_path = get_media_path(path)
        if media_path:
            fullpath = Path(safe_join(document_root, media_path))
        else:
            raise Http404(_("“%(path)s” does not exist") % {"path": fullpath})
    # Respect the If-Modified-Since header.
    statobj = fullpath.stat()
    if not was_modified_since(
            request.META.get("HTTP_IF_MODIFIED_SINCE"), statobj.st_mtime
    ):
        return HttpResponseNotModified()
    content_type, encoding = mimetypes.guess_type(str(fullpath))
    content_type = content_type or "application/octet-stream"
    response = FileResponse(fullpath.open("rb"), content_type=content_type)
    response.headers["Last-Modified"] = http_date(statobj.st_mtime)
    if encoding:
        response.headers["Content-Encoding"] = encoding
    return response

核心代码:

    if not fullpath.exists():
        media_path = get_media_path(path)
        if media_path:
            fullpath = Path(safe_join(document_root, media_path))
        else:
            raise Http404(_("“%(path)s” does not exist") % {"path": fullpath})

当访问的图片资源不存在的时候,通过 get_media_path 方法自动获取

访问资源如下:

原图访问:a4109256273c5f0aa78cd0bd7e35a1f1.png (512×512) (dvcloud.xin)icon-default.png?t=N7T8https://xadmin.dvcloud.xin/media/system/userinfo/1/a4109256273c5f0aa78cd0bd7e35a1f1.png

默认访问为一个缩放倍数缩略图,将.png替换为_1.jpg

 

详细代码参考github仓库 xadmin-server/common/fields/image.py at main · nineaiyu/xadmin-server (github.com)

;