BytesIO stream image is blank when uploading SimpleUploadedFile - python

On my model save method I want to generate an avatar and upload to a ImageField (self.avatar).
The code below runs but the image it uploads is blank when I view it. I've tested the generator actually works by saving directly to disk without BytesIO stream i.e. image.save("test.jpeg", format=filetype, optimize=True) and this works. So, the issue appears to be with how am using BytesIO and SimpleUploadedFile.
Save method
from .generate_avatar import Avatar
from django.core.files.uploadedfile import SimpleUploadedFile
avatar = Avatar.generate(128, self.display_name, "JPEG")
self.avatar = SimpleUploadedFile("temp.jpeg", avatar.read1(0))
Generate Method
def generate(cls, size, string, filetype="JPEG"):
"""
Generates a squared avatar with random background color.
:param size: size of the avatar, in pixels
:param string: string to be used to print text and seed the random
:param filetype: the file format of the image (i.e. JPEG, PNG)
"""
render_size = max(size, Avatar.MIN_RENDER_SIZE)
image = Image.new('RGB', (render_size, render_size),
cls._background_color(string))
draw = ImageDraw.Draw(image)
font = cls._font(render_size)
text = cls._text(string)
draw.text(cls._text_position(render_size, text, font),
text,
fill=cls.FONT_COLOR,
font=font)
stream = BytesIO()
image = image.resize((size, size), Image.ANTIALIAS)
image.save(stream, format=filetype, optimize=True)
return stream

You need to reset file position. Otherwise file position will be at the end of the file; Reading from there will return empty byte string.
avatar = Avatar.generate(128, self.display_name, "JPEG")
avatar.seek(0) # <---
self.avatar = SimpleUploadedFile("temp.jpeg", avatar.read())

Related

Can't save changes to BytesIO buffer

While I am learning Flask, I wrote a small service that receives a image, resize and reduce it's quality.
To avoid write it to the disk and then delete it, I use a buffer, and it worked fine. But now I can't send it using flask send_file. I tried a few solutions that include wrap it using werkzeug FileWrapper and send using Response, but it also did not work. Also, it does't show any kind of error...
#app.route('/api/convert', methods=['POST'])
def convert():
if 'image' not in request.files:
return 'No image!'
if request.files['image'].content_type not in ALLOWED_CONTENT:
return 'Not an allowed image!'
filename = request.files['image'].filename
mimetype = request.files['image'].mimetype
print(mimetype)
buffer = io.BytesIO()
request.files['image'].save(buffer)
image = Image.open(buffer)
w, h = resize_image(*image.size)
image = image.resize((w, h), Image.ANTIALIAS)
print(type(buffer))
image.save(buffer,
format=mimetype.split('/')[-1],
optimize=True,
quality=DEFAULT_QUALITY)
return send_file(buffer,
mimetype=mimetype,
attachment_filename=filename,
as_attachment=True)
When I point to a file that exist in my system, it works fine...
UPDATE
It was pointed out that I was not using buffer.seek(0), after doing putting it on, i started to receive the image in my requests, but the image is far from what I expected.
For example, my test image is 5.2MB, when I save it to the disk instead of the buffer, it goes to 250KB, but when i try to save it to the buffer, and send it using send_file, it goes to 5.5MB...
#app.route('/api/convert', methods=['POST'])
def convert():
if 'image' not in request.files:
return 'No image!'
if request.files['image'].content_type not in ALLOWED_CONTENT:
return 'Not an allowed image!'
filename = request.files['image'].filename
mimetype = request.files['image'].mimetype
buffer = io.BytesIO()
request.files['image'].save(buffer)
image = Image.open(buffer)
w, h = resize_image(*image.size)
buffer.seek(0)
image = image.resize((w, h), Image.ANTIALIAS)
image.save(buffer,
format=mimetype.split('/')[-1],
optimize=True,
quality=DEFAULT_QUALITY)
buffer.seek(0)
return send_file(buffer,
mimetype=mimetype,
attachment_filename=filename,
as_attachment=True)
I am editing this question title and removing the tags for flask because it seems that my problem is just the lack of knowledge about io's BytesIO library.
UPDATE 2
I was working in another project when it came to my mind. What if I create a new buffer to save the image already modified?
And it worked.
#app.route('/api/convert', methods=['POST'])
def convert():
if 'image' not in request.files:
return 'No image!'
if request.files['image'].content_type not in ALLOWED_CONTENT:
return 'Not an allowed image!'
filename = request.files['image'].filename
mimetype = request.files['image'].mimetype
buffer = io.BytesIO()
buffer_final = io.BytesIO()
request.files['image'].save(buffer)
image = Image.open(buffer)
w, h = resize_image(*image.size)
image = image.resize((w, h), Image.ANTIALIAS)
image.save(buffer_final,
format=mimetype.split('/')[-1],
optimize=True,
quality=75)
buffer_final.seek(0)
return send_file(buffer_final,
mimetype=mimetype,
attachment_filename=filename,
as_attachment=True)
So, apparently I can't replace the content of the BytesIO buffer? Anyone knows what I am doing wrong? (yeah, I made it work, but I guess that other people would benefit from the same problem?)
Tested on my machine and truncate() after save(...) works just fine.
import math
import shutil
from PIL import Image
from io import BytesIO
src = r"C:\Users\justin\Desktop\test.jpg"
f = open(src, 'rb')
buffer = BytesIO()
shutil.copyfileobj(f, buffer)
print(f.tell(), buffer.tell())
f.close()
buffer.seek(0)
image = Image.open(buffer)
image.show()
print(buffer.tell())
print(image.size)
w, h = image.size
w, h = math.floor(w*0.75), math.floor(h*0.75)
print(w, h)
smaller = image.resize((w, h), Image.ANTIALIAS)
smaller.show()
print(smaller.size)
buffer.seek(0)
smaller.save(buffer, format='JPEG', optimize=True)
print(buffer.tell())
buffer.truncate()
buffer.seek(0)
out = Image.open(buffer)
out.show()
print(out.size)

