我正在切换到SVG图像来表示我的电子商务平台上的类别。我之前在类别模型中使用models.ImageField
来存储图像,但forms.ImageField
验证无法处理基于矢量的图像(因此拒绝它(。
我不需要对有害文件进行彻底验证,因为所有上传都将通过 Django 管理员完成。看起来我必须在模型中切换到models.FileField
,但我确实想要警告不要上传无效图像。
Nick Khlestov 在 django-rest-framework 的 ImageField 上写了一个 SVGAndImageFormField(在文章中找到源代码,我没有足够的声誉来发布更多链接(。我如何在 Django 的 ImageField(而不是 DRF 的(上使用这个解决方案?
我从未使用过SVGAndImageFormField
所以我无法对此发表评论。就个人而言,我会选择一个简单的FileField
应用程序,但这显然取决于项目要求。我将在下面对此进行扩展:
正如评论中提到的,ImageField 和 FileField 之间的基本区别在于,第一个使用 Pillow 检查文件是否是图像:
参考:Django从 FileField 继承所有属性和方法,但也验证上载的对象是否为有效图像。
文档,Django源代码
它还提供了几个可能与 SVG 案例无关的属性(高度、宽度(。
因此,模型字段可以是:
svg = models.FileField(upload_to=..., validators=[validate_svg])
您可以使用相关问题中提供的类似is_svg
函数:
如何在不使用幻数的情况下说文件是 SVG?
然后是一个验证 SVG 的函数:
def validate_svg(file, valid):
if not is_svg(file):
raise ValidationError("File not svg")
事实证明,SVGAndImageFormField
对DRF的ImageField
没有依赖关系,它只会增加django.forms.ImageField
完成的验证。
因此,为了在 Django 管理中接受 SVG,我将模型的指定了一个覆盖,如下所示:ImageField
更改为FileField
并
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
exclude = []
field_classes = {
'image_field': SVGAndImageFormField,
}
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
admin.site.register(MyModel, MyModelAdmin)
它现在接受所有以前的图像格式以及 SVG。
编辑:刚刚发现即使您不从models.ImageField
切换到models.FileField
,这也有效。models.ImageField
的height
和width
属性仍适用于光栅图像类型,并将设置为 SVG 的None
。
这是一个用作简单模型字段的解决方案,您可以将其代替models.ImageField
:
class Icon(models.Model):
image_file = SVGAndImageField()
您需要在代码中的某处定义以下类和函数:
from django.db import models
class SVGAndImageField(models.ImageField):
def formfield(self, **kwargs):
defaults = {'form_class': SVGAndImageFieldForm}
defaults.update(kwargs)
return super().formfield(**defaults)
这是SVGAndImageFieldForm
的样子:
from django import forms
from django.core.exceptions import ValidationError
class SVGAndImageFieldForm(forms.ImageField):
def to_python(self, data):
try:
f = super().to_python(data)
except ValidationError:
return validate_svg(data)
return f
我从其他解决方案中获取的功能validate_svg
:
import xml.etree.cElementTree as et
def validate_svg(f):
# Find "start" word in file and get "tag" from there
f.seek(0)
tag = None
try:
for event, el in et.iterparse(f, ('start',)):
tag = el.tag
break
except et.ParseError:
pass
# Check that this "tag" is correct
if tag != '{http://www.w3.org/2000/svg}svg':
raise ValidationError('Uploaded file is not an image or SVG file.')
# Do not forget to "reset" file
f.seek(0)
return f
此外,如果您只想使用 SVG 文件模型字段 - 您可以做得更简单。
只需创建从models.FileField
继承的类,就可以__init__
方法中添加validate_svg
函数到kwargs['validators']
。
或者只是将此验证器添加到models.FileField
并快乐:)
如注释中所述,SVGAndImageFormField
验证将失败,因为扩展是使用 django.core.validators.validate_image_file_extension
检查的,这是ImageField
的默认验证器。
解决此问题的方法是创建自定义验证器,将"svg"
添加到接受的扩展中。
编辑:感谢谢苗诺夫@Ilya的评论
from django.core.validators import (
get_available_image_extensions,
FileExtensionValidator,
)
def validate_image_and_svg_file_extension(value):
allowed_extensions = get_available_image_extensions() + ["svg"]
return FileExtensionValidator(allowed_extensions=allowed_extensions)(value)
然后,覆盖SvgAndImageFormField
中的 default_validators
属性:
class SVGAndImageFormField(DjangoImageField):
default_validators = [validate_image_and_svg_file_extension]
# ...
我的 Django4.2 解决方案:
此外,我按照 Python 文档的建议在这里使用了 defusedxml:
警告
XML 模块对于错误或恶意构造的数据并不安全。如果需要解析不受信任或未经身份验证的数据,请参阅 XML 漏洞和 defusedxml 包部分。
# form_fields.py
import defusedxml.cElementTree as et
from django.core import validators
from django.core.exceptions import ValidationError
from django.forms import ImageField
def validate_image_file_extension(value):
return validators.FileExtensionValidator(
allowed_extensions=validators.get_available_image_extensions()+['svg']
)(value)
class ImageAndSvgField(ImageField):
default_validators = [validate_image_file_extension]
def to_python(self, data):
try:
f = super().to_python(data)
except ValidationError as e:
if e.code != 'invalid_image':
raise
# Give it a chance - maybe its SVG!
f = data
if not self.is_svg(f):
# Nope it is not.
raise
f.content_type = 'image/svg+xml'
if hasattr(f, "seek") and callable(f.seek):
f.seek(0)
return f
def is_svg(self, f):
if hasattr(f, "seek") and callable(f.seek):
f.seek(0)
try:
doc = et.parse(f)
root = doc.getroot()
return root.tag == '{http://www.w3.org/2000/svg}svg'
except et.ParseError:
return False
# model_fields.py
from django.db.models.fields.files import ImageField
from . import form_fields
class ImageAndSvgField(ImageField):
def formfield(self, **kwargs):
return super().formfield(
**{
"form_class": form_fields.ImageAndSvgField,
**kwargs,
}
)
# modesl.py
from django.db import models
from .model_fields import ImageAndSvgField
class MyModel(models.Model):
...
image = ImageAndSvgField(upload_to='mymodel_images/', blank=True)
...
from django.forms import ModelForm, FileField
class TemplatesModelForm(ModelForm):
class Meta:
model = Templates
exclude = []
field_classes = {
'image': FileField,
}
@admin.register(Templates)
class TemplatesAdmin(admin.ModelAdmin):
form = TemplatesModelForm
它的工作