57

I'm trying to convert an UploadedFile to a PIL Image object to thumbnail it, and then convert the PIL Image object that my thumbnail function returns back into a File object. How can I do this?

orokusaki
  • 55,146
  • 59
  • 179
  • 257

7 Answers7

110

The way to do this without having to write back to the filesystem, and then bring the file back into memory via an open call, is to make use of StringIO and Django InMemoryUploadedFile. Here is a quick sample on how you might do this. This assumes that you already have a thumbnailed image named 'thumb':

import StringIO

from django.core.files.uploadedfile import InMemoryUploadedFile

# Create a file-like object to write thumb data (thumb data previously created
# using PIL, and stored in variable 'thumb')
thumb_io = StringIO.StringIO()
thumb.save(thumb_io, format='JPEG')

# Create a new Django file-like object to be used in models as ImageField using
# InMemoryUploadedFile.  If you look at the source in Django, a
# SimpleUploadedFile is essentially instantiated similarly to what is shown here
thumb_file = InMemoryUploadedFile(thumb_io, None, 'foo.jpg', 'image/jpeg',
                                  thumb_io.len, None)

# Once you have a Django file-like object, you may assign it to your ImageField
# and save.
...

Let me know if you need more clarification. I have this working in my project right now, uploading to S3 using django-storages. This took me the better part of a day to properly find the solution here.

