IntegrityError with django-resized and height_field, width_field

Turns out my recent upgrade to Django 5.2 was not, in fact, “without issue.” I had to patch a problem saving images with stored dimensions with django-resized.

Turns out my recent upgrade from Django 4.2 to 5.2 had an unexpected side effect. I thought a problem with uploading images might occur with the STATICFILES_STORAGE setting change, but it actually came in Django 5.2 and django-resized with this change (I think)

ImageField.update_dimension_fields(force=True) is no longer called after saving the image to storage. If your storage backend resizes images, the width_field and height_field will not match the width and height of the image.

Django 5.1 release notes: Miscellaneous

I can’t find where this change happened in the Django source code, but saving images with django-resized worked in 5.0 and broke in 5.1 with IntegrityError NOT NULL constraint failed saying that the corresponding height_field, which is required, was null.

The relevant part of my Photo model looked like this:

class Photo(MastodonSyndicatable, FeedItem):
    image = ResizedImageField(
        size=[1188,1188], 
        quality=70, 
        upload_to=upload_to_callable,
        storage=PublicAzureStorage, 
        height_field="image_height", 
        width_field="image_width",
        keep_meta=False)

    image_height = models.PositiveIntegerField()
    image_width = models.PositiveIntegerField()

The height_field and width_field arguments are optional on ImageField and allow for saving the image dimensions in the database as the specified fields. ResizedImageField, from django-resized, inherits from ImageField and, while it doesn’t document those parameters, they get passed along and have worked for years now.

It looks like Django calculates the image size after the field initialization

This method is hooked up to model's post_init signal to update dimensions after instantiating a model instance.

django.db.models.fields.files:479

But django-resized does its image manipulation right before the file is actually saved. From the Django changelog, it sounds like the image size used to be recalculated after saving, so that was fine. Now that it doesn’t do that, those corresponding image dimension fields were null. I’m not sure if they are initially sized and cleared at some point or never sized at all.

I filed an issue on the django-resized project, but in the meantime my workaround is to subclass the django-resized classes and call update_dimension_fields after the image is saved. My updated code looks like this

class OGResizedImageFieldFile(ResizedImageFieldFile):
    def save(self, name, content, save=True):
        super().save(name, content, save)
        self.field.update_dimension_fields(self.instance, force=True)

class OGResizedImageField(ResizedImageField):
    attr_class = OGResizedImageFieldFile

class Photo(MastodonSyndicatable, FeedItem):
    image = OGResizedImageField(
        size=[1188,1188], 
        quality=70, 
        upload_to=upload_to_callable,
        storage=PublicAzureStorage, 
        height_field="image_height", 
        width_field="image_width",
        keep_meta=False)

    image_height = models.PositiveIntegerField()
    image_width = models.PositiveIntegerField()

The image saving happens in the ResizedImageFieldFile class, so I subclass that, override the save method, and add the call to update_dimension_fields at the end of it.

Then I subclass the ResizedImageField to assign the new OGResizedImageFieldFile as the attr_class and use the new OGResizedImageField as my image field.

That all works fine now. I do wonder if django-resized should do the image manipulation in ResizedImageField.__init__ so that the right-sized image already exists when the normal life-cycle call of update_dimension_fields happens.