How to validate of dimensions in default ImageField?

Have a following rule in the project: the image field is optional and a default image is an uninformed case for the user. Images are sent by the user in django and must have a dimension (width> = 900, height> = 400).
I am trying to validate the dimensions in admin.py, but it is giving problem when I try to register when it has default argument in imagefield.
Gives an image error not found even though it is in the directory. Without the validation function in admin.py it works normally.
models.py
class Event(models.Model):
banner = models.ImageField('banner', upload_to='events/banners', default='events/banners/banner_padrao_eventos.png', blank=True)
admin.py
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = '__all__'
def clean_banner(self):
banner = self.cleaned_data.get('banner')
if banner:
img = Image.open(banner)
width, height = img.size
max_width = 900
max_height = 400
if width < max_width or height < max_height:
raise forms.ValidationError(
'Image is incorrectly sized:% s x% s pixels. Please insert an image with% s x% s pixels.'
% (width, height, max_width, max_height))
if len(banner) > (3 * 1024 * 1024):
raise forms.ValidationError('Very large image file (maximum of 3MB).')
name_img, ext = banner.name.split('.')
if not (ext.lower() in ['png', 'jpg', 'jpeg']):
raise forms.ValidationError('Please use the image in JPG, JPEG or PNG format.')
else:
raise forms.ValidationError('The loaded image could not be read.')
return banner
How can I make the default image to be considered in the validation?
The file uploaded is Django InMemoryUploadedFile file it's not saved in your server yet, open() needs that the file exists physically or as string bytes. To do that you need the package PIL.Image and io.BytesIO
from io import BytesIO as StringIO
from PIL import Image
img = Image.open(StringIO(banner.read()))
So now, img.size is a tuple containing your width and height
width, height = img.size

Generate a image which produces gif like effect

I am trying to generate a image which produces gif like effect by flushing the response continuously to the browser, I am relatively new to django/python and tried with following code testing for both text and image. The generator for text works fine but in case of image only first version of image is being generated.
I tried searching but can't find anything on this. I am confused how to proceed with this or if this is at all possible with django or If the idea is conceptually wrong.
Image Generator :
def refreshx():
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
size = (1000,500)
im = Image.new('RGB', size)
draw = ImageDraw.Draw(im)
red = (255,255,255)
text_pos = (10,30)
draw.text(text_pos, str(datetime.datetime.today()), fill=red)
buffer = StringIO.StringIO()
im.save(buffer, 'PNG')
yield buffer.getvalue()
time.sleep(5)
buffers = StringIO.StringIO()
ims = Image.new('RGB', size)
draws = ImageDraw.Draw(ims)
text_poss = (30,80)
draws.text(text_poss, 'dasdasdsa', fill=red)
print 'been there'
ims.save(buffers, 'PNG')
yield buffers.getvalue()
Text Generator :
def testgenerator():
yield str(datetime.datetime.today())
time.sleep(5)
yield str(datetime.datetime.today())
View Function :
def test(request):
#return HttpResponse(testgenerator());
return HttpResponse(refreshx(), mimetype="image/png")
EDIT :
I learned while researching that there's a concept called gifsocket, I'm looking into it..please suggest if anyone has experience with these

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