11

Our project uses Python 2.7, PIL 1.1.7 and Django 1.5.1. There is an ImageField which works OK for many image formats, including bmp, gif, ico, pnm, psd, tif and pcx. However the requirement is to only allow png or jpg images. How can it be done?

Upd. I know I can validate file extension and http Content-Type header. But neither method is reliable. What I'm asking is whether there's a way to check uploaded file content for being png/jpg.

Dmitry
  • 113
  • 1
  • 1
  • 6
  • I know you can put some validation in your form's settings. I'll let you know about the details. – Zeinab Abbasimazar Dec 24 '13 at 13:45
  • See this [answer](http://stackoverflow.com/questions/4853581/django-get-uploaded-file-type-mimetype?answertab=active#tab-top) and this [link](https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#uploadedfile-objects). – Zeinab Abbasimazar Dec 24 '13 at 13:52
  • Also this: [django-vimage](https://github.com/manikos/django-vimage)! – nik_m Apr 27 '18 at 17:16

3 Answers3

10

You don't specify whether you're using a Django form to upload the image, I assume so as it is in the form field that the validation is carried out.

What you could do is create a subclass of django.forms.fields.ImageField to extend the functionality of to_python.

The file type check currently carried out in Django in to_python looks like this

Image.open(file).verify()

Your subclass could look something like.

class DmitryImageField(ImageField):

    def to_python(self, data):
        f = super(DmitryImageField, self).to_python(data)
        if f is None:
            return None

        try:
            from PIL import Image
        except ImportError:
            import Image

        # We need to get a file object for PIL. We might have a path or we might
        # have to read the data into memory.
        if hasattr(data, 'temporary_file_path'):
            file = data.temporary_file_path()
        else:
            if hasattr(data, 'read'):
                file = BytesIO(data.read())
            else:
                file = BytesIO(data['content'])

        try:
            im = Image.open(file)
            if im.format not in ('BMP', 'PNG', 'JPEG'):
                raise ValidationError("Unsupport image type. Please upload bmp, png or jpeg")
        except ImportError:
            # Under PyPy, it is possible to import PIL. However, the underlying
            # _imaging C module isn't available, so an ImportError will be
            # raised. Catch and re-raise.
            raise
        except Exception: # Python Imaging Library doesn't recognize it as an image
            raise ValidationError(self.error_messages['invalid_image'])

        if hasattr(f, 'seek') and callable(f.seek):
            f.seek(0)
        return f

You may notice this is most of the code from ImageField.to_python and might prefer to just create a sub-class of FileField to use instead of ImageField rather than subclassing ImageField and duplicating much of its functionality. In this case make sure to add im.verify() before the format check.

EDIT: I should point out that I've not tested this subclass.

Stephen Paulger
  • 5,204
  • 3
  • 28
  • 46
  • Thanks! It has worked for me (with some slight modifications) – Dmitry Dec 25 '13 at 09:50
  • 2
    That try...except block will catch the `raise ValidationError`, plus you can do this more easily in the form `clean_field` method. – nitely May 30 '14 at 00:10
  • Here's where it happens in Django's ImageField: https://github.com/django/django/blob/2ea3fb3e6386c43f124b542e92b817dbc227c76b/django/forms/fields.py#L634 – Boris Verkhovskiy Jan 06 '20 at 22:17
  • As of Django 3.1.7, the `ImageField` class [annotates](https://github.com/django/django/blob/3.1.7/django/forms/fields.py#L647) the return value with a `.image` attribute that contains the PIL object. This could be used to avoid repeating a lot of the super logic – Addison Klinke Jul 09 '22 at 06:00
1

You will probably want to use os for this. From the Python docs.

os.path.splitext(path) Split the pathname path into a pair (root, ext) such that root + ext == path, and ext is empty or begins with a period and contains at most one period. Leading periods on the basename are ignored; splitext('.cshrc') returns ('.cshrc', ''). Changed in version 2.6: Earlier versions could produce an empty root when the only period was the first character.

example

import os
fileName, fileExtension = os.path.splitext('yourImage.png')

print fileName 
>>> "yourImage"

print fileExtension
>>> ".png"

So once you have your ext separated from the filename you should just use a simple string comparison to verify it's the right format.

Chris Hawkes
  • 11,923
  • 6
  • 58
  • 68
  • 2
    Thanks for the answer. What I'm asking however is whether I can check the uploaded file content for being png/jpg. Checking the file extension is not reliable. E.g., I can rename a bmp file to png and then upload. – Dmitry Dec 24 '13 at 13:38
  • oh so more than just an extension you want to make sure the file is what the extension is supposed to be by the contents within. Sorry I misunderstood the question. – Chris Hawkes Dec 24 '13 at 13:43
  • In most cases, it is enough to check the file extension! – Mohammed Shareef C Feb 24 '18 at 06:01
1

You can use python-magic, a ctype wrapper around libmagic, the library used by the file on Linux.

From its doc:

>>> import magic
>>> magic.from_file("testdata/test.pdf")
'PDF document, version 1.2'
>>> magic.from_buffer(open("testdata/test.pdf").read(1024))
'PDF document, version 1.2'
>>> magic.from_file("testdata/test.pdf", mime=True)
'application/pdf'

However this method simply look at mime information. You can still upload a non-valid PNG with the correct mime or embed unauthorized data in file's metadata.

smeso
  • 4,165
  • 18
  • 27