Django Resize Image Before Saving - python

Goal: Upload a resized image (with same file name and aspect ratio) to AWS S3.
Problem: Currently upon saving, the original image is uploaded and not the resized one.
What have I tried?: I've tried multiple different ways to accomplish this but I run into various issues such as not the correct aspect ratio, poor image quality (when using django-resize) etc. The code below seems really close but I just can't seem to find where I am going wrong.
models.py
class Profile(BaseModel):
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True)
image = models.ImageField(default='default.jpg', upload_to='profile_pics')
def save(self, commit=True, *args, **kwargs): #Edited
if commit:
img = Image.open(self.image)
if img.height > 300 or img.width > 300:
output_size = (300, 300)
img.thumbnail(output_size)
img.save(self.image.name, optimize=True, quality=100)
super().save()

Solution:
After a very long time I finally found the answer in this blog.
In the end I made a new function in the users/utils.py file:
from django.core.files import File
from pathlib import Path
from PIL import Image
from io import BytesIO
image_types = {
"jpg": "JPEG",
"jpeg": "JPEG",
"png": "PNG",
"gif": "GIF",
"tif": "TIFF",
"tiff": "TIFF",
}
def image_resize(image, width, height):
# Open the image using Pillow
img = Image.open(image)
# check if either the width or height is greater than the max
if img.width > width or img.height > height:
output_size = (width, height)
# Create a new resized “thumbnail” version of the image with Pillow
img.thumbnail(output_size)
# Find the file name of the image
img_filename = Path(image.file.name).name
# Spilt the filename on “.” to get the file extension only
img_suffix = Path(image.file.name).name.split(".")[-1]
# Use the file extension to determine the file type from the image_types dictionary
img_format = image_types[img_suffix]
# Save the resized image into the buffer, noting the correct file type
buffer = BytesIO()
img.save(buffer, format=img_format)
# Wrap the buffer in File object
file_object = File(buffer)
# Save the new resized file as usual, which will save to S3 using django-storages
image.save(img_filename, file_object)
and then overwrote the save() function in the models.py:
models.py
from users.utils import image_resize
class Profile(BaseModel):
#some other fields
image = models.ImageField(default='default.jpg', upload_to='profile_pics')
def save(self, commit=True, *args, **kwargs):
if commit:
image_resize(self.image, 250, 250)
super().save(*args, **kwargs)

Related

Django Value Error "seek of closed file" using PIL for image resize when updating a record

I have the following model:
class Players(models.Model):
team = models.ForeignKey(Teams, verbose_name=_('Team'), on_delete=models.CASCADE)
player_name = models.CharField(_('Player Name'),max_length=200)
player_description = models.TextField(_('Player Description'))
player_image = models.ImageField(_('Profile Pic'),upload_to='upload/player_image', null=True, blank=True)
player_social = models.CharField(_('Social Media Tag'),max_length=200)
class Meta:
verbose_name = _("Players")
verbose_name_plural = _("Players")
def __str__(self):
return self.team.team_name + ' - ' + self.player_name
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.player_image:
image_resize(self.player_image, 250)
The last function will call another for image resizing taking the file and an expected max width:
# Use PIL to resize images; set target width on each
def image_resize(image, tgt_width):
img = PIL.Image.open(image)
img.load()
width, height = img.size
target_width = tgt_width
h_coefficient = width/tgt_width
target_height = height/h_coefficient
img = img.resize((int(target_width), int(target_height)), PIL.Image.ANTIALIAS)
img.save(image.path, quality=100)
img.close()
image.close()
My view for updating is as follows:
#method_decorator(superuser_required, name='dispatch')
class PlayerUpdateView(UpdateView):
model = Players
template_name = 'scoreboard/players/player_update.html'
form_class = PlayersForm
context_object_name: str = 'player'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
team_id = Players.objects.get(pk=self.kwargs['pk']).team.id
context["team"] = Teams.objects.get(pk=team_id)
return context
def form_valid(self, form):
form.save()
return super().form_valid(form)
def get_success_url(self):
team_id = Players.objects.get(pk=self.kwargs['pk']).team.id
return reverse_lazy('team_detail', kwargs={'pk': team_id})
It doesn't matter if I provide a new image file or not, I get the same "seek of closed file" error:
File "D:\00_www\hts-score\overlay\scoreboard\models.py", line 62, in save
image_resize(self.player_image, 250)
File "D:\00_www\hts-score\overlay\scoreboard\models.py", line 15, in image_resize
img = PIL.Image.open(image)
File "D:\00_www\hts-score\venv\lib\site-packages\PIL\Image.py", line 3096, in open
fp.seek(0)
ValueError: seek of closed file
How can I get the image to be processed anyway? What am I missing?
I tried adding the if method so in case there is no file, it won't trigger the resize function. I was expecting the function to check for the presence of an image file, then process it. Adding img.load() doesn't help.
So, after a few tries I found this answer. I'm not very sure why it works, but it seems like the with method opens and closes the file correctly. Found this on PIL Documentation. The image_resize() function will look like this:
def image_resize(image, tgt_width):
with Image.open(image) as img:
width, height = img.size
ratio = width / height
tgt_height = int(tgt_width / ratio)
img = img.resize((tgt_width, tgt_height), Image.ANTIALIAS)
img.save(image.path)
With the if condition at the save method, it works every time.

