Reputation: 104
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?
Upvotes: 5
Views: 3400
Reputation: 9767
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:
sample
field only when source
field has changed, and only right before saving the model.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)
Upvotes: 1
Reputation: 9767
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.
Upvotes: 1
Reputation: 104
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
?
Upvotes: 3