Django Image upload and resize - python

I have a standard Django form with an image field. When the image is uploaded, I would like to make sure that the image is no larger than 300px by 300px. Here is my code:
def post(request):
if request.method == 'POST':
instance = Product(posted_by=request.user)
form = ProductModelForm(request.POST or None, request.FILES or None)
if form.is_valid():
new_product = form.save(commit=False)
if 'image' in request.FILES:
img = Image.open(form.cleaned_data['image'])
img.thumbnail((300, 300), Image.ANTIALIAS)
# this doesnt save the contents here...
img.save(new_product.image)
# ..because this prints the original width (2830px in my case)
print new_product.image.width
The problem I am facing is, it is not clear to me how I get the Image type converted to the type that ImageField type.

From the documentation on ImageField's save method:
Note that the content argument should be an instance of django.core.files.File, not Python's built-in file object.
This means you would need to convert the PIL.Image (img) to a Python file object, and then convert the Python object to a django.core.files.File object. Something like this (I have not tested this code) might work:
img.thumbnail((300, 300), Image.ANTIALIAS)
# Convert PIL.Image to a string, and then to a Django file
# object. We use ContentFile instead of File because the
# former can operate on strings.
from django.core.files.base import ContentFile
djangofile = ContentFile(img.tostring())
new_product.image.save(filename, djangofile)

There you go, just change a little bit to suit your need:
class PhotoField(forms.FileField, object):
def __init__(self, *args, **kwargs):
super(PhotoField, self).__init__(*args, **kwargs)
self.help_text = "Images over 500kb will be resized to keep under 500kb limit, which may result in some loss of quality"
def validate(self,image):
if not str(image).split('.')[-1].lower() in ["jpg","jpeg","png","gif"]:
raise ValidationError("File format not supported, please try again and upload a JPG/PNG/GIF file")
def to_python(self, image):
try:
limit = 500000
num_of_tries = 10
img = Image.open(image.file)
width, height = img.size
ratio = float(width) / float(height)
upload_dir = settings.FILE_UPLOAD_TEMP_DIR if settings.FILE_UPLOAD_TEMP_DIR else '/tmp'
tmp_file = open(os.path.join(upload_dir, str(uuid.uuid1())), "w")
tmp_file.write(image.file.read())
tmp_file.close()
while os.path.getsize(tmp_file.name) > limit:
num_of_tries -= 1
width = 900 if num_of_tries == 0 else width - 100
height = int(width / ratio)
img.thumbnail((width, height), Image.ANTIALIAS)
img.save(tmp_file.name, img.format)
image.file = open(tmp_file.name)
if num_of_tries == 0:
break
except:
pass
return image
Source: http://james.lin.net.nz/2012/11/19/django-snippet-reduce-image-size-during-upload/

How about using standard image field https://github.com/humanfromearth/django-stdimage

Here is an app that can take care of that: django-smartfields
from django.db import models
from smartfields import fields
from smartfields.dependencies import FileDependency
from smartfields.processors import ImageProcessor
class Product(models.Model):
image = fields.ImageField(dependencies=[
FileDependency(processor=ImageProcessor(
scale={'max_width': 300, 'max_height': 300}))
])

Try my solution here: https://stackoverflow.com/a/25222000/3731039
Highlight
Using Pillow for image processing (two packages required: libjpeg-dev, zlib1g-dev)
Using Model and ImageField as storage
Using HTTP POST or PUT with multipart/form
No need to save the file to disk manually.
Create multiple resolutions and stores their dimensions.

You can use my library django-sizedimagefield for this, it has no extra dependency and is very simple to use.

Related

Custom save method for images/avatars in Django - how to use

