需求
django使用默认的 ImageFieldFile 字段,可以上传和访问图片,但是访问的图片是原图,对于有限带宽的服务器网络压力很大,因此,我们上传的图片资源,访问的时候,字段访问缩略图,则可以为服务器节省大量资源。
需求:访问图片连接的时候,可以访问原图,也可以访问缩略图,同时支持缩放的缩略图
方案
缩略图生成是通过 django-imagekit这个库自动生成
官方文档:Installation --- ImageKit 3.2.6 documentation
官方使用
当然,我们可以根据文档直接使用
在model中直接定义使用
python
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
python
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方法
python
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)
python
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,在模型中使用自定义的图片字段
python
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,重写静态资源访问
python
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方法如下:
python
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
核心代码:
python
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 方法自动获取
访问资源如下:
默认访问为一个缩放倍数缩略图,将.png替换为_1.jpg
详细代码参考github仓库 xadmin-server/common/fields/image.py at main · nineaiyu/xadmin-server (github.com)