Skitz
  • 1,226
  • 1
  • 9
  • 5
  • Nice and simple. Working flawlessly in the pre_save signal. – jmagnusson Feb 07 '11 at 12:34
  • 30
    I've found you can reduce the amount of work even a bit more by using the Django ContentFile class instead. In that case, import ContentFile from django.core.files.base and then you'd do: thumb_file = ContentFile(thumb_io.getvalue()) – Bialecki Aug 31 '12 at 08:01
  • Thanks Bialecki for that answer. ContentFile worked great for me even though I couldn't get it to work using InMemoryUploadedFile. – Spike Nov 11 '12 at 02:48
  • Thanks for this answer, even though I didn't select it as the correct answer until 2 years later :/ sorry about that. – orokusaki Dec 26 '12 at 16:55
  • @Bialecki Thanks. How you specify filename when using ContentFile(thumb_io.getvalue())? What should be done after that line? – Akseli Palén Feb 19 '13 at 20:04
  • 3
    @Bialecki Nevermind, I found the answer. We assign the name when saving to models imagefield like following: mymodel.myimagefield.save(myfilename, imagecontentfile, save=True) – Akseli Palén Feb 19 '13 at 20:12
  • Why not use SimpleUploadedFile? – Andrew B. Dec 02 '14 at 20:38
  • 6
    @madzohan how did you get `StringIO` to work in Python 3? I had to use `BytesIO` (and since it doesn't have a _len()_ function, had to use _tell()_ to determine its length. – John C Jun 02 '16 at 19:38
  • 4
    @JohnC yep you should use `BytesIO` try my answer here http://stackoverflow.com/a/30435175/3033586 BTW previous comment was outdated and removed – madzohan Jun 03 '16 at 03:04
  • Solved a lot of my s3 problems too. – Edwinner Jun 13 '16 at 21:00
14

I've had to do this in a few steps, imagejpeg() in php requires a similar process. Not to say theres no way to keep things in memory, but this method gives you a file reference to both the original image and thumb (usually a good idea in case you have to go back and change your thumb size).

  1. save the file
  2. open it from filesystem with PIL,
  3. save to a temp directory with PIL,
  4. then open as a Django file for this to work.

Model:

class YourModel(Model):
    img = models.ImageField(upload_to='photos')
    thumb = models.ImageField(upload_to='thumbs')

Usage:

#in upload code
uploaded = request.FILES['photo']
from django.core.files.base import ContentFile
file_content = ContentFile(uploaded.read())
new_file = YourModel() 
#1 - get it into the DB and file system so we know the real path
new_file.img.save(str(new_file.id) + '.jpg', file_content)
new_file.save()

from PIL import Image
import os.path

#2, open it from the location django stuck it
thumb = Image.open(new_file.img.path)
thumb.thumbnail(100, 100)

#make tmp filename based on id of the model
filename = str(new_file.id)

#3. save the thumbnail to a temp dir

temp_image = open(os.path.join('/tmp',filename), 'w')
thumb.save(temp_image, 'JPEG')

#4. read the temp file back into a File
from django.core.files import File
thumb_data = open(os.path.join('/tmp',filename), 'r')
thumb_file = File(thumb_data)

new_file.thumb.save(str(new_file.id) + '.jpg', thumb_file)
orokusaki
  • 55,146
  • 59
  • 179
  • 257
Lincoln B
  • 2,184
  • 1
  • 13
  • 12
11

This is actual working example for python 3.5 and django 1.10

in views.py:

from io import BytesIO
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import InMemoryUploadedFile

def pill(image_io):
    im = Image.open(image_io)
    ltrb_border = (0, 0, 0, 10)
    im_with_border = ImageOps.expand(im, border=ltrb_border, fill='white')

    buffer = BytesIO()
    im_with_border.save(fp=buffer, format='JPEG')
    buff_val = buffer.getvalue()
    return ContentFile(buff_val)

def save_img(request)
    if request.POST:
       new_record = AddNewRecordForm(request.POST, request.FILES)
       pillow_image = pill(request.FILES['image'])
       image_file = InMemoryUploadedFile(pillow_image, None, 'foo.jpg', 'image/jpeg', pillow_image.tell, None)
       request.FILES['image'] = image_file  # really need rewrite img in POST for success form validation
       new_record.image = request.FILES['image']
       new_record.save()
       return redirect(...)
Aditya Kresna Permana
  • 11,869
  • 8
  • 42
  • 48
dEll
  • 428
  • 4
  • 7
3

Putting together comments and updates for Python 3+

from io import BytesIO
from django.core.files.base import ContentFile
import requests

# Read a file in

r = request.get(image_url)
image = r.content
scr = Image.open(BytesIO(image))

# Perform an image operation like resize:

width, height = scr.size
new_width = 320
new_height = int(new_width * height / width)
img = scr.resize((new_width, new_height))

# Get the Django file object

thumb_io = BytesIO()
img.save(thumb_io, format='JPEG')
photo_smaller = ContentFile(thumb_io.getvalue())
Peter
  • 3
  • 2
Rob
  • 1,656
  • 2
  • 17
  • 33
3

To complete for those who, like me, want to couple it with Django's FileSystemStorage: (What I do here is upload an image, resize it to 2 dimensions and save both files.

utils.py

def resize_and_save(file):
    size = 1024, 1024
    thumbnail_size = 300, 300
    uploaded_file_url = getURLforFile(file, size, MEDIA_ROOT)
    uploaded_thumbnail_url = getURLforFile(file, thumbnail_size, THUMBNAIL_ROOT)
    return [uploaded_file_url, uploaded_thumbnail_url]

def getURLforFile(file, size, location):
    img = Image.open(file)
    img.thumbnail(size, Image.ANTIALIAS)
    thumb_io = BytesIO()
    img.save(thumb_io, format='JPEG')
    thumb_file = InMemoryUploadedFile(thumb_io, None, file.name, 'image/jpeg', thumb_io.tell, None)
    fs = FileSystemStorage(location=location)
    filename = fs.save(file.name, thumb_file)
    return fs.url(filename)  

In views.py

if request.FILES:
        fl, thumbnail = resize_and_save(request.FILES['avatar'])
        #delete old profile picture before saving new one
        try:
            os.remove(BASE_DIR + user.userprofile.avatarURL)
        except Exception as e:
            pass         
        user.userprofile.avatarURL = fl
        user.userprofile.thumbnailURL = thumbnail
        user.userprofile.save()
mrj
  • 589
  • 1
  • 7
  • 17
  • This is very useful if you want to use FileSystemStorage to grab the image URL once it has been saved. It's exacly what I was looking for - thanks! – knowledge_seeker Mar 21 '22 at 19:44
1

Here is an app that can do that: django-smartfields

from django.db import models

from smartfields import fields
from smartfields.dependencies import FileDependency
from smartfields.processors import ImageProcessor

class ImageModel(models.Model):
    image = fields.ImageField(dependencies=[
        FileDependency(processor=ImageProcessor(
            scale={'max_width': 150, 'max_height': 150}))
    ])

Make sure to pass keep_orphans=True to the field, if you want to keep old files, otherwise they are cleaned up upon replacement.

lehins
  • 9,642
  • 2
  • 35
  • 49
1

For those using django-storages/-redux to store the image file on S3, here's the path I took (the example below creates a thumbnail of an existing image):

from PIL import Image
import StringIO
from django.core.files.storage import default_storage

try:
    # example 1: use a local file
    image = Image.open('my_image.jpg')
    # example 2: use a model's ImageField
    image = Image.open(my_model_instance.image_field)
    image.thumbnail((300, 200))
except IOError:
    pass  # handle exception

thumb_buffer = StringIO.StringIO()
image.save(thumb_buffer, format=image.format)
s3_thumb = default_storage.open('my_new_300x200_image.jpg', 'w')
s3_thumb.write(thumb_buffer.getvalue())
s3_thumb.close()
Cloud Artisans
  • 4,016
  • 3
  • 30
  • 37