So, I'm trying to do a little bit of compression of images as well as some other operations. I had a few questions...I have the following save() method for my user class:
class User(AbstractBaseUser, PermissionsMixin):
...
avatar = models.ImageField(storage=SITE_UPLOAD_LOC, null=True, blank=True)
def save(self, *args, **kwargs):
if self.avatar:
img = Img.open(BytesIO(self.avatar.read()))
if img.mode != 'RGB':
img = img.convert('RGB')
new_width = 200
img.thumbnail((new_width, new_width * self.avatar.height / self.avatar.width), Img.ANTIALIAS)
output = BytesIO()
img.save(output, format='JPEG', quality=70)
output.seek(0)
self.avatar= InMemoryUploadedFile(file=output, field_name='ImageField', name="%s.jpg" % self.avatar.name.split('.')[0], content_type='image/jpeg', size=, charset=None)
super(User, self).save(*args, **kwargs)
I had two questions:
Best way for deleting the old avatar file on save, if a previous avatar exists
What do I pass into InMemoryUploadedFile for the size kwarg? Is this the size of the file? in what unit/(s)?
You need to get file size. Try this:
import os
size = os.fstat(output.fileno()).st_size
You can read this for how to get file size:
https://docs.python.org/3.0/library/stat.html
and for deleting old avtar. According to your code it is foreign key hence before saving you can check if avtar is already exists and so yoy can delete it.
Add this lines code after output.seek(0) this line:
if self.avtar:
self.avtar.delete()
What you want is the size in bytes.
You can get the byte size of a BytesIO object like this.
size = len(output.getvalue())

Django - Get I/O error on changing object attribute

In my views.py file, I am trying to add 1 to a BigIntegerField named visited_counter.
views.py :
def view_page(request,id_page):
page = get_object_or_404(Page,title=id_page)
page.visited_counter= page.visited_counter +1
page.save()
return render(request,'app/page.html',locals())
models.py :
class Page(models.Model):
title = models.CharField(max_length=30)
visited_counter= models.BigIntegerField()
landscape= models.BooleanField()
thumbnail = models.ImageField(storage=OverwriteStorage(),upload_to=thumbnail_path)
backgroundpicture =models.ImageField(storage=OverwriteStorage(),upload_to=background_path)
def save(self, *args, **kwargs):
if self.backgroundpicture.width >= self.backgroundpicture.height:
self.landscape=True
else:
self.landscape=False
if self.backgroundpicture:
from PIL import Image
import glob, os
from cStringIO import StringIO
from django.core.files.uploadedfile import SimpleUploadedFile
image = Image.open(self.backgroundpicture) ####LINE ERROR####
try:
# thumbnail
THUMBNAIL_SIZE = (160, 160) # dimensions
# Convert to RGB if necessary
if image.mode not in ('L', 'RGB'): image = image.convert('RGB')
# create a thumbnail + use antialiasing for a smoother thumbnail
image.thumbnail(THUMBNAIL_SIZE, Image.ANTIALIAS)
# fetch image into memory
temp_handle = StringIO()
image.save(temp_handle, 'JPEG')
temp_handle.seek(0)
# save it
file_name, file_ext = os.path.splitext(self.backgroundpicture.name.rpartition('/')[-1])
suf = SimpleUploadedFile(file_name + file_ext, temp_handle.read(), content_type='JPEG')
self.thumbnail.save(file_name + '.jpg', suf, save=False)
except ImportError:
pass
super(Page, self).save(*args, **kwargs)
When I create a 'Page object', I have no problem.... The save function is doing her job very well, but when I want to access to the object via the view_page function. I get a I/O operation on closed file error on that line: image = Image.open(self.backgroundpicture).
I didn't find any other Q/A related to this case, so I am stuck...
The trick is to add an if condition in your save method and check if it is necessary to read the whole code in the save function.
For this you add a function named, has_changed
def has_changed(instance, field, manager='objects'):
"""Returns true if a field has changed in a model
May be used in a model.save() method.
"""
if not instance.pk:
return True
manager = getattr(instance.__class__, manager)
old = getattr(manager.get(pk=instance.pk), field)
return not getattr(instance, field) == old
And you use it in the model definition like this:
if has_changed(self, 'backgroundpicture'):
if self.backgroundpicture.width >= self.backgroundpicture.height:
self.landscape=True
...

Django: Image Resize and Upload with PIL, Amazon S3 and Boto

