Django with PIL - '_io.BytesIO' object has no attribute 'name' - django

I'm using PIL to resize an uploaded photo before saving. Note that I'm using formsets to upload the pictures. I'm using BytesIO to open the file. At the last step, I get the error - '_io.BytesIO' object has no attribute 'name'. Why is this?
def fsbo_create_listing(request):
PhotoFormSet = formset_factory(OwnerListingPhotoForm, extra=15)
if request.method == 'POST':
form = OwnerListingForm(request.POST)
photo_formset = PhotoFormSet(request.POST, request.FILES)
if form.is_valid() and photo_formset.is_valid():
form.instance.user = request.user
form.save()
for i in photo_formset:
if i.instance.pk and i.instance.photo == '':
i.instance.delete()
elif i.cleaned_data:
temp = i.save(commit=False)
temp.listing = form.instance
temp.save() # Where the error happens
def clean_photo(self):
picture = self.cleaned_data.get('photo')
# I had to import ImageFieldFile. If picture is already uploaded, picture would still be retrieved as ImageFieldFile. The following line checks the variable type of `picture` to determine whether the cleaning should proceed.
if type(picture) != ImageFieldFile:
image_field = self.cleaned_data.get('photo')
image_file = BytesIO(image_field.read())
image = Image.open(image_file)
image = ImageOps.fit(image, (512,512,), Image.ANTIALIAS)
image_file = BytesIO()
image.save(image_file, 'JPEG', quality=90)
image_field.file = image_file
#if picture._size > 2*1024*1024:
#raise ValidationError("Image file too large. Max size is 2MB.")
return picture
class OwnerListingPhoto(models.Model):
listing = models.ForeignKey(OwnerListing, on_delete=models.CASCADE, related_name='owner_listing_photo')
photo = models.ImageField(upload_to=owner_listing_folder_name)

The issue is that new versions of Django default to using MemoryFileUploadHandler, which doesn't create a temporary file, and therefore there is no file "name." See related Django ticket.
You'll probably have to modify your code a bit to make this work, but you can at least start getting the name property by setting:
FILE_UPLOAD_HANDLERS = [
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]
In your settings.py file.
You may find the code I've used to solve almost the exact same issue as helpful.
def clean_logo_file(self):
logo_file_field = self.cleaned_data.get('logo_file')
if logo_file_field:
try:
logo_file = logo_file_field.file
with Image.open(logo_file_field.file.name) as image:
image.thumbnail((512, 512), Image.ANTIALIAS)
image.save(logo_file, format=image.format)
logo_file_field.file = logo_file
return logo_file_field
except IOError:
logger.exception("Error during image resize.")
Additional information on upload handlers.

If file is bigger than 2.5mb (2621440 bytes) - Django will
use TemporaryFileUploadHandler.
Otherwise Django will use MemoryFileUploadHandler.
You can change FILE_UPLOAD_MAX_MEMORY_SIZE (doc) in settings.py
Or change FILE_UPLOAD_HANDLERS (doc) as Nostalg.io mentioned above.
My example with Django Rest Framework serializers:
Broken code:
# models.py
class ImageModel(Model):
image = models.ImageField(upload_to='images/', null=False, blank=False)
# serializers.py
class ImageSerializer(serializers.ModelSerializer):
class Meta:
model = ImageModel
fields = ["id", "image"]
read_only_fields = ["id"]
def validate_image(self, user_img):
img = Image.open(user_img)
... # process image here
img_io = io.BytesIO()
img.save(img_io, format='JPEG', quality=100)
filename = "%s.jpg" % user_img.name.split('.')[0]
user_img.name = "%s.jpg" % user_img.name.split('.')[0]
user_img.file = img_io # BAD IDEA!!!
# This overrides django's tempfile._TemporaryFileWrapper() with _io.BytesIO() !!!
...
return user_img # if picture bigger than 2.5mb -> gives an error!
Fixed code:
#settings.py
FILE_UPLOAD_HANDLERS = [
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]
# serializers.py
class ImageSerializer(serializers.ModelSerializer):
class Meta:
model = ImageModel
fields = ["id", "image"]
read_only_fields = ["id"]
def validate_image(self, user_img):
img = Image.open(user_img)
... # process image here
# override old TemporaryFile image with edited image
path_to_tmp = user_img.file.name
new_filename = "%s.jpeg" % user_img.name.split('.')[0]
# set new image name
img.save(path_to_tmp, format='JPEG', quality=100)
user_img.name = new_filename
...
return user_img # no errors more :)
It might be more rational to process image by rewriting save() method in models.py, but I convert images in serializers.py because of handly ValidationError() :)

