Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Design

A Print Filter for UNIX


SEP94: A Print Filter for UNIX

A Print Filter for UNIX

More power for your LaserJet 4M printer

Michael A. Covington and Mark Juric

Michael is an associate research scientist and manages the artificial-intelligence lab at the University of Georgia. He is the author of Natural Language Processing for Prolog Programmers (Prentice-Hall, 1994). Mark is a master's degree candidate and Sun system administrator at the University of Georgia. His specialties are genetic algorithms and neural networks. Contact the authors at [email protected].


Printers are getting smarter. The Hewlett Packard LaserJet 4M, for example, can print in three modes: PostScript, HP-control code, and plain ASCII. In this article, we will present lj4m, a UNIX print filter that enhances the power of this versatile printer and keeps it out of mischief. Among other things, our program performs the following:

Print Filtering

Our lj4m filter was developed under SunOS 4.1.3, but it should work under any UNIX-like operating system that uses /etc/printcap in the conventional way, including Linux.

Like any other filter, a print filter copies standard input to standard output, making appropriate changes along the way. The filter resides in any convenient directory (we used /etc), has global execute permissions, and is invoked in the printer's /etc/printcap entry.

Example 1(a) shows the printcap entry for our LaserJet 4M. Decoded, this means: "The default printer (lp) is connected to /dev/ttyb at 19,200 baud, with the litout and ixon stty parameters. Suppress formfeeds, suppress headers, allow unlimited length files, and communicate with /dev/ttyb in read/write mode. Filter each print job through /etc/lj4m (our program); use /var/spool/lpd as the spool directory; and use /dev/null as the accounting file."

Why use /dev/null as an accounting file? Because we want accounting information to be supplied to the filter (as command-line parameters; more about this in a moment), but we don't actually need an accounting file. If the af= line were not there, the filter would not receive accounting information.

Notice that our program is an input filter (if=), not an output filter (of=). The difference is that an input filter is started afresh for every file sent to the printer, while an output filter is started only once for a whole series of jobs and cannot handle the individual jobs separately.

For printing through a network, the input filter resides and runs only on the print server. The other machines send their print jobs to the server through the rp= printcap field and do not need if= fields.

What Kind of File am I?

Listing One is the C source code for the lj4m filter. The heart of the program is main(), which classifies each job by looking at its first two characters: %! for PostScript, Esc-E or Esc-% for Hewlett Packard code. Anything else is presumed to be plain ASCII. PostScript and HP code are just copied to the printer, preceded and followed by appropriate printer commands.

ASCII jobs are more complicated to handle, for two reasons. First, ASCII lines must be truncated at column 80 to keep them from wrapping around and throwing page headers out of sync with the pages. Consider what happens when a user pipes a file to lpr --p. The --p option generates a heading every 66 lines. If any line wraps around to the next, there will not be 66 lines on the page, and the headers will start shifting. This is not an arcane situation; it arises whenever anyone prints e-mail or netnews, because the headers almost always contain long lines.

We chose, therefore, to truncate at column 80 and underline the last character of each line that was cut off. In counting columns, our program recognizes that Ctrl-H is a backspace and that Return, New Line, and Form Feed return the print position to column 1. Users can still print long lines by using fold to wrap them before the file gets divided into pages: fold < myfile | lpr --p.

Second, any putative ASCII file may turn out to contain unprintable data. The filter uses a table of printable characters stored in an array. The printable characters are considered to be codes 32--126, plus Ctrl-D (UNIX end-of-file), Ctrl-G (bell), Ctrl-H (backspace), Ctrl-I (tab), Ctrl-J (new line), Ctrl-L (form feed), Ctrl-M (return), and Ctrl-Z (MS-DOS end-of-file mark, often present in uploaded text files). This table can, of course, be altered to fit local requirements.

When it hits an unprintable character, the filter resets the printer, prints a message, dumps 30 lines of data in (hopefully) readable form, and terminates the job; see Figure 2.

Printer Control

The printer-control codes used by the filter are #defined at the beginning of the program. Several are familiar LaserJet PCL code sequences that begin with Escape (octal 033): Esc E to reset the printer, Esc & k 2 G to tell the printer to insert returns before all line feeds, and similar-looking codes to select an appropriate font and margins for 80x66 ASCII printing.

