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_field
andheight_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.