允许SVG文件通过Django admin上传到ImageField



我正在切换到SVG图像来表示我的电子商务平台上的类别。我之前在类别模型中使用models.ImageField来存储图像,但forms.ImageField验证无法处理基于矢量的图像(因此拒绝它(。

我不需要对有害文件进行彻底验证,因为所有上传都将通过 Django 管理员完成。看起来我必须在模型中切换到models.FileField,但我确实想要警告不要上传无效图像。

Nick Khlestov 在 django-rest-framework 的 ImageField 上写了一个 SVGAndImageFormField(在文章中找到源代码,我没有足够的声誉来发布更多链接(。我如何在 Django 的 ImageField(而不是 DRF 的(上使用这个解决方案?

我从未使用过SVGAndImageFormField所以我无法对此发表评论。就个人而言,我会选择一个简单的FileField应用程序,但这显然取决于项目要求。我将在下面对此进行扩展:

正如评论中提到的,ImageField 和 FileField 之间的基本区别在于,第一个使用 Pillow 检查文件是否是图像:

从 FileField 继承所有属性和方法,但也验证上载的对象是否为有效图像。

参考:Django

文档,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.ImageFieldheightwidth属性仍适用于光栅图像类型,并将设置为 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

它的工作

最新更新