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, thewidth_fieldandheight_fieldwill 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.
