Writing a CUPS filter for Mac OS X Snow Leopard (Mac OS v10.6)

I wanted to be able to save a PDF version of anything I printed automatically - without having to manually generate the PDF as a separate action, or needing to send the documents to print twice - once to a physical printer, then to a virtual printer.

My solution was to dig into the CUPS (Common UNIX Printing System) components in Mac OS X. CUPS uses a series of filters (command line programs) to convert a document from its original content into a PostScript file for sending to the printer (assuming a PostScript printer that is).

By working with configuration files, it is possible to insert an additional filter step (my own program) into the standard filter chain - giving me access to the PostScript content before it is sent to the printer (from which a PDF can then be easily generated by calling existing command line programs).

The first stage is to add an entry to the filters pipeline.

If you inspect the contents of:

/usr/share/cups/mime/

There are 4 files:

apple.convs
apple.types
mime.convs
mime.types

It's worth reading the contents of these (especially the comments) - but we're not going to edit these files. Some things worth noting are:

From /usr/share/cups/mime/mime.convs

# DO NOT EDIT THIS FILE, AS IT IS OVERWRITTEN WHEN YOU INSTALL NEW
# VERSIONS OF CUPS. Instead, create a "local.convs" file that
# reflects your local configuration changes.
# Format of Lines:
#
# source/type destination/type cost filter
# All filters *must* accept the standard command-line arguments
# (job-id, user, title, copies, options, [filename or stdin]) to
# work with CUPS.

From /usr/share/cups/mime/apple.convs

# The "cost" field must be less than the value for the same entry
# in mime.convs so that these filters replaces the ones defined
# in that file.

So, based on this, we will create 2 new files in the same directory:

sudo touch /usr/share/cups/mime/local.convs
sudo touch /usr/share/cups/mime/local.types

The contents of /usr/share/cups/mime/local.convs should be:

application/postscript application/foo 1 bar
application/foo application/vnd.cups-postscript 1 pstops

The contents of /usr/share/cups/mime/local.types should be:

application/foo

We're introducing a new mime type locally and we're replicating the application/postscript to application/vnd.cups-postscript conversion with 2 steps, both of which have a lower 'cost'; the option we're 'replacing' is the following line from /usr/share/cups/mime/mime.convs

application/postscript application/vnd.cups-postscript 66 pstops

Note that we're using the same program to handle the conversion (pstops), but that our combined 'cost' is not a realistic one - it's deliberately lower so that our filter(s) will be picked; we're not altering any of the existing configuration files (there is no need to comment things out).

The second stage is to add the filter program itself.

Here is an example written in PHP (but intended to be run as a command line program):

#!/usr/bin/php
<?php

$arguments = array();
$arguments['job'] = $_SERVER['argv'][1]; // $_SERVER['argv'][0] is the program path
$arguments['user'] = $_SERVER['argv'][2];
$arguments['title'] = $_SERVER['argv'][3];
$arguments['copies'] = $_SERVER['argv'][4];
$arguments['options'] = $_SERVER['argv'][5];

if (count($_SERVER['argv']) == 7) {
$postscript = file_get_contents($_SERVER['argv'][6]);
} else {
$postscript = stream_get_contents(STDIN);
}

/*
Here you could:
- write the arguments to a file
- write the PostScript content to a file
- call the pstopdf program (passing the PostScript file path) to generate a PDF file from the PostScript file (it will be generated in the same directory that the passed PostScript file resides in)
- modify the PostScript before it is passed to the next filter (if you know what you're doing - watch out for errors!)
*/

echo $postscript; // for passing on to the next filter
exit;

The filter programs reside in:

/usr/libexec/cups/filter/

Following the example, this file would be saved as simply 'bar' (it does not have a .php extension). It's important to note that your program should have the same owner, group and permissions as the other filters in the directory; if you're using a symlink, both the symlink and the program it references must have these same owner, group and permissions as well. Otherwise you will receive errors when try to print ("The printer software was installed incorrectly. Please reinstall the printer's software or contact the manufacturer for assistance.") and the cups error_log (at /private/var/cups/error_log - use the Console app) will report something similar to:

Unable to execute /usr/libexec/cups/filter/bar: insecure file permissions (0100755)
Unable to start filter "bar" - Operation not permitted.
Stopping job because the scheduler could not execute a filter.

Tagged , , and .

Image ASCII85 encoding with Java

While trying to write a CUPS filter, I needed to add an image to a PostScript document. There are some handy PostScript functions for importing design elements in direct PostScript, but I only managed to get the PNG function working which embeds an image using ASCII85 encoding. The only problem: how to encode a PNG image into ASCII85.

Jarmor "is a tiny collection of java filters implementing ASCII armors" - which includes ASCII85, but aside from the source code, I found the download package didn't contain any examples, only tests and API documentation. Some people will find that perfectly adequate, but I prefer some simple examples so I can quickly use the code: EncodeFile will take the filename of an image as an argument and output an ASCII85 encoded version suitable for use in a PostScript document (place the unzipped .java file in the 'src' directory of the unzipped 'jarmor-1.0-src' download and compile in the usual manner).

Tagged , and .

Most used blog post tags

Blog entries by month

  1. January 2012 (1)
  2. August 2011 (2)
  3. March 2011 (1)
  4. February 2011 (1)
  5. November 2010 (1)
  6. October 2010 (1)
  7. August 2010 (2)
  8. January 2009 (1)
  9. December 2008 (1)
  10. November 2008 (1)
  11. October 2008 (2)
  12. September 2008 (3)