Revisiting AirPrint

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

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

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 announcements, and it’s somewhat aggravating that can’t seem to bother to retrofit it to older Macs when it’s an altogether trivial hack.

Picking up where we left off , to get this working on 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 devices send URF data to the printer - and that because since there is no apparent way to tweak the way CUPS registers printers in , I set up a duplicate service with the URF record set to none like I did - but this time, I whipped up the requisite script:

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

class Alarm(Exception):
    pass

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)
        alarm(timeout)

    result = []

    try:
        # readline will block when dns-sd enters its loop
        while True:
            line = h.stdout.readline()
            if pattern in line:
                result.append(line)
    except Alarm:
        h.kill()
    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],
      timeout=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 renderer/redirector on a , 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 announcements.


  1. Since I have to run on my server anyway due to 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. ↩︎

This page is referenced in: