Reading an Image from a Django FileResponse Object
The Issue:
So this was an interesting thing that came up.
I am running a client's tests and they are failing. They were using an external WMS Server and it was too unreliable. So the solution was to create a Mock WMS Server. That's a different article, but in their failing Unit test they had this code:
import StringIO
...
def test():
....
tile_link = data['tiles'].replace('{z}', '0')\
.replace('{x}', '0')\
.replace('{y}', '0')
# fetch a tile
response = client.get(tile_link)
self.assertEqual(response.status_code, status.HTTP_200_OK)
img = Image.open(StringIO.StringIO(response.content))
self.assertGreater(img.size[0], 0) # height > 0
self.assertGreater(img.size[1], 0) # width > 0
Which is getting the tile URL, replacing the ZXY parameters in it and requesting a tile, then checking that the tile returned works. It is unknown if this ever worked as it was commented out.
However, when I changed the external wms service and then ran the code it returned this error:
This FileResponse instance has no `content` attribute. Use `streaming_content` instead.
What is that? It wasn't what I was expecting. It turns out the request was returning a Django FileResponse object, rather than a simple requests.Response that I was expecting. In looking at this request, it was sent using send_file() rather than just a normal response.content, and in the context of a Unit Test in Django, the Django FileResponse is received.
So how to load the image?
Try 1 - The simple change:
The simple change, just switch content for streaming_content and cross fingers:
content = StringIO.StringIO(response.streaming_content)
img = Image.open(content)
Which returns this error:
IOError: cannot identify image file <StringIO.StringIO instance at 0x7fa5eef000e0>
So it can get something, but it's not an image. Looking in the debugger I can see the bytes are an image, so that's not an issue. Time to check the documentation. However the documentation talks about how to create a FileResponse and send back a file, but not how to Recieve one. It mentions that it may use wsgi.file_wrapper or may not. Not sure if that "use" means inherit or encapsulate, but let's give it a try.
Try 2 and 3 - wsgi.file_wrapper read() and raw.read()
wsgi.file_wrapper has a read() function, so let's try that.
image_bytes = response.read()
image_buffer = StringIO.StringIO(image_bytes)
img = Image.open(image_buffer)
and
img = Image.open(StringIO(response.raw.read()))
Nope, returns this error:
AttributeError: This FileResponse instance has no `read` attribute.
or this one:
AttributeError: This FileResponse instance has no `raw` attribute.
Try 4 ish - iter_content()
So it isn't a wsgi.file_response, or users means not inherit. Which makes sense as it inherits from HttpResponse. However, given python doesn't have nice and simple strict typing, it's just guess and check now. Moving on to the second part where it says it will "streams the file out in small chunks." How do we get those chunks?
now a quick search will turn up this:
response.iter_content()
However, Django's FileResponse inherits from Django's HttpResponse, neither of which inherit from requests.response. So that function isn't available. A quick check will prove this:
img_data = StringIO.StringIO()
for chunk in response.iter_content(1024):
img_data.write(chunk)
img_data.seek(0)
img = Image.open(img_data)
returns this expected error:
'FileResponse' object has no attribute 'iter_content'
Try 5 - back to streaming_content
What is streaming_content? Using the debugger it says it's <itertools.imap object at 0x7f6221709f10>
What's that? Let's look at the documentation
That's a function that returns an iterator, so we should be able to iterate over the streaming_content object.
stream = response.streaming_content
for item in stream:
print(item)
Returns this:
?PNGIHDR ??1 ...more human unreadable stuff... ?@?????0b IEND?B`?
Great! lots of data, now how do I get that into an array so I can send it to the image. Send in the good old join() function
import StringIO
...
stream = response.streaming_content
content = StringIO.StringIO(''.join(stream))
img = Image.open(content)
Which works.
Finishing Touches
However, the client's code is still in python 2.7, however I don't want to leave some work for later, so I'll change the library to support python3.
from io import BytesIO
stream = response.streaming_content
content = BytesIO(''.join(stream))
img = Image.open(content)
Which is the final fix for this particular issue.