Django - Uploaded image gets saved twice: under original and validated name - django

I'm trying to solve a problem.
On image file upload, I want to rename the file and resize it. I came up with a method below.
I managed to get the image resized and saved under a valid name.
Unfortunately the file gets saved twice, also under the invalid name. And the invalid one is stored in my object. How can I fix this? Thanks in advance
class SparePartImages(models.Model):
sparepart = models.ForeignKey('SparePart', on_delete=models.CASCADE)
image = models.ImageField(upload_to='spare-part/', blank=True, null=True)
def save(self, *args, **kwargs):
super(SparePartImages, self).save(*args, **kwargs)
max_size = settings.IMAGE_MAX_SIZE
file = Image.open(self.image)
(width, height) = file.size
if (width/max_size < height/max_size):
factor = height/max_size
else:
factor = width/max_size
size = (int(width/factor), int(height/factor))
file = file.resize(size, Image.ANTIALIAS)
file.save(removeAccent(self.image.path))

You are calling save twice one with super and other with save to fix it do this:
from io import BytesIO
from django.core.files.uploadedfile import InMemoryUploadedFile
import sys
class SparePartImages(models.Model):
# rest of the code
def save(self, *args, **kwargs):
# rest of the code
# but remove file.save(removeAccent(self.image.path))
output = BytesIO()
file.save(output, format='JPEG', quality=100)
output.seek(0)
self.image = InMemoryUploadedFile(
output ,'ImageField', "%s.jpg" %self.image.name.split('.')[0], 'image/jpeg', sys.getsizeof(output), None
)
super(SparePartImages, self).save(*args, **kwargs)

Related

Django how to check if user uploaded an image file when saving model with ImageField?