I'm trying to figure out the best way to take a user uploaded image, resize it, and store the original image as well as the resized image on Amazon S3.
I'm running Django 1.5, using PIL to resize the image, and using Boto to handle uploading the image file to S3. Right now I've got it to work by uploading the original image to S3, using PIL to open the image using the S3 path and resize it, and then saving the resized version to S3, however this doesn't seem to be the most efficient way to do this.
I'm wondering if there's a way to resize the image before uploading to S3 using the user-uploaded image itself (been having trouble getting PIL to open the image file itself), and whether this would be faster than the way I've set things up now. I can't seem to find an answer to this, either in the PIL documentation or anywhere else. I should mention that I don't want to just use a third party app to handle this, as part of my goal is to learn and understand fundamentally what is going on.
Is there a more efficient way to do this than what I've currently set up? A general explanation of what is happening at each step and why it makes the most sense to set things up that way would be ideal.
I should also mention that it seems to take much longer to upload the image to S3 than when I was just storing the image on my server. Is there a normal lag when uploading to S3 or is there potentially something in how things are set up that could be slowing down the S3 uploads?
I have an architecture consisting of a Django + Tastypie in Heroku and the image wharehouse in S3. What I do when a user uploads a photo from the frontend (written in JS), is resize the photo to a certain size (600 x 600 max size) always mantaining the aspect ratio. I'll paste the code to do this (it works).
views.py:
class UploadView(FormView):
form_class = OriginalForm
def form_valid(self, form):
original = form.save()
if original.image_width > 280 and original.image_height > 281:
if original.image_width > 600 or original.image_height > 600:
original.resize((600, 600))
if not original.image:
return self.success(self.request, form, None, errors = 'Error while uploading the image')
original.save()
up = UserProfile.objects.get(user = request.user.pk)
#Save the images to s3
s3 = S3Custom()
new_image = s3.upload_file(original.image.path, 'avatar')
#Save the s3 image path, as string, in the user profile
up.avatar = new_image
up.save
else:
return self.success(self.request, form, None, errors = 'The image is too small')
return self.success(self.request, form, original)
Here what I do is checking if the image is larger than 280 x 281 (the crop square, in the frontend, has that size), and also check if one of the sides of the image is larger than 600px. If that's the case, I call the (custom) method resize, of my Original class...
models.py:
class Original(models.Model):
def upload_image(self, filename):
return u'avatar/{name}.{ext}'.format(
name = uuid.uuid4().hex,
ext = os.path.splitext(filename)[1].strip('.')
)
def __unicode__(self):
return unicode(self.image)
owner = models.ForeignKey('people.UserProfile')
image = models.ImageField(upload_to = upload_image, width_field = 'image_width', height_field = 'image_height')
image_width = models.PositiveIntegerField(editable = False, default = 0)
image_height = models.PositiveIntegerField(editable = False, default = 0)
def resize(self, size):
if self.image is None or self.image_width is None or self.image_height is None:
print 'Cannot resize None things'
else:
IMG_TYPE = os.path.splitext(self.image.name)[1].strip('.')
if IMG_TYPE == 'jpeg':
PIL_TYPE = 'jpeg'
FILE_EXTENSION = 'jpeg'
elif IMG_TYPE == 'jpg':
PIL_TYPE = 'jpeg'
FILE_EXTENSION = 'jpeg'
elif IMG_TYPE == 'png':
PIL_TYPE = 'png'
FILE_EXTENSION = 'png'
elif IMG_TYPE == 'gif':
PIL_TYPE = 'gif'
FILE_EXTENSION = 'gif'
else:
print 'Not a valid format'
self.image = None
return
#Open the image from the ImageField and save the path
original_path = self.image.path
fp = open(self.image.path, 'rb')
im = Image.open(StringIO(fp.read()))
#Resize the image
im.thumbnail(size, Image.ANTIALIAS)
#Save the image
temp_handle = StringIO()
im.save(temp_handle, PIL_TYPE)
temp_handle.seek(0)
#Save image to a SimpleUploadedFile which can be saved into ImageField
suf = SimpleUploadedFile(os.path.split(self.image.name)[-1], temp_handle.read(), content_type=IMG_TYPE)
#Save SimpleUploadedFile into image field
self.image.save('%s.%s' % (os.path.splitext(suf.name)[0],FILE_EXTENSION), suf, save=False)
#Delete the original image
fp.close()
os.remove(original_path)
#Save other fields
self.image_width = im.size[0]
self.image_height = im.size[1]
return
The last thing you need is a "library" containing custom s3 methods:
class S3Custom(object):
conn = S3Connection(settings.AWS_ACCESS_KEY_ID, settings.AWS_SECRET_ACCESS_KEY)
b = Bucket(conn, settings.AWS_STORAGE_BUCKET_NAME)
k = Key(b)
def upload_file(self, ruta, prefix):
try:
self.k.key = '%s/%s' % (prefix, os.path.split(ruta)[-1])
self.k.set_contents_from_filename(ruta)
self.k.make_public()
except Exception, e:
print e
return '%s%s' % (settings.S3_URL, self.k.key)
You should have AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_STORAGE_BUCKET_NAME, S3_URL in your settings file.

