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.
Related
I'm trying to delete all occurences of a certain key in a JSON-Field when a certain key is deleted.
What I've been trying is just popping all occurences of the given key in the json-field. However, saving a JSONField with a popped key doesn't seem to work - the data isn't changed on the Element-objects. Is there a way to do this?
class Element(models.Model):
data = models.JSONField(default=dict, blank=True)
class Key(moedls.Model):
[...]
def delete(self, *args, **kwargs):
to_update = Element.objects.filter(data__has_key=self.slug)
for element in to_update:
element.data.pop(self.slug)
GraphElement.objects.bulk_update(to_update, ["data"])
super().delete(*args, **kwargs)
Edit: I just realized that this code actually seems to work - but just sometimes.
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
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.
Model definitions:
class Footprint(models.Model)
date = models.DateTimeField(auto_now = True)
class Stuff(Footprint):
name = models.CharField(max_length = 255)
some_other_field = models.CharField(max_length = 255)
In a Stuff object, I'd like to only update the name field, and leave all the other fields unchanged, except those defined in the associated Footprint object.
Fields in the Footprint object are correctly updated if I don't use update_fields:
s = Stuff.objects.get(pk = 1)
s.name = 'Alexander'
s.save()
s.date # is correctly set
But if I specify fields to update, the associated Footprint is not not even saved.
s = Stuff.objects.get(pk = 1)
s.name = 'Tim'
s.save(update_fields = ['name'])
s.date # unfortunately, remains unchanged!!
I have to use update_fields to avoid interferences between several scripts.
At the same time, I'd like to always keep track of the last modification, defined by the "Footprint" object (it contains the last modification date, as well as several other fields; their update is triggered by a custom save() method).
Is there a way to force a call to Footprint.save() even if update_fields doesn't contain any field from Footprint?
Instead of rewriting the save() method, the other possibility is to add all the fields with the auto_now option to the update parameter. Like this:
some_object.save(update_fields = ['updated_field', 'auto_now_date']
It was enough to simply rewrite the Footprint model definition like this:
class Footprint(models.Model)
date = models.DateTimeField(auto_now = True)
def save(self, *args, **kwargs):
if kwargs.has_key('update_fields'):
kwargs['update_fields'] = list(set(list(kwargs['update_fields']) + ['date']))
return super(Footprint, self).save(*args, **kwargs)
Of course, if you have more fields to update than just a date field, you just have to append them in the list.
You can do this by changing your .save() method. Now, I now sure what it is that you want to do exactly, so I will leave you a template, since I believe wanting to have your own way of saving changes is indeed what do yo want.
In your class add this function definition:
def save(self, *args, **kwargs):
# Do something here, whatever it is that you want
return super(<YourClassName>, self).save(self, *args, **kwargs)
This keep the features of the normal save() function unchanged and add your custom changes to the changes you want to make as well.
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.