1

Possible Duplicate:
resize image on save

Am trying to create a thumbnail in django, am trying to build a custom class specifically to be used for generating thumbnails. As following

from cStringIO import StringIO
from PIL import Image

class Thumbnail(object):

    SIZE = (50, 50)

    def __init__(self, source):
        self.source = source
        self.output = None

    def generate(self, size=None, fit=True):
        if not size:
            size = self.SIZE

        if not isinstance(size, tuple):
            raise TypeError('Thumbnail class: The size parameter must be an instance of a tuple.')

        # resize properties
        box = size
        factor = 1
        image = Image.open(self.source)
        # Convert to RGB if necessary
        if image.mode not in ('L', 'RGB'): 
            image = image.convert('RGB')
        while image.size[0]/factor > 2*box[0] and image.size[1]*2/factor > 2*box[1]:
            factor *=2
        if factor > 1:
            image.thumbnail((image.size[0]/factor, image.size[1]/factor), Image.NEAREST)

        #calculate the cropping box and get the cropped part
        if fit:
            x1 = y1 = 0
            x2, y2 = image.size
            wRatio = 1.0 * x2/box[0]
            hRatio = 1.0 * y2/box[1]
            if hRatio > wRatio:
                y1 = int(y2/2-box[1]*wRatio/2)
                y2 = int(y2/2+box[1]*wRatio/2)
            else:
                x1 = int(x2/2-box[0]*hRatio/2)
                x2 = int(x2/2+box[0]*hRatio/2)
            image = image.crop((x1,y1,x2,y2))

        #Resize the image with best quality algorithm ANTI-ALIAS
        image.thumbnail(box, Image.ANTIALIAS)

        # save image to memory
        temp_handle = StringIO()
        image.save(temp_handle, 'png')
        temp_handle.seek(0)

        self.output = temp_handle

        return self

    def get_output(self):
        self.output.seek(0)
        return self.output.read()

the purpose of the class is so i can use it inside different locations to generate thumbnails on the fly. The class works perfectly, I've tested it directly under a view.. I've implemented the thumbnail class inside the save method of the forms to resize the original images on saving.

in my design, I have two fields for thumbnails. I was able to generate one thumbnail, if I try to generate two it crashes and I've been stuck for hours not sure whats the problem.

Here is my model

class Image(models.Model):
    article         = models.ForeignKey(Article)
    title           = models.CharField(max_length=100, null=True, blank=True)
    src             = models.ImageField(upload_to='publication/image/')
    r128            = models.ImageField(upload_to='publication/image/128/', blank=True, null=True)
    r200            = models.ImageField(upload_to='publication/image/200/', blank=True, null=True)

    uploaded_at     = models.DateTimeField(auto_now=True)

Here is my forms

class ImageForm(models.ModelForm):
    """

    """
    class Meta:
        model = Image
        fields = ('src',)


    def save(self, commit=True):
        instance = super(ImageForm, self).save(commit=True)


        instance.r128 = SimpleUploadedFile(
                    instance.src.name,
                    Thumbnail(instance.src).generate((128, 128)).get_output(),
                    content_type='image/png'
                )


        instance.r200 = SimpleUploadedFile(
            instance.src.name,
            Thumbnail(instance.src).generate((200, 200)).get_output(),
            content_type='image/png'
        )

        if commit:
            instance.save()
        return instance

the strange part is, when i remove the line which contains instance.r200 in the form save. It works fine, and it does the thumbnail and stores it successfully. Once I add the second thumbnail it fails..

Any ideas what am doing wrong here?

Thanks

Update:

as per the comment request, am appending the error trace

IOError at /en/publication/new/

cannot identify image file

Request Method:     POST
Request URL:    http://127.0.0.1:8000/en/publication/new/?image-extra=
Django Version:     1.4.2
Exception Type:     IOError
Exception Value:    

cannot identify image file

Exception Location:     /Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/PIL/Image.py in open, line 1980
Python Executable:  /Users/mo/Projects/pythonic/snowflake-env/bin/python
Python Version:     2.7.2

Update

Tried to create print statement and below is the output

Source: publication/image/tumblr_m9o7244nZM1rykg1io1_1280_11.jpg
Source: publication/image/tumblr_m9o7244nZM1rykg1io1_1280_11.jpg
ERROR:root:cannot identify image file
ERROR:django.request:Internal Server Error: /en/publication/new/
Traceback (most recent call last):
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/core/handlers/base.py", line 111, in get_response
    response = callback(request, *callback_args, **callback_kwargs)
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/contrib/auth/decorators.py", line 20, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/db/transaction.py", line 209, in inner
    return func(*args, **kwargs)
  File "/Users/mo/Projects/pythonic/snowflake-env/snowflake/snowflake/apps/publication/views.py", line 69, in new
    formset.save()
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/forms/models.py", line 497, in save
    return self.save_existing_objects(commit) + self.save_new_objects(commit)
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/forms/models.py", line 628, in save_new_objects
    self.new_objects.append(self.save_new(form, commit=commit))
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/forms/models.py", line 727, in save_new
    obj = form.save(commit=False)
  File "/Users/mo/Projects/pythonic/snowflake-env/snowflake/snowflake/apps/publication/forms.py", line 113, in save
    Thumbnail(instance.src).generate((200, 200)).get_output(),
  File "/Users/mo/Projects/pythonic/snowflake-env/snowflake/snowflake/apps/core/utils.py", line 23, in generate
    image = Image.open(self.source)
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/PIL/Image.py", line 1980, in open
    raise IOError("cannot identify image file")
IOError: cannot identify image file

As seen, the first image is printed and processed successfully the second image is failing.

update

traceback error update after applying the copy() in the thumbnail class

ERROR:root:cannot identify image file
ERROR:django.request:Internal Server Error: /en/publication/new/
Traceback (most recent call last):
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/core/handlers/base.py", line 111, in get_response
    response = callback(request, *callback_args, **callback_kwargs)
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/contrib/auth/decorators.py", line 20, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/db/transaction.py", line 209, in inner
    return func(*args, **kwargs)
  File "/Users/mo/Projects/pythonic/snowflake-env/snowflake/snowflake/apps/publication/views.py", line 69, in new
    formset.save()
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/forms/models.py", line 497, in save
    return self.save_existing_objects(commit) + self.save_new_objects(commit)
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/forms/models.py", line 628, in save_new_objects
    self.new_objects.append(self.save_new(form, commit=commit))
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/django/forms/models.py", line 727, in save_new
    obj = form.save(commit=False)
  File "/Users/mo/Projects/pythonic/snowflake-env/snowflake/snowflake/apps/publication/forms.py", line 113, in save
    f128.write(Thumbnail(instance.src).generate((128, 128)).get_output())
  File "/Users/mo/Projects/pythonic/snowflake-env/snowflake/snowflake/apps/core/utils.py", line 15, in __init__
    self._pilImage = Image.open(self.source)
  File "/Users/mo/Projects/pythonic/snowflake-env/lib/python2.7/site-packages/PIL/Image.py", line 1980, in open
    raise IOError("cannot identify image file")
IOError: cannot identify image file

Update

Finally, I managed to get it to work, but I had to stream the file into self.source as belo

def __init__(self, source):
    self.source = StringIO(file(source.path, "rb").read())
    self.output = None

    self._pilImage = Image.open(self.source)

is the above ideal approach? is it a good idea to read the file at each hit? if no, what are my alternatives?

Community
  • 1
  • 1
Mo J. Mughrabi
  • 6,747
  • 16
  • 85
  • 143

2 Answers2

4

The problem I see is in the way you have designed your Thumbnail class. It is using class attributes to store instance variables, meaning that you will have conflicts when you try to use the class more than once.

There is no need for the static load method, as once you move the attributes to the instance, it does the exact same thing as the constructor of the class. And by requiring a source in the constructor, you ensure a crash will not occur later in generate when it looks for empty string values.

