Django: Uploaded Filename Issue - django

In my django app i'm trying to resize & compress an Image before saving it to the database.
Here's how I did it inside the models
class Data(models.Model):
image = models.ImageField(upload_to='all_images/', null=True, blank=True)
def save(self, *args, **kwargs):
if self.image:
img = Image.open(self.image)
resize = img.resize((240, 240), Image.ANTIALIAS)
new_image = BytesIO()
resize.save(new_image, format=img.format, quality=80, save=False)
temp_name = os.path.split(self.image.name)[1]
self.image.save(temp_name, content=ContentFile(new_image.getvalue()), save=False)
super(Data, self).save(*args, **kwargs)
Here's the problem, I saved an image named tesla.jpg into the database, it compressed & resized it well, but it renamed it something like, tesla_CKBw0Kr_iWKC4Ry_ucPoh4b_BB2C8Ck_WrfWkPR_Tzig2T1_tdhst4b_3Bysn6k_i4ffhPR_yhig2T1.jpg
I'm worried about the new name because normally it should be tesla_CKBw0Kr.jpg or something smaller, so what's the problem in my code & how can we fix that?

Django mangles the image filename so that you don't run into filename collisions in the filesystem. Consider what if you had to save another image named tesla.jpg and don't want it to accidentally overwrite the first one.
You don't have to worry about that though. Django stores the real, original filename in the UploadeFile object.
UPDATE
Django will keep adding random characters to the filename if you upload more files with the same filename:
https://github.com/django/django/blob/master/django/core/files/storage.py#L60-L89
If you worry about hitting the filesystem's filename length limit, then set an appropriate max_length on the ImageField. The function will then keep truncating the file_name and generating new names within the filename length limit, until it finds a free name.

Related

Accessing data using Pillow corrupts it on response in Django Rest Framework (The file you uploaded was either not an image or a corrupted image.)

I want to extract the format and mode parameter from the uploaded image within the Serializer and dynamically update my fields. Here is the code ...
class ImageDataSerializer(serializers.ModelSerializer):
class Meta:
model = models.ImageData
exclude = ['height','width']
And in my view
serializer = serializer(data=request.data,partial=True)
serializer.is_valid(raise_exception=True)
obj = serializer.save(user=request.user,extension="PNG",image_type="RGB")
return Response(serializer.data)
This works perfectly. I am sending my InMemeoryUploadedFile instance as my data and the serializer does its job saving it to the database. However, I would like to determine the extension and image_type automatically using the Pillow library.
This is what I have tried so far ...
class ImageDataSerializer(serializers.ModelSerializer):
def __init__(self,*args,**kwargs):
super(ImageDataSerializer,self).__init__(*args,**kwargs)
myimage = self.initial_data['data']
with Image.open(myimage) as myimage:
self.fields['extension'].initial = myimage.format
self.fields['image_type'].initial = myimage.mode
# Update the extension and image_type initial values
class Meta:
model = models.ImageData
exclude = ['height','width']
What happens is my image file get corrupted and in the response I am getting the message "Upload a valid image. The file you uploaded was either not an image or a corrupted image."
I have also tried determining the extension and mode within the view and pass it to request.data dictionary but accessing the image file once using Pillow.Image.open() is later corrupting it.
Issue was the cursor.
After opening the file using Pillow once you need to reset the cursor to the beginning of the file for future usage by Django. Below is fully working code.
class ImageDataSerializer(serializers.ModelSerializer):
def __init__(self,*args,**kwargs):
data = kwargs.get("data",None)
img = data.get("data",None)
with Image.open(img) as myimage:
data['extension'] = myimage.format
data['image_type'] = myimage.mode
img.seek(0)
super(ImageDataSerializer,self).__init__(*args,**kwargs)
class Meta:
model = models.ImageData
exclude = ['height','width']
read_only_fields = ['user']
img.seek(0) Resets the cursor to the beginning of the file

How to get a current date for upload_to in Django models

