ImageField validation

In my model I defined and ImageField()

image = models.ImageField(upload_to='images/%Y/%m/%d/', blank=True, null=True)

I want to validate the uploaded image on:
extensions = (‘gif’, ‘jpg’, ‘jpeg’, ‘png’)
dimensions = (360, 80))
size = (32*1024)

I had a look at validate_image_file_extension but that only validates the extension. Is there a validator that validates all three.

I saw validators being defined in models.py, is that the best place or should I put the validator in views.py or forms.py.

Kind regards,

Johanna

It kinda depends upon what you want to have happen if the submitted file is not valid. I’d be inclined to validate the image in the form - the earliest possible point, and before it gets saved.

I’d probably write a custom file handler* to allow me to access the data before it gets written to a file and then use Pillow to examine that image to determine its dimensions. (The file object provides access to verify the size. So I’d probably check the extension and size first, before calling Pillow to identify the dimensions.)

(* Also see Custom upload handlers)

I followed the links you provided, and read through a lot of ImageField() and validators documentation.

I thought the easiest way to validate an image would be making use of the attributes in the class ImageField()

class ImageField(upload_to=None, height_field=None, width_field=None, max_length=100, **options)

In models.py I have:

from PIL import Image
from django.core.validators import FileExtensionValidator
from config.settings import ALLOWED_IMAGE_EXTENSIONS

class Logo(models.Model):
    node = models.OneToOneField(Node, on_delete=models.CASCADE, primary_key=True, related_name='logos', related_query_name='logo')
    image = models.ImageField(
        upload_to='images/%Y/%m/%d/',
        width_field='240',
        height_field='120',
        max_length=64*1024,
        validators=[FileExtensionValidator(allowed_extensions=ALLOWED_IMAGE_EXTENSIONS)])

When I upload an image I get an AttributeError:

Exception Type:	AttributeError
Exception Value:	
'Logo' object has no attribute '240'
Exception Location:	/Users/annet/Documents/project/venv/lib/python3.10/site-packages/django/db/models/fields/files.py, line 475, in update_dimension_fields

Obviously my understanding of the attributes is wrong, what did I miss reading the documentation?

Quoting from the docs for the model ImageField.width_field:

Name of a model field which will be auto-populated with the width of the image each time the model instance is saved.

That attribute identifies a field to be populated by Django when the model is saved.

In other words, in your image field definition, you would have width_field = image_width, and then another field in your model: image_width = IntegerField(). Similarly, you would have a height field defined and reference that field name in the height_field attribute of the ImageField.

Note that these are populated when the model instance is saved, and so they don’t provide any capability for you to check them before then. (They also don’t define or create any limitation on those values. Any defaults defined in those fields are going to be overwritten by the actual value obtained from the image.)

I think the confusing part here is that width_field, height_field, and max_length don’t actually define validation limits.

width_field and height_field are names of model fields that Django populates with the dimensions of the uploaded image. max_length limits the length of the file path stored by the ImageField, not the file size.

If 360x80 and 32 KB are actual constraints for every Logo, I’d keep them in a reusable validator attached to the model field:

from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from PIL import Image, UnidentifiedImageError


def validate_logo(image):
    if image.size > 32 * 1024:
        raise ValidationError("Image must be 32 KB or smaller.")

    position = image.tell()

    try:
        with Image.open(image) as img:
            if img.size != (360, 80):
                raise ValidationError(
                    f"Expected 360x80, got {img.width}x{img.height}."
                )

            if img.format not in {"GIF", "JPEG", "PNG"}:
                raise ValidationError("Unsupported image format.")

            img.verify()

    except (UnidentifiedImageError, OSError):
        raise ValidationError("The uploaded file is not a valid image.")

    finally:
        image.seek(position)

Then:

image = models.ImageField(
    upload_to="images/%Y/%m/%d/",
    validators=[
        FileExtensionValidator(["gif", "jpg", "jpeg", "png"]),
        validate_logo,
    ],
)

I’d still treat the extension check as a convenience rather than proof of the file type. A filename can end in .jpg while containing something else, so inspecting the actual image with Pillow is still useful.

If this restriction only applies to one admin or form workflow, clean_image() on the ModelForm is also reasonable. If it’s an invariant of the Logo model, keeping the validator on the model field makes the rule reusable across ModelForms and explicit model validation.

One caveat is that Model.save() doesn’t call full_clean() automatically, so code that creates or updates Logo instances directly needs to run validation explicitly if you want the same guarantee outside forms.

A custom upload handler can make sense when you need to reject very large uploads before Django buffers them, but for a 32 KB limit I’d keep this in the normal validation layer unless early stream rejection is specifically required.