5

I'm using PIL to compress uploaded images(FileField). However I'm getting an error which I believe is a problem of double saving? (saving my image, and then saving the whole form which includes the image). I wanted to perform commit=False when I'm saving the image but it doesn't appear it's possible. Here's my code:

...
if form_post.is_valid():
    instance = form_post.save(commit=False)
    instance.user = request.user

if instance.image:
    filename = instance.image
    instance.image = Image.open(instance.image)
    instance.image.thumbnail((220, 130), Image.ANTIALIAS)
    instance.image.save(filename, quality=60)

instance.save()

returns 'JpegImageFile' object has no attribute '_committed' error on the last line (instance.save())

Can someone identify the problem? - and any idea how I can fix it?

Full Traceback:

File "/Users/zorgan/Desktop/app/lib/python3.5/site-packages/django/core/handlers/exception.py" in inner
  41.             response = get_response(request)

File "/Users/zorgan/Desktop/app/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  187.                 response = self.process_exception_by_middleware(e, request)

File "/Users/zorgan/Desktop/app/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  185.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/Users/zorgan/Desktop/app/lib/python3.5/site-packages/django/contrib/auth/decorators.py" in _wrapped_view
  23.                 return view_func(request, *args, **kwargs)

File "/Users/zorgan/Desktop/project/site/post/views.py" in post
  68.                 if uploaded_file_type(instance) is True:

File "/Users/zorgan/Desktop/project/site/functions/helper_functions.py" in uploaded_file_type
  12.     f = file.image.read(1024)

Exception Type: AttributeError at /post/
Exception Value: 'JpegImageFile' object has no attribute 'read'

Full models:

class Post(models.Model):
    user = models.ForeignKey(User, blank=True, null=True)
    title = models.TextField(max_length=95)
    image = models.FileField(null=True, blank=True)

and the accompanying PostForm:

class PostForm(forms.ModelForm):
    title = forms.TextInput(attrs={'placeholder': 'title'})

    class Meta:
        model = Post
        fields = [
            'user',
            'title',
            'image',
        ]

views.py

def post(request):    
    if request.user.is_authenticated():
        form_post = PostForm(request.POST or None, request.FILES or None)
        if form_post.is_valid():
            instance = form_post.save(commit=False)

            if instance.image:
                filename = instance.image
                instance.image = Image.open(instance.image)
                instance.image.thumbnail((220, 130), Image.ANTIALIAS)
                instance.image.save(filename, quality=60)

            instance.save()

            return HttpResponseRedirect('/home/')
        else:
            form_post = PostForm()

        context = {
            'form_post': form_post,
        }

        return render(request, 'post/post.html', context)
    else:
        return HttpResponseRedirect("/accounts/signup/")

This following code:

if instance.image:
    im = Image.open(instance.image)
    print("Filename:", im.filename) #doesn't print anything
    thumb = im.thumbnail((220, 130), Image.ANTIALIAS)
    thumb.save(im.filename, quality=60)

returns an AttributeError : 'NoneType' object has no attribute 'save'. I believe this is because im.filename doesn't print anything. Any idea why?

The other method:

if instance.image:
    im = Image.open(instance.image)
    thumb = im.thumbnail((220, 130), Image.ANTIALIAS)
    thumb_io = BytesIO()
    thumb.save(thumb_io, im.format, quality=60)
    instance.image.save(im.filename, ContentFile(thumb_io.get_value()), save=False)

also returns AttributeError : 'NoneType' object has no attribute 'save', on this line: thumb.save(thumb_io, im.format, quality=60). Not sure why though?

Zorgan
  • 8,227
  • 23
  • 106
  • 207
  • You say "returns `'JpegImageFile' object has no attribute '_committed'` error on the last line (`instance.save()`)" but the traceback shows `'JpegImageFile' object has no attribute 'read'` at `if uploaded_file_type(instance) is True:` which is not included in your code sample. You will also need to show your `uploaded_file_type` helper function. – Antoine Pinsard Mar 14 '18 at 14:42
  • Please upload your model and your full view (with indentation) – Walucas Mar 14 '18 at 15:46
  • You must use a subclass of the django File object, instead of a PIL Image. See this answer for how to save the Image to the correct type using an in-memory BytesIO. https://stackoverflow.com/a/39710093/1977847 – Håken Lid Mar 14 '18 at 21:06
  • There are multiple other questions about using Pillow Images with django model.FileField, but since there's a bouny on this question, I can't use the "duplicate" feature. This answer should also work in this case, I think. https://stackoverflow.com/a/42846368/1977847 The last 3 lines are the important ones. – Håken Lid Mar 14 '18 at 21:10
  • @HåkenLid Both of those methods don't work - I've added the attempts in my edit. – Zorgan Mar 15 '18 at 07:54

1 Answers1

17

You must pass an instance of django's File object to FileField.save() to change the content of a file field. It works a bit differently from other types of model fields.

FieldFile.save(name, content, save=True)

This method takes a filename and file contents and passes them to the storage class for the field, then associates the stored file with the model field. If you want to manually associate file data with FileField instances on your model, the save() method is used to persist that file data.

from PIL import Image
from django.core.files.base import ContentFile

if instance.image:
    im = Image.open(instance.image)
    im.thumbnail((220, 130), Image.ANTIALIAS)
    thumb_io = BytesIO()
    im.save(thumb_io, im.format, quality=60)
    instance.image.save(im.filename, ContentFile(thumb_io.getvalue()), save=False)
instance.save()

But if you are not using a remote file storage backend, you could just overwrite the file itself. The file was created when you called form.save(). Since you are using the same filename and location, you don't really have to touch the model or tell django that you are messing with the file itself.

if instance.image:
    im = Image.open(instance.image)
    im.thumbnail((220, 130), Image.ANTIALIAS)
    im.save(im.filename, quality=60)
Håken Lid
  • 22,318
  • 9
  • 52
  • 67
  • 1
    Both methods don't work, i've added my attempts in my edit if you could look. I use remote S3 storage but I'm just testing in my local development environment now. But why does it matter whether it's a remote backend or not - the setup is still the same (static_root, media_root etc, just different locations? – Zorgan Mar 15 '18 at 11:22
  • If you use S3, you have to use the `instance.image.save` method. `PIL.Image.save` will only save to the local file system. It doesn't know about django storage backends. – Håken Lid Mar 15 '18 at 12:53
  • Right I get you. But the problem with the first method is `thumb.save(thumb_io, im.format, quality=60)` returns `AttributeError: 'NoneType' object has no attribute 'save'` - when I print `thumb` it prints `None` - any idea? – Zorgan Mar 15 '18 at 21:56
  • Ah. I made a mistake. I thought Image.thumbnail() should return a new Image object, but instead it modifies the Image object in place and returns `None`. I've corrected the code in my answer now. – Håken Lid Mar 15 '18 at 22:17
  • 2
    in the first code block, line 9, `thumb_io.get_value()` should be `thumb_io.getvalue()` (no underscore in `getvalue()`) – onurmatik Jun 25 '20 at 09:21
  • @HåkenLid thanks for posting and updating the ans. it helped to overcome major pain point for image compression. kudos to u. – optimists Feb 26 '22 at 15:33