Since some image files have name conflicts, I decided to make the system to change a uploaded file's name automatically. However, after changing the system, I got in trouble with getting a current date for the file path.
This is how my previous Image model looks like, and it stores an image with a name like boutique/index/2018/9/20/FILE_NAME.jpg.
class Image(TimeStampedModel):
...
image = ImageField(..., upload_to='boutique/index/index/%Y/%m/%d')
...
After that, I changed it to like this. This successfully changes a uploaded image's name automatically. However, it stores a name like boutique/%Y/%m/%d/FILE_NAME.jpg.
def image_path(instance, filename):
basefilename, file_extension = os.path.splitext(filename)
chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'
randomstr = ''.join((random.choice(chars)) for x in range(10))
return 'boutique/index/%Y/%m/%d/{imageid}/{basename}{randomstring}{ext}'.format(imageid=instance.store.domainKey, basename=basefilename, randomstring=randomstr, ext=file_extension)
class Image(TimeStampedModel):
...
image = ImageField(..., upload_to=image_path)
...
Like you see the above, the problem is that %Y, %m, and %d don't provide date data I need anymore. What is wrong here? image_path function returns the same thing in the same place. I don't know why they are just like recognized as a normal string
You have to set those values manually.
Add the import
from datetime import datetime
And replace in your function %Y, %m, %d with {year}, {month}, {day} variables and add the values to the format call.
def image_path(instance, filename):
basefilename, file_extension = os.path.splitext(filename)
chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'
randomstr = ''.join((random.choice(chars)) for x in range(10))
_now = datetime.now()
return 'boutique/index/{year}/{month}/{day}/{imageid}/{basename}{randomstring}{ext}'.format(
imageid=instance.store.domainKey,
basename=basefilename, randomstring=randomstr, ext=file_extension,
year=_now.strftime('%Y'), month=_now.strftime('%m'), day=_now.strftime('%d')
)
More about file uploads in django

Dynamic File Path for image in Django

I'm trying to generate dynamic file paths in django. I want to make a file system like this
hierarchy:
--user_12
--channel_14
--program 2
--image1.jpg
--image2.jpg
--program 1
--image1.jpg
--image2.jpg
--user_14
--channel_13
--program 1
--image1.jpg
--image2.jpg
When a user want to upload image it will upload the image to the corresponding program folder.if program folder does not create it will automatically create a folder and store image..
my image path will look like this: media/images/john/johnchannel/birthday/img1.jpg ( where john=user,johnchannel=channel,birthday=program,images is the pre created folder where all image file should be stored)
I am very new in django. urgent help needed.
Are you looking for something like this?
misc.py
def get_upload_path(instance, filename):
""" creates unique-Path & filename for upload """
ext = filename.split('.')[-1]
filename = "%s%s.%s" % ('img', instance.pk, ext)
return os.path.join(
'images', instance.user.username, instance.channel, instance.something, filename
#images/ john/ johnchannel/ birthday/ img1.jpg
)
models.py
from misc import get_upload_path
class MyThing(models.Model):
user = models.ForeignKey(User)
channel = models.CharField(max_length=100) # johnchannel
something = models.CharField(max_length=100) # birthday
img = models.ImageField(upload_to=get_upload_path)
When using an ImageField in a Django model, you can specify a callable in the upload_to keyword argument: See the FileField docs here
So create a function that you'll point upload_to at, which can generate all your subdirectories as needed.

ImageField resizing on save also updating width_field and height_field