Related

Upload and manipulate xml and image file in django

This code return a TypeError as expected str, bytes or os.PathLike object, not InMemoryUploadedFile
I don't know how to pass user data in the form of file and image to my code.py file for making changes to the original.
views.py
def home(request):
new_image = None
file = None
form = ScanForm()
if request.method == 'POST':
form = ScanForm(request.POST, request.FILES)
if form.is_valid():
image = request.FILES['image']
xml_file = request.FILES['xml_file']
new_image = code.create(image, code.search(
xml_file)[0], code.search(xml_file)[1])
form.save()
return render(request, 'app/home.html', {'form': form, 'new_image': new_image})
else:
form = ScanForm()
return render(request, 'app/home.html', {'form': form, 'new_image': new_image})
printing image and xml_file successfully prints out their names
forms.py
class ScanForm(forms.ModelForm):
class Meta:
model = Scan
fields = '__all__'
models.py
class Scan(models.Model):
image = models.ImageField(upload_to='images')
xml_file = models.FileField(upload_to='files')
processed_at = models.DateTimeField(auto_now_add=True)
description = models.CharField(max_length=500, null=True)
class Meta:
ordering = ['-processed_at']
def __str__(self):
return self.description
Here is the code for manipulation of image according to the data in the xml
code.py
def search(path):
new = []
object_names = []
object_values = []
txt = Path(path).read_text()
txt.strip()
names = et.fromstring(txt).findall('object')
for i in names:
object_names.append(i[0].text)
values = et.fromstring(txt).findall('object/bndbox')
for i in values:
for j in i:
object_values.append(int(j.text))
return object_names, object_values
def create(image, object_names, object_values):
img = cv.imread(image)
on = len(object_names)
ov = len(object_values)
for i in list(range(0, ov, on)):
cv.rectangle(img, (object_values[i], object_values[i+1]),
(object_values[i+2], object_values[i+3]), (0, 0, 255), thickness=5)
return img
This code.py works fine if tested by passing data manually using local folder.
Here is the Traceback:
Traceback image
pathlib.Path() handles file paths, not memory objects. request.FILES are the data attached to the POST request. During your handling of a POST request, you can validate this data and decide to save it to the server disk.
If you would like your image processing to read the file from the server disk, you have to save the new model instance first. You can then access the file's path on disk through the name attribute of the model's ImageField, see Using files in models.
If you want to handle the uploaded data before saving it to disk, you can read it as follows:
txt = request.FILES["xml_file"].read()
See UploadedFile.read()

Django. Override the html format of a FileField field in the change page via ModelAdmin