However, to select or deselect PostScript and manipulate the display on the printer console, it is necessary to use HP's higher-level Printer Job Language (PJL). Sequences of PJL commands are introduced and terminated by the string Esc%--12345X (no spaces between characters), presumably chosen because it is unlikely to appear in a print job. The first command in a sequence must be @PJL; other commands used in this program are listed in Example 1(b). The last of these, of course, displays a string on the printer console.

Identifying the User

The print filter receives command-line arguments from the system. For example, if the print filter is named lj4m, it executes as if invoked by a command like this: lj4m --w132 --l66 --i0 --n username --h hostname /dev/null.

The first three arguments give nominal values for page width, page length, and extra indentation; our filter, like most, ignores them. The arguments we use are the fifth and seventh, which identify the user and the host; the program puts them together into a string, hostname:username, which is used in the log and on the printer display. If the printcap entry does not contain an af= field, the filter does not receive these arguments, and it displays "Unknown username" instead.

Writing in the Log

We chose to log all print jobs in the LPD error log rather than in a separate accounting file. Accordingly, our program outputs log entries using syslog() rather than ordinary file-output routines.Actually, three function calls are involved. First, openlog("LaserJet 4M",0,LOG_LPR); specifies that the program will be writing on the printer log and that each message will be preceded by "LaserJet 4M:" (complete with the UNIX-supplied colon). Next, calls of the form syslog(LOG_DEBUG,"format string",arg,arg_); actually write the log entries. The first argument specifies the priority of the message; the priorities we use are LOG_DEBUG for normal events and LOG_ERR for errors. The format string and subsequent arguments work just like those of printf(). Finally, closelog(); closes the log.

Where do the messages go? Wherever /etc/syslog.conf says they should. For example, our /etc/syslog.conf contains, among other things, the code in Example 1(c). ERR messages from any process get written on the console and on /var/adm/messages; DEBUG messages from the printer daemon (including those from our filter) get written on /var/adm/lpd-errs.

Further Possibilities

A print filter like this can easily be extended. One obvious possibility is to extract the %%Pages: comments in PostScript files and thereby log the number of pages that each job claims to have. Another possibility is to perform additional diagnosis on every unprintable file: Identify it (perhaps even using "file", or at least /etc/magic) and print a more meaningful message for the user.

Unlike ordinary filters, print filters are allowed to lseek() (reposition) their input files under some flavors of UNIX; this raises the possibility of a two-pass filter. For example, a filter could read a whole ASCII file, determine the maximum line length, and set the typeface and margins accordingly.

We encourage you to customize this filter rather than install it unaltered. Every printer and every printing situation has its own needs; an intelligent print filter can go a long way toward giving users what they want in every situation, even if they don't explicitly ask for it.

Figure 1 PJL commands make the printer display user's name and machine.

Example 1: (a) Printcap entry for LaserJet 4M; (b) sample commands used in the lj4m program; (c) logging messages.

(a)
lp|LaserJet 4M:\
 :lp=/dev/ttyb:\
 :br#19200:ms=litout,ixon:\
 :sf:sh:mx#0:rw:\
 :if=/etc/lj4m:\
 :sd=/var/spool/lpd:\
 :af=/dev/null:

(b)
@PJL ENTER LANGUAGE = POSTSCRIPT
@PJL ENTER LANGUAGE = PCL
@PJL RDYMSG DISPLAY = "your string here"

(c)
*.err               /dev/console
*.err               /var/adm/messages
lpr.debug           /var/adm/lpd-errs

Figure 2: Sample dump produced when a file is found to be unprintable.

aisun1:mcovingt
Unprintable data! Partial dump follows...
..... ... ...........  ......... [email protected].* .......@......"........b.........
...@.......@.......@....#. @[email protected]/......(......"[email protected].. .......\@
..K..  ..  ...O.......^..a....K......`...d...h......../......H.. ... .. ......
[email protected].. ............d.. .. .........@..+..../......t......".@..#.. .......h.
......./......H.......... .. .........@..... ....p.....#.|.#...#.......b..#...
.....`..#........`d.#.... ...... |.............. ....... G...... ... .........
......... .......... ... ... ... ... ...@./usr/lib/ld.so./dev/zero.......crt0:
 no /usr/lib/ld.so.......&crt0: /usr/lib/ld.so mapping failure........crt0: no