Also, one of the major problems I think you are facing is when you are using the file-like object wrappers that your django model is returning for the ImageField's. While you would not see this if you were passing in string paths, when you pass in the file object, the generate method reads it to the end. Then you call generate a second time with the same source object, but it is at the end and you get an IOError. Now one approach would be to make sure to seek the source back to 0 before calling Thumbnail again with it, but instead you can save yourself the trouble and just have your Thumbnail class open and cache the PIL image once in the constructor. Then generate does not need to constantly re-read it again each time.

# Example from your code #
def generate(self, size=None, fit=True):
    ...
    # The first time you do this, it will read
    # self.source to the end, because in Django, you
    # are passing a file-like object.
    image = Image.open(self.source)

# this will work the first time
generate()
# uh oh. self.source was a file object that is at the end
generate() # crash

Re-written Thumbnail Class

from cStringIO import StringIO
from PIL import Image

class Thumbnail(object):

    SIZE = (50, 50)

    def __init__(self, source):
        self.source = source
        self.output = None

        self._pilImage = Image.open(self.source)

    def generate(self, size=None, fit=True):
        if not size:
            size = self.SIZE

        if not isinstance(size, tuple):
            raise TypeError('Thumbnail class: The size parameter must be an instance of a tuple.')

        # resize properties
        box = size
        factor = 1
        image = self._pilImage.copy()

        # Convert to RGB if necessary
        if image.mode not in ('L', 'RGB'): 
            image = image.convert('RGB')
        while image.size[0]/factor > 2*box[0] and image.size[1]*2/factor > 2*box[1]:
            factor *=2
        if factor > 1:
            image.thumbnail((image.size[0]/factor, image.size[1]/factor), Image.NEAREST)

        #calculate the cropping box and get the cropped part
        if fit:
            x1 = y1 = 0
            x2, y2 = image.size
            wRatio = 1.0 * x2/box[0]
            hRatio = 1.0 * y2/box[1]
            if hRatio > wRatio:
                y1 = int(y2/2-box[1]*wRatio/2)
                y2 = int(y2/2+box[1]*wRatio/2)
            else:
                x1 = int(x2/2-box[0]*hRatio/2)
                x2 = int(x2/2+box[0]*hRatio/2)
            image = image.crop((x1,y1,x2,y2))

        #Resize the image with best quality algorithm ANTI-ALIAS
        image.thumbnail(box, Image.ANTIALIAS)

        # save image to memory
        temp_handle = StringIO()
        image.save(temp_handle, 'png')
        temp_handle.seek(0)

        self.output = temp_handle

        return self

    def get_output(self):
        self.output.seek(0)
        return self.output.read()

Usage: Thumbnail(src).generate((200, 200)).get_output()

The source and output need to be unique for each instance. But in your version you would set output to the class level, which means that two instances of the Thumbnail use the shared most recent version of output.

# your code #
    # this is assigning the most recently processed
    # object to the class level. shared among all.
    self.output = temp_handle

    return self

def get_output(self):
    # always read the shared class level
    return self.output.read()

Also, I feel there is an easier way to perform your resize/fit/crop. If you explain the exact transformation you want to do for the image, I can probably simplify that as well.

Update

I forgot to specifically mention that with my suggestions for saving the source image once, your usage should look like this:

def save(self, commit=True):
    instance = super(ImageForm, self).save(commit=True)

    thumb = Thumbnail(instance.src)

    instance.r128 = SimpleUploadedFile(
        instance.src.name,
        thumb.generate((128, 128)).get_output(),
        content_type='image/png'
    )

    instance.r200 = SimpleUploadedFile(
        instance.src.name,
        thumb.generate((200, 200)).get_output(),
        content_type='image/png'
    )

Notice that we only create one instance of Thumbnail using the source, which will open it only once in PIL. Then you can generate as many images as you want from it.

