Uploading image to ImageField from BytesIO - python

I want upload a generated image directly in my model save without have to save it to file first.
Model:
avatar = models.ImageField(upload_to="img/", null=True, blank=True)
def save(self, *args, **kwargs):
from .generate_avatar import Avatar
self.avatar = Avatar.generate(128, self.display_name, "PNG")
This is my generate_avatar class.
def generate(cls, size, string, filetype="JPEG"):
"""
Generates a squared avatar with random background color.
:param size: size of the avatar, in pixels
:param string: string to be used to print text and seed the random
:param filetype: the file format of the image (i.e. JPEG, PNG)
"""
render_size = max(size, Avatar.MIN_RENDER_SIZE)
image = Image.new('RGB', (render_size, render_size),
cls._background_color(string))
draw = ImageDraw.Draw(image)
font = cls._font(render_size)
text = cls._text(string)
draw.text(cls._text_position(render_size, text, font),
text,
fill=cls.FONT_COLOR,
font=font)
stream = BytesIO()
image = image.resize((size, size), Image.ANTIALIAS)
image.save(stream, format=filetype, optimize=True)
return stream.seek(0)
However, this does not work, no error just saves 0. Why?

To "simmulate" a file upload in django take a look at django's SimpleUploadedFile and do something like this :
from django.core.files.uploadedfile import SimpleUploadedFile
def save(self, *args, **kwargs)
avatar = Avatar.generate(128, self.display_name, "PNG")
self.avatar = SimpleUploadedFile(avatar.name, avatar.read())

Related

Django avoiding infinite save() loop, with ImageField saved using custom model method

I want to make my model call save_image() everytime user creates new Post model.
I tried to override save() method, but it causes infinite loop (because save_image() calls self.image.save(). ).
I tried disabling signals, but they did nothing.
I wanted to add something like #receiver(post_save, sender=Post) for the save_image, and remove override of save() (I don't even know if it will solve problem with infinite loop).
The main problem here is that, save_post() MUST be called after standard save (because it needs Post.pk, Post.video). If you have any ideas, please help me.
Here's my code:
class Post(BaseModel):
owner = models.ForeignKey(User, on_delete=models.CASCADE)
# Other fields
video = models.FileField(upload_to='post_videos/', default='defaults/video.mp4')
image = models.ImageField(upload_to='post_images', default='defaults/post_image.png')
def save(self, *args, **kwargs):
super(Post, self).save(*args, **kwargs)
self.save_image()
#receiver(post_save, sender=Post)
def save_image(self):
# Setting self.image to some frame of Video
cap = cv2.VideoCapture(self.video.path)
video_length = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - 1
if cap.isOpened() and video_length > 0:
success, image = cap.read()
if success:
rescaled = (255.0 / image.max() * (image - image.min())).astype(np.uint8)
PILimg = Image.fromarray(rescaled)
filename = "postimage"+str(self.pk)+".png"
f = BytesIO()
try:
PILimg.save(f, format='png')
s = f.getvalue()
self.image.save(filename, ContentFile(s))
finally:
f.close()
I found it!
I had to add save=False to self.image.save(filename, ContentFile(s)). If you had similiar issue, you may also want to save whole model again (calling super(Post, self).save(*args, **kwargs) again).
I had a similar issue.. I was creating a Reservation Confirmation pdf object by catching a post_save signal from Reservation model and ended up in an infinite loop when saving the pdf.
Below is the correct version of the code. The bug was in the last line which previously was:
self.saved_file.save(File(file))
class ReservationConfrimation(models.Model):
reservation = models.OneToOneField(Reservation, on_delete=models.CASCADE)
saved_file = models.FileField(null=True, upload_to="confirmations/")
def save(self, *args, **kwargs):
self._create_pdf(self.reservation)
super().save(*args, **kwargs)
def _create_pdf(self, reservation):
# buffer = io.BytesIO()
file = io.BytesIO()
c = canvas.Canvas(file, pagesize=letter, bottomup=1, verbosity=0)
# w,h = letter
c.line(0, 700, 612, 700)
c.line(0, 100, 612, 100)
c.drawString(306, 690, f"RESERVATION: {reservation.reservation_number}")
c.save()
file.name = f"{reservation.reservation_number}.pdf"
self.saved_file = File(file)

