RecursionError when trying to save image

Viewed 66

I am attempting to create a model with an image input that is to be resized and hashed on save. However, I appear to have created an infinite recursive call. I am basing the code for my resize_and_hash_image() method off of this answer to a previous question of mine. Is there a way I can save the image without calling self.instance.save()? Any help is appreciated.

models.py

import hashlib

from base64 import b16encode
from functools import partial
from io import BytesIO
from PIL import Image

from django.db import models

class Person(models.Model):
    prefix = models.CharField(blank=True, null=True, max_length=5)
    first_name = models.CharField(max_length=35)
    last_name = models.CharField(max_length=35)
    suffix = models.CharField(blank=True, null=True, max_length=5)
    image = models.ImageField(default=None, upload_to='people/')
    _image_hash = models.BinaryField(blank=True, null=True, default=None, max_length=16)
    bio = models.TextField()
    phone = models.CharField(max_length=10)
    email = models.EmailField()

    def __str__(self):
        return self.full_name

    def save(self, *args, **kwargs):
        if self.image:
            self.resize_and_hash_image()

        super().save(*args, **kwargs)

    def hash_file(self, file, block_size=65536):
        hasher = hashlib.md5()
        for buf in iter(partial(file.read, block_size), b''):
            hasher.update(buf)

        return hasher.digest()

    def resize_and_hash_image(self):
        img = Image.open(self.image).convert('RGB')
        width, height = img.size
        longest, shortest = 960, 720

        if (width >= height and (width > longest or height > shortest)) or (height > width and (height > longest or width > shortest)):
            if width > height:
                if (height * longest/ width) > shortest:
                    new_height = shortest
                    new_width = int(width * new_height / height)
                else:
                    new_width = longest
                    new_height = int(height * new_width / width)
            else:
                if (width * longest / height) > shortest:
                    new_width = shortest
                    new_height = int(height * new_width / width)
                else:
                    new_height = longest
                    new_width = int(width * new_height / height)

            img = img.resize((new_width, new_height), Image.ANTIALIAS)

        img_file = BytesIO()
        img.save(img_file, 'JPEG', quality=90)

        self._image_hash = self.hash_file(img_file)
        new_name = self.image_hash + '.jpg'
        self.image.save(new_name, img_file)

    @property
    def full_name(self):
        full_name = self.first_name + ' ' + self.last_name

        if self.prefix:
            full_name = self.prefix + ' ' + full_name

        if self.suffix:
            full_name = full_name + ' ' + self.suffix

        return full_name

    @property
    def display_phone(self):
        return '(%s) %s-%s' % \
            (self.phone[0:3], self.phone[3:6], self.phone[6:10]) \
            if self.phone else ''

    @property
    def image_hash(self):
        return str(b16encode(self._image_hash).lower(), 'utf-8')

    class Meta:
        verbose_name_plural = 'people'

Console Output

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
March 09, 2020 - 18:52:05
Django version 2.2.6, using settings 'mtm.settings'
Starting development server at http://10.0.0.100:8000/
Quit the server with CONTROL-C.
Internal Server Error: /admin/home/person/add/
Traceback (most recent call last):
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/contrib/admin/options.py", line 606, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/contrib/admin/sites.py", line 223, in inner
    return view(request, *args, **kwargs)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/contrib/admin/options.py", line 1634, in add_view
    return self.changeform_view(request, None, form_url, extra_context)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/contrib/admin/options.py", line 1522, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/contrib/admin/options.py", line 1561, in _changeform_view
    self.save_model(request, new_object, form, not add)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/contrib/admin/options.py", line 1088, in save_model
    obj.save()
  File "/home/matt/Repositories/mtm/home/models.py", line 44, in save
    self.resize_and_hash_image()
  File "/home/matt/Repositories/mtm/home/models.py", line 83, in resize_and_hash_image
    self.image.save(new_name, img_file)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/db/models/fields/files.py", line 93, in save
    self.instance.save()
  File "/home/matt/Repositories/mtm/home/models.py", line 44, in save
    self.resize_and_hash_image()
  File "/home/matt/Repositories/mtm/home/models.py", line 83, in resize_and_hash_image
    self.image.save(new_name, img_file)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/db/models/fields/files.py", line 93, in save
    self.instance.save()
  ...
  File "/home/matt/Repositories/mtm/home/models.py", line 44, in save
    self.resize_and_hash_image()
  File "/home/matt/Repositories/mtm/home/models.py", line 83, in resize_and_hash_image
    self.image.save(new_name, img_file)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/db/models/fields/files.py", line 93, in save
    self.instance.save()
  File "/home/matt/Repositories/mtm/home/models.py", line 44, in save
    self.resize_and_hash_image()
  File "/home/matt/Repositories/mtm/home/models.py", line 83, in resize_and_hash_image
    self.image.save(new_name, img_file)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/db/models/fields/files.py", line 93, in save
    self.instance.save()
  File "/home/matt/Repositories/mtm/home/models.py", line 44, in save
    self.resize_and_hash_image()
  File "/home/matt/Repositories/mtm/home/models.py", line 56, in resize_and_hash_image
    img = Image.open(self.image).convert('RGB')
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/PIL/Image.py", line 2804, in open
    im = _open_core(fp, filename, prefix)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/PIL/Image.py", line 2790, in _open_core
    im = factory(fp, filename)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/PIL/JpegImagePlugin.py", line 789, in jpeg_factory
    im = JpegImageFile(fp, filename)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/PIL/ImageFile.py", line 106, in __init__
    self._open()
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/PIL/JpegImagePlugin.py", line 376, in _open
    handler(self, i)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/PIL/JpegImagePlugin.py", line 67, in APP
    s = ImageFile._safe_read(self.fp, n)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/PIL/ImageFile.py", line 551, in _safe_read
    return fp.read(size)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/core/files/utils.py", line 16, in <lambda>
    read = property(lambda self: self.file.read)
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/db/models/fields/files.py", line 41, in _get_file
    self._require_file()
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/db/models/fields/files.py", line 37, in _require_file
    if not self:
  File "/home/matt/Repositories/mtm/env/lib/python3.5/site-packages/django/core/files/base.py", line 26, in __bool__
    return bool(self.name)
RecursionError: maximum recursion depth exceeded while calling a Python object
1 Answers

I see where the problem is. The last block after img = img.resize((new_width, new_height), Image.ANTIALIAS) should be indented as such:

            img = img.resize((new_width, new_height), Image.ANTIALIAS)
            img_file = BytesIO()
            img.save(img_file, 'JPEG', quality=90)

            new_name = self.image.name.split('.')[0] + '.jpg'
            self.image.save(new_name, img_file)

This way self.image.save() is only called once after the image is resized the first time. Next on my list is separating the method into separate resize_image() and hash_image() methods, to be called successively in save().

Related