Django PIL : IOError Cannot identify image file

I'm learning Python and Django.
An image is provided by the user using forms.ImageField(). Then I have to process it in order to create two different sized images.
When I submit the form, Django returns the following error:
IOError at /add_event/
cannot identify image file
I call the resize function:
def create_event(owner_id, name, image):
image_thumb = image_resizer(image, name, '_t', 'events', 180, 120)
image_medium = image_resizer(image, name, '_m', 'events', 300, 200)
I get en error when image_resizer is called for the second time:
def image_resizer(image, name, size, app_name, length, height):
im = Image.open(image)
if im.mode != "RGB":
im = im.convert("RGB")
im = create_thumb(im, length, height)
posit = str(MEDIA_ROOT)+'/'+app_name+'/'
image_2 = im
image_name = name + size +'.jpg'
imageurl = posit + image_name
image_2.save(imageurl,'JPEG',quality=80)
url_image='/'+app_name+'/'+image_name
return url_image
Versions:
Django 1.3.1
Python 2.7.1
PIL 1.1.7
I'm trying to find the problem, but i don't know what to do. Thank you in advanced!
EDIT
I solved rewriting the function; now it creates the different images in batch:
I call the resize function:
url_array = image_resizer.resize_batch(image, image_name, [[180,120,'_t'], [300,200,'_m']], '/events/')
so:
image_thumb = url_array[0]
image_medium = url_array[1]
and the resize function:
def resize_batch(image, name, size_array, position):
im = Image.open(image)
if im.mode != "RGB":
im = im.convert("RGB")
url_array = []
for size in size_array:
new_im = create_thumb(im, size[0], size[1])
posit = str(MEDIA_ROOT) + position
image_name = name + size[2] +'.jpg'
imageurl = posit + image_name
new_im.save(imageurl,'JPEG',quality=90)
new_url_array = position + image_name
url_array.append(new_url_array)
return url_array
Thanks to all!
As ilvar asks in the comments, what kind of object is image? I'm going to assume for the purposes of this answer that it's the file property of a Django ImageField that comes from a file uploaded by a remote user.
After a file upload, the object you get in the ImageField.file property is a TemporaryUploadedFile object that might represent a file on disk or in memory, depending on how large the upload was. This object behaves much like a normal Python file object, so after you have read it once (to make the first thumbnail), you have reached the end of the file, so that when you try to read it again (to make the second thumbnail), there's nothing there, hence the IOError. To make a second thumbnail, you need to seek back to the beginning of the file. So you could add the line
image.seek(0)
to the start of your image_resizer function.
But this is unnecessary! You have this problem because you are asking the Python Imaging Library to re-read the image for each new thumbnail you want to create. This is a waste of time: better to read the image just once and then create all the thumbnails you want.
I'm guessing that is a TemporaryUploadedFile ... find this with type(image).
import cStringIO
if isinstance(image, TemporaryUploadedFile):
temp_file = open(image.temporary_file_path(), 'rb+')
content = cStringIO.StringIO(temp_file.read())
image = Image.open(content)
temp_file.close()
I'm not 100% sure of the code above ... comes from 2 classes I've got for image manipulation ... but give it a try.
If is a InMemoryUploadedFile your code should work!

How do you convert a PIL `Image` to a Django `File`?

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?
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.
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).
save the file
open it from filesystem with PIL,
save to a temp directory with PIL,
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)
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(...)
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())
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()
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.
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()

Categories

Resources