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

24
我正在将SVG图像用于我的电子商务平台上的类别表示。以前我在类别模型中使用models.ImageField来存储图像,但forms.ImageField验证不能处理基于矢量的图像(因此会拒绝它)。
我不需要对有害文件进行彻底的验证,因为所有上传都将通过Django Admin完成。看起来我必须在我的模型中切换到models.FileField,但我确实希望警告无效图像的上传。
Nick Khlestov编写了一个SVGAndImageFormField(在文章中查找源代码,我没有足够的声望发布更多链接),而不是django-rest-framework's ImageField。我如何在Django的ImageField上使用这个解决方案(而不是DRF的解决方案)?

1
ImageField和FileField之间的基本区别在于,前者使用Pillow检查文件是否为图像,并提供了一些可能与您无关的属性(高度,宽度)。你的需求不是用FileField就足够了吗?https://github.com/django/django/blob/master/django/db/models/fields/files.py - Wtower
@Wtower,我希望它不要让非图像格式通过。 - Yash Tewari
1
尽管 OP 不担心有害文件,但记住制作有害的 svg 有多容易可能是有好处的。例如,billion laughs 如果您使用 xml.etree(如此处许多答案所建议)将会使您的服务器崩溃。 - djvg
6个回答

16

我从未使用过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所做的事情。 - Yash Tewari
那个第二个“valid”参数是什么?它在旧版Django中使用过吗?(在1.11上出现了问题) - Sebastián Vansteenkiste

7
原来 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


1
也许对某些人会有帮助。在表单的元数据中,field_classes 只在 Django 1.9 及更高版本中可用,对于那些使用旧版本的人,你需要明确地在表单中定义字段。 - Sergii V.
不再工作,原因与 https://dev59.com/lloT5IYBdhLWcg3w8S-2#s7wioYgBc1ULPQZFJGq4 相同。 - Adrien Lemaire

4

以下是一个解决方案,它作为一个简单的模型字段工作,您可以将其放在 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 中,并快乐 :)


不再与Django 2.0一起工作。即使svg验证通过并且to_python返回文件,表单仍将引发错误:“不允许文件扩展名为'svg'。允许的扩展名为:'blp,bmp,bufr,cur,pcx,dcx,dds,ps,eps,fit,fits,fli,flc,ftc,ftu,gbr,gif,grib,h5,hdf,png,jp2,j2k,jpc,jpf,jpx,j2c,icns,ico,im,iim,tif,tiff,jfif,jpe,jpg,jpeg,mpg,mpeg,mpo,msp,palm,pcd,pdf,pxr,pbm,pgm,ppm,psd,bw,rgb,rgba,sgi,ras,tga,webp,wmf,emf,xbm,xpm”。 - Adrien Lemaire
我不得不以与SVGAndImageFieldForm.run_python相同的方式覆盖SVGAndImageFieldForm.run_validators,以使其正常工作。 - Adrien Lemaire
@Fandekasp,你能否澄清一下你是否已经在Django 2.0上使其工作?SVGAndImageFieldForm.run_validators在哪里? - CodeBiker
1
class SVGAndImageFieldForm(forms.ImageField): default_validators = [FileExtensionValidator(allowed_extensions=get_available_image_extensions() + ["svg"])] - Ilya Semenov

2

如评论中所述,SVGAndImageFormField 的验证将失败,因为使用 django.core.validators.validate_image_file_extension 进行扩展名检查,这是 ImageField 的默认验证器。

解决方法是创建一个自定义验证器,将 "svg" 添加到接受的扩展名中。

已编辑: 感谢 @Ilya Semenov 的评论。

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属性: "default_validators"的意思是“默认验证器”。
class SVGAndImageFormField(DjangoImageField):
    default_validators = [validate_image_and_svg_file_extension]
# ...

1
这段代码不正确,因为它通过引用就地修改了get_available_image_extensions()的返回值。根据底层实现的不同,这可能会导致错误。你应该使用get_available_image_extensions() + ["svg"] - Ilya Semenov

1
这是我在Django4.2中使用的解决方案:
此外, 我在这里使用defusedxml, 依照Python文档的建议:

警告

XML模块对错误或恶意构造的数据不安全。 如果您需要解析不受信任或未经身份验证的数据,请参阅XML漏洞和The defusedxml Package部分。

# 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)
    ...

-1
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

它工作了


模型上的字段仍然是“ImageField”,因此验证仍然在模型上进行。 - Bobort

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接