Unable to resize image before uploading to Google Storage

I have the following code with reference to https://github.com/jschneier/django-storages/issues/661, but it doesnt work, the default unsized image is being stored.
models.py
class Product(models.Model):
name=models.CharField(max_length=100)
image=models.ImageField(default='default.jpg',upload_to='productimages')
price=models.FloatField()
def generate_thumbnail(self,src):
image = Image.open(src) # in memory
image.thumbnail((400,300), Image.ANTIALIAS)
buffer = BytesIO()
image.save(buffer, 'JPEG')
file_name = os.path.join(settings.MEDIA_ROOT, self.image.name)
temp_file = InMemoryUploadedFile(buffer, None, file_name, 'image/jpeg', len(buffer.getbuffer()), None)
return temp_file
def save(self, *args, **kwargs):
self.image=self.generate_thumbnail(self.image)
print(self.image.width) #prints the original size
print(self.image.height)
super(Product,self).save(*args, **kwargs)
if the PIL library is not working then you might use OpenCV as a workaround
import cv2
image = cv2.imread(<your Image name>)
cv2.resize(image, size=(600, 600))
cv2.imshow(image)
cv2.imwrite(filename='resized image.png/.jpg', image)

How to validate of dimensions in default ImageField?

Have a following rule in the project: the image field is optional and a default image is an uninformed case for the user. Images are sent by the user in django and must have a dimension (width> = 900, height> = 400).
I am trying to validate the dimensions in admin.py, but it is giving problem when I try to register when it has default argument in imagefield.
Gives an image error not found even though it is in the directory. Without the validation function in admin.py it works normally.
models.py
class Event(models.Model):
banner = models.ImageField('banner', upload_to='events/banners', default='events/banners/banner_padrao_eventos.png', blank=True)
admin.py
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = '__all__'
def clean_banner(self):
banner = self.cleaned_data.get('banner')
if banner:
img = Image.open(banner)
width, height = img.size
max_width = 900
max_height = 400
if width < max_width or height < max_height:
raise forms.ValidationError(
'Image is incorrectly sized:% s x% s pixels. Please insert an image with% s x% s pixels.'
% (width, height, max_width, max_height))
if len(banner) > (3 * 1024 * 1024):
raise forms.ValidationError('Very large image file (maximum of 3MB).')
name_img, ext = banner.name.split('.')
if not (ext.lower() in ['png', 'jpg', 'jpeg']):
raise forms.ValidationError('Please use the image in JPG, JPEG or PNG format.')
else:
raise forms.ValidationError('The loaded image could not be read.')
return banner
How can I make the default image to be considered in the validation?
The file uploaded is Django InMemoryUploadedFile file it's not saved in your server yet, open() needs that the file exists physically or as string bytes. To do that you need the package PIL.Image and io.BytesIO
from io import BytesIO as StringIO
from PIL import Image
img = Image.open(StringIO(banner.read()))
So now, img.size is a tuple containing your width and height
width, height = img.size

Custom save method for images/avatars in Django - how to use