/dev/zero......%d  %[email protected]...... ..'........ c... ...... ..'....... c.....
................. [email protected]..........`......'..............
.`......'............................... ........... ...`... ....... ..`... ..
.`...@... ...................... ..*`...............?.2.......................
. ...`....... .......... ....... ...............#@@..*.. ..................`..
. ....... ......"`..............@l..........@x..........@...........@.........
[email protected]...................................................................
..............................................................................
.............'H.........@...........@...........  ......P........%..........,.
....."....2..d<..@...;[email protected]...>.......K../...$....S..3.......[..b1..#@.
..n..W...@....}.....@.......%[email protected]!..#[email protected]>..._etext._e
data._end.start.start_float.__exit._main._environ._DYNAMIC._exit.__main._print
f.___do_global_dtors.__DTOR_LIST__.__exit_dummy_ref.__exit_dummy_decl.__do_glo
bal_ctors.___CTOR_LIST_._on_exit...../usr/local/lib/gcc-lib/sparc-sun4-sunos4.
1.3/2.5.2:/usr/local/lib......... .............."............c.dl.............
..............................................................................
..............................................................................
..............................................................................
..............................................................................
..............................................................................
..............................................................................
..............................................................................

Listing One


/*  lj4m.c -- Michael A. Covington and Mark L. Juric, 1994
   Pre-spooler filter for LaserJet 4M. Compile with gcc or ANSI C.
   Install with the "if=" (not "of=") option in /etc/printcap.
   Print jobs are logged as lpr.debug messages.
*/

#include <syslog.h>
#include <stdio.h>
#include <string.h>

#define CTRLD      "\004"
#define LINELENGTH 80

/***  HP LaserJet control sequences  ***/
#define RESET      "\033E"
#define LF_TO_CRLF "\033&k2G"
#define LMARGIN    "\033&a11L"
#define FONT       "\033(s0p(s12h(s4b(s4099T"
#define START_PJL  "\033%-12345X@PJL\n"
#define END_PJL    "\033%-12345X"
#define POSTSCRIPT "@PJL ENTER LANGUAGE = POSTSCRIPT\n"
#define PCL        "@PJL ENTER LANGUAGE = PCL\n"


/***  Global variables  ***/
int        c0, c;               /* first 2 chars of file      */
long int   bytes;               /* character count            */
char       userinfo[64] = "";   /* will be "machine:username" */

/*** Printer console display functions ***/
void DisplayOnPrinterConsole(char *s)
{
  fprintf(stdout,"%s%s%s%s%s%s%s",
          RESET,
          START_PJL,
          "@PJL RDYMSG DISPLAY = \"",
          s,
          "\"\n",
          END_PJL,
          RESET);
}
void ResetPrinterConsole()     /* to display "00 READY" */
{
  fprintf(stdout,"%s%s%s%s%s",
          RESET,
          START_PJL,
          "@PJL RDYMSG DISPLAY = \"\"\n",
          END_PJL,
          RESET);
}

/*** PostScript and HP file handling ***/
void PrintPostScriptFile()
{
  /* Choose language */
  fprintf(stdout,"%s%s%s",RESET,START_PJL,POSTSCRIPT);
  /* Transmit file transparently */
  putc(c0,stdout);
  for (bytes=1; !feof(stdin); bytes++)
  {
     putc(c,stdout);
     c = getc(stdin);
  }
  /* Add newline, Ctrl-D, and reset at end */
  fprintf(stdout,"\n%s%s%s",CTRLD,END_PJL,RESET);
  /* Log results */
  syslog(LOG_DEBUG,"%s, PostScript file, %d bytes",userinfo,bytes);
}
void PrintHPFile()
{
  /* Choose language */
  fprintf(stdout,"%s%s%s",RESET,START_PJL,PCL);
  /* Transmit file transparently */
  putc(c0,stdout);
  for (bytes=1; !feof(stdin); bytes++)
  {
     putc(c,stdout);
     c = getc(stdin);
  }
  /* Reset printer at end */
  fprintf(stdout,"%s%s",END_PJL,RESET);
  /* Log results */
  syslog(LOG_DEBUG,"%s, HP file, %d bytes",userinfo,bytes);
}