I am looking for a way to resize, compress, and optimize the uploaded image when saving an ImageField.
class Image(models.Model):
name = models.CharField(max_length=254, blank=True)
caption = models.TextField(max_length=1000, blank=True)
height = models.IntegerField()
width = models.IntegerField()
image = models.ImageField(upload_to='', height_field='height', width_field='width', storage=S3MediaStorage())
My first thought was to override the model's save() and implement this logic there, but I don't want the resize/compression/optimization to run again if the user doesn't update the image file (i.e. if he only updates name or caption on an existing object and saves it).
What is a proper way to check when a new image file is uploaded to the ImageField, but not when the user only changes another field in the Model, eg. the user updates caption but leaves everything else as-is?
How can the uploaded image file be accessed in code? I.e. what is the variable that contains the actual image file that can be passed to Pillow?
edit: This is unique from the suspected duplicate. I am not asking if the field has changed, because that would always cause false positives. I am asking if the user has uploaded an image file, which I will immediately change (resize/optimize/compress), so if the user immediately downloads his uploaded image he'll find that has a different binary with a randomly generated filename, and therefore comparing the filename or binary are not valid methods to determine if the user is uploading a different image.
Your model could use a different name.
Nevertheless, you can try manipulating the image through a post_save signal (https://docs.djangoproject.com/en/1.9/ref/signals/#post-save)
from PIL import Image
from django.db.models.signals import post_save
#receiver(post_save, sender=Image)
def crop_image(sender, instance, **kwargs):
img = instance.image
original = Image.open(img.src.path)
# ... your code here...
EDIT: Apologies. Jumped the gun a bit. One of your actual problems was to not manipulate the image if it's the same. You can do it on save() like this (UNTESTED):
def save(self, **kwargs):
try:
related_img = Image.objects.get(id=self.id)
if related_img.image != self.image:
crop_me(self.image)
except Image.DoesNotExist:
# object doesn't exist. Passing...
pass
return super(Image, self).save(**kwargs)
def crop_me(img):
original_img = Image.open(img.src.path)
# ... your code here...
EDIT 2: If the name changes you could save the original filename in an helper field
class Image(models.Model):
image = models.ImageField(upload_to='', height_field='height', width_field='width', storage=S3MediaStorage())
__original_image_filename = None
def __init__(self, *args, **kwargs):
super(Image, self).__init__(*args, **kwargs)
self.__original_image_filename = self.image.name
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if self.image.name != self.__original_image_filename:
# name changed - do something here
super(Image, self).save(force_insert, force_update, *args, **kwargs)
self.__original_image_filename = self.image.name
I am modifying another answer on the fly so there could be an error or two. Please check the original answer. There are other methods on that question that could help you.

Resizing and updating image in Django

I'm overriding my save method because I want to resize my image. My problem is obvious in this code, what happens after I save my data is that django starts an endless loop. Of course is because I'm checking if self.vertical is set or not (and is always set). Save method is always called.
I could solve this problem checking if my self.id is none, but the code will work only for new entries, if the user tries to update, nothing changes because the id is not null.
I also tried to pop an false argument to kwargs and after saving, set true, but also didnt work. Someone has an idea how can I solve this?
PS: get_thumbnail is a method of sorl thumbnail plugin.
def save(self, *args, **kwargs):
if self.vertical_poster:
super(Movie, self).save(*args, **kwargs)
resized_vertical = get_thumbnail(self.vertical_poster, "155x240", quality=99)
#save the new resized file
self.vertical_poster.save(resized_vertical.name, ContentFile(resized_vertical.read()), True)
super(Movie, self).save(*args, **kwargs)
Any idea or sample code will be appreciated! Thanks
You can check the image whether it is already in your desired dimensions. I think you can use https://pypi.python.org/pypi/dimensions to make it simple
def save(self, *args, **kwargs):
if self.vertical_poster:
import dimensions
# Set desired dimensions
width = 155
height = 240
image_info = dimensions.dimensions(self.vertical_poster)
if image_info[0] != width and image_info[1] != height:
dimension = '%dx%d' % (width, height)
resized_vertical = get_thumbnail(self.vertical_poster, dimension, quality=99)
#save the new resized file
self.vertical_poster.save(resized_vertical.name, ContentFile(resized_vertical.read()), True)
super(Movie, self).save(*args, **kwargs)
Hope it helps.
If you use ImageField you can do it like this:
def save(self, *args, **kwargs):
if self.vertical_poster.x != 155 or self.vertical_poster.y != 240:
resized_vertical = get_thumbnail(self.vertical_poster, "155x240", quality=99)
# save the new resized file
self.vertical_poster.save(resized_vertical.name, ContentFile(resized_vertical.read()), True)
super(Movie, self).save(*args, **kwargs)
My solution was not override my save method. I did something different:
I decided to use "django-resized". This plugin is compatible with "sorl-thumbnail" and resizes image origin to specified size on the model field, as below:
from django_resized import ResizedImageField
class Movie(models.Model):
....
vertical_poster = ResizedImageField(upload_to="movies", width=155, height=240)
I hope that can help other people.
Link: https://github.com/un1t/django-resized
You can create a mixin and use wherever you want likewise.
mixins.py
from io import BytesIO
from PIL import Image
class ShrinkImageMixin:
def shrink_image(self, field_name, resize_shape):
img: Image = Image.open(getattr(self, field_name))
img.thumbnail(self.get_shrinked_size(field_name, resize_shape), Image.ANTIALIAS)
image_file = BytesIO()
img.save(image_file, 'jpg')
getattr(self, field_name).file = image_file
def get_shrinked_size(self, field_name, resize_shape):
actual_img_width, actual_img_height = getattr(self, field_name).width, getattr(self, field_name).height
ratio = min(resize_shape[0] / actual_img_width, resize_shape[1] / actual_img_height)
return int(actual_img_width * ratio), int(actual_img_height * ratio)
models.py
from .mixins import ShrinkImageMixin
class Article(ShrinkImageMixin, models.Model):
...
background_image = models.ImageField()
...
def save(self, *args, **kwargs):
self.shrink_image('background_image', (750, 375))
super(Article, self).save()
This will shrink your image down to your desired size maintaining the aspect ratio. You just need to provide the image field name in the model and resize_shpe tuple.

Django save override ImageField handling

After the problems I had on this thread, there is still a big problem in my models.py when I'm using the Django Admin. Here is my code (I removed stuff non related to my problem) :
from django.core.files.uploadedfile import InMemoryUploadedFile
from PIL import Image as Img
import StringIO
class Mymodel(models.Model):
photo = models.ImageField(upload_to="photo/", blank=True, null=True)
def save(self, *args, **kwargs):
width = 500
height = 500
size = (width,height)
if self.photo:
image = Img.open(StringIO.StringIO(self.photo.read()))
(imw, imh) = image.size
if (imw>width) or (imh>height) :
image.thumbnail(size, Img.ANTIALIAS)
#If RGBA, convert transparency
if image.mode == "RGBA":
image.load()
background = Img.new("RGB", image.size, (255, 255, 255))
background.paste(image, mask=image.split()[3]) #3 is alpha channel
image=background
output = StringIO.StringIO()
image.save(output, format='JPEG', quality=60)
output.seek(0)
self.photo = InMemoryUploadedFile(output,'ImageField', "%s.jpg" %self.photo_principale.name.split('.')[0], 'image/jpeg', output.len, None)
try:
this = Mymodel.objects.get(id=self.id)
if this.photo != self.photo:
this.photo.delete(save=False)
except: pass # when new photo then we do nothing, normal case
super(Mymodel, self).save(*args, **kwargs)
It works, the files are uploaded, resized and converted to JPEG successfully when needed. The problem, every time I edit it, even when NOT uploading a new image, it creates a new image (for example, I save my model a first time with image "hello.jpg", then I edit it, it'll create a new image called "hello_1.jpg" even if I didn't upload anything).
I thought the try/except block would work when only editing (so no new file upload), but apparently not.
Thanks in advance for the help :)
Final solution, working for me :
from django.core.files.uploadedfile import InMemoryUploadedFile
from PIL import Image as Img
import StringIO
from django.db.models.signals import post_delete
from django.dispatch import receiver
Class Mymodel(models.Model):
photo= models.ImageField(upload_to="photo/", blank=True, null=True)
def save(self, *args, **kwargs):
width = 500
height = 500
size = (width,height)
isSame = False
if self.photo:
try:
this = Mymodel.objects.get(id=self.id)
if this.photo==self.photo :
isSame= True
except: pass # when new photo then we do nothing, normal case
image = Img.open(StringIO.StringIO(self.photo.read()))
(imw, imh) = image.size
if (imw>width) or (imh>height) :
image.thumbnail(size, Img.ANTIALIAS)
#If RGBA, convert transparency
if image.mode == "RGBA":
image.load()
background = Img.new("RGB", image.size, (255, 255, 255))
background.paste(image, mask=image.split()[3]) # 3 is the alpha channel
image=background
output = StringIO.StringIO()
image.save(output, format='JPEG', quality=60)
output.seek(0)
self.photo = InMemoryUploadedFile(output,'ImageField', "%s.jpg" %self.photo.name.split('.')[0], 'image/jpeg', output.len, None)
try:
this = Mymodel.objects.get(id=self.id)
if this.photo==self.photo or isSame :
self.photo=this.photo
else :
this.photo.delete(save=False)
except: pass # when new photo then we do nothing, normal case
super(Mymodel, self).save(*args, **kwargs)
#receiver(post_delete, sender=Mymodel)
def photo_post_delete_handler(sender, **kwargs):
instance = kwargs['instance']
storage, path = instance.photo.storage, instance.photo.path
if (path!='.') and (path!='/') and (path!='photo/') and (path!='photo/.'):
storage.delete(path)
Hope it can help somebody ;)
This Builds on Ralph's answer worked for me for python 3 and django 2
first you must import io:
from io import BytesIO
To resize and add white background where necessary:
def resize_with_white_background(pil_image: Image.Image, desired_width, desired_height):
img_copy = pil_image.copy()
# get proportioned image ie (if image is 200X600 and trying to resize to 100X200
# thumbnail will NOT do this but resize to keep the ratio so it would be 67x200 to maintain the ratio (uses the larger)
# img_copy changed in place (does not create new image)
img_copy.thumbnail((desired_width, desired_height), Image.ANTIALIAS)
# create white background
background = Image.new('RGB', (desired_width, desired_height), (255,255,255))
pixels_to_move_left = int((background.width - img_copy.width) * 0.50) # centered horizontally
pixels_to_move_down = int((background.height - img_copy.height) * 0.50) # centered vertically
# paste image into white background box argument tells where to paste
background.paste(img_copy, box=(pixels_to_move_left, pixels_to_move_down))
return background # this will return the background with img_copy pasted in and will be resized to fit your desired size
To set the resized image to and ImageField create a method in your model:
def set_image(self, desired_width, desired_height):
try:
this = MyModel.objects.get(id=self.id)
except MyModel.DoesNotExist:
pass
else:
# will not resize or set to new image (this avoids setting image every single time you edit and save
if this.image == self.image and (self.image.width, self.image.height) == (desired_width, desired_height):
return
im = Image.open(BytesIO(self.image.read()))
resized_image = resize_with_white_background(
pil_image=im,
desired_width=desired_width,
desired_height=desired_height
)
# output (file like object)
output = BytesIO()
# save image into file-like object
resized_image.save(output, format='JPEG', quality=94)
# get size of file
a_size = output.tell()
# reset to beginning of file-like object
output.seek(0)
self.image.file = InMemoryUploadedFile(
output,
'ImageField',
f"{self.image.name.split('.')[0]}.jpg",
'image/jpeg',
a_size,
None
)
override the save() method of your Model and call the set_image() method before calling the Super().save(*args, **kwargs) method
def save(self, *args, **kwargs):
self.set_image(
desired_width=100, # can be whatever you want
desired_height=200
)
super().save(*args, **kwargs)
Try:
if self.photo.name != '':
or
if self.photo.size > 0:

Django: Validate file type of uploaded file

I have an app that lets people upload files, represented as UploadedFiles. However, I want to make sure that users only upload xml files. I know I can do this using magic, but I don't know where to put this check - I can't put it in the clean function since the file is not yet uploaded when clean runs, as far as I can tell.
Here's the UploadedFile model:
class UploadedFile(models.Model):
"""This represents a file that has been uploaded to the server."""
STATE_UPLOADED = 0
STATE_ANNOTATED = 1
STATE_PROCESSING = 2
STATE_PROCESSED = 4
STATES = (
(STATE_UPLOADED, "Uploaded"),
(STATE_ANNOTATED, "Annotated"),
(STATE_PROCESSING, "Processing"),
(STATE_PROCESSED, "Processed"),
)
status = models.SmallIntegerField(choices=STATES,
default=0, blank=True, null=True)
file = models.FileField(upload_to=settings.XML_ROOT)
project = models.ForeignKey(Project)
def __unicode__(self):
return self.file.name
def name(self):
return os.path.basename(self.file.name)
def save(self, *args, **kwargs):
if not self.status:
self.status = self.STATE_UPLOADED
super(UploadedFile, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
os.remove(self.file.path)
self.file.delete(False)
super(UploadedFile, self).delete(*args, **kwargs)
def get_absolute_url(self):
return u'/upload/projects/%d' % self.id
def clean(self):
if not "XML" in magic.from_file(self.file.url):
raise ValidationError(u'Not an xml file.')
class UploadedFileForm(forms.ModelForm):
class Meta:
model = UploadedFile
exclude = ('project',)
Validating files is a common challenge, so I would like to use a validator:
import magic
from django.utils.deconstruct import deconstructible
from django.template.defaultfilters import filesizeformat
#deconstructible
class FileValidator(object):
error_messages = {
'max_size': ("Ensure this file size is not greater than %(max_size)s."
" Your file size is %(size)s."),
'min_size': ("Ensure this file size is not less than %(min_size)s. "
"Your file size is %(size)s."),
'content_type': "Files of type %(content_type)s are not supported.",
}
def __init__(self, max_size=None, min_size=None, content_types=()):
self.max_size = max_size
self.min_size = min_size
self.content_types = content_types
def __call__(self, data):
if self.max_size is not None and data.size > self.max_size:
params = {
'max_size': filesizeformat(self.max_size),
'size': filesizeformat(data.size),
}
raise ValidationError(self.error_messages['max_size'],
'max_size', params)
if self.min_size is not None and data.size < self.min_size:
params = {
'min_size': filesizeformat(self.min_size),
'size': filesizeformat(data.size)
}
raise ValidationError(self.error_messages['min_size'],
'min_size', params)
if self.content_types:
content_type = magic.from_buffer(data.read(), mime=True)
data.seek(0)
if content_type not in self.content_types:
params = { 'content_type': content_type }
raise ValidationError(self.error_messages['content_type'],
'content_type', params)
def __eq__(self, other):
return (
isinstance(other, FileValidator) and
self.max_size == other.max_size and
self.min_size == other.min_size and
self.content_types == other.content_types
)
Then you can use FileValidator in your models.FileField or forms.FileField as follows:
validate_file = FileValidator(max_size=1024 * 100,
content_types=('application/xml',))
file = models.FileField(upload_to=settings.XML_ROOT,
validators=[validate_file])
From django 1.11, you can also use FileExtensionValidator.
from django.core.validators import FileExtensionValidator
class UploadedFile(models.Model):
file = models.FileField(upload_to=settings.XML_ROOT,
validators=[FileExtensionValidator(allowed_extensions=['xml'])])
Note this must be used on a FileField and won't work on a CharField (for example), since the validator validates on value.name.
ref: https://docs.djangoproject.com/en/dev/ref/validators/#fileextensionvalidator
For posterity: the solution is to use the read method and pass that to magic.from_buffer.
class UploadedFileForm(ModelForm):
def clean_file(self):
file = self.cleaned_data.get("file", False)
filetype = magic.from_buffer(file.read())
if not "XML" in filetype:
raise ValidationError("File is not XML.")
return file
class Meta:
model = models.UploadedFile
exclude = ('project',)
I think what you want to do is to clean the uploaded file in Django's Form.clean_your_field_name_here() methods - the data is available on your system by then if it was submitted as normal HTTP POST request.
Also if you consider this inefficient explore the options of different Django file upload backends and how to do streaming processing.
If you need to consider the security of the system when dealing with uploads
Make sure uploaded file has correct extension
Make sure the mimetype matches the file extension
In the case you are worried about user's uploading exploit files (for attacking against your site)
Rewrite all the file contents on save to get rid of possible extra (exploit) payload (so you cannot embed HTML in XML which the browser would interpret as a site-origin HTML file when downloading)
Make sure you use content-disposition header on download
Some more info here: http://opensourcehacker.com/2013/07/31/secure-user-uploads-and-exploiting-served-user-content/
Below is my example how I sanitize the uploaded images:
class Example(models.Model):
image = models.ImageField(upload_to=filename_gen("participant-images/"), blank=True, null=True)
class Example(forms.ModelForm):
def clean_image(self):
""" Clean the uploaded image attachemnt.
"""
image = self.cleaned_data.get('image', False)
utils.ensure_safe_user_image(image)
return image
def ensure_safe_user_image(image):
""" Perform various checks to sanitize user uploaded image data.
Checks that image was valid header, then
:param: InMemoryUploadedFile instance (Django form field value)
:raise: ValidationError in the case the image content has issues
"""
if not image:
return
assert isinstance(image, InMemoryUploadedFile), "Image rewrite has been only tested on in-memory upload backend"
# Make sure the image is not too big, so that PIL trashes the server
if image:
if image._size > 4*1024*1024:
raise ValidationError("Image file too large - the limit is 4 megabytes")
# Then do header peak what the image claims
image.file.seek(0)
mime = magic.from_buffer(image.file.getvalue(), mime=True)
if mime not in ("image/png", "image/jpeg"):
raise ValidationError("Image is not valid. Please upload a JPEG or PNG image.")
doc_type = mime.split("/")[-1].upper()
# Read data from cStringIO instance
image.file.seek(0)
pil_image = Image.open(image.file)
# Rewrite the image contents in the memory
# (bails out with exception on bad data)
buf = StringIO()
pil_image.thumbnail((2048, 2048), Image.ANTIALIAS)
pil_image.save(buf, doc_type)
image.file = buf
# Make sure the image has valid extension (can't upload .htm image)
extension = unicode(doc_type.lower())
if not image.name.endswith(u".%s" % extension):
image.name = image.name + u"." + extension
I found an interesting package who can do upload file validation recently. You can see the package here. the package approach is similar with sultan answer, thus we can just implement it right away.
from upload_validator import FileTypeValidator
validator = FileTypeValidator(
allowed_types=['application/msword'],
allowed_extensions=['.doc', '.docx']
)
file_resource = open('sample.doc')
# ValidationError will be raised in case of invalid type or extension
validator(file_resource)

Django PIL error with reading file using S3

Django newbie :)
I'm using S3 storage via the package django-storages. This appears to work perfect when I upload/update a new image via the admin.
models.py (image field)
image = models.ImageField(
upload_to=path_and_rename("profiles"),
height_field="image_height",
width_field="image_width",
null=True,
blank=True,
editable=True,
help_text="Profile Picture",
verbose_name="Profile Picture"
)
image_height = models.PositiveIntegerField(null=True, blank=True, editable=False, default="100")
image_width = models.PositiveIntegerField(null=True, blank=True, editable=False, default="100")
I then decided I wanted to resize the image upon upload so try by adding the following code on save override method...
def save(self, *args, **kwargs):
if not self.id and not self.image:
return
super(Profile, self).save(*args, **kwargs)
image = Image.open(self.image).seek(0)
(width, height) = image.size
size = ( 100, 100)
image = image.resize(size, Image.ANTIALIAS)
image.save(self.image.path)
Here is the problem, this gave the following error....
cannot identify image file
I then posted a question on stack yesterday (which I deleted) and a user linked to this answer Django PIL : IOError Cannot identify image file which I sorta understand (because the image has not uploaded it cannot read it yet). But I'm not sure that that is my issue! When I get the error cannot identify image file I can see the original file has actually been uploaded to S3 (without the resize of course).
Remembering I'm a newbie can anyone modify my example save method (and explain) with a way to resolve this issue? i.e. a way to rezise a new image to 100x100 on upload?
Many thanks
Use the storage to read the file if its already written then resize....
def save(self, *args, **kwargs):
if not self.id and not self.image:
return
super(Profile, self).save(*args, **kwargs)
import urllib2 as urllib
from cStringIO import StringIO
from django.core.files.uploadedfile import SimpleUploadedFile
'''Open original photo which we want to resize using PIL's Image object'''
img_file = urllib.urlopen(self.image.url)
im = StringIO(img_file.read())
resized_image = Image.open(im)
'''Convert to RGB if necessary'''
if resized_image.mode not in ('L', 'RGB'):
resized_image = resized_image.convert('RGB')
'''We use our PIL Image object to create the resized image, which already
has a thumbnail() convenicne method that constrains proportions.
Additionally, we use Image.ANTIALIAS to make the image look better.
Without antialiasing the image pattern artificats may reulst.'''
resized_image.thumbnail((100,100), Image.ANTIALIAS)
'''Save the resized image'''
temp_handle = StringIO()
resized_image.save(temp_handle, 'jpeg')
temp_handle.seek(0)
''' Save to the image field'''
suf = SimpleUploadedFile(os.path.split(self.image.name)[-1].split('.')[0],
temp_handle.read(), content_type='image/jpeg')
self.image.save('%s.jpg' % suf.name, suf, save=True)
If you expect
image.save(self.image.path)
to work. Shouldn't you open it with
image = Image.open(self.image.path).seek(0)
?