So, I'm trying to do a little bit of compression of images as well as some other operations. I had a few questions...I have the following save() method for my user class:
class User(AbstractBaseUser, PermissionsMixin):
...
avatar = models.ImageField(storage=SITE_UPLOAD_LOC, null=True, blank=True)
def save(self, *args, **kwargs):
if self.avatar:
img = Img.open(BytesIO(self.avatar.read()))
if img.mode != 'RGB':
img = img.convert('RGB')
new_width = 200
img.thumbnail((new_width, new_width * self.avatar.height / self.avatar.width), Img.ANTIALIAS)
output = BytesIO()
img.save(output, format='JPEG', quality=70)
output.seek(0)
self.avatar= InMemoryUploadedFile(file=output, field_name='ImageField', name="%s.jpg" % self.avatar.name.split('.')[0], content_type='image/jpeg', size=, charset=None)
super(User, self).save(*args, **kwargs)
I had two questions:
Best way for deleting the old avatar file on save, if a previous avatar exists
What do I pass into InMemoryUploadedFile for the size kwarg? Is this the size of the file? in what unit/(s)?
You need to get file size. Try this:
import os
size = os.fstat(output.fileno()).st_size
You can read this for how to get file size:
https://docs.python.org/3.0/library/stat.html
and for deleting old avtar. According to your code it is foreign key hence before saving you can check if avtar is already exists and so yoy can delete it.
Add this lines code after output.seek(0) this line:
if self.avtar:
self.avtar.delete()
What you want is the size in bytes.
You can get the byte size of a BytesIO object like this.
size = len(output.getvalue())

Save image created via PIL to django model

I have successfully created and rotated an image that was uploaded via email to a directory on my server using the following code:
image = ContentFile(b64decode(part.get_payload()))
im = Image.open(image)
tempfile = im.rotate(90)
tempfile.save("/srv/www/mysite.com/public_html/media/images/rotate.jpg", "JPEG")
img = Photo(user=user)
img.img.save('rotate.jpg', tempfile)
img.save()
The rotated image exists in the directory, however when I try to add that image to my model, it is not saving. What am I missing? Any help would be greatly appreciated.
I solved the issue with the following code:
image = ContentFile(b64decode(part.get_payload()))
im = Image.open(image)
tempfile = im.rotate(270)
tempfile_io =StringIO.StringIO()
tempfile.save(tempfile_io, format='JPEG')
image_file = InMemoryUploadedFile(tempfile_io, None, 'rotate.jpg','image/jpeg',tempfile_io.len, None)
img = Photo(user=user)
img.img.save('rotate.jpg', image_file)
img.save()
I found the answer here How do you convert a PIL `Image` to a Django `File`?. Works flawlessly!!!
from django.db import models
from .abstract_classes import DateTimeCreatedModified
from PIL import Image
from io import BytesIO
from django.core.files.base import ContentFile
from enum import Enum
class MediaSize(Enum):
# size base file name
DEFAULT = ((1080, 1080), 'bus_default')
MEDIUM = ((612, 612), 'bus_medium')
THUMBNAIL = ((161, 161), 'bus_thumbnail')
class Media(DateTimeCreatedModified):
# The original image.
original = models.ImageField()
# Resized images...
default = models.ImageField()
medium = models.ImageField()
thumbnail = models.ImageField()
def resizeImg(self, img_size):
img: Image.Image = Image.open(self.original)
img.thumbnail(img_size, Image.ANTIALIAS)
outputIO = BytesIO()
img.save(outputIO, format=img.format, quality=100)
return outputIO, img
def handleResize(self, mediaSize: MediaSize):
imgSize = mediaSize.value[0]
imgName = mediaSize.value[1]
outputIO, img = self.resizeImg(imgSize)
return {
# Files aren't be overwritten
# because `AWS_S3_FILE_OVERWRITE = False`
# is set in `settings.py`.
'name': f'{imgName}.{img.format}',
'content': ContentFile(outputIO.getvalue()),
'save': False,
}
def save(self, **kwargs):
if not self.default:
self.default.save(
**self.handleResize(MediaSize.DEFAULT)
)
if not self.medium:
self.medium.save(
**self.handleResize(MediaSize.MEDIUM)
)
if not self.thumbnail:
self.thumbnail.save(
**self.handleResize(MediaSize.THUMBNAIL)
)
super().save(**kwargs)

Categories

Resources