/*** ASCII and unprintable file handling ***/
#define PRINTABLE(c) (printable[(unsigned char) c])
char printable[256] =
   /* Table of which ASCII codes are printable characters */
       { 0,0,0,0,1,0,0,1,1,1,1,0,1,1,0,0,   /* NUL to ^O  */
         0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,   /* ^P to 31   */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,   /* 32 to 47   */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,   /* 48 to 63   */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,   /* 64 to 79   */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,   /* 80 to 95   */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,   /* 96 to 112  */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,   /* 113 to 127 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,   /* 128 to 143 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,   /* 144 to 159 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,   /* 160 to 175 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,   /* 176 to 191 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,   /* 192 to 207 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,   /* 208 to 223 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,   /* 224 to 239 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 }; /* 240 to 255 */
                                /* -1 (eof) maps onto 255 */
void RejectFileAsUnprintable()
{
  /* Set up printer again because it may have been disrupted */
  fprintf(stdout,"\n%s%s%s%s%s%s",
          RESET,
          START_PJL,
          PCL,
          LF_TO_CRLF,

          FONT,
          LMARGIN);
  /* Dump 30 lines, printing '.' for unprintable characters */
  fprintf(stdout, "%s\nUnprintable data! Partial dump follows...\n", userinfo);
  for (bytes=1; bytes<LINELENGTH*30+1; bytes++)
  {
    if (feof(stdin)) break;
    if (c<32 || c>126) c = '.';
    fputc(c,stdout);
    if (bytes % LINELENGTH == 0) fputc('\n',stdout);
    c = getc(stdin);
  }
  /* Log the error */
  syslog(LOG_ERR, "LJ4M: %s: tried to print unprintable data\n",userinfo);
}
void PrintASCIIFile()
{
  int  position = 1;   /* where the next char on the line will be */
  int  ok_to_print;    /* true if no bad chars found */
  /* Set up printer */
  fprintf(stdout,"%s%s%s%s%s%s",
          RESET,
          START_PJL,
          PCL,
          LF_TO_CRLF,
          FONT,
          LMARGIN);
  /* Deal with the first character already read */
  ok_to_print = PRINTABLE(c0) && PRINTABLE(c);
  if (ok_to_print && (c0 != EOF)) putc(c0,stdout);
  /* Process rest of file, breaking at column 80 and
     underlining last character if line continues beyond */
  for(bytes=1; ok_to_print && !feof(stdin); bytes++)
  {
    if (c==4 || c==26) break;       /* Skip UNIX or DOS EOF mark */
    /* Compute where c will print */
    if (c==10 || c==12 || c==13) position=0;    /* CR, FF, or LF */
    else if (c==8 && position>0) position--;    /* Backspace */
    else position++;
    /* If in a printable column, print it */

    if (position <= LINELENGTH) fputc(c,stdout);
    /* If we have just run past margin, underline last character */
    if (position == LINELENGTH+1) fputs("\b_",stdout);
    /* Obtain and check next character */
    c = getc(stdin);
    ok_to_print = PRINTABLE(c);
  }
  /* If a bad byte was found, print messages and dump */
  if(!ok_to_print) RejectFileAsUnprintable();
  /* Reset printer at end */
  fprintf(stdout,"%s%s",END_PJL,RESET);
  /* If normal termination, report results */
  if (ok_to_print)
     syslog(LOG_DEBUG,"%s, ASCII file, %d bytes",userinfo,bytes);
}

/*** Main program ***/
main(int argc, char* argv[])
{
  /* Obtain machine name and user name from cmd line args */
  if (argc>8)
    sprintf(userinfo,"%s:%s",argv[7],argv[5]);
  else
    strcpy(userinfo,"Unknown username");
  DisplayOnPrinterConsole(userinfo);
  openlog("LaserJet 4M",0,LOG_LPR);
  /* Examine first 2 bytes, decide how to handle file */
  c0 = getc(stdin);
  c  = getc(stdin);
  if (c0=='%' && c=='!')
    PrintPostScriptFile();
  else if (c0==27 && (c=='E' || c=='%'))
    PrintHPFile();
  else
    PrintASCIIFile();
  /* Clean up */
  ResetPrinterConsole();
  closelog();
  return(0);       /* UNIX insists on return code 0. */
}

Copyright © 1994, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.