5

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?

netomo
  • 104
  • 1
  • 7

3 Answers3

3

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?

Community
  • 1
  • 1
netomo
  • 104
  • 1
  • 7
1

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.

lehins
  • 9,642
  • 2
  • 35
  • 49
  • Tryed with "from django.core.files import File" and "from django.core.files.images import ImageFile" and tried many other ways and nothing seems to happen. A thousand thanks in advance – netomo Jan 31 '15 at 07:24
  • also tried with: pil_result_pic = pil_parser.close() django_file = File(open(pil_result_pic)) self.sample.save(self.name, django_file, False) And now getting the following error: coercing to Unicode: need string or buffer, instance found. I do not know but somehow I think I'm closer @alexey-kuleshevich – netomo Jan 31 '15 at 15:29
  • I think coersing to Unicode happens because it wasn't opened it binary mode, but I am not 100% sure. In any case let me know if a new proposed solution works for you. – lehins Jan 31 '15 at 18:41
  • haha thanks!! we reached to the same solution, but yours is better skipping the "pil_parser" The proposed solution worked great! =D – netomo Jan 31 '15 at 18:52
  • Yeah, it was funny, that we got to same solution almost at the same time :) I can also show you a way to generate sample only whenever `source` field changes, instead of every time when model is saved. Let me know if you want me to type it up here. – lehins Jan 31 '15 at 18:59
  • Hey! that would be great! – netomo Jan 31 '15 at 22:54
  • There you go. Just added another answer. – lehins Jan 31 '15 at 23:14
1

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)
lehins
  • 9,642
  • 2
  • 35
  • 49
  • cool, let me know if you'll have any question about this solution or smartfields app in general. – lehins Jan 31 '15 at 23:42