#!/usr/local/bin/bash # lpr-wrapper --- a wrapper around the lpr command, which can handle # any-to-postscript conversion, options for duplex and n-up printing, # and printer-specific options (such as trays and papertypes). # # Author: Peter Selinger, selinger@uottawa.ca # License: GNU Public License # Copyright (C) 2001-2004 Peter Selinger # The background for this script is that (a) most printing packages # (such as LPRng), which presumably can handle options like duplex # printing, actually do a rotten job at it, and (b) in today's # environment, all print jobs are eventually converted to PostScript, # so there is no reason not to do it as part of the lpr command, # rather than rely on the print spooler to do it. # # We also implement improved n-up printing via the psdim command; this # detects the actual margins of a document and shrinks it only to the # extent necessary, rather than shrinking everything to a standard # microscopic size. Note that 1-up printing is a special case and a # convenient way of fitting the document to the printer's paper size; # this might shrink the document, but not enlarge it. The default is to # do nothing, i.e., just pipe the document through. # # An additional feature is input filtering: this script can accept dvi, pdf, # plain text, and various types of compressed files, and converts them to # postscript automatically. The file type is determined by looking at the # content, rather than the name, of the file, so (unlike a2ps) it works # properly if the file comes from stdin. # Requirements: recommended version # GNU bash 2.04.21(1)-release # lpr 0.50-7 (bsd compatible) # ppdfilt 0.4 # pstops from psutils 1.17 # file 3.33 # mktemp 1.5 # sed GNU 3.02 # # Optional: # getopt (enhanced) 1.1.0 # psdim 1.2 # ghostscript (gs) 5.50 (used by psdim and pdf2ps) # mpage 2.5.1pre2 # pdf2ps (comes with ghostscript) # dvips 5.86 p1.5d # gzip 1.3 # bzip2 1.0.1 # # Some of these utilities are optional. If psdim is not present, we # revert to "dumb" settings for n-up printing. If getopt is not # present, we use "dumb" option parsing, which means the user has to # type something like -a -b instead of -ab. If no appropriate # printfilters are available, we simply reject that kind of input. # However, postscript input (the most common case) can always be # handled. # How to install this script: # # 1) get and install the required software listed above. I also # recommend getting the optional software, particularly "psdim", # which enables *greatly* prettier n-up printing. # # 2) get each printer's postscript definition (ppd) file. It typically # comes on the printer manufacturer's CD-ROM. You can also often # download it from the manufacturer's web page. Rename the file as # $PRINTER.ppd, for instance, "lp0.ppd". Install the file # in some appropriate directory, for instance, # /usr/local/share/ppd/. Edit the "PPDDIR" line below to point to # this directory. You can also have a default ppd file called # default.ppd, which will act as a catchall for any unknown # printer. # # 3) find out where 'lpr' is installed - here we use /usr/bin/lpr as # an example. Install the lpr-wrapper script as /usr/bin/lpr-wrapper. # Rename /usr/bin/lpr as /usr/bin/lpr-orig. Create a symbolic link # from /usr/bin/lpr to /usr/bin/lpr-wrapper. Edit the "LPR" line # below to point to the "real" lpr, for instance, /usr/bin/lpr-orig. # # 4) Optional: change the other configurable parameters below, which are # the locations of the default configuration files, and the default # "tmp" directory. # # 5) Optional: set up a system-wide configutation file (default # /etc/lprrc) with your default options. If your site prefers # duplex printing, put "-od" into this file, if your site prefers # A4 paper, put "-oa4", and so on. Notice that individual users can # override these options. # ---------------------------------------------------------------------- # the following settings are configurable: # "real" lpr program to use (this should be BSD compatible). If this # is unspecified, we proceed as follows: if our *own* name is not lpr, # we try lpr, else we try lpr-orig. #LPR=lpr-orig # directory relative to which we are installed prefix=@prefix@ # directory where postscript printer definitions are found PPDDIR=@datadir@/@PACKAGE@ # global options file SYST_CONFIG=@prefix@/etc/lprrc # user's options file USER_CONFIG=.lprrc # default directory for temporary files TMPDIR=/tmp # end of configurable settings # ---------------------------------------------------------------------- # figure out the name of the "real" lpr program to use as a backend. NAME=`basename $0` if [ "$LPR" = "" ]; then if [ "$NAME" = "lpr" ]; then LPR=lpr-orig else LPR=lpr fi fi # ---------------------------------------------------------------------- # version information NAME=`basename $0` VERSION="@VERSION@" DATE="@DATE@" # ---------------------------------------------------------------------- # setup for temporary files # ensure privacy of temporary files umask 077 # a template for temporary filenames, containing process id to allow # easy cleanup TEMPLATE=lpr-wrapper-$$ # make a temporary file tmpfile () { mktemp $TMPDIR/$TEMPLATE.XXXXXX } # clean up all temp-files on exit (even after error or SIGINT) cleanup () { rm -f $TMPDIR/$TEMPLATE.?????? } trap cleanup EXIT # ---------------------------------------------------------------------- # check for available features have () { if which $1 2> /dev/null 1>&2; then echo yes fi } # list of required and optional features: REQUIRED="bash $LPR ppdfilt pstops file mktemp sed" OPTIONAL="getopt psdim mpage gzip bzip2 dvips pdf2ps" for i in $REQUIRED; do if [ -z "`have $i`" ]; then echo "$NAME: Error: $i is required, but not installed" > /dev/stderr exit 1 fi done for i in $OPTIONAL; do HAVE[$i]=`have $i` done # ---------------------------------------------------------------------- # auxiliary functions # quote: concatenate arguments, then escape special bash characters. quote () { echo "$@" | sed 's/\\/\\\\/g;s/\"/\\\"/g;s/\$/\\\$/g;s/(/\\(/g;s/)/\\)/g;s/~/\\~/g;'s/"'"/"\\\'"/g';s/\ /\\\ /g;s/`/\\`/g;s//\\>/g;s/\;/\\\;/g;s/|/\\|/g;s/?/\\?/g;s/\*/\\\*/g;s/\[/\\\[/g;s/&/\\&/g;s/^$/\"\"/' } # ---------------------------------------------------------------------- # part 1: read command line DEBUG_FILE=/dev/null LPROPTS="" PPDOPTS="" PPDFILE="" FILES="" DUPLEX= PAPER= TRAY= # UP=0 means no processing, UP=1 means 1-up printing (fit-to-size) UP=0 BOX= INFO= BACKEND="" DUMB="" REMOVE="" UNCOLLATED="" NUMCOPIES=1 usage() { echo -n "\ Usage: $NAME [options] [name ...] Enhanced lpr with options for duplexing and n-up printing. Options: same options as for lpr, plus: --help print this message and exit --version print version info and exit --verbose print some diagnostics to stderr --test send output to stdout instead of printer --ppd FILE postscript printer definition file to use --tmpdir TMPDIR choose directory for temporary files -oduplex, -od duplex printing (with long edge binding) -ohduplex, -oh duplex printing (with short edge binding) -osimplex, -os no duplex printing (default) -oPAPERSIZE select a paper size: letter, executive, legal, a3, a4, a5, b5, tabloid, statement, folio, quarto, 10x14 (default is letter) -oTRAY select a tray: upper, middle, lower, manual -o2up 2-up printing -o4up 4-up printing -o8up 8-up printing -o9up 9-up printing -o16up 16-up printing -odumb faster, but uglier n-up printing (don't fit to size) -ofit fit to size in 1-up mode -obox print a box around each page (plain text only) -oinfo print date and user name on each page (plain text only) -oPARAMETER:VALUE set arbitrary parameter defined for your printer -ouncollated with -#, make NUM copies of each page, not of each file Note that -duplex and -hduplex refer to whether you turn the page on the long edge or on the short edge, with respect to the *input* page. That is, if you select -o2up, you should still select -oduplex, just as you would if you hadn't selected -o2up. But if your input file is in landscape format, you should probably select -ohduplex. The following standard lpr options are also supported: -P PRINTER direct output to a specific printer -h do not print burst page -m send mail upon completion -r remove the file upon completion of spooling -# NUM number of copies to make of each file -[1234] FONT specify a font to be mounted on font position i -C CLASS job classification to use on the burst page -J JOB job name to print on the burst page -U USER user name to print on the burst page and for accounting. This option is only honored if the real user-id is daemon (or that specified in the printcap file instead of daemon), and is intended for those instances where print filters wish to requeue jobs. The following options are ignored for backwards compatibility: -T TITLE, -i NUMCOLS, -w NUM, -c, -d, -f, -g, -l, -n, -p, -t, -v, -s Command line options are also read from the two files $SYST_CONFIG and \$HOME/$USER_CONFIG, if they exist, and processed in that order before any other command line options are processed. For instance, if you like duplex printing to be your default, put \"-od\" into one of these files, or if you would like to use a private temp directory, put \"--tmpdir TMPDIR\". You can also use this to set the default paper size. A PPD printer description file is necessary for each printer. These files should be located in $PPDDIR, and should be named .ppd. If a file default.ppd exists in this location, it is used for all printers that don't have a custom ppd file. You can also use the --ppd option to specify a specific PPD file to be used. " } # read options dopts () { while [ $# -gt 0 ]; do case $1 in -o ) doopts $2 shift 2 ;; -P ) PRINTER="$2" LPROPTS="$LPROPTS `quote $1` `quote $2`" shift 2 ;; -h | -m ) LPROPTS="$LPROPTS `quote $1`" shift 1 ;; -r ) REMOVE=1 shift 1 ;; -C | -J | -T | -U | -i | -w ) LPROPTS="$LPROPTS `quote $1` `quote $2`" shift 2 ;; # because of a special quirk in lpr, the following options *must* # pass there arguments *without* a white space to lpr. -1 | -2 | -3 | -4 ) LPROPTS="$LPROPTS `quote $1$2`" shift 2 ;; -# ) NUMCOPIES="$2" shift 2 ;; -c | -d | -f | -g | -l | -n | -p | -t | -v | -s ) echo $NAME: Warning: option $1 ignored > /dev/stderr shift 1 ;; -T | -i | -w ) echo $NAME: Warning: option $1 ignored > /dev/stderr shift 2 ;; --help ) usage exit 0 ;; --test ) BACKEND=test shift 1 ;; --version ) echo lpr-wrapper $VERSION by Peter Selinger, $DATE. exit 0 ;; --verbose ) DEBUG_FILE=/dev/stderr shift 1 ;; --ppd ) PPDFILE="$2" shift 2 ;; --tmpdir ) TMPDIR="$2" shift 2 ;; -- ) shift 1 break ;; -* ) echo "$NAME: invalid option: $1" > /dev/stderr echo "Try --help for more information" > /dev/stderr exit 1 ;; * ) # getopt must be missing and this is a filename break ;; esac done # done with options, now read filenames while [ $# -gt 0 ]; do FILES="$FILES `quote $1`" shift done # end of dopts () } # read -o options doopts () { case $@ in d | duplex ) DUPLEX=DuplexNoTumble ;; h | hduplex ) DUPLEX=DuplexTumble ;; s | simplex ) DUPLEX=None ;; letter ) PAPER=Letter ;; executive ) PAPER=Executive ;; legal ) PAPER=Legal ;; tabloid ) PAPER=Tabloid ;; statement ) PAPER=Statement ;; folio ) PAPER=Folio ;; quarto ) PAPER=Quarto ;; 10x14 ) PAPER=10x14 ;; a4 ) PAPER=A4 ;; a3 ) PAPER=A3 ;; a5 ) PAPER=A5 ;; b5 ) PAPER=B5 ;; upper ) TRAY=Upper ;; middle ) TRAY=Middle ;; lower ) TRAY=Lower ;; manual ) TRAY=ManualFeed ;; fit ) UP=1 ;; 2up ) UP=2 ;; 4up ) UP=4 ;; 8up ) UP=8 ;; 9up ) UP=9 ;; 16up ) UP=16 ;; dumb ) DUMB=1 ;; box ) BOX=1 ;; info ) INFO=1 ;; uncollated ) UNCOLLATED=1 ;; *:* ) PPDOPTS="$PPDOPTS --option `quote $@`" ;; * ) echo "$NAME: invalid option: -o$@" > /dev/stderr echo "Try --help for more information" > /dev/stderr exit 1 ;; esac # end of doopts () } # first read options from configuration files, if possible XOPTS="`cat $SYST_CONFIG 2>/dev/null` `cat $HOME/$USER_CONFIG 2>/dev/null`" # if getopt is available, use it to pre-process the options. If not, # the user has to give the options in dumb format (all options first, # with options and arguments separated by whitespace, followed by # optional '--' and filenames) OPTSTRING=o:P:#:K:C:J:T:U:i:1:2:3:4:w:cdfghlnmprstv LONGOPTS=help,version,verbose,test,ppd:,tmpdir: #if [ "$HAVE[getopt]" ]; then # OPTIONS=`getopt -n $NAME -s bash -l $LONGOPTS -o $OPTSTRING -- $XOPTS "$@"` # if [ $? != 0 ]; then # echo "Try --help for more information" > /dev/stderr # exit 1 # fi # eval set -- "$OPTIONS" # dopts "$@" #else dopts $XOPTS "$@" #fi #if no printer given (as option or through environment), use default if [ x"$PRINTER" == x ]; then PRINTER=lp fi # invert duplex mode (tumble vs. notumble) if 2up or 8up selected if [ x"$UP" == x2 ] || [ x"$UP" == x8 ]; then if [ x"$DUPLEX" == xDuplexTumble ]; then DUPLEX=DuplexNoTumble elif [ x"$DUPLEX" == xDuplexNoTumble ]; then DUPLEX=DuplexTumble fi fi # set up remaining PPDOPTS and LPROPTS (options to pass to ppdfilt and # lpr, respectively if [ x"$DUPLEX" != x ]; then PPDOPTS="$PPDOPTS --option `quote Duplex:$DUPLEX`" fi if [ x"$PAPER" != x ]; then PPDOPTS="$PPDOPTS --option `quote PageSize:$PAPER`" fi if [ x"$TRAY" != x ]; then PPDOPTS="$PPDOPTS --option `quote InputSlot:$TRAY`" fi if [ x"$NUMCOPIES" != x1 ]; then # ensure it is a number, else set to 1 NUMCOPIES=`expr "$NUMCOPIES" + 0 2> /dev/null || echo 1` if [ x"$UNCOLLATED" != x ]; then PPDOPTS="$PPDOPTS -c `quote $NUMCOPIES`" else # missing space after -# is essential LPROPTS="$LPROPTS -#`quote $NUMCOPIES`" fi fi # if we don't have psdim, we must use dumb nup-mode if [ -z "$HAVE[psdim]" ]; then DUMB=2 fi # check that PPD-file for printer exists if [ x"$PPDFILE" == x ]; then PPDFILE="$PPDDIR/$PRINTER.ppd" if [ ! -r "$PPDFILE" ]; then PPDFILE="$PPDDIR/default.ppd" fi fi if [ ! -r "$PPDFILE" ]; then echo "$NAME: Error: no PPD file found for this printer." > /dev/stderr echo "Please install it in $PPDDIR/$PRINTER.ppd or use --ppd option." > /dev/stderr exit 1 fi echo LPROPTS=$LPROPTS > $DEBUG_FILE echo PPDOPTS=$PPDOPTS > $DEBUG_FILE echo FILES=$FILES > $DEBUG_FILE echo PRINTER=$PRINTER > $DEBUG_FILE echo DUPLEX=$DUPLEX > $DEBUG_FILE echo UP=$UP > $DEBUG_FILE echo BOX=$BOX > $DEBUG_FILE echo INFO=$INFO > $DEBUG_FILE echo PAPER=$PAPER > $DEBUG_FILE echo TRAY=$TRAY > $DEBUG_FILE echo PPDFILE=$PPDFILE > $DEBUG_FILE echo BACKEND=$BACKEND > $DEBUG_FILE echo REMOVE=$REMOVE > $DEBUG_FILE echo DUMB=$DUMB > $DEBUG_FILE echo UNCOLLATED=$UNCOLLATED > $DEBUG_FILE echo NUMCOPIES=$NUMCOPIES > $DEBUG_FILE echo DEBUG_FILE=$DEBUG_FILE > $DEBUG_FILE echo ----------------- > $DEBUG_FILE # ---------------------------------------------------------------------- # part 2: convert input to generic postscript. # the output is fed to the command $DOWNSTREAM. This way, if there is # an error, we don't get a broken pipe. # the following code is inspired by rhs-printfilters (redhat) filter () { # figure out the magic of the input file # we work with temporary files, rather than rewinding a stream - # it is just so much clearer that way! TMP=`tmpfile` if [ $? != 0 ]; then echo "$NAME: Error: not create temporary file" > /dev/stderr exit 1 fi cat > $TMP magic=$(file $TMP) magic=${magic#*: } case `echo $magic | tr 'A-Z' 'a-z'` in *bzip2* ) if [ "$HAVE[bzip2]" ]; then bzip2 -dc $TMP | filter else echo "$NAME: Can't print this type of file, because bzip2 not installed:" > /dev/stderr echo "$magic" > /dev/stderr fi ;; *packed*|*gzip*|*compress* ) if [ "$HAVE[gzip]" ]; then gzip -dc $TMP | filter else echo "$NAME: Can't print this type of file, because gzip not installed:" > /dev/stderr echo "$magic" > /dev/stderr fi ;; postscript* ) cat $TMP | $DOWNSTREAM ;; "tex dvi file"* ) if [ "$HAVE[dvips]" ]; then dvips -q $TMP -o - | $DOWNSTREAM else echo "$NAME: Can't print this type of file, because dvips not installed:" > /dev/stderr echo "$magic" > /dev/stderr fi ;; *pdf* ) if [ "$HAVE[pdf2ps]" ]; then pdf2ps $TMP - | $DOWNSTREAM else echo "$NAME: Can't print this type of file, because pdf2ps not installed:" > /dev/stderr echo "$magic" > /dev/stderr fi ;; * ) # check whether it is text if echo $magic | egrep -qi ".*(ascii|text|english|script).*"; then ISTEXT="yes" elif egrep -aq '[^ [:print:]]' $TMP; then ISTEXT="" else ISTEXT="yes" fi if [ -z "$ISTEXT" ]; then echo "$NAME: Don't know how to print this type of file:" > /dev/stderr echo "$magic" > /dev/stderr elif [ "$HAVE[mpage]" ]; then MPAGEOPTS="" # we handle an interesting special case: if n-up printing # is selected for a text file, mpage does a better job at # it than pstops. So we handle it here, then reset the UP # variable to 0. This works because our downstream # function is not called until after this code is # evaluated! (However, mpage can only handle UP=1,2,4,8, # so we do the remaining cases UP=9,16 in pstops anyway) if [ "$UP" == 1 ] || [ "$UP" == 2 ] || [ "$UP" == 4 ] || [ "$UP" == 8 ]; then MPAGEOPTS="$MPAGEOPTS -$UP" UP=0 fi if [ -z "$BOX" ]; then MPAGEOPTS="$MPAGEOPTS -o" fi if [ "$INFO" ]; then DATE=`date` INFOSTRING="`quote $FILENAME - printed by $USER on $DATE`" MPAGEOPTS="$MPAGEOPTS -H -h $INFOSTRING" fi eval set -- $MPAGEOPTS MPAGEPAPER="-b$PAPER" if [ "$PAPER" = "" ]; then MPAGEPAPER="" fi mpage -1 -f $MPAGEPAPER "$@" $TMP | $DOWNSTREAM else echo "$NAME: Can't print this type of file, because mpage not installed:" > /dev/stderr echo "$magic" > /dev/stderr fi ;; esac rm -f $TMP } # ---------------------------------------------------------------------- # part 3: convert to n-up # find format string for pstops if we don't have psdim. This currently # only works for letter size paper; I didn't have the patience to # figure this out for other paper sizes. dumbdim () { case ${UP} in 2 ) echo "2:0@0.647L(7.81in,0in)+1@0.647L(7.81in,5.5in)" ;; 4 ) echo "4:0@0.5(0in,5.5in)+1@0.5(4.25in,5.5in)+2@0.5(0in,0in)+3@0.5(4.25in,0in)" ;; 8 ) echo "8:0@0.3235L(3.905in,0in)+1@0.3235L(3.905in,2.75in)+2@0.3235L(3.905in,5.5in)+3@0.3235L(3.905in,8.25in)+4@0.3235L(8.155in,0in)+5@0.3235L(8.155in,2.75in)+6@0.3235L(8.155in,5.5in)+7@0.3235L(8.155in,8.25in)" ;; 9 ) echo "9:0@0.33(0in,7.333in)+1@0.33(2.833in,7.333in)+2@0.33(5.667in,7.333in)+3@0.33(0in,3.667in)+4@0.33(2.833in,3.667in)+5@0.33(5.667in,3.667in)+6@0.33(0in,0in)+7@0.33(2.833in,0in)+8@0.33(5.667in,0in)" ;; 16 ) echo "16:0@0.25(0in,8.25in)+1@0.25(2.125in,8.25in)+2@0.25(4.25in,8.25in)+3@0.25(6.375in,8.25in)+4@0.25(0in,5.5in)+5@0.25(2.125in,5.5in)+6@0.25(4.25in,5.5in)+7@0.25(6.375in,5.5in)+8@0.25(0in,2.75in)+9@0.25(2.125in,2.75in)+10@0.25(4.25in,2.75in)+11@0.25(6.375in,2.75in)+12@0.25(0in,0in)+13@0.25(2.125in,0in)+14@0.25(4.25in,0in)+15@0.25(6.375in,0in)" ;; * ) echo "1:0@1(0in,0in)" esac } # do n-up formatting from stdin to stdout nup () { if [ x$UP != x0 ]; then TMP=`tmpfile` if [ $? != 0 ]; then echo "$NAME: Error: cannot create temporary file" exit 1 fi cat > $TMP if [ x"$DUMB" == x ]; then case "$UP" in 2 ) SEP=.5in ;; 4 ) SEP=.35in ;; 8 ) SEP=.25in ;; 9 ) SEP=.24in ;; 16 ) SEP=.18in ;; *) SEP=.5in esac PSDIMPAPER="$PAPER" if [ "$PAPER" = "" ]; then PSDIMPAPER="letter" fi PSDIM=`psdim -q --${UP}up -s $SEP -p "$PSDIMPAPER" -S "$TMP"` # psdim will sometimes fail on strange postscript files; # in this case we use the default dimensions. if [ $? -ne 0 ]; then PSDIM=`dumbdim "$TMP"` fi else PSDIM=`dumbdim "$TMP"` fi pstops -q "$PSDIM" "$TMP" rm -f "$TMP" else cat fi } # ---------------------------------------------------------------------- # part 4: apply printer-and-job-specific postscript conversions ppd () { if [ "$PPDOPTS" = "" ]; then cat else eval set -- $PPDOPTS ppdfilt --ppd "$PPDFILE" "$@" 2> $DEBUG_FILE fi } # ---------------------------------------------------------------------- # part 5: send to printer, using the standard "lpr" program. # this is a dummy function for now. NOTE: Input is read from stdin, but the # filename is given as $FILENAME send () { if [ x"$BACKEND" == xtest ]; then cat else eval set -- $LPROPTS $LPR -l -T "$FILENAME" -J "$FILENAME" "$@" fi } # ---------------------------------------------------------------------- # part 6: iterate through a list of files # pipe combines all the above; it takes a filename as argument, and # reads the input file from stdin downstream () { nup | ppd | send } pipe () { DOWNSTREAM=downstream filter } if [ x"$FILES" == x ]; then FILENAME="(stdin)" pipe else eval set -- $FILES while [ $# -gt 0 ]; do if [ ! -e "$1" ]; then echo $NAME: $1: No such file or directory > /dev/stderr elif [ -d "$1" ]; then echo $NAME: $1: Is a directory > /dev/stderr elif [ ! -r "$1" ]; then echo $NAME: $1: Permission denied > /dev/stderr else FILENAME="$1" cat -- "$1" | pipe if [ x"$REMOVE" == x1 ]; then rm "$1" fi fi shift done fi