Can't save changes to BytesIO buffer - python

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)

Related

I have a problem uploading images to DO Spaces resized with PILL

I'm uploading images to Digital Ocean Spaces using boto3. It's working really good until I add PILL.
In django view I have this code when I get the image:
from digitalocean_spaces import DigitalOceanSpaces
from helpers import resize_maintain_its_aspect_ratio
def images_view(request):
if request.method == "POST":
images = request.FILES.getlist('images')
for index, image in enumerate(images):
size = image.size
content_type = image.content_type
file_name = image.name
# TODO: fix method
# image = resize_maintain_its_aspect_ratio(image, 500)
DigitalOceanSpaces().upload_file(
key=key,
file=image,
content_type=content_type,
acl='private'
)
I can see all the information of each image.
To upload the image I use this method that is working too:
class DigitalOceanSpaces:
def default_session_client(self):
session = boto3.session.Session()
client = session.client(
's3',
region_name=REGION_NAME,
endpoint_url=ENDPOINT_URL,
aws_access_key_id=ACCESS_KEY_ID,
aws_secret_access_key=ACCESS_SECRET_KEY
)
return client
def upload_file(self, key, file, content_type, acl='private'):
client = self.default_session_client()
client.put_object(
Bucket=BUCKET_NAME,
Key=key,
Body=file,
ACL=acl,
ContentType=content_type,
Metadata={
'x-amz-meta-my-key': '*****'
}
)
The problem start when I call this another method to resize the image
from PIL import Image
def resize_maintain_its_aspect_ratio(image, base_width):
pillow_image = Image.open(image)
width_percent = (base_width / float(pillow_image.size[0]))
height_size = int((float(pillow_image.size[1]) * float(width_percent)))
resized_image = pillow_image.resize((base_width, height_size), Image.ANTIALIAS)
return resized_image
I see the error even if resize_maintain_its_aspect_ratio method just have:
pillow_image = Image.open(image)
So, the error is:
An error occurred (BadDigest) when calling the PutObject operation
(reached max retries: 4): Unknown
Does anyone know what the problem is ?

How can I return an image generated in pillow in django without saving in db?

def build_image(image_data):
template = Image.open("path/to/file/template.jpg")
draw = ImageDraw.Draw(template)
draw.text("(553,431)", f"{image_data.text}", fill=4324234, font=font)
file = InMemoryUploadedFile(template, None, 'result.jpg', 'image/jpeg', template.tell, None)
return FileResponse(file, as_attachment=True, filename='result.jpg')
I need to return the image that was modified, without saving it to the database. The above code gives the following error:
A server error occurred. Please contact the administrator.
I also tried the following option:
rea_response = HttpResponse(template, content_type='image/png')
rea_response['Content-Disposition'] = 'attachment; filename={}'.format('result.png')
return rea_response
But in this case, I get a corrupted file. (This format is not supported).
django==3.0.6
If you want to return raw bytes (of the image), then pass a file handler to Image.save() as said here.
E.g.
import io
def foo():
template = Image.open("path/to/file/template.jpg")
draw = ImageDraw.Draw(template)
draw.text("(553,431)", f"{image_data.text}", fill=4324234, font=font)
buff = io.BytesIO()
template.save(buff, format='JPEG')
return buff

Scale Django ImageField before saving to disk

I would like to scale an ImageField before the model gets saved to the disk, but somehow get an unreadable image out. The goal is to scale it without ever saving it to the disk.
This is my attempt so far:
IMAGE_MAX_SIZE = 800, 800
class Picture(models.Model):
...
image = models.ImageField(upload_to='images/%Y/%m/%d/')
# img is a InMemoryUploadedFile, received from a post upload
# removing the scale function results in a readable image
def set_image(self, img):
self.image = img
self.__scale_image()
def __scale_image(self):
img = Image.open(StringIO(self.image.read()))
img.thumbnail(IMAGE_MAX_SIZE, Image.ANTIALIAS)
imageString = StringIO()
img.save(imageString, img.format)
self.image.file = InMemoryUploadedFile(imageString, None, self.image.name, self.image.file.content_type, imageString.len, None)
I'm not getting an error, but the resulting image can not be displayed correctly. Any ideas how to correct this?
Thanks
Simon
I was close, but not quite there. This function works now fine and the image does not get saved to the disc at any point during the scaling.
IMAGE_MAX_SIZE = 800, 800
class Picture(models.Model):
...
image = models.ImageField(upload_to='images/%Y/%m/%d/')
# img is a InMemoryUploadedFile, received from a post upload
def set_image(self, img):
self.image = img
self.__scale_image(self.image, IMAGE_MAX_SIZE)
def __scale_image(self, image, size):
image.file.seek(0) # just in case
img = Image.open(StringIO(image.file.read()))
img.thumbnail(size, Image.ANTIALIAS)
imageString = StringIO()
img.save(imageString, img.format)
# for some reason content_type is e.g. 'images/jpeg' instead of 'image/jpeg'
c_type = image.file.content_type.replace('images', 'image')
imf = InMemoryUploadedFile(imageString, None, image.name, c_type, imageString.len, None)
imf.seek(0)
image.save(
image.name,
imf,
save=False
)
It would better idea to use sorl-thubnail.

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.

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