[Django]-Allow SVG files to be uploaded to ImageField via Django admin

16πŸ‘

I have never used SVGAndImageFormField so I cannot really comment on that. Personally I would have opted for a simple application of FileField, but that clearly depends on the project requirements. I will expand on that below:

As mentioned in the comment, the basic difference between an ImageField and a FileField is that the first checks if a file is an image using Pillow:

Inherits all attributes and methods from FileField, but also validates that the uploaded object is a valid image.

Reference: Django docs, Django source code

It also offers a couple of attributes possibly irrelevant to the SVG case (height, width).

Therefore, the model field could be:

    svg = models.FileField(upload_to=..., validators=[validate_svg])

You can use a function like is_svg as provided in the relevant question:

How can I say a file is SVG without using a magic number?

Then a function to validate SVG:

def validate_svg(file, valid):
    if not is_svg(file):
        raise ValidationError("File not svg")
πŸ‘€Wtower

7πŸ‘

It turns out that SVGAndImageFormField has no dependencies on DRF’s ImageField, it only adds to the validation done by django.forms.ImageField.

So to accept SVGs in the Django Admin I changed the model’s ImageField to a FileField and specified an override as follows:

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)

It now accepts all previous image formats along with SVG.

EDIT: Just found out that this works even if you don’t switch from models.ImageField to models.FileField. The height and width attributes of models.ImageField will still work for raster image types, and will be set to None for SVG.

πŸ‘€Yash Tewari

4πŸ‘

Here is a solution that works as a simple model field, that you can put instead of models.ImageField:

class Icon(models.Model):
    image_file = SVGAndImageField()

You need to define following classes and functions somewhere in your code:

from django.db import models

class SVGAndImageField(models.ImageField):
    def formfield(self, **kwargs):
        defaults = {'form_class': SVGAndImageFieldForm}
        defaults.update(kwargs)
        return super().formfield(**defaults)

And here is how SVGAndImageFieldForm looks like:

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

Function validate_svg I took from other solutions:

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

Also if you want to use SVG files only model field – you can do it more simple.

Just create class, inherited from models.FileField, and in __init__ method you can add validate_svg function to kwargs['validators'].

Or just add this validator to models.FileField and be happy πŸ™‚

πŸ‘€Denis Krumko

2πŸ‘

As stated in the comments, validation for SVGAndImageFormField will fail because extensions are checked using django.core.validators.validate_image_file_extension, which is the default validator for an ImageField.

A workaround for this would be creating a custom validator adding "svg" to the accepted extensions.

Edited: Thanks @Ilya Semenov for your comment

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)

Then, override the default_validators attribute in the SvgAndImageFormField:

class SVGAndImageFormField(DjangoImageField):
    default_validators = [validate_image_and_svg_file_extension]
# ...

1πŸ‘

This is the solution that works for me with Django4.2:

Also I make use of defusedxml here as suggested per Python docs:

Warning

The XML modules are not secure against erroneous or maliciously constructed data. If you need to parse untrusted or unauthenticated data see the XML vulnerabilities and The defusedxml Package sections.

# 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)
    ...
πŸ‘€an0o0nym

-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

its work

πŸ‘€Malik

Leave a comment