netomo
netomo

Reputation: 104

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?

Upvotes: 5

Views: 3400

Answers (3)

lehins
lehins

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:

  • 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)

Upvotes: 1

lehins
lehins

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

netomo
netomo

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

Related Questions