Attention Internet Explorer users: This site won't look like ass if you use a better browser. My site, my rules.

Improved text image view

I just found this in my django-ego-feed: 23 excuses: Simple Django View for Dynamic Text Replacement

I’ve been using something similar to generate the titles for the site (look at the title above for an example), so I’m pretty familiar with the technique.

Andrew’s code over there is pretty good, but I’ve got a few improvements he and you might be interested in:

  • The business to writing to a temp file is ugly and will break as soon as you get two simultaneous requests. The right way to do this is by remembering that an HTTPResponse exposes a file-like interface, so you can write the image directly to it:

    response = HTTPResponse(mimetype="image/png")
    im.save(response, "PNG")
    
  • You’ll save on bandwidth and processor time if you add in E-tag support. This’ll let browsers cache your image locally and save you from generating and sending it each time.

    This is actually pretty easy to do. At the start of the view, calculate the E-tag and check it against the If-None-Match header:

    etag = md5.new(header + fontalias).hexdigest()
    if request.META.get("HTTP_IF_NONE_MATCH") == etag:
        return HttpResponseNotModified()
    

    Then, just before you return the response, insert the E-tag header:

    response["e-tag"] = etag
    
  • As long as we’re being pedantic about HTTP, let’s return a 403 Forbidden for a bad font file (instead of a 200 OK)

Here’s the complete view_header() code with my modifications:

import md5
from django.conf import settings
from django.http import (HttpResponse,
                         HttpResponseNotModified,
                         HTTPResponseForbidden)
import Image, ImageFont, ImageDraw

def view_header(request, fontalias):
    try:
        fontfile = settings.DYNAMIC_FONT_ALIASES[fontalias]
    except:
        return HttpResponseForbidden("font alias not supported")

    if request.GET.has_key('text'):
        header = request.GET['text']
    else:
        header = 'Hello world'

    etag = md5.new(header + fontalias).hexdigest()
    if request.META.get("HTTP_IF_NONE_MATCH") == etag:
        return HttpResponseNotModified()

    imf = ImageFont.truetype(fontfile, 20)
    size = imf.getsize(header)
    im = Image.new("RGB", size)
    draw = ImageDraw.Draw(im)
    draw.text((0, 0), header, font=imf)

    response = HTTPResponse(mimetype="image/png")
    im.save(response, "PNG")
    response["e-tag"] = etag
    return response

Enjoy!

Comments

Andrew

June 30th, 2006

2:35 p.m.

Excellent, I knew someone would come rescue the crap that I posted. :)

antrix

June 30th, 2006

10:48 p.m.

Hah! Just last week, I put up a stand alone web script that does just this: fetches text as image files based on request parameters.

Check out: http://antrix.net/text/

Paul

July 1st, 2006

6:31 a.m.

Why not use sifr? it has so many advantages over image replacement :
clientside, text is selectable and .....
http://www.mikeindustries.com/sifr/

Jacob Kaplan-Moss

July 1st, 2006

7:25 a.m.

Meh, too heavyweight for me. Plus there's the whole trusting Macrodobe to do the right thing wrt Flash, and I'll bet you can guess how I feel about that.

Jeff Croft

July 1st, 2006

1:25 p.m.

I've used sIFR quite a bit myself, with both Inman and Mike being friends of mine. They'll be the first to admit, though, that it's not a long-term solution. It was more designed as a way to tell the font foundries and W3C "Us web designers are here, and we're going to find a way to use your fonts on the web whether you sanction it or not." Hopefully they'll come up with a good long-term solution. In the interim, both sIFR and server-generated image replacement are good solutions.

sIFR has the positive of being scaleable -- but how much does that really matter when you're dealing with larger headers anyway (which is sIFR's intended purpose). Selectable is not an advantage -- image replacement is selectable too, when done with CSS instead of HTML img elements. Being clientside could be an advantage -- although you'll find as many people that disagree with that as agree with it. To me, the one real advantage of sIFIR is that it's in Flash, which is a comfortable place for designer to work -- most of us designers would rather not have to write view code in order to get pretty type.

Image replacement has the advantage of being much better-looking (Flash's antialiasing, even in the latest version, just isn't to Photoshop-like levels) and not requiring Flash (although I question that advantage a bit, as Flash is so ubiquitous these days). And, like Jacob pointed out, not relying on Flash means not relying on a giant corporation to do the right thing.

I think both are good interim solutions over all, and it makes perfect sense that programmers would flock to image replacement and designers would prefer Flash.

Either way, this Django view is a clever solution. Way to go, guys. :)

jim

July 2nd, 2006

10:37 a.m.

Being a 64bit linux user I have to encourage you not to use Flash :P

Btw, using your django solution, won't it return the same etag if you change font/etag (i.e. won't people have to use ctrl+f5 to see any changes)? I was wondering if you should use a version number and add it to the etag or something so that won't happen.

jim

July 2nd, 2006

10:39 a.m.

that was supposed to say font/style

Jacob

July 2nd, 2006

8:36 p.m.

Jim -- good eyes! I've updated the view to hash the font name along with the string.

Thanks!

Andrew

July 4th, 2006

11:17 p.m.

I hate SIFR, I've had way too many problems with it. A few clients looked for _exact_ dimensions and line height, spacing and it was impossible to do reliably cross browser. It's unfortunate, because it's a decent idea, but replacing with pngs is much more friendly to all users. Unfortunately PIL doesn't have very good font support, so it's not easy (if even possible) to customize line height, and letter spacing, and therefore sifr is still being used on that site.

Jeremy Walker

July 15th, 2006

9:29 p.m.

In the line:

from django.http import (HttpResponse,
HttpResponseNotModified,
HTTPResponseForbidden)

Shouldn't "HTTPResponseForbidden" be "HttpResponseForbidden"?

Jeremy Walker

July 16th, 2006

4:53 p.m.

Similarly, shouldn't this line:
response = HTTPResponse(mimetype="image/png")

use "HttpResponse" instead?

Shev

March 11th, 2007

11:23 p.m.

I got this method,

def silent_update():
# Do secret stuff
return HttpResponseNotModified()

---------------------------------------------------
But i got this error:

Traceback (most recent call last):

File "C:\Python24\lib\site-packages\django\core\servers\basehttp.py", line 272, in run
self.result = application(self.environ, self.start_response)

File "C:\Python24\lib\site-packages\django\core\servers\basehttp.py", line 614, in __call__
return self.application(environ, start_response)

File "C:\Python24\lib\site-packages\django\core\handlers\wsgi.py", line 193, in __call__
response = middleware_method(request, response)

File "C:\Python24\lib\site-packages\django\contrib\sessions\middleware.py", line 74, in process_response
patch_vary_headers(response, ('Cookie',))

File "C:\Python24\lib\site-packages\django\utils\cache.py", line 104, in patch_vary_headers
if response.has_header('Vary'):

TypeError: unbound method has_header() must be called with HttpResponseNotModified instance as first argument (got str instance instead)

I do not want the view to refresh my html page.

Jacob

March 12th, 2007

12:14 a.m.

Yeah, I don't really want my blog to be a clearing-house for Django questions. Please ask this on django-users or some other appropriate forum.

Tom W. Most

July 22nd, 2007

5:03 p.m.

Um, the ETag header is spelled without the hyphen--see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19 It won't do you much good like that, will it?

Your 2¢

Comment