jdi
  • 90,542
  • 19
  • 167
  • 203
  • Hi jdi, thanks very much for the details explination I appreciate the time spent. Yet, am still faced with the same error, I copied the same exact code you proposed but its same issue. Am starting to suspect the problem to be from the forms.save() method. By looking at it, do you think it could be the problem? – Mo J. Mughrabi Dec 01 '12 at 17:50
  • the reason am suspecting forms.save() method, is because i just tried to use the thumbnail class into a view to render HttpResponse with the image and give it a memtype as png. I tried generating two and three thumbnails and retruning only the last and it worked. so, wouldn't that mean the forms.save() or the model are not accepting – Mo J. Mughrabi Dec 01 '12 at 17:52
  • i just updated the question with the latest code am using and traceback error – Mo J. Mughrabi Dec 01 '12 at 18:07
  • Can you do me a favor and put a print statement right before this line `Image.open(self.source)`, in your `Thumbnail` class: `print "Source:", self.source`. I am curious what you are really passing to PIL – jdi Dec 01 '12 at 18:36
  • I did the print and I got the following Source: publication/image/1207281205471202-tonylawfirmpic_33.jpg – Mo J. Mughrabi Dec 01 '12 at 18:39
  • but what is strange, it is processing the first imgae successfully and failing in the second one. I will append the output to the question now – Mo J. Mughrabi Dec 01 '12 at 18:41
  • I think I might have figured this whole thing out. Try my latest update to the code. It only reads the source once instead of every time. – jdi Dec 01 '12 at 18:47
  • same error even after moving the image.Open in __init__ :( – Mo J. Mughrabi Dec 01 '12 at 18:53
  • You ran it with my most recent updates? Including the `copy()`? Well then it doesn't make sense because at that point the image is read only once. All I can suggest now (which I would not think is necessary, is to seek your `instance.src` back to 0 before running it a second time through Thumbnail. This update was supposed to make it unnecessary to do that. Could you update your traceback to reflect running it through my most recent code? – jdi Dec 01 '12 at 19:10
  • yeah i used the copy() function, I just copied the latest traceback error into my question. What do you mean by seek your instance.src back to 0? – Mo J. Mughrabi Dec 01 '12 at 19:42
  • Hey jdi, I just managed to make it work.. I don't know if the approach am following is a good idea or not. Plesae take a look at the question update and let me know if you have any advise for me.. many thanks – Mo J. Mughrabi Dec 01 '12 at 20:18
  • I think that approach is just fine. It was the same direction I was trying to get you to go. The problem was the fact that once you read the file, it is now at the end. So the next time you try and read it, it will fail unless you reset it. What you are doing should work because you read it once into a buffer, then you use that each time. And you aren't even reading it multiple times anyways because you reuse the instance in the form. It reads once into the buffer, then passes that along to PIL which stays open for the instance. And you generate simply uses copies of that for new thumbs. – jdi Dec 01 '12 at 22:15
  • @MoJ.Mughrabi: There was a very important point that I think wasn't communicated well by me. You ended up trying to fix it with wrapping the source in a `StringIO`. But I realized you were still creating two instances of `Thumbnail` which was the cause of the problem before. You do not need to use that wrapping. See my update. Create only once instance from your source. Then generate all the thumbnail copies off of it. – jdi Dec 01 '12 at 23:30
  • If you still happen to have problems, it isn't totally bad that you read it in a second time by wrapping it with `StringIO(file(path).read())`. It would be preferable not to read it twice when you already have the data loaded. – jdi Dec 01 '12 at 23:45
2

The parameter of PIL.Image.open(...) can be a filename or a file object. The read position should be at the start of file if a file like object is used. You use a file object. (It is sure because you use instance.src.name and then you pass Thumbnail(instance.src).)

Solution: Rewind the file to the beginning by instance.src.seek(0) before creating the second thumbnail or pass only the filename, not the file object: Thumbnail(instance.src.name).

hynekcer
  • 14,942
  • 6
  • 61
  • 99