Printing with Ghostscript and without it ======================================== At some point of time I got quite tired of a horribly slow inkjet printer (which also tended to clog the printing head when not used for a month) and bought a decent fast laser printer. And since now I was able to really print things not in an agonizing way that brought me to the point where I finally took time to configure the print subsystem in a convenient way. This process was not quite obvious, so now I want to share the experience. I wanted to control the printing easily: print the files in printer's native HPLJ PCL language or in Ghostscript, simplex or duplex (that is, printing on both sides of a page, now the affordable printers can do that), with 150 or 300 or 600dpi, with Ghostscript output optimised for half-tone or line graphics images, and so on. Well, one problem is that the BSD print subsystem is rather ugly and underpowered. (If you wonder, by comparison the SVR4 print subsystem is not exciting as well but much less underpowered though more ugly). The problem is that the control of the file format filters is very inflexible. There are only few predefined filter types (in SVR4 they can be defined in any numbers and with any names), no way to combine them in a pipeline controlled by the lpr's options, and no way to pass arguments to them from lpr's command line. So normally to work around these problems people turn to dirty tricks. And I'm no exception. The first typical trick is to define multiple logical printers for a single physical one. I've defined the following ones: lpt - no filters at all, just dump the file to the printer as it is. Useful for printing files in the HPLJ PCL language. lp (also known as the default printer) - printing of raw text files in the portrait orientation. lpl - printing of raw text files in the landscape orientation. lp4 - printing of Postscript files filtered through Ghostscript, translating them to the LaserJet-4 (A.K.A. PCL5) format at 300dpi. lp4150 - the same thing but at 150dpi. lp4600 - the same thing but at 600dpi. lp5g - printing of Postscript files filtered through Ghostscript, translating them to the LaserJet-5 (A.K.A. PCL6) format at 600dpi with the resolution enhancement technique (which is supposed to simulate the resolution of 1200dpi) in gray-shade mode. lp5m - the same thing but with resolution enhancement in monochrome mode. This printer also has aliases "lpps" and "ps", to remember it easily as the default Postscript printer. Why so many variations ? There are reasons. First, in the plain text files LF has to be replaced by CR-LF and that needs a filter. (If you don't know what that means, here is the explanation: the text files in Unix have lines terminated with a single character 0xA, also known as Line Feed or LF - it's expected to move the cursor to the start of next line. But the LaserJet language has a different, more picky, understanding of this character: it moves the current position to the next line but leaves it in the current column instead of returing it to the start of the line. The character 0xD also known as Carriage Return or CR is required to return the current position to the beginning of the line. So every LF in the text file has to be converted to a CR-LF pair before printing.) Second, despite what the HP whitepapers say, the PCL6 documents are much larger than the PCL5 ones. So transmitting them into the printer takes more time, and Ghostscript works slower as well. I like when computers work fast, so in cases when the quality is not important I'd rather save time by using the PCL5 format. For the same reason lower resolution may be a good thing too. All these printers share the same output file but still have to have different spooling directories, so their records in /etc/printcap would look like: lpt|local line printer - transparent:\ ... :lp=/dev/lpt0:sd=/var/spool/output/lpt:lf=/var/log/lpd-errs: lp|local line printer:\ ... :lp=/dev/lpt0:sd=/var/spool/output/lpd:lf=/var/log/lpd-errs: lpl|local line printer - landscape orientation:\ ... :lp=/dev/lpt0:sd=/var/spool/output/lpl:lf=/var/log/lpd-errs: ... lp5m|lpps|ps|Postscript - best mono:\ ... :lp=/dev/lpt0:sd=/var/spool/output/lp5m:lf=/var/log/lpd-errs: (For those who don't know about this file, /etc/printcap is the file where all the printers are described as a set of configuration parameters also known as "capabilities", see "man printcap" for details). The print daemon lpd provides interlocking between its processes trying to open the same device, so this trick is safe and the printed files won't intermix with each other even if spooled to different logical printers at the same time. Instead one file will be printed first, another one after it, and so on. There are two more parameters which should be normally defined for all the printers, so a complete definition would look like: lpt|local line printer - transparent:\ :sh:mx#0:\ :lp=/dev/lpt0:sd=/var/spool/output/lpt:lf=/var/log/lpd-errs: :sh: disables printing of banner pages which are an inheritance of old line printers and only a nuisance nowadays. :mx#0: allows to print files of unlimited size. If it is not set then the file size will be limited to 1000KB by default and this limit is far too low for the typical print jobs with graphics. Now it's time to look at the filters. Lpd provides two kinds of filters, input filters and output filters, and the exact rules for their use are weird and strange. For each particular printer only one output filter may be defined but muptiple input filters, for different types of input files (and one of these types is "catch all the rest"). For each file being printed no more than one filter is used, with a direct consequence that if a "catch all" input filter is specified, the output filter will never be used. If there are no suitable filters specified, lpd will just pass the file as it is and add the Form Feed character (with code 0xC) at the end. An important difference is that a separate input filter is started for each print jobs but an output filter can be carried between multiple consecutive print jobs, which are thus concatenated together. This makes the output filters useful only for classic line printers. The printers understanding the advanced page description languages usually have problems when multiple print jobs are concatenated together. Because of this we will further consider only the input filters. The "catch all" input filter is specified as parameter "if". For my printer "lp" this filter should convert all LFs to CR-LF pairs. Also there is an issue of the Form Feed character at the end of file: this character is required by the PCL printers at the end of each page. If a file does not contain this character at the end its last page won't be printed. So an obvious solution is to add this character in the filter. But if this character is present in the file and then added once more in the filter then an extra empty page will be printed at the end. So I wrote a small filter program that tries to solve this issue intelligently and add the final Form Feed only if it's missing. So the printer definition would look like: lp|local line printer:\ :sh:mx#0:\ :if=/usr/local/lp/lj:\ :lp=/dev/lpt0:sd=/var/spool/output/lpd:lf=/var/log/lpd-errs: And the source code of /usr/local/lp/lj is: /* * Simple filter for LaserJet printers, to be used in * :if=...: in /etc/printcap. If the parameter '-c' * is passed (from lpr -l) then the data is passed * through, else LF are changed to CR/LF. Does not * make extre form feed */ #include #include int main(ac, av) int ac; char **av; { int crlf=1; int c, last=0; if(ac>1 && !strcmp(av[1],"-c")) crlf=0; if(crlf) { while(( c=getchar() )!=EOF) { if(c=='\n') putchar('\r'); putchar(c); last=c; } } else { while(( c=getchar() )!=EOF) { putchar(c); last=c; } } /* if no form feed at the end then add it */ if(last!='\f') putchar('\f'); return 0; } As you can see a filter reads the data from its standard input, does some processing and prints them to the standard output. It also receives some arguments describing the print job. We will look closer at these arguments when discussing the banners. This filter uses only one of the arguments, the option "-c". This option is passed to the "catch all" input filter when lpr is called with the option "-l" meaning "pass the file transparently". So the result of commands lpr -Plpt somefile and lpr -Plp -l somefile would be almost identical except that in the first case the Form Feed at the end of file would be added always and in the second case only if it's missing in the file. Other file types can be specified with other options of lpr: -c - cifplot -d - DVI (TeX) -g - plot -n - ditroff -f - source code in Fortran -t - troff -v - raster image For each of these types a separate input filter can be defined. The name of the filter configuration parameter corresponds to its option name, "cf" for "-c" and so on. These options were born in the old times when many different page description languages were widespread and thus different conversions had to be done for different printers. Nowadays PostScript is the common language and most programs can generate data directly in PostScript. So the best approach is to make all the printers understand PostScript, and then reuse these options for some more interesting things. And anyway quite a few of these old file formats are obsolete by now, so reusing their options for something more useful looks like a good idea. I like to reuse the option "-d" to enable the duplex mode (printing on both sides of a page) and "-n" to disable it. If no option is specified, the printer's default setting will be used. (And no, the DVI format is not obsolete but I can use dvi2ps manually to print the TeX files). Logically it would be a pipeline from two filters, like: -d: cat file | /usr/local/lp/lj | /usr/local/lp/duplex on -n: cat file | /usr/local/lp/lj | /usr/local/lp/duplex off But the print subsystem calls only one filter and does not allow such pipelines. So we make a workaround: we would call "duplex" with the name of the preceding filter as an argument, like: -d: cat file | /usr/local/lp/duplex on lj -n: cat file | /usr/local/lp/duplex off lj But that still does not work because no arguments for the filters can be specified in /etc/printcap. The next step is a workaround of a workaround: encode the argument in the name of the filter, like: -d: cat file | /usr/local/lp/dp.on.lj -n: cat file | /usr/local/lp/dp.off.lj And then create symbolic links with these encoded names to the actual duplex filter: ln -s /usr/local/lp/duplex /usr/local/lp/dp.on.lj ln -s /usr/local/lp/duplex /usr/local/lp/dp.off.lj The source code of the duplex filter itself is: #!/bin/sh # # make a link with the name # dp.{on|off}.real_filter nm=`basename "$0"` FLTDIR=`echo "$0" | sed 's|/[^/]*$|/|'` SWITCH=`expr "x$nm" : 'x.*\.\(.*\)\..*'` FILTER=`expr "x$nm" : 'x.*\..*\.\(.*\)'` case "$SWITCH" in on|ON) printf '\033%%-12345X@PJL SET DUPLEX=ON\n' printf '@PJL SET BINDING=LONGEDGE\n' printf '\033E' ;; off|OFF) printf '\033%%-12345X@PJL SET DUPLEX=OFF\n' printf '\033E' ;; *) echo "duplex: unknown switch, must be on or off" >&2 exit 1 ;; esac ${FLTDIR}${FILTER} && printf '\033%%-12345X\033E' This script uses PJL, the HP Printer Job Language of the LaserJet5-compatible printers, to control the printer settings for a job. Actually, that trick with the Form Feed I did in the "lj" filter was not strictly neccessary: I could have instead wrapped the job into a PJL wraparound with the same result except that missing FormFeed at the end of job causes a little delay when printing. The resulting printcap entry would look: lp|local line printer:\ :sh:mx#0:\ :if=/usr/local/lp/lj:\ :df=/usr/local/lp/dp.on.lj:\ :nf=/usr/local/lp/dp.off.lj:\ :lp=/dev/lpt0:sd=/var/spool/output/lpd:lf=/var/log/lpd-errs: To define a logical printer with landscape orientation we use the same trick: lpl|local line printer - landscape orientation:\ :sh:mx#0:\ :if=/usr/local/lp/dpl.off.lj:\ :df=/usr/local/lp/dpl.on.lj:\ :nf=/usr/local/lp/dpl.off.lj:\ :lp=/dev/lpt0:sd=/var/spool/output/lpl:lf=/var/log/lpd-errs: Note that for this printer if the duplex option is not specified, the duplex will be disabled by default while for the printer "lp" it was left to the printer's default. The scripts dpl.* are symbolic links to duplex-lsc: #!/bin/sh # # make a link with the name # dpl.{on|off}.real_filter nm=`basename "$0"` FLTDIR=`echo "$0" | sed 's|/[^/]*$|/|'` SWITCH=`expr "x$nm" : 'x.*\.\(.*\)\..*'` FILTER=`expr "x$nm" : 'x.*\..*\.\(.*\)'` case "$SWITCH" in on|ON) printf '\033%%-12345X@PJL SET DUPLEX=ON\n' printf '@PJL SET BINDING=SHORTEDGE\n' printf '@PJL SET ORIENTATION=LANDSCAPE\n' printf '\033E' ;; off|OFF) printf '\033%%-12345X@PJL SET DUPLEX=OFF\n' printf '@PJL SET ORIENTATION=LANDSCAPE\n' printf '\033E' ;; *) echo "duplex-lsc: unknown switch, must be on or off" >&2 exit 1 ;; esac ${FLTDIR}${FILTER} && printf '\033%%-12345X\033E' This script is very like "duplex" except that it always explicitly sets the page orientation and specifies different binding direction for duplex. I suppose there should be a better way to specify a combination of job control settings as arguments for a single script but in this case creation of a separate script for landscape orientation looked simpler. Finally we get to the PostScript support. Of course, one way to get it is to buy a built-in PostScript module for the printer. Another way is to use the Ghostscript converter. I consider using Ghostscript a far better solution. First, it's free compared to something like $200 for a built-in module plus some money for extra printer memory to let this module run. Second, the CPUs in modern PCs are usually much faster than in the printers, and the rasterisation of PostScript page desccriptions is quite CPU-intensive. Of course, consuming the CPU cycles on a server machine is not such a good idea but for a workstation this is fine. Third, Ghostscript contains less bugs and implements more advanced levels of the PostScript language than most of the PostScript modules for printers. Fourth, as the new features are intorduced into PostScript, the Ghostscript can be easily upgraded while the built-in implementations are usually not upgradable, their only upgrade path is buying a new printer. Fifth, if a PostScript file is buggy it's much easier to get diagnostics from Ghostscript than from a printer. Since I defined multiple logical printers for various resolutions, I needed multiple Ghostscript filter scripts as well, defined in /etc/printcap as: lp4|local line printer:\ :sh:mx#0:\ :if=/usr/local/lp/gs4:\ :df=/usr/local/lp/dp.on.gs4:\ :nf=/usr/local/lp/dp.off.gs4:\ :lp=/dev/lpt0:sd=/var/spool/output/lp4:lf=/var/log/lpd-errs: lp4150|local line printer:\ :sh:mx#0:\ :if=/usr/local/lp/gs4150:\ :df=/usr/local/lp/dp.on.gs4150:\ :nf=/usr/local/lp/dp.off.gs4150:\ :lp=/dev/lpt0:sd=/var/spool/output/lp4150:lf=/var/log/lpd-errs: lp4600|local line printer:\ :sh:mx#0:\ :if=/usr/local/lp/gs4600:\ :df=/usr/local/lp/dp.on.gs4600:\ :nf=/usr/local/lp/dp.off.gs4600:\ :lp=/dev/lpt0:sd=/var/spool/output/lp4600:lf=/var/log/lpd-errs: lp5g|Poscscript - best gray:\ :sh:mx#0:\ :if=/usr/local/lp/gs5gray:\ :df=/usr/local/lp/dp.on.gs5gray:\ :nf=/usr/local/lp/dp.off.gs5gray:\ :lp=/dev/lpt0:sd=/var/spool/output/lp5g:lf=/var/log/lpd-errs: lp5m|lpps|ps|Postscript - best mono:\ :sh:mx#0:\ :if=/usr/local/lp/gs5mono:\ :df=/usr/local/lp/dp.on.gs5mono:\ :nf=/usr/local/lp/dp.off.gs5mono:\ :lp=/dev/lpt0:sd=/var/spool/output/lp5m:lf=/var/log/lpd-errs: Note that I reused the same duplex wrapper as for the simpler filters. Internally I made these Ghostscript filters call a common script /usr/local/lp/gsfilter with varying options: gs4: #!/bin/sh exec /usr/local/lp/gsfilter -sDEVICE=ljet4 -r300 gs4150: #!/bin/sh exec /usr/local/lp/gsfilter -sDEVICE=ljet4 -r150 gs4600: #!/bin/sh exec /usr/local/lp/gsfilter -sDEVICE=ljet4 -r600 gs5gray: #!/bin/sh exec /usr/local/lp/gsfilter -sDEVICE=lj5gray -r600 gs5mono: #!/bin/sh exec /usr/local/lp/gsfilter -sDEVICE=lj5mono -r600 My first version of gsfilter was simple: #!/bin/sh exec /usr/local/bin/gs -sOutputFile=- -dSAFER -dNOPAUSE -q "$@" -_ Note the option -dSAFER: the filters run not with the privileges of the user who submitted the job but as pseudo-user "daemon", the owner of many system processes. If this option would be not specified, that would allow malicious users to overwrite many files on the machine. Actually, even with -dSAFER present there still is an issue left that any user may read too many sensitive files. That's not such a big issue for my home machine but in production environment you should run Ghostscript in a chroot-ed cage. But this script turned out being not very good: when Ghostscript finds some bug in the file being printed it puts the error message to its standard output where it gets mixed with the result of the conversion and usually causes the printer to waste a lot of paper printing gibberish. Since Ghostscript is unable to write its error messages to its standard error file descriptor, the only way to separarate them from the normal output was to write the normal output somewhere else and do a redirection: #!/bin/sh exec nice -1 /usr/local/bin/gs -sOutputFile=/dev/fd/3 -dSAFER \ -dNOPAUSE -q "$@" -_ 3>&1 >&2 Here /dev/fd/3 is a special system pseudo-device that is connected to the 3rd file descriptor of the process that opens it. I also added the "nice" command to educe interference of the filters with performance of the foreground processes. This version of the script is able to produce decent error messages which are as usually send by e-mail to the owner of the job. For convenience I also added Ghostscript filters to the printers "lp" and "lpl" when they are requested with options "-v" and "-g". So the full resulting /etc/printcap is: # forced duplex goes as -d, simplex ("normal") as -n # -v for Ghostscript in mono images # -g for Ghostscript in gray-scale images # to combine duplex and Ghostscript use other printers lp|local line printer:\ :sh:mx#0:\ :if=/usr/local/lp/lj:\ :df=/usr/local/lp/dp.on.lj:\ :nf=/usr/local/lp/dp.off.lj:\ :vf=/usr/local/lp/gs5mono:\ :gf=/usr/local/lp/gs5gray:\ :lp=/dev/lpt0:sd=/var/spool/output/lpd:lf=/var/log/lpd-errs: lpl|local line printer - landscape orientation:\ :sh:mx#0:\ :if=/usr/local/lp/dpl.off.lj:\ :df=/usr/local/lp/dpl.on.lj:\ :nf=/usr/local/lp/dpl.off.lj:\ :vf=/usr/local/lp/dpl.on.gs5mono:\ :gf=/usr/local/lp/dpl.on.gs5gray:\ :lp=/dev/lpt0:sd=/var/spool/output/lpl:lf=/var/log/lpd-errs: # this one has no filters, just pass-through lpt|local line printer - transparent:\ :sh:mx#0:\ :lp=/dev/lpt0:sd=/var/spool/output/lpt:lf=/var/log/lpd-errs: # all the variations as separate printers # forced duplex goes as -d, simplex ("normal") as -n lp4|local line printer:\ :sh:mx#0:\ :if=/usr/local/lp/gs4:\ :df=/usr/local/lp/dp.on.gs4:\ :nf=/usr/local/lp/dp.off.gs4:\ :lp=/dev/lpt0:sd=/var/spool/output/lp4:lf=/var/log/lpd-errs: lp4150|local line printer:\ :sh:mx#0:\ :if=/usr/local/lp/gs4150:\ :df=/usr/local/lp/dp.on.gs4150:\ :nf=/usr/local/lp/dp.off.gs4150:\ :lp=/dev/lpt0:sd=/var/spool/output/lp4150:lf=/var/log/lpd-errs: lp4600|local line printer:\ :sh:mx#0:\ :if=/usr/local/lp/gs4600:\ :df=/usr/local/lp/dp.on.gs4600:\ :nf=/usr/local/lp/dp.off.gs4600:\ :lp=/dev/lpt0:sd=/var/spool/output/lp4600:lf=/var/log/lpd-errs: lp5g|Poscscript - best gray:\ :sh:mx#0:\ :if=/usr/local/lp/gs5gray:\ :df=/usr/local/lp/dp.on.gs5gray:\ :nf=/usr/local/lp/dp.off.gs5gray:\ :lp=/dev/lpt0:sd=/var/spool/output/lp5g:lf=/var/log/lpd-errs: lp5m|lpps|ps|Postscript - best mono:\ :sh:mx#0:\ :if=/usr/local/lp/gs5mono:\ :df=/usr/local/lp/dp.on.gs5mono:\ :nf=/usr/local/lp/dp.off.gs5mono:\ :lp=/dev/lpt0:sd=/var/spool/output/lp5m:lf=/var/log/lpd-errs: Note that printing in landscape orientation through the Ghostscript filter makes no sense because Ghosctscript will override the page orientation anyway. But setting the duplex binding direction to landscape mode still makes sense, so the Ghostscript filters are defined for the printer "lpl" with duplex on. Other small things ------------------ Banners ------- For my home printer I don't need banners. But at work I use a network printer and I need the banners to distinguish my printer jobs from the others'. Lpd provides some rudimentary banners but they are inadequate: they are in plain text format and badly confuse the PostScript printers. This rudimentary banner could be wrapped into a PostScript page but worse yet, it's generated only if the output filter is used. And as we have seen, the output filter is something unusable nowadays. But the input filters can be used in a slightly unorthodoxal way to generate banners as well. When a filter runs it gets a few arguments from lpd. These arguments are different for the filter "if" and all other filters: if_filter [-c] -wwidth -llength -iindent -n login -h host acct-file other_filter -xwidth -ylength -n login -h host acct-file The options -w, -l, -i, -x and -y are not interesting for our purposes so we can just ignore them. The option -c was described earlier, during discussion of my "lj" filter. User and host are what we need for banner and "acct-file" is the value of the capability "af" in /etc/printcap - it can be used to pass some printer-specific information to the script. I use it to pass the printer name and then use this name to extract more information about the job. Though of course this data can also be passes in the name of the filter as in the previous examples. But if this value is the same for all the filters on a printer then placing it in "af" lets us avoid repeating it for each filter. The printer definition looks like: tpb346r|PostScript printer (transparent):\ ... :if=/usr/local/lp/filter/pstflt:\ :af=tpb346r:\ ... and the banner printing is done in the filter as follows: #!/bin/sh BASEDIR=/usr/local/lp/filter prtbanner() { cat $BASEDIR/banner.ps echo "/smallbanner {" if [ -n "$LPRINTER" ] then { echo "" echo "" echo "Host: $HOST" echo "User: $USER" echo "Printer: $LPRINTER" echo "Job: " echo "" lpq -P$LPRINTER -l | sed '1,2d;/: 1st/,$d' } | $BASEDIR/bnr2ps fi echo "} def" echo "(`date '+%a %H:%M %h %d, %Y'`)" echo "(`hostname`)" echo "banner" } args=`getopt cw:l:i:n:h:x:y: $*` [ $? -eq 0 ] || exit 1 set -- $args for i do case "$i" in -c) shift;; -w|-l|-i|-x|-y) shift; shift;; -n) USER="$2"; shift; shift;; -h) HOST="$2"; shift; shift;; --) shift; break;; esac done LPRINTER=$1 prtbanner /bin/cat Here banner.ps is a PostScript file with prototype banner having the standard look defined by our IT department and bnr2ps is a small program converting the data into the arguments format expected by that PostScript file. Note how the "lpq" command it used in an unusual way, to get the name of the current job by the printer name. This trick would not have been required if lpd would pass this information directly. Network printing ---------------- The capabilities "rm" and "rp" in /etc/printcap are used in /etc/printcap to print to a remote printer. "rm" specifies the name of the print server host, "rp" is the printer name at that host. And of course the capability "lp" is not needed for a remote printer. Only the "if" filters are used at both local host before sending the data and at remote host before printing it (or the "of" filters, but again "of" is mostly useless nowadays). Using any other filters is not permitted. The file /etc/hosts.lpd is used on the server to control the access to the printers from remote hosts. The typical /etc/printcap entries would look like: On the server: lpt|local line printer - transparent:\ :sh:mx#0:\ :if=/usr/local/lp/cat:\ :lp=/dev/lpt0:sd=/var/spool/output/lpt:lf=/var/log/lpd-errs: With /usr/local/lp/cat containing: #!/bin/sh exec /bin/cat On the client: rp|remote printer:\ :sh:mx#0:\ :if=/usr/local/lp/lj:\ :rm=remote.host.at.domain:rp=lpt:\ :sd=/var/spool/output/rp:lf=/var/log/lpd-errs: Using of "cat" as input filter on one side is required to avoid printing of an extra empty page after request because if no filter is specified, lpd adds a Form Feed by itself. And no, "cat" can not be just specified as the parameter "if" because the input filters are called with arguments totally unexpected by "cat". Another and probably better way is to use a PJL job control filter (assuming that the printer is HP-compatible): lpt|local line printer - transparent:\ :sh:mx#0:\ :if=/usr/local/lp/job:\ :lp=/dev/lpt0:sd=/var/spool/output/lpt:lf=/var/log/lpd-errs: With /usr/local/lp/job containing: #!/bin/sh printf '\033%%-12345X@PJL\n\033E' /bin/cat printf '\033%%-12345X\033E' Some hardware print servers use strange proprietary network protocols. Their manufacturers usually provide the source code of their communication programs. To use these programs a local printer may be defined with a filter that will pass the data to this communication program instead of passing it through. The printer definition would look like: lpw|network printer - weird:\ :sh:mx#0:\ :if=/usr/local/lp/redirect:\ :lp=/dev/null:sd=/var/spool/output/lpw:lf=/var/log/lpd-errs: Note that no data ever passes through the filter, so the output device is set to /dev/null just to make lpd happy. And the script /usr/local/lp/redirect would look somewhat like: #!/bin/sh /usr/local/lp/proprietary_program server_name printer_port An interesting twist is to use another call to lpr instead of a proprietary program, that would allow specifying various file types for an usual network printer. For example: rpf|network printer - with many filters:\ :sh:mx#0:\ :if=/usr/local/lp/dp.off.redirect:\ :df=/usr/local/lp/dp.on.redirect:\ :nf=/usr/local/lp/dp.off.redirect:\ :lp=/dev/null:sd=/var/spool/output/rpf:lf=/var/log/lpd-errs: With the script /usr/local/lp/redirect containing: #!/bin/sh lpr -Prp LPRng ----- A new version of the print spooling subsystem is available from Patrick Powell, the next generation is named LPRng. It seems like all the problems I described and more are successfully solved in it but I did not look closer at it yet, so I can't say for sure. Please look at http://www.lprng.org/ for more information. See also -------- Some information about PJL can be found by searching at the HP Web site. Slightly more detailed description of PJL is available in the book "Developer's Guide to HP Pinters" by Norman E. Smith, ISBN 1-55622-603-9. Though generally this book is not quite useful and a better name for it would be "Guide to HP Printers for Complete Idiots". Another interesting thing is that different printers implement different subsets of PJL. Printers -------- Anticipating the question, what do I mean as "affordable duplex printers", I answer: NEC 1800. Initially I was looking for some cheap model but when I saw a duplex printer at a reasonable price I just could not pass by it. Compared to HP 2100, NEC 1800 has duplex mode and a real front panel (not just a stupid button) and larger standard memory (that it, not requiring to buy more memory immediately together with the printer) and higher speed and higher duty cycle and lower toner cost per page and lower price of the printer itself. And all these wonderful things for $700, including shipping, not including the sales tax. So I just felt tantalized. I've bought it over half a year ago and I'm still quite happy with it.