Revisiting AirPrint

Apple keeps breaking AirPrint on older gear for no reason, and I keep fixing it.

Neary two years ago I had a go at making it work with Leopard, and in the meantime a few things changed - I upgraded my home server setup to Snow Leopard, iOS is now in its sixth revision, and I also changed printers.

AirPrint is not something I need to use often (if at all), but it’s handy enough for me to want it to work. It’s basically a set of tweaked CUPS filters and Bonjour announcements, and it’s somewhat aggravating that Apple can’t seem to bother to retrofit it to older Macs when it’s an altogether trivial hack.

Picking up where we left off earlier, to get this working on Snow Leopard I first added the “new” MIME types to CUPS (inspired by this post, which was my first and last stop while investigating the status quo):

sudo launchctl stop org.cups.cupsd
sudo sh -c "echo 'image/urf urf string(0,UNIRAST<00>)' > /usr/share/cups/mime/airprint.types"
sudo sh -c "echo 'image/urf application/pdf 100 cgpdftoraster' > /usr/share/cups/mime/airprint.convs"
sudo launchctl start org.cups.cupsd

However, I’m pretty sure this isn’t actually used, since I’ve yet to see my iOS devices send URF data to the printer - and that because since there is no apparent way to tweak the way CUPS registers printers in Bonjour, I set up a duplicate service with the URF record set to none like I did earlier - but this time, I whipped up the requisite Python script:

from subprocess import Popen, PIPE
from signal import alarm, signal, SIGALRM

class Alarm(Exception):

def handler(signum, frame):
    # map the signal to an exception
    raise Alarm

def dnssd(params, pattern='local.', timeout=3):
    h = Popen('dns-sd %s' % params, shell=True, stdout=PIPE)

    # set up a timeout
    if timeout:
        signal(SIGALRM, handler)

    result = []

        # readline will block when dns-sd enters its loop
        while True:
            line = h.stdout.readline()
            if pattern in line:
    except Alarm:
    return result

# grab all instances of _ipp._tcp services
printers = map( lambda entry: entry.split('_ipp._tcp.')[1].strip(),
                dnssd('-B _ipp._tcp.', 'local.', 3) )

# tack on 'URF=none' to each service info
entries = []
for p in printers:
    entries.extend(map( lambda record: (p.split("@")[0].split(' ')[0],
                        record.strip() + ' URF=none'),
                        dnssd('-L "%s" _ipp' % p, 'txtvers', 1) ))

# now advertise the first entry (advertising more than one would require
# something like multiprocessing.Pool() or just plain exec(), and I have
# just the one printer...)
dnssd('-R "%s AirPrint" _ipp._tcp,_universal local. 631 %s' % entries[0],

This is somewhat of a hack since dns-sd isn’t really supposed to be used this way - but it’s simpler and easier to understand than my attempt at doing the same with ctypes.

You can grab the script here, and if you right-click on it and pick the Applet Builder you’ll get an application bundle you can toss into your Login Items and forget about1.

I’ve also tinkered with the notion of setting up an AirPrint renderer/redirector on a Raspberry Pi, and will probably end up doing that later - it should be pretty much the same thing, really, except you’ll want to look at airprint-generate to make it easier to set up the Bonjour announcements.

  1. Since I have to run iTunes on my server anyway due to Apple’s unwillingness to split it into a content sharing service and a front-end app, there was hardly any point in setting up a launchd entry. ↩︎