django-imagekit compress image if image size is GT 300KB

I have managed to install (after a lot of effort) django-imagekit and I am now able to use django-imagekit to compress the file size of uploaded images.
I can upload an image of 6MB and django-imagekit will compress the image to 230KB when I use a quality of 10 (see below).
Is there a way to use a different file compression (django-imagekit refers to this as quality) when the uploaded image is a size of 300Kb, 1MB , 2MB, 3MB or larger (I am thinking an if/elseif/else statement that would confirm the size of the image and apply a lower quality the larger the size (KB) of the image? The file compression of 10 works well for larger sized images but radically degrades the quality of the image for smaller sized images for example 25Kb.
I am not even sure how I would write the code and where I would place the code that would achieve this. So any help would be appreciated.
Here is my relevant models.py file code:
from imagekit.processors import Adjust, ResizeToFill
from imagekit.models import ProcessedImageField
class NameDetails(models.Model, FillableModelWithLanguageVersion):
user = models.ForeignKey(User)
....
#name_details_photograph = models.ImageField(upload_to=_get_name_details_photograph_upload_location, null=True, blank=True)
name_details_photograph = ProcessedImageField(upload_to=_get_name_details_photograph_upload_location, null=True, blank=True, options={'quality': 25}, processors=[Adjust(sharpness=1.1),])
....
def __unicode__(self):
return unicode(self.user)
EDIT:
I have tried to implement the form field version of the ProcessedImageField class, but this does not upload the image.
Here is the forms code I have tried while changing the models.py code back to a image field (that is commented out above):
from imagekit.forms import ProcessedImageField
from imagekit.processors import Adjust, ResizeToFill
class NameDetailsForm(forms.ModelForm):
def __init__(self, available_languages, language_preference, file_required, *args, **kwargs):
"""
available_languages should be a valid choices list
"""
super(NameDetailsForm, self).__init__(*args, **kwargs)
self.fields['language_code'] = forms.ChoiceField(choices=available_languages, initial=language_preference, label=_('Language'),)
#self.fields['name_details_photograph'] = forms.FileField(label=_('Photograph'), required=file_required)
self.fields['name_details_photograph'] = ProcessedImageField(label=_('Photograph'), required=file_required, spec_id='myapp:profile:name_details_photograph', options={'quality': 25}, processors=[Adjust(sharpness=1.1),])
class Meta:
model = NameDetails
The solution I'm gonna offer is totally untested. It's based on the source code of the imagekit library.
The options kwarg is used by the ImageSpec class to passe it to PIL's Image.save().
So for a dynamic options you can create your own Spec class defining options as a property and use the getter to return an on-the-fly options. Something like:
from imagekit import ImageSpec
from imagekit.processors import Adjust, ResizeToFill
class ThumbnailSpec(ImageSpec):
format = 'JPEG'
options={'quality': 50}
processors=[Adjust(sharpness=1.1),]
#property
def options(self):
options = self._options
#You can create some checks here and choose to change the options
#you can access the file with self.source
if self.source.size > 2*100*100:
options['quality'] -= 25
return options
#options.setter
def options(self, value):
self._options = value
Finally use your ThumbnailSpec by passing it to the ProcessedImageField
name_details_photograph = ProcessedImageField(
upload_to=_get_name_details_photograph_upload_location,
null=True,
blank=True,
spec=ThumbnailSpec
)
You can create a custom processor with django imagekit and then use it in your model. The processor will check the size of the image and then return the edited image. Something like this -
class ConditionalResizer(object):
min_size = None # minimum size to reduce in KB
def __init__(self, *args, min_size=1000, **kwargs):
super().__init__(self, *args, **kwargs) # code is for python > 3.0, modify for python < 3.0 as necessary
self.min_size = min_size
def process(self, image):
size = # code to get size of image
if size > self.min_size:
# process the image
image = # processed image
return image
Then in your Form, add the processor -
from imagekit.forms import ProcessedImageField
from imagekit.processors import Adjust, ResizeToFill
class NameDetailsForm(forms.ModelForm):
def __init__(self, available_languages, language_preference, file_required, *args, **kwargs):
"""
available_languages should be a valid choices list
"""
super(NameDetailsForm, self).__init__(*args, **kwargs)
self.fields['language_code'] = forms.ChoiceField(choices=available_languages, initial=language_preference, label=_('Language'),)
#self.fields['name_details_photograph'] = forms.FileField(label=_('Photograph'), required=file_required)
self.fields['name_details_photograph'] = ProcessedImageField(label=_('Photograph'), required=file_required, spec_id='myapp:profile:name_details_photograph', options={'quality': 25}, processors=[Adjust(sharpness=1.1), ConditionalResize(min_size=1000)])
class Meta:
model = NameDetails
I haven't tested this code yet, but should be able to solve your problem. Let me know if it does not.
You can find more about processors here - https://django-imagekit.readthedocs.org/en/latest/#processors

Save an image generated with wand to django ImageField

I'm trying to generate a preview for an "overlay" config stored in a django model than will be applied later to other model. I have not much experience manipulating files with python... =(
Here is my code:
import io
from django.conf import settings
from django.db import models
from wand.image import Image
from PIL.ImageFile import ImageFile, Parser, Image as PilImage
class Overlay(models.Model):
RELATIVE_POSITIONS = (...)
SIZE_MODES = (...)
name = models.CharField(max_length=50)
source = models.FileField(upload_to='overlays/%Y/%m/%d')
sample = models.ImageField(upload_to='overlay_samples/%Y/%m/%d', blank=True)
px = models.SmallIntegerField(default=0)
py = models.SmallIntegerField(default=0)
position = models.CharField(max_length=2, choices=RELATIVE_POSITIONS)
width = models.SmallIntegerField(default=0)
height = models.SmallIntegerField(default=0)
size_mode = models.CharField(max_length=1, choices=SIZE_MODES, default='B')
last_edit = models.DateTimeField(auto_now=True)
def generate_sample(self):
"""
Generates the sample image and saves it in the "sample" field model
:return: void
"""
base_pic = Image(filename=os.path.join(settings.BASE_DIR, 'girl.jpg'))
overlay_pic = Image(file=self.source)
result_pic = io.BytesIO()
pil_parser = Parser()
if self.width or self.height:
resize_args = {}
if self.width:
resize_args['width'] = self.width
if self.height:
resize_args['height'] = self.height
overlay_pic.resize(**resize_args)
base_pic.composite(overlay_pic, self.px, self.py)
base_pic.save(file=result_pic)
result_pic.seek(0)
while True:
s = result_pic.read(1024)
if not s:
break
pil_parser.feed(s)
pil_result_pic = pil_parser.close()
self.sample.save(self.name, pil_result_pic, False)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.generate_sample()
super(Overlay, self).save(force_insert, force_update, using, update_fields)
But i'm getting AttributeError read here is part on my django debug data:
/usr/local/lib/python2.7/dist-packages/django/core/files/utils.py in <lambda>
"""
encoding = property(lambda self: self.file.encoding)
fileno = property(lambda self: self.file.fileno)
flush = property(lambda self: self.file.flush)
isatty = property(lambda self: self.file.isatty)
newlines = property(lambda self: self.file.newlines)
read = property(lambda self: self.file.read)
readinto = property(lambda self: self.file.readinto)
readline = property(lambda self: self.file.readline)
readlines = property(lambda self: self.file.readlines)
seek = property(lambda self: self.file.seek)
softspace = property(lambda self: self.file.softspace)
tell = property(lambda self: self.file.tell)
▼ Local vars
Variable Value
self <File: None>
/usr/local/lib/python2.7/dist-packages/PIL/Image.py in __getattr__
# numpy array interface support
new = {}
shape, typestr = _conv_type_shape(self)
new['shape'] = shape
new['typestr'] = typestr
new['data'] = self.tobytes()
return new
raise AttributeError(name)
def __getstate__(self):
return [
self.info,
self.mode,
self.size,
▼ Local vars
Variable Value
self <PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=1080x1618 at 0x7F1429291248>
name 'read'
What's wrong?
Solved!
Such as #Alexey Kuleshevich was saying django FileField need a Fileobjeto, but what was missing is that we must first save the image to a file on disk or in memory, as will guess it's better memory... so here is the final solution. I think it could be improved to not use two step "conversion"
from django.core.files.base import ContentFile
and within the method:
result_pic = io.BytesIO()
pil_parser = Parser()
...
overlay_pic.resize(**resize_args)
base_pic.composite(overlay_pic, self.px, self.py)
base_pic.save(file=result_pic)
result_pic.seek(0)
while True:
s = result_pic.read(1024)
if not s:
break
pil_parser.feed(s)
result_pic = io.BytesIO()
pil_result_pic = pil_parser.close()
pil_result_pic.save(result_pic, format='JPEG')
django_file = ContentFile(result_pic.getvalue())
self.sample.save(self.name, django_file, False)
Thanks to this answer:
How do you convert a PIL Image to a Django File?
Whenever you save a file to ImageField or FileField you need to make sure it is Django's File object. Here the reference to documentation: https://docs.djangoproject.com/en/1.7/ref/models/fields/#filefield-and-fieldfile
from django.core.files import File
and within a method:
def generate_sample(self):
...
pil_result_pic = pil_parser.close()
self.sample.save(self.name, File(pil_result_pic), False)
Otherwise it looks good, although I might have missed something. Try it out and see if it fixes the problem, if not I'll look more into it.
Edit
You actually don't need a parser. I think that should solve it:
from django.core.files import ContentFile
class Overlay(models.Model):
...
def generate_sample(self):
base_pic = Image(filename=os.path.join(settings.BASE_DIR, 'girl.jpg'))
overlay_pic = Image(file=self.source)
result_pic = io.BytesIO()
if self.width or self.height:
resize_args = {}
if self.width:
resize_args['width'] = self.width
if self.height:
resize_args['height'] = self.height
overlay_pic.resize(**resize_args)
base_pic.composite(overlay_pic, self.px, self.py)
base_pic.save(file=result_pic)
content = result_pic.getvalue()
self.sample.save(self.name, ContentFile(content), False)
result_pic.close()
base_pic.close()
overlay_pic.close()
There is one thing that can be a potential problem, it will perform this operation every time Overlay model is saved, even if original images are the same. But if it is saved rarely, it shouldn't be an issue.
Just in case, here is a more elegant (in my opinion) implementation. First of all it requires this app: django-smartfields. How this solution is better:
It updates the sample field only when source field has changed, and only right before saving the model.
if keep_orphans is omitted, old source files will be cleaned up.
The actual code:
import os
from django.conf import settings
from django.db import models
from django.utils import six
from smartfields import fields
from smartfields.dependencies import FileDependency
from smartfields.processors import WandImageProcessor
from wand.image import Image
class CustomImageProcessor(WandImageProcessor):
def resize(self, image, scale=None, instance=None, **kwargs):
scale = {'width': instance.width, 'height': instance.height}
return super(CustomImageProcessor, self).resize(
image, scale=scale, instance=instance, **kwargs)
def convert(self, image, instance=None, **kwargs):
base_pic = Image(filename=os.path.join(settings.BASE_DIR, 'girl.jpg'))
base_pic.composite(image, instance.px, instance.py)
stream_out = super(CustomImageProcessor, self).convert(
image, instance=instance, **kwargs):
if stream_out is None:
stream_out = six.BytesIO()
base_pic.save(file=stream_out)
return stream_out
class Overlay(models.Model):
RELATIVE_POSITIONS = (...)
SIZE_MODES = (...)
name = models.CharField(max_length=50)
source = fields.ImageField(upload_to='overlays/%Y/%m/%d', dependencies=[
FileDependency(attname='sample', processor=CustomImageProcessor())
], keep_orphans=True)
sample = models.ImageField(upload_to='overlay_samples/%Y/%m/%d', blank=True)
px = models.SmallIntegerField(default=0)
py = models.SmallIntegerField(default=0)
position = models.CharField(max_length=2, choices=RELATIVE_POSITIONS)
width = models.SmallIntegerField(default=0)
height = models.SmallIntegerField(default=0)
size_mode = models.CharField(max_length=1, choices=SIZE_MODES, default='B')
last_edit = models.DateTimeField(auto_now=True)

save ImageField mongoengine

I have following class definition in mongoengine orm:
import mongoengine as me
class Description(me.Document):
user = me.ReferenceField(User, required=True)
name = me.StringField(required=True, max_length=50)
caption = me.StringField(required=True, max_length=80)
description = me.StringField(required=True, max_length=100)
image = me.ImageField()
in my post method of my tornado web requesthandler:
from PIL import Image
def post(self, *args, **kwargs):
merchant = self._merchant
data = self._data
obj_data = {}
if merchant:
params = self.serialize() # I am getting params dict. NO Issues with this.
obj_data['name'] = params.get('title', None)
obj_data['description'] = params.get('description', None)
path = params.get('file_path', None)
image = Image.open(path)
print image # **
obj_data['image'] = image # this is also working fine.
obj_data['caption'] = params.get('caption', None)
obj_data['user'] = user
des = Description(**obj_data)
des.save()
print obj_data['image'] # **
print des.image # This is printing as <ImageGridFsProxy: None>
** print obj_data['image'] and print image are printing following:
<PIL.PngImagePlugin.PngImageFile image mode=1 size=290x290 at 0x7F83AE0E91B8>
but
des.image still remains None.
Please suggest me what is wrong here.
Thanks in advance to all.
You can not just put PIL objects into a field with obj.image = image that way. You must do:
des = Description()
des.image.put(open(params.get('file_path', None)))
des.save()
In other words, ImageField should be filled with file object after creating an instance by calling put method.

How to get content of Django ImageField before it is saved?

I'm trying to resize an image while saving an instance of my model.
class Picture(models.Model):
image_file = models.ImageField(upload_to="pictures")
thumb_file = models.ImageField(upload_to="pictures", editable=False)
def save(self, force_insert=False, force_update=False):
image_object = Image.open(self.image_file.path)
#[...] nothing yet
super(Picture, self).save(force_insert, force_update)
The problem is that self.image_file.path does not exist before the model is being saved. It returns a correct path, but the image is not there yet. Since there is no image, I can't open it in PIL for resize.
I want to store the thumbnail in thumb_file (another ImageField), so I need do the processing before saving the model.
Is there a good way to open the file (maybe get the tmp image object in memory) or do I have to save the whole model first, resize and then save again ?
I used this snippet:
import Image
def fit(file_path, max_width=None, max_height=None, save_as=None):
# Open file
img = Image.open(file_path)
# Store original image width and height
w, h = img.size
# Replace width and height by the maximum values
w = int(max_width or w)
h = int(max_height or h)
# Proportinally resize
img.thumbnail((w, h), Image.ANTIALIAS)
# Save in (optional) 'save_as' or in the original path
img.save(save_as or file_path)
return True
And in models:
def save(self, *args, **kwargs):
super(Picture, self).save(*args, **kwargs)
if self.image:
fit(self.image_file.path, settings.MAX_WIDTH, settings.MAX_HEIGHT)
Maybe you can open the file directly and pass the resulting file handle to Image.open:
image_object = Image.open(self.image_file.open())
Sorry, I can't test that now.
On your model save method the field value will be a valid FileField or ImageFileField in the case of an ImageField. This django class implements the object file interface (i.e read, write) and it works even before the file is saved with your model, so you can use it as the parameter to PIL.Image.open:
class Picture(models.Model):
image_file = models.ImageField(upload_to="pictures")
thumb_file = models.ImageField(upload_to="pictures", editable=False)
def save(self, force_insert=False, force_update=False):
img = Image.open(self.image_file)
# work with img, is an Image object
super(Picture, self).save(force_insert, force_update)
This works on django >= 1.5.

Categories