This is my sample code (and see the screenshot):
models.py
class User(models.Model):
# the variable to take the inputs
user_name = models.CharField(max_length=100)
user_avatar = models.FileField(upload_to = 'images/%Y%m%d')
admin.py
class UserAdmin(admin.ModelAdmin):
exclude = ('user_avatar',)
readonly_fields = ('avatar_readonly',)
def avatar_readonly(self, instance):
value_link = "/mediafileupdown/download/" + str(instance.id)
value_desc = str(instance.user_avatar.name)
return format_html('{}', value_link, value_desc)
avatar_readonly.short_description = "User avatar (read only)"
avatar_readonly.allow_tags=True
screenshot
The problem is: i'd like obtain the 'Currently' user_avatar's link (that's a models.FileField field) via the my 'download' views and not as simple url. I can do that if the field is in the 'readonly_fields' list and so I can overide the html format ... in that case, on the change page, the 'avatar_readonly' link is redirected to the my 'download' view.
The question is: how can I get the same result (overide the html format) in a FileField (like my 'user_avatar') when is not in the readonly_fileds list?
Sorry for my english and many Thanks for the help.
You can use formfield_overrides to modify the AdminFileWidget. This AdminFileWidget can display images and mp4 files, but you can use only for images omitting the if file_name[-4:] == '.mp4':
part.
from django.db.models.fields.files import FileField
from django.contrib.admin.widgets import AdminFileWidget
class AdminMediaWidget(AdminFileWidget):
def render(self, name, value, attrs=None):
output = []
if value and getattr(value, "url", None):
image_url = value.url
file_name = str(value)
if file_name[-4:] == '.mp4':
output.append('<video width="320" height="240" controls>'
'<source src="{0}" type="video/mp4">'
'Your browser does not support the video tag.'
'</video>'.format(image_url))
else:
output.append('<a href="{0}" target="_blank">'
'<img height=200 src="{1}" alt="{2}" />'
'</a>'.format(image_url, image_url, file_name))
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(''.join(output))
class UserAdmin(admin.ModelAdmin):
formfield_overrides = {
FileField: {'widget': AdminMediaWidget},
}
exclude = ('user_avatar',)
readonly_fields = ('avatar_readonly',)
def avatar_readonly(self, instance):
value_link = "/mediafileupdown/download/" + str(instance.id)
value_desc = str(instance.user_avatar.name)
return format_html('{}', value_link, value_desc)
avatar_readonly.short_description = "User avatar (read only)"
avatar_readonly.allow_tags=True
Hope it helps!

Uploading and saving files using django nonrel in Google App engine?

According to the official django documentation about uploads, small files are saved to memory and big files are saved to disk.
I would like to know how to save uploaded files to disk? Is it possible?
This is my code so far. But it only works on memory. When I try to write the file to disk, or the file is big in size the app crashes.
views.py
# ...
def spreadsheet_form(request, id = None):
if is_admin_user(request):
instance = get_object_or_404(Spreadsheet, id=id) if id is not None else None
form = SpreadsheetForm(request.POST or None, request.FILES or None, instance=instance)
if form.is_valid():
spreadsheet = form.save(commit=False)
spreadsheet.name = request.POST['name']
spreadsheet.spreadsheet_file = request.FILES['spreadsheet_file'].name
spreadsheet.size = request.FILES['spreadsheet_file'].size
spreadsheet.save()
handle_uploaded_file(request.FILES['spreadsheet_file'])
return redirect('/spreadsheets/')
return render_to_response("pages/spreadsheet_form.html", {"form": form,"id":id},context_instance=RequestContext(request))
else:
return redirect('/', False)
# ...
def handle_uploaded_file(f):
with open(f.name, 'wb+') as destination:
for chunk in f.chunks():
destination.write(chunk)
models.py
# ...
class Spreadsheet(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=256)
spreadsheet_file = models.FileField(upload_to='spreadsheets/')
size = models.CharField(max_length=32)
created_at = models.DateTimeField(auto_now=True)
# ...
def __unicode__(self):
return u'%s' % (self.name )
settings.py
FILE_UPLOAD_HANDLERS = (
"django.core.files.uploadhandler.MemoryFileUploadHandler",
"django.core.files.uploadhandler.TemporaryFileUploadHandler",
)
FILE_UPLOAD_MAX_MEMORY_SIZE = 7000000
FILE_UPLOAD_TEMP_DIR = '/tmp'
Thanks in advance
There's no such thing as 'save to disk' in the App Engine world. The closest was Blobstore, and now it's GCS.
django-nonrel includes a django storage class to upload to Blobstore. Follow this:
http://www.allbuttonspressed.com/blog/django/2010/06/Uploads-to-Blobstore-and-GridFS-with-Django
You don't need to fiddle with the FILE_UPLOAD_HANDLERS, the defaults are fine.

Import csv data into database in Django Admin

I've tried to import a csv file into a database by tweaking the modelform inside the admin doing this:
models.py:
class Data(models.Model):
place = models.ForeignKey(Places)
time = models.DateTimeField()
data_1 = models.DecimalField(max_digits=3, decimal_places=1)
data_2 = models.DecimalField(max_digits=3, decimal_places=1)
data_3 = models.DecimalField(max_digits=4, decimal_places=1)
Forms.py:
import csv
class DataImport(ModelForm):
file_to_import = forms.FileField()
class Meta:
model = Data
fields = ("file_to_import", "place")
def save(self, commit=False, *args, **kwargs):
form_input = DataImport()
self.place = self.cleaned_data['place']
file_csv = request.FILES['file_to_import']
datafile = open(file_csv, 'rb')
records = csv.reader(datafile)
for line in records:
self.time = line[1]
self.data_1 = line[2]
self.data_2 = line[3]
self.data_3 = line[4]
form_input.save()
datafile.close()
Admin.py:
class DataAdmin(admin.ModelAdmin):
list_display = ("place", "time")
form = DataImport
admin.site.register(Data, DataAdmin)
But i'm stuck trying to import the file i put in "file_to_import" field. Getting AttributeError in forms.py : 'function' object has no attribute 'FILES'.
What i'm doing wrong?
After a long search i found an answer: Create a view inside the admin using a standard form
Form:
class DataInput(forms.Form):
file = forms.FileField()
place = forms.ModelChoiceField(queryset=Place.objects.all())
def save(self):
records = csv.reader(self.cleaned_data["file"])
for line in records:
input_data = Data()
input_data.place = self.cleaned_data["place"]
input_data.time = datetime.strptime(line[1], "%m/%d/%y %H:%M:%S")
input_data.data_1 = line[2]
input_data.data_2 = line[3]
input_data.data_3 = line[4]
input_data.save()
The view:
#staff_member_required
def import(request):
if request.method == "POST":
form = DataInput(request.POST, request.FILES)
if form.is_valid():
form.save()
success = True
context = {"form": form, "success": success}
return render_to_response("imported.html", context,
context_instance=RequestContext(request))
else:
form = DataInput()
context = {"form": form}
return render_to_response("imported.html", context,
context_instance=RequestContext(request))
The rest is part of this post:
http://web.archive.org/web/20100605043304/http://www.beardygeek.com/2010/03/adding-views-to-the-django-admin/
Take a look at django-admin-import, it does more or less exactly what you want -- you can upload a XLS (not a CSV, but that should not matter) and lets you assign columns to model fields. Default values are also supported.
https://pypi.org/project/django-admin-import/
Additionally, it does not take away the possibility to modify individual records by hand because you don't have to replace the default model form used in the administration.
In the save() method, you don't have any access to the request object - you can see that it's not passed in. Normally you would expect to have a NameError there, but I suspect that you've got a function elsewhere in the file called request().
At the point of saving, all the relevant data should be in cleaned_data: so you should be able to do
file_csv = self.cleaned_data['file_to_import']
At that point you'll have another problem, which is when you get to open - you can't do that, as file_to_import is not a file on the server filesystem, it's an in-memory file that has been streamed from the client. You should be able to pass file_csv directly to csv.reader.

resize image on save

How can I easily resize an image after it has been uploaded in Django? I am using Django 1.0.2 and I've installed PIL.
I was thinking about overriding the save() method of the Model to resize it, but I don't really know how to start out and override it.
Can someone point me in the right direction? Thanks :-)
#Guðmundur H:
This won't work because the django-stdimage package does not work on Windows :-(
I recommend using StdImageField from django-stdimage, it should handle all the dirty work for you. It's easy to use, you just specify the dimensions of the resized image in the field definition:
class MyModel(models.Model):
image = StdImageField(upload_to='path/to/img', size=(640, 480))
Check out the docs — it can do thumbnails also.
You should use a method to handle the uploaded file, as demonstrated in the Django documentation.
In this method, you could concatenate the chunks in a variable (rather than writing them to disk directly), create a PIL Image from that variable, resize the image and save it to disk.
In PIL, you should look at Image.fromstring and Image.resize.
I use this code to handle uploaded images, resize them on memory(whithout saving them permanently on disk), and then saving the thumb on a Django ImageField.
Hope can help.
def handle_uploaded_image(i):
import StringIO
from PIL import Image, ImageOps
import os
from django.core.files import File
# read image from InMemoryUploadedFile
image_str = “”
for c in i.chunks():
image_str += c
# create PIL Image instance
imagefile = StringIO.StringIO(image_str)
image = Image.open(imagefile)
# if not RGB, convert
if image.mode not in (“L”, “RGB”):
image = image.convert(“RGB”)
#define file output dimensions (ex 60x60)
x = 130
y = 130
#get orginal image ratio
img_ratio = float(image.size[0]) / image.size[1]
# resize but constrain proportions?
if x==0.0:
x = y * img_ratio
elif y==0.0:
y = x / img_ratio
# output file ratio
resize_ratio = float(x) / y
x = int(x); y = int(y)
# get output with and height to do the first crop
if(img_ratio > resize_ratio):
output_width = x * image.size[1] / y
output_height = image.size[1]
originX = image.size[0] / 2 - output_width / 2
originY = 0
else:
output_width = image.size[0]
output_height = y * image.size[0] / x
originX = 0
originY = image.size[1] / 2 - output_height / 2
#crop
cropBox = (originX, originY, originX + output_width, originY + output_height)
image = image.crop(cropBox)
# resize (doing a thumb)
image.thumbnail([x, y], Image.ANTIALIAS)
# re-initialize imageFile and set a hash (unique filename)
imagefile = StringIO.StringIO()
filename = hashlib.md5(imagefile.getvalue()).hexdigest()+’.jpg’
#save to disk
imagefile = open(os.path.join(‘/tmp’,filename), ‘w’)
image.save(imagefile,’JPEG’, quality=90)
imagefile = open(os.path.join(‘/tmp’,filename), ‘r’)
content = File(imagefile)
return (filename, content)
#views.py
form = YourModelForm(request.POST, request.FILES, instance=profile)
if form.is_valid():
ob = form.save(commit=False)
try:
t = handle_uploaded_image(request.FILES[‘icon’])
ob.image.save(t[0],t[1])
except KeyError:
ob.save()
I highly recommend the sorl-thumbnail app for handling image resizing easily and transparently. It goes in every single Django project I start.
Here is a complete solution for ya using a form. I used admin views for this:
class MyInventoryItemForm(forms.ModelForm):
class Meta:
model = InventoryItem
exclude = ['thumbnail', 'price', 'active']
def clean_photo(self):
import StringIO
image_field = self.cleaned_data['photo']
photo_new = StringIO.StringIO(image_field.read())
try:
from PIL import Image, ImageOps
except ImportError:
import Image
import ImageOps
image = Image.open(photo_new)
# ImageOps compatible mode
if image.mode not in ("L", "RGB"):
image = image.convert("RGB")
image.thumbnail((200, 200), Image.ANTIALIAS)
image_file = StringIO.StringIO()
image.save(image_file, 'png')
image_field.file = image_file
return image_field
My inventory model looks like this:
class InventoryItem(models.Model):
class Meta:
ordering = ['name']
verbose_name_plural = "Items"
def get_absolute_url(self):
return "/products/{0}/".format(self.slug)
def get_file_path(instance, filename):
if InventoryItem.objects.filter(pk=instance.pk):
cur_inventory = InventoryItem.objects.get(pk=instance.pk)
if cur_inventory.photo:
old_filename = str(cur_inventory.photo)
os.remove(os.path.join(MEDIA_ROOT, old_filename))
ext = filename.split('.')[-1]
filename = "{0}.{1}".format(uuid.uuid4(), ext)
return os.path.join('inventory', filename)
#return os.path.join(filename)
def admin_image(self):
return '<img height="50px" src="{0}/{1}"/>'.format(MEDIA_URL, self.photo)
admin_image.allow_tags = True
photo = models.ImageField(_('Image'), upload_to=get_file_path, storage=fs, blank=False, null=False)
thumbnail = models.ImageField(_('Thumbnail'), upload_to="thumbnails/", storage=fs, blank=True, null=True)
....
I ended overwriting the save function of the model instead to save the photo and a thumb instead of just resizing the photo:
def save(self):
# Save this photo instance first
super(InventoryItem, self).save()
from PIL import Image
from cStringIO import StringIO
from django.core.files.uploadedfile import SimpleUploadedFile
# Set our max thumbnail size in a tuple (max width, max height)
THUMBNAIL_SIZE = (200, 200)
# Open original photo which we want to thumbnail using PIL's Image object
image = Image.open(os.path.join(MEDIA_ROOT, self.photo.name))
if image.mode not in ('L', 'RGB'):
image = image.convert('RGB')
image.thumbnail(THUMBNAIL_SIZE, Image.ANTIALIAS)
# Save the thumbnail
temp_handle = StringIO()
image.save(temp_handle, 'png') # image stored to stringIO
temp_handle.seek(0) # sets position of file to 0
# Save to the thumbnail field
suf = SimpleUploadedFile(os.path.split(self.photo.name)[-1],
temp_handle.read(), content_type='image/png') # reads in the file to save it
self.thumbnail.save(suf.name+'.png', suf, save=False)
#Save this photo instance again to save the thumbnail
super(InventoryItem, self).save()
Both work great though depending on what you want to do :)
I know this is old, but for anybody stumbling upon it, there is a package, django-thumbs at Django-thumbs - Easy powerful thumbnails for Django integrated with StorageBackend, which automatically generates thumbnails in sizes you specify, or none if you don't. You then call the thumbnail you want with the dimensions you want.
For instance, if you want an image to have thumbnails of 64x64 and 128x128, you simply import thumbs.models.ImageWithThumbsField, and use it in place of ImageField. Add a parameter sizes=((64,64),(128,128)) to the field definition, then from your template you can call:
{{ ClassName.field_name.url_64x64 }}
and
{{ ClassName.field_name.url_128x128 }}
to display the thumbnails. Voila! All the work is done in this package for you.
If you are using Django Rest Framework, this might of use:
First define function to compress and resize image
def compress_image(photo):
# start compressing image
image_temporary = Image.open(photo)
output_io_stream = BytesIO()
image_temporary.thumbnail((1250, 1250), Image.ANTIALIAS)
# change orientation if necessary
for orientation in ExifTags.TAGS.keys():
if ExifTags.TAGS[orientation] == 'Orientation':
break
exif = dict(image_temporary._getexif().items())
# noinspection PyUnboundLocalVariable
if exif.get(orientation) == 3:
image_temporary = image_temporary.rotate(180, expand=True)
elif exif.get(orientation) == 6:
image_temporary = image_temporary.rotate(270, expand=True)
elif exif.get(orientation) == 8:
image_temporary = image_temporary.rotate(90, expand=True)
# saving output
image_temporary.save(output_io_stream, format='JPEG', quality=75, optimize=True, progressive=True)
output_io_stream.seek(0)
photo = InMemoryUploadedFile(output_io_stream, 'ImageField', "%s.jpg" % photo.name.split('.')[0],
'image/jpeg', getsizeof(output_io_stream), None)
return photo
Second, now you can use the function in Serializers:
class SomeSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
# сжимаем рисунок
if 'photo' in validated_data:
validated_data.update({'photo': compress_image(validated_data['photo'])})
return super(SomeSerializer, self).update(instance, validated_data)
def create(self, validated_data):
# сжимаем рисунок
if 'photo' in validated_data:
validated_data.update({'photo': compress_image(validated_data['photo'])})
return super(SomeSerializer, self).create(validated_data)