I have a model containing ImageField which should be resized after uploading.
class SomeModel(models.Model):
banner = ImageField(upload_to='uploaded_images',
width_field='banner_width',
height_field='banner_height')
banner_width = models.PositiveIntegerField(_('banner width'), editable=False)
banner_height = models.PositiveIntegerField(_('banner height'), editable=False)
def save(self, *args, **kwargs):
super(SomeModel, self).save(*args, **kwargs)
resize_image(filename=self.banner.path,
width=MAX_BANNER_WIDTH,
height=MAX_BANNER_HEIGHT)
resize_image is a custom function which does the resizing, and everything works fine, except that banner_width and banner_height are populated with dimensions of original image, before resizing.
Actual size of resized image may be smaller than MAX values given, so I have to open resized file to check it's actual dimensions after resizing. I could then manually set banner_width and banner_height, and save again, but it's not efficient way.
I could also do the resizing first, set width and height fields, and then save, but file at location self.banner.path doesn't exist before save is performed.
Any suggestions on how should this be done properly?
After several hours of trying to do this efficiently, I've changed my approach to this problem and defined CustomImageField like this:
class CustomImageField(ImageField):
attr_class = CustomImageFieldFile
def __init__(self, resize=False, to_width=None, to_height=None, force=True, *args, **kwargs):
self.resize = resize
if resize:
self.to_width = to_width
self.to_height = to_height
self.force = force
super(CustomImageField, self).__init__(*args, **kwargs)
class CustomImageFieldFile(ImageFieldFile):
def save(self, name, content, save=True):
super(CustomImageFieldFile, self).save(name, content, save=save)
if self.field.resize:
resized_img = resize_image(filename=self.path,
width=self.field.to_width,
height=self.field.to_height,
force=self.field.force)
if resized_img:
setattr(self.instance, self.field.width_field, resized_img.size[0])
setattr(self.instance, self.field.height_field, resized_img.size[1])
Now I can just define:
class SomeModel(models.Model):
my_image = CustomImageField(resize=True, to_width=SOME_WIDTH, to_height=SOME_HEIGHT, force=False,
width_field='image_width', height_field='image_height')
image_width = models.PositiveIntegerField(editable=False)
image_height = models.PositiveIntegerField(editable=False)
And depending on resize argument, image can be automatically resized after uploading, and width/height fields are correctly updated, without saving object twice. After quick tests it seems to be working fine.

Hacking django-stdimage2 to support multiple images

I am working on a Django project in which users should be allowed to upload multiple images at once. That portion of the project will likely be handled by SWFUpload, unless you have a better suggestion.
The image renaming, resizing, and thumbnail creation will be handled by django-stdimage2.
The Problem
django-stdimage2 renames each image using the field name and object primary key.
If five images exist for gear row with primary key 1, all five images will be renamed "image_1.jpeg".
Before I introduce a possible solution, here are my models.
Basically, one gear row can have many gear_image rows.
class gear(models.Model):
id = models.AutoField(primary_key=True)
model = models.CharField(max_length=100)
class gear_images(models.Model):
id = models.AutoField(primary_key=True)
gear_id = models.ForeignKey(gear)
image = StdImageField(upload_to='images/gear', blank=True, size=(640, 480, True), thumbnail_size=(100, 100, True))
A Solution
I was thinking of adding a timestamp, in milliseconds and rounded, to the filename.
I'm neither a Python or Django pro but I poked around in django-stdimage2's fields.py file and I think I located the code I need to edit to make this work.
The two lines of code that are commented out are my proposed solutions:
def _rename_resize_image(self, instance=None, **kwargs):
'''
Renames the image, and calls methods to resize and create the thumbnail
'''
if not kwargs.get('raw', None):
if getattr(instance, self.name):
filename = getattr(instance, self.name).path
ext = os.path.splitext(filename)[1].lower().replace('jpg', 'jpeg')
# time_stamp = int(round(time.time() * 1000))
# dst = self.generate_filename(instance, '%s_%s_%s%s' % (self.name, instance._get_pk_val(), time_stamp, ext))
dst = self.generate_filename(instance, '%s_%s%s' % (self.name, instance._get_pk_val(), ext))
dst_fullpath = os.path.join(settings.MEDIA_ROOT, dst)
if os.path.normcase(os.path.abspath(filename)) != os.path.normcase(os.path.abspath(dst_fullpath)):
os.rename(filename, dst_fullpath)
if self.size:
self._resize_image(dst_fullpath, self.size)
if self.thumbnail_size:
thumbnail_filename = self._get_thumbnail_filename(dst_fullpath)
shutil.copyfile(dst_fullpath, thumbnail_filename)
self._resize_image(thumbnail_filename, self.thumbnail_size)
setattr(instance, self.attname, dst)
instance.save()
Each image name would look something like: image_1_159753456.jpeg
Do you think this is a good work-around? I am open to other ideas also.
Thank you :)
I do not think you have a problem here at all.
django-stdimage{2} will rename to the object id of your gear_images model, not its parent gear model. So one gear can have many images, each will have the gear_images pk appended to the filename.
So really, you only have a problem if it's important to you to use the gear model's pk in the filename instead of the gear_images pk. If you're ok with the latter, then you don't need to do anything, it should just work.