23

I'm switching to SVG images to represent categories on my e-commerce platform. I was using models.ImageField in the Category model to store the images before, but the forms.ImageField validation is not capable of handling a vector-based image (and therefore rejects it).

I don't require thorough validation against harmful files, since all uploads will be done via the Django Admin. It looks like I'll have to switch to a models.FileField in my model, but I do want warnings against uploading invalid images.

Nick Khlestov wrote a SVGAndImageFormField (find source within the article, I don't have enough reputation to post more links) over django-rest-framework's ImageField. How do I use this solution over Django's ImageField (and not the DRF one)?

Yash Tewari
  • 760
  • 1
  • 6
  • 19
  • 1
    The basic difference between an ImageField and a FileField is that the first checks if a file is an image using Pillow and offers a couple of attributes possibly irrelevant to you (height, width). Isn't a FileField adequate for your requirements? https://github.com/django/django/blob/master/django/db/models/fields/files.py – Wtower Jun 24 '16 at 06:55
  • @Wtower I would prefer if it doesn't let non-image formats through. – Yash Tewari Jun 24 '16 at 08:59
  • 1
    Even though the OP does not worry about harmful files, it may be good to keep in mind how easy it is to make a harmful `svg`. For example, [billion laughs](https://stackoverflow.com/questions/3451203/) **will** crash your server if you use `xml.etree` (as suggested in many of the answers here). – djvg Apr 06 '21 at 20:24

6 Answers6

15

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")
Community
  • 1
  • 1
Wtower
  • 18,848
  • 11
  • 103
  • 80
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
  • 760
  • 1
  • 6
  • 19
  • 1
    May be helpful for someone. field_classes in meta of the form available only from django 1.9, for those who uses older version, you need to explicitly define field in form. – Sergii V. Aug 16 '16 at 09:19
  • Not working anymore, same reason as https://stackoverflow.com/questions/38006200/allow-svg-files-to-be-uploaded-to-imagefield-via-django-admin#comment86755700_46846452 – Adrien Lemaire Apr 17 '18 at 05:03
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
  • 301
  • 3
  • 5
  • Not working anymore with Django 2.0. Even if the svg validation passes and to_python returns the file, the form will still raise an error with "File extension 'svg' is not allowed. Allowed extensions are: '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 Apr 17 '18 at 05:03
  • I had to override SVGAndImageFieldForm.run_validators in the same fashion as SVGAndImageFieldForm.run_python in order to get this to work – Adrien Lemaire Apr 17 '18 at 05:25
  • @Fandekasp, can you clarify if you were able to get it to work on Django 2.0? Where is `SVGAndImageFieldForm.run_validators`? – CodeBiker Oct 30 '18 at 19:17
  • 1
    `class SVGAndImageFieldForm(forms.ImageField): default_validators = [FileExtensionValidator(allowed_extensions=get_available_image_extensions() + ["svg"])]` – Ilya Semenov May 26 '20 at 10:47
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 code is not correct, as it modifies the return value of `get_available_image_extensions()` in-place by reference. Depending on the underlying implementation, this may break things. You should be doing `get_available_image_extensions() + ["svg"]`. – Ilya Semenov May 26 '20 at 10:51
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,456
  • 16
  • 33
-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
  • 74
  • 3
  • The field on the model is still an `ImageField`, so the validation still happens on the model. – Bobort May 20 '22 at 15:57