Go to the previous, next section.

The Stdgraph Graphics Kernel

SM can use a single set of subroutine calls to plot on almost any terminal, and on many printers. The routines that it uses, called stdgraph, were originally taken from the IRAF GIO package written at Kitt Peak by Doug Tody @footnote #{Graphics I/O Design, Doug Tody, March 1985. NOAO (Kitt Peak)} and converted to C and partially re-written to be integrated into SM. Despite our extensive rewrite, these routines should probably still be considered to be in the public domain.

The Graphcap File

Stdgraph uses a file called a graphcap file to specify the properties of terminals, in a way that is similar to the termcap facility of Unix. You don't have to know anything about termcap to read this section; you don't have to read this section unless you want to change the graphcap file to add a new device, to fix a bug, or to change the way that SM treats your plotting device. The name of the graphcap file is given by the variable graphcap in the environment file.

A list of files to be searched in order may be given instead of a single graphcap file (up to a current maximum of three). The usual way to accomplich this is to add an entry

+graphcap          /u/rhl/sm/graphcap
above any other graphcap entries in your `.sm' file, which instructs SM to put `/u/rhl/sm/graphcap' first in the list of files, followed by any others that might appear, either in your file or in some other that the system provides (ask the person who installed SM where the default `.sm' file is; usually something like `/usr/local/lib/.sm').

A graphcap file is a way of describing a terminal in a concise way, so a programme can discover which idiosyncrasies a terminal has without having to be recompiled. A graphcap file consists of a number of entries, one for each device supported, and to add a new terminal all that one has to do is to add another entry. It is also possible to define variables in graphcap files, which are used in SY entries. You can compile selected entries in the graphcap file, so as to improve access time for popular terminals. If this has been done, changing the graphcap file for one of these terminals will have no effect until it is recompiled, see section Compiling Graphcap for details.

For a list of all the capabilities that SM uses see the index to graphcap at the end of this appendix.

Some devices are not supported through stdgraph (graphics drawn to a SunView window would be an example), but they still appear in graphcap with a special entry (DV) giving the name of the appropriate hard-coded device driver.

Each entry consists of a name for the device, followed by a list of aliases, followed by a list of fields, separated by colons. A \ may be used to continue an entry onto the next line, and lines starting with a # are comments (comment lines are permitted both between and within entries). As a rather complex example, the graphcap entry for a Tektronix 4012 reads:

tek4010|tek4012|TEK4010|TEK4012|Tektronix 4010/2:\
        :OW=^]^_:RC=^[^Z:SC=(,!3, & *, &+!1, & *, &+!2:\
This is one of the longest entries in the graphcap file - all of the terminals which are Tektronix emulators explicitly include this entry, so they only need provide the capabilities that are different from the Tektronix. As an example, the entry for a Pericom reads
The | separate the aliases, and the final field tc=tek4012 tells stdgraph to take all other fields from the entry for tek4012, given above. If you have specified a list of graphcap files, each will be searched in order for each :tc= continuation. If you don't want the search to begin again use TC, e.g.
graphon|Graphon which claims to support lw:\
if you had used :tc=graphon: this would have been recursive and illegal, but as TC doesn't restart the search it merely has the effect of adding (or in general, replacing) an capability in a preexisting graphcap entry.

Control characters are entered as ^A, ^B, and so on (those are two characters, ^ and `A'). `Escape' may be represented as ^[, \E, or in octal as \033. Because the normal way of handling strings in C treats \0 as meaning `end of string' you can't simply put a \000 into a graphcap entry, instead write \377 and SM'll interpret it as \0. (If you need a real \377 enter \377\377). If a delay of so many milliseconds is required before the transmission of a string, it is given first (followed by a * if it is to be applied to each line affected). This leads to problems with graphcap entries that start with numbers, you must precede them with a space or (if the string is run through the encoder) insert a no-op e.g. :CP=()1000:. Numerical values are preceded by a #, so :co#80: means that co (the number of columns displayed) is 80, while :MC=^M: means that MC (the cursor delimiter) consists of the character ^M. This could just as well have been written :MC=\010:. If the first character of a capability is `@', it specifies that that capability is not present for that terminal (e.g. :lt@=1234: specifies that lt is not defined). A field may simply not be provided if it is irrelevant, although in this case it may be supplied by a tc or TC continuation. A common set of graphcap entries to `comment out' are TB and TE, which deal with hardware character sets. If you don't want your plotter to use it's internal fonts simply insert `@' before the `='. By inserting their private file before the system one in the list of graphcap files, users can tailor the entries to their liking.

We use a subset of the graphcap capabilities defined by the IRAF group, and the distinction between upper and lower case parameters comes from them. In a few cases our usage is different from theirs, in these cases we have specified our own capabilities (CD MC, DD SY, LT ML, and TS TB. We have also added the lt, BP, BR, CO, CS, CT, DC, DT, DV, EP, ER, and TC. capabilities.). First the lower case, which specify mostly dimensions:

Height of a character, relative to the screen height being 1.0.
Number of columns displayable, with characters of width cw.
Width of a character, relative to the screen width being 1.0.
Number of lines displayable, with characters of height ch.
Which linetypes are supported in hardware.
Pad character for delays (use NUL if not supplied).
x dimension of plotting device.
y dimension of plotting device.
Of these, co and li are not currently used.

The capitalised capabilities mostly tell the stdgraph routines how to plot lines, clear the screen and so forth. Some of these are no more than character strings to send to the terminal, (e.g. CL to clear a screen), but some use the graphcap entries to programme a sort of RPN calculator, which computes the bit-patterns that the terminals demand. This calculator is usually referred to as the `encoder'. We'll first list all the capabilities in a reasonably ordered way, then describe the encoder and what it can do, and then go through a number of examples.

First the fields which are simple character strings to be written to the terminal. The second column is an attempt to explain the etymology of the two character name.

CL (CLear)
Clear the screen, possibly also the text screen.
CW (Close Workstation)
Close terminal, expect no more graphics.
DS (Draw Start)
Prepare the terminal to draw a line.
DE (Draw End)
Finish a line.
FD (Fill Draw)
Draw a side of a filled polygon.
FE (Fill End)
Finish drawing a filled region.
FS (Fill Start)
Start drawing a filled region.
GD (Graphics Disable)
Return the terminal to a character mode.
GE (Graphics Enable)
Set the terminal to graphics mode.
IF (Initialisation File)
Used to supplement OW if sequence is too long.
LR (Load Registers)
(Used by the RPN encoder, see `binary encoding').
ME (Mark End)
Finish a series of dots movements.
MS (Mark Start)
Start a sequence of dots movements.
OW (Open Workstation)
Prepare a terminal to produce plots.
OX (Open workstation)
(a continuation of OW).
OY (Open workstation)
(a continuation of OX).
OZ (Open workstation)
(a continuation of OY).
PG (PaGe)
Start a new page.
VE (? End)
Finish a series of pen (beam) movements.
VS (? Start)
Start a sequence of pen movements.
For hardcopy devices PG should start a new page. The GD and GE are used by terminals which spend some of their time being graphics terminals, and some being regular text terminals. The various "... Start" and "... End" capabilities assume that the points in question are specified by the XY entry (except for FS/FE where FD is used instead). Typically, the `start' is used to put the device into (e.g.) line-drawing mode, then the line is drawn with a sequence of XY's, then it is taken out of (e.g.) line mode with the `end'. The support for filling areas assumes that a region is specified by drawing a line around it; if this isn't so, you'll have to omit area fill from graphcap, and rely on SM emulating it for you. An example would be a Graphon GO-250, which has an area fill where you fill rectangular areas by specifying opposing corners; this is not acceptable to SM.

Some operations require an argument, for instance setting the hardware line type, specifying which cursor to read @footnote #{Actually, SM always uses cursor 1}, or specifying coordinates. In the following properties, the expected parameters are listed after the field names, the first to go into register 1, the second into register 2, and so on. If you haven't skipped forward to the section on the encoder this will seem obscure, but all will become clearer.

CO(r,g,b) (COlour)
Set next colour to (r,g,b).
CS(n) (Colour Start)
Start defining n colours.
CT(i) (Colour Type)
Set a colour.
DC (Default Colour)
Set default colour.
LW(f) (Line Weight)
Set the lineweight to f.
MC(i,x,y) (sM Cursor)
Decode a cursor response.
ML(i) (sM Line)
Set the linetype to i.
RC(c) (Read Cursor)
Read the cursor. `c' is for compatibility.
SC (Scan Cursor)
Decode the cursor reply following a RC.
TB(x,y) (Text Begin)
Start writing text at (x,y).
TE (Text End)
Stop interpreting characters as text.
XY(x,y) (X Y)
Encode the coordinate pair (x,y).
Some of the above comments are a little cryptic, but we return to the various graphcap parameters that take arguments as examples after describing the encoder. Note that it isn't sufficient to change the ML entry -- for a linetype to be supported in hardware it must also be included in the lt list, e.g. lt=01234. Similarly, for hardware fonts you must include ch and cw, and TB must be present even if it does nothing. Note that LW is passed a floating point number, and that the special case 0 is special, meaning choose the most efficient line thickness for the device.

The folowing capabilities have to do with rasterising and are discussed in their own section near the bottom of this appendix:

BP (Bit Pattern)
Bit patterns for rastered data.
BR(i) (Begin Row)
Begin row of rastered data.
EP (Empty Pattern)
Bit pattern for rastered empty pixel.
ER (End Row)
End of a row in rastered data.
ll (lINE lENGTH)
Length of a row for DR=hex.
MR (Many Rows)
Number of rows output at once.
nb (nUM bYTES)
Number of bytes to process at once for MR.
RA (RAster)
(RA is no longer supported -- see DV).
RD (Raster Device)
Type of device if not generic.
Raster devices also make use of xr, yr, CW, OW, OX, OY, OZ, OF, and SY which are also used by stdgraph itself.

Finally there are some capabilities that are designed for driving hardcopy devices and devices that may not use stdgraph at all:

DT (Device Type)
Type of device in use.
DV (DriVer)
Name of hard-coded driver.
OF (Out File)
The file to direct output to.
RT (Record Terminator)
(RT is no longer supported).
SY (SYstem)
The action to be taken upon closing the OF file.
The OF file may be specified with the last characters being `XXXXXX', in this case the Xs are replaced by a random characters, to make a unique filename. If the variable temp_dir is defined in the environment file, then OF is created in that directory, otherwise it is put in the current directory. The DT string, if present, specifies the type of device in use. Currently the values are only used under VMS, where they are used to decide how to open files. The recognised values are "qms" and "imagen". In general DT should be omitted, as it requires programming support, but it can help stdgraph to deal with hostile operating systems. For a discussion of the DV entry see section New Devices and New Machines.

The SY string is passed to the operating system after graphcap variables have been expanded (they are similar to macros in Unix's make). A variable is defined with a line like:

	name = value
where name must start in the first column. Any white space surrounding the equals sign is removed, as are any trailing blanks. If value starts with a $ it is taken to be a regular SM variable. Variables may be defined in any of the graphcap files in the search path, and if a name appears more than once the first value found will be used (if you change graphcap without leaving SM the variables are re-read). There is no guarantee that all the graphcap files in the path will be read but this is unlikely to be a problem. The major use for graphcap variables is probably for encoding rasterise's full name:
BIN = /usr/local/bin
device|some device:\
        :SY=${BIN2/rasterise -r $0 $F $1:

Variables are written as ${name2 not $name, which means that they will not (usually) conflict with the operating system's uses for dollar signs. The graphcap variable F is special, as it always expands to the filename specified as OF. As a concession to history it may be written as $F instead of ${F2. Also special are $"prompt", which is replaced by a string read from the keyboard (you are prompted with prompt), and $n which is replaced by the n'th argument to the DEVICE command. For example, if the DEVICE command were DEVICE qms lca0 Hello (or DEVICE 1 qms lca0 Hello), then the device name qms would be $0, lca0 would be $1 and Hello $2. If a `$' is found under other circumstances it is simply treated as a dollar sign, but if you wish you can escape it with a \ (but remember that the \ must itself be escaped so to explicitly escape a dollar in an SY string you must type \\$). This means that (under Unix) you can access environment variables from SY strings, e.g. :SY=mv ${F2 $HOME:. If a variable is referenced but no value is provided when the device is opened a warning message is printed; this message can be suppressed by referring to the variable as (e.g.) $%1. The SY string is only used if an OF file has been specified. There is no guarantee that SY is supported by all operating systems, but it is certainly available under Unix and VMS (SY requires the C call `system()', as defined for Unix. We have provided one for VMS, and any serious SM implementation would have to have one too.) A trivial example of SY in use on an Unix system would be:

:SY=cat $F ; rm $F:OF=out_XXXXXX:
(cat prints a file, ; separates multiple commands on a line, rm deletes a file). Because not all operating systems can support multiple commands on one line, you can use \n within a SY string to separate commands. For example, under VMS that SY string could have been written
:SY=type $F. \n delete $F..*:OF=out_XXXXXX:
(Type adds a `.lis' unless explicitly given a closing `.', delete requires a version number, hence the $F. and $F..*.) An example of the use of $"" would be
:SY=mv $F $"Output filename? ":
which renames the OF file to whatever you want.

The RT capability has been deleted in version 2.0, in favour of using DT; The RA capability has been replaced in version 2.1 by :DV=raster:.

Stdgraph's Binary Encoder

Different terminals have very different ways of doing the same thing. For example to move the beam to (200,200), a vt240 in REGIS mode needs to be told `[200,259]', while a Tektronix 4010 needs `&h&H'. In order to cope with this much diversity, stdgraph has a binary encoder with a 50 element stack, 10 registers and about a dozen operators. The encoder communicates with the rest of the world through its registers - for example in encoding a coordinate pair it expects to find x in register 1, and y in register 2. When reading a graphcap string, initially stdgraph simply copies the input characters to an output string, which is then written to the terminal. This is exactly what it does when it interprets the OW string for a Tektronix, OW=^]^_. However, in addition to characters such as ^ being special, it also recognises the following as being special:

escape special meaning of next character
Begin a format string
Switch from copy into encode mode.
When in `encode' mode, the following operators are available:
Escape next character (recognised everywhere)
Formatted output, e.g. %d, %g, or %t
Revert to copy mode
Push signed decimal number nnn onto the stack
Part of a switch statement
Pop a number from the stack, and put it in the output string
Get next number from input string, and push it onto the stack
Prompt with str, then read a character and push it onto the stack
Modulus operator (similar to an AND of the low order bits)
Add (similar to OR)
Subtract (similar to AND)
Multiply (a left shift if number is a power of 2)
Divide (a right shift if number is a power of 2)
Less than (0:false, 1:true)
Greater than (0:false, 1:true)
Equals (0:false, 1:true)
Branch, <boolean><offset>; (; is at 0 offset)
Push register 0-9 onto the stack
Pop the bottom of the stack into register N.
Pop the stack, and delay that many milliseconds.
Convert the bottom of the stack from float to int (it is rounded rather than truncated).

Unless otherwise specified the stack is taken to be integer-valued, although in fact it can support either integer or floating point values. There is no type checking -- if you ask the encoder to print the bottom of the stack as a float, but you stored an int, you can expect trouble. If it is needed we might add more floating point support; apart from printing the bottom of the stack, the only floating point operation supported is `|' which rounds the bottom of the stack (taken to be a float), converting it to an integer (so, for example, 1|1! converts the contents of register 1 from float to int).

All the binary operators operate on the bottom 2 elements of the stack, and push the answer onto the bottom. Any other character is interpreted as an integer, and pushed onto the stack - for instance, `' is the same as `#64', octal 100. A blank is the octal constant 040.

The % command means, `format the bottom of the stack, and write it to the output string'. The format string may be any printf format specifier (printf is the C formatted i/o function. In practice, the only formats that you are likely to need are %c, %d, %g and %t -- and %t isn't even in C! %c means `write the integer as a character', %d means `format the number as a decimal integer', %6d means `and make it fill 6 characters', and %g means format a floating point number. If you should need to know more, look at any book on C.) The special format %t means `take x and y from registers 1 and 2, and format them for a Tektronix'. As we shall see below, you can programme the encoder to do this, but Tektronix emulators are so common that %t is provided for efficiency's sake. In fact there are two Tektronics formats, %t for 10 bit addresses, and %T for 12 bit addresses. The switch and branch instructions are discussed below, while examining specimen ML and SC strings.

Examples of Graphcap Entries

As a simple example, the ANSI command to set a non-graphics cursor to a given line and column is

^[[ line ; column H
Assuming that the x and y coordinates are in registers 1 and 2 respectively, the corresponding graphcap string would be
(where the quotes are not part of the format.) What if line and column coordinates start at 1, but the terminal wants them starting at 0? then the format would be
You could write those #1's as ^A which would be slightly faster, but why bother?

As promised above, it is also possible to encode Tektronix-type coordinates. The desired bit format for a 10-bit address is

0 1 ya y9 y8 y7 y6
1 1 y5 y4 y3 y2 y1
0 1 xa x9 x8 x7 x6 
1 0 x5 x4 x3 x2 x1
where x1 is the least significant bit in x, and ya is the tenth bit in y. If x and y are in registers 1 and 2, the simplest XY (move/draw to (x,y)) string is
but if this weren't available the following string would work:
"(2 / +.2 &`+.1 / +.1 &@+."
(as before, the double quotes don't belong to the format). To understand this, First look up the octal values of ` ' (040), "' (0140), and `@' (0100). Then the first `(' puts the encoder into encode mode. `2 /' pushes the Y value onto the stack, and right shifts it by 5 bits (` ' is 100000 in binary). The next ` +.' adds the resulting bit pattern `0 0 ya y9 y8 y7 y6' to 0100000 and transfers it to the output string, and we have produced the desired first byte. The other bytes are produced in a similar fashion.

As another example consider an AED512, which is reputed to desire the bit sequence

xa x9 x8 yb ya y9 y8
x7 x6 x5 x4 x3 x2 x1
y7 y6 y5 y4 y3 y2 y1
The graphcap string
will accomplish this. We could further optimise this by loading the value `#128' into register 9 once and for all with the LR capability, so a part of the graphcap entry would appear as
I've never seen an AED512, but this should work anyway.

The switch instruction has the form

$i ... $j-k ... $l ... $D ... $$
where i, j, k, and l are integers. The encoder pops the bottom value off the stack adds `0' to make it a character, and scans forward looking for a $ followed by that character. $2-5 would match the characters `2', `3', `4', or `5'. When it has met its match, it executes the instructions that it meets until it reaches the next $ in execute mode. The encoder then skips forward until just after the $$, and resumes scanning. If the character from the stack is not matched by any of the cases, the encoder will use the $D (i.e. default) case, if present.

As an example, consider how stdgraph sets the type of line to draw. SM expects linetype 0 to be solid, 1 to be dotted, and so on. We expect a linetype in register 1 and have to do something with it.

For a Tektronix, the linetypes are set by an ML entry:

What does this do? The ^[ is simple, it is executed in copy mode, and writes the character ^[ to the output string. The (1 enters encode mode, and places the contents of register 1, the desired linetype, on the stack. Then begins the switch. If the linetype is 0, then the encoder scans past the $0 and starts reading the string again with )`. The ) takes the encoder back to copy mode, so it copies ` to the output string, and encounters a ($ which puts it back into encode mode. Once in encode mode it recognises the $ as the end-of-case, and scans forward until it reaches $$, where it stops. We deduce that the set-linetype-0 escape sequence is ^[`. If register 1 had contained a 2, after entering the switch the encoder would have scanned forward to $2 (ignoring all characters as it went), and copied c to the output string.

If you want to support erasing of individual lines (LTYPE ERASE or LTYPE 10) you'll have to include a $\: case in your switch (as : follows 9 in the ascii character set, and an un-escaped : would end the graphcap entry). You'll have to escape the : in the lt list as well. When leaving erase mode, by specifying any other line type, the device will first be set to LTYPE 11 (i.e. ML'll get a ;) before it's set to the desired LTYPE; this gives the driver a chance to reset itself. It's wise to also turn off erase mode when closing the device. An example of an entry supporting erasing lines is a graphon, which includes

as ^[^P puts a graphon into erase mode, and ^[^A takes it out. Note that in erase mode the linetype is set to solid (^[`), so as to erase all types of lines.

There is also a branch instruction, which has syntax

If the boolean is true (non-zero), then skip (offset - 1) characters in the programme string. The offset may be either positive or negative, and the `;' is at offset 0. For example,
will print `Goodbye\n' if register 0 contains zero, or `Hello\n' otherwise. As an example of the use of `;', consider using the encoder to decode a string. Remember that `,' meant `read a character onto the stack', and that there was a graphcap capability SC to decode cursor responses. Suppose that we are dealing with a vt240 in REGIS mode, then a cursor read will return a string of the form `k[nnn,mmm]' where `k' is the character you hit, and (nnn,mmm) is the cursor position. We want to put k into register 3, and (x,y) into registers 1 and 2. This is a little messy, as we'll have to convert the ascii positions into integers. The desired graphcap entry is
The first part is simple enough, store 0 in registers 1 and 2, store the first character in register 3, read a character (the [), and store 0 in register 8. Then we come to ,#48-!99$0-91#10*9+!1#1!8$$8#1=#-39;. The ,#48- reads a character and converts it to an digit (48 is the decimal code for `0'), then stores it in register 9. The switch then checks if we do have a digit, if so we multiply register 1 by 10 and add the new digit. We then set register 8 to 1 and finish the switch which is here being used as an if statement. The 8#1=#-39; tests register 8 against 1 (i.e. checks if we found a digit), and if we did it jumps back 39 characters, to read the next character@footnote #{In counting characters for jumps, the ; is at character 0 and combinations such as ^N count as one character}. So we are accumulating the integer nnn in register 1, just as we needed to. The rest of the string deals with decoding the y coordinate.

Sometimes you don't want to read from the input string, but from the keyboard instead. In this case use `str`, e.g. (`Hello\: `#48-$0)False($D)True($$)\n: will prompt you with Hello: , then read a character from the keyboard. If you enter a `0' it'll print False, otherwise it'll print True. Of course, in reality you'd want to do something more useful (such as erasing the screen).

Using Cursors with Graphcap

We have just been through a long explanation of how to decode a cursor string, but how did stdgraph know what to read in the first place? After receiving the RC string, the terminal will send back a sequence of bytes, and the format of these bytes must be specified in graphcap.@footnote #{If the RC string is given as prompt, then you will be prompted for the key you would have hit, and the (x,y) position the cursor would have been at, if the terminal that you were using could support a cursor.} There are two ways to do this, either by specifying a sequence of characters which `end' the response string along with a minimum number of characters to read, or by specifying a pattern that the terminal response is to match. A typical example of the former is a Tektronix whose cursor response may be chosen to be ^M (this is called the GIN response, and can usually be set in the terminal setup). We know that the terminal will also send 5 other bytes (the key struck and the encoded x,y coordinates so we would specify
On the other hand, a REGIS terminal sends `k[nnn,mmm]'. This can be specified as
where the negative value of CN means that we are providing a pattern not just a terminator (as before, the absolute value of CN is the minimum number of bytes in a cursor response). In MC strings, but nowhere else, the characters ?, #, and * are special (although their special meanings may be escaped with a \). ? will match any character, # any digit, and * means `match zero or more of the preceding characters'. So a MC string of a#*?ba will match `aaa1111bbaa' at the third character. (Incidently, a#*?a would match at the first). Because this special character syntax is different from that used in standard graphcap files for IRAF, the name of this graphcap parameter has been changed from CD to MC.

If your cursor is atached to a mouse, if possible the buttons should be set up to generate `e', `p', and `q' from left to right (if you have that many buttons). If you have only one button, `p' is probably the best choice.

Using Colours with Graphcap

The number passed to CT are the same as those specified with the CTYPE INTEGER command, so initially they specify default, white, black, blue, red, green, magenta, yellow, and cyan (white is 1). These are the colours corresponding to turning one, zero, two, or three of the primary colours on. The default colour to use for a device is specified by the DC capability, e.g. :DC="red":.

The CS and CO capabilities are used to support the CTYPE = expr command. First CS is used to tell the device how many colours to expect, then CO is used for each number, with red, green, and blue as its arguments. In this case CT passes an index into the set of CO values. If you want to get an index, but don't need CS and CO, you must still provide them; just provide a no-op such as :CS=():.

Writing a New Graphcap Entry

So, if you're faced with a new piece of hardware what should you do? First of all, don't panic -- writing entries is quite simple. Second, see if your device is basically the same as one that already exists in graphcap, for example the entry for `graphon' uses the `selanar' entry, and it in turn uses `tek4010'. You might be able to get away with using tc to satisfy most of your device's needs.

But let's assume that you are faced with a totally new type of device and really do have to start from scratch. First find out how large your device is, and fill in the xr and yr entries. If you are going to use hardware character sets you also need ch and cw. Next decide on the string to initialise the device -- does it need to be set into some weird mode -- and put it into OW. Put the string to reset it into CW. Now, if the initialised device needs to be put into a special graphics mode put it into GE and its inverse into GD. Next, you need to tell SM how to draw a line and move the plot pointer. So enter the DS, XY, DE, VS, and VE capabilities. Of course, if one isn't required, don't put it in. If you have some sort of printer you probably want to store all the commands in a file (OF=), and to plot them (SY=). You should now be ready to make your first test, so plot a box. If it doesn't look right, fix it. Or you might like to try printing the cover (load cover cover).

When all is well, you can begin looking into options that might make your graphcap entry more efficient. Look through this appendix to see what is available. Does your device support line types? Add ML and lt. Heavy lines? LW. Coloured lines or a cursor? See section Using Colours with Graphcap. Filled polygons? FS, FD, and FE. Dots? MS ME. Hardware characters? TS TE. If your device produces hardcopy you should arrange to start a new page with PG (the PAGE command). When you have finished please send us your new entry.

Support for Raster Devices

Stdgraph can only handle devices that can plot vectors specified by their endpoints; unfortunately some devices (such as most line printers) can only plot graphs when they have been reduced to rows of `on' and `off' pixels. SM supports such devices through DEVICE raster and a separate programme called rasterise. You specify that a device in a graphcap file is a raster device by using DV: :DV=raster: (The old form :RA: is no longer supported). It communicates with the rasteriser through graphcap, so the whole process is user transparent. A separate rasterising programme was written so as to allow the plot to be produced in the background while you do more productive things, and to allow the rasterising to be done on a remote machine.

DEVICE raster produces a file, whose name is specified as usual by the OF field in graphcap, containing the vectors to be plotted (as groups of four short integers) in device coordinates, where the size of the device is taken from xr and yr. When the device is closed, the command specified by SY are executed, and these will usually be of the form rasterise -r $0 $F outfile\n print_it outfile\n delete outfile where print_it is the proper way of actually getting a plot. Under Unix, the command might well be something like (rasterise -r $0 $F - | lpr -v -r -P$1)& dispensing with the temporary outfile.

What do these rasterise commands do? The command syntax is rasterise [-flags] device infile outfile, where the infile may be specified as `-' to use standard input (sys$input to VMS), where the outfile may be specifed as `-' to use standard output (sys$output to VMS). Possible flags are r to remove the infile after use, R to rotate the plot through 90 degrees, and v for more verbose operation. Rasterise then reads the data in the infile, and produces a rasterised version, row by row, on the outfile. In order to do this, it looks in graphcap for an entry for device, and uses the xr, yr, OW (and O[XYZ]), and CW fields as usual. @footnote #{In looking for the graphcap file, any environment file or search path specified on the SM command line with a -f or -u flag is ignored. }

Let's first consider a simple, one-line-at-a-time device such as a line printer. Before writing each row to outfile, rasterise encodes the BR (Begin Row) capability, using the current row number as an argument, and encodes ER (End Row) at the end of the line. By default, it assumes that the raster device simply wants bits turned on where a dot is required, but this can be overridden using the BP and EP capabilities. EP (Empty Pixel) specifies the bit pattern for a character to represent white space. In the simple case mentioned a moment ago, this would be simply NUL, with no bits on, but sometimes this doesn't suffice (see examples below). BP (Bit Pattern) is a string, giving the bit patterns required to turn on the various pixels. In the default case, BP could be specified as BP=\001\002\004\010\020\040\100\200, so \001 would turn on the first (rightmost) dot. Because there are eight characters given in the string, raster assumes that it can fit eight pixels into a single character. If you don't specify a BP this is what will be used. Some devices desire or require that the data be sent as hexadecimal numbers rather than as binary; see the RD=hex graphcap entry.

Some other devices (e.g. Epson printers) choose to print several lines at a time, so a single byte transmitted to the device might print 8 lines, but only the first pixel of each line. Such devices are described to graphcap by being given the MR (Many Rows) capability and a number nb which describes how many bytes deep the printing band is (if omitted nb defaults to 1). In this case, BP is used to describe which bits are turned on vertically rather than horizontally but everything is otherwise the same as for the simple case.

As an example, consider the HP laserjet. You'd specify it as DEVICE laserjet, and its Unix graphcap entry reads:

laserjet|HP laserjet (high resolution):\
        :SY=/usr/local/sm/rasterise -r $0 $F - > /dev/hp&:
On opening the device, it gets the string ^[*r1280^[*rA, setting the resolution and raster mode. Then, at the beginning of each rastered line it gets ^[*b160W specifying that 160 bytes are coming its way, then finally ^[*rB to restore it to alpha mode. (It doesn't need to know which row it is on, so the BR string doesn't tell it, and the default BP and EP are fine). After the input file is read it is deleted, and the output file is sent to the standard output, whence it is redirected to the proper device, in this case directly rather than through a spooler.

A more complex example is a printronix printer, which encodes 6 pixels in each byte, and requires that bit 7 be turned on. It also needs an escape sequence at the end of each line. The corresponding graphcap entry is

printronix|DEC printronix printer:\
        :SY=(/usr/local/sm/rasterise -r $0 $F - | rsh wombat lpr)&:
We use EP to turn on the seventh bit everywhere, as required, and specify only 6 values for BP, so only 6 dots will be packed into each character. The BR entry is empty, and ER provides the needed escape sequences at ends of lines. In this case SY sends the plot over a network to machine wombat.

Some devices are not able to simply accept a string of bytes with an occasional escape sequence. For example, a versatec needs to have the bit order changed, or a simple screen plotter might want to write a * if a bit is set and a space otherwise. If this is the extent of your pathology, you can deal with it via the provided capabilities. (Fortunately adding a * onto a space makes a *, so you can use :EP= :BP=*: for the latter.) If you have a really bad device, it is possible to add new coded device drivers to rasterise. For the convenience of such devices there is a graphcap capability RD which specifies the name of a type of raster device. If rasterise recognises the device it it calls a different set of routines to deal with the rows of data. Otherwise it proceeds as discussed in the previous paragraph. This behaviour is similar to that of the DEVICE command in using stdgraph if it doesn't recognise a device name.

If you find that you do need to write routines for some device, don't be too disheartened. Rasterise will still do the book-keeping and rasterising for you, your work will be limited to a couple of output routines. If you need to know more, see the source for rasterise. The only time that I used this capability came about two years after rasterise was written, and was RD=hex which specifies that lines be written as hexadecimal numbers rather than as 8-bit characters (e.g. write the two characters FF instead of the single character `\377'). The line length is given as ll.

Compiling Graphcap

(This section is really for someone maintaining SM.) Rather than have stdgraph read the graphcap every time that it opens a devices, it is possible to compile the capabilities of the more popular devices into the executable. This is done by preparing an include file which initialises the appropriate arrays, using the programme `compile_g' in the main directory. After this file (called cacheg.dat) has been prepared, files depending on it must be recompiled and SM must be relinked. The use of compile_g is pretty much self-explanatory, you give it a list of the devices you want and it produces the cacheg.dat file. Problems arise, however, if you don't have a valid cacheg.dat file, as then you can't compile compile_g in the first case. Fortunately, it is possible to bootstrap a cacheg.dat file (by defining BOOTSTRAP to the C-preprocessor), and proceed from there.

When stdgraph attempts to use the compiled capabilities, it checks that the current graphcap file has exactly the same name as the one that cacheg.dat was compiled from, if it isn't then it reads the graphcap file anyway. This provides a mechanism for those without C compilers to change the graphcap entries of pre-compiled devices. If you have a list of graphcap files, the name of the first is checked against the name in the `cacheg.dat' file.

@c doesn't throw away a chunk of text

Index to Graphcap Capabilities


  • BP (Bit Pattern)
  • BR (Begin Row)


  • ch (Character Height)
  • CL (CLear)
  • CO (COlour)
  • co (number of COlumns)
  • CS (Colour Start)
  • CT (Colour Type)
  • cw (Character Width)
  • CW (Close Workstation)


  • DC (Default Colour)
  • DE (Draw End)
  • DS (Draw Start)
  • DT (Device Type)
  • DV (DriVer)


  • EP (Empty Pattern)
  • ER (End Row)


  • FD (Fill Draw)
  • FE (Fill End)
  • FS (Fill Start)


  • GD (Graphics Disable)
  • GE (Graphics Enable)


  • IF (Initialisation File)


  • li (number of LInes)
  • ll (lINE lENGTH)
  • lt (Line Type)
  • LW (Line Weight)


  • MC (sM Cursor)
  • ME (Mark End)
  • ML (sM Line)
  • MR (Many Rows)
  • MS (Mark Start)


  • nb (nUM bYTES)


  • OF (Out File)
  • OW (Open Workstation)
  • OX (Open workstation)
  • OY (Open workstation)
  • OZ (Open workstation)


  • pc (Pad Character)
  • PG (PaGe)


  • RA (RAster)
  • RC (Read Cursor)
  • RD (Raster Device)
  • RT (Record Terminator)


  • SC (Scan Cursor)
  • SY (SYstem)


  • TB (Text Begin)
  • TE (Text End)


  • VE (? End)
  • VS (? Start)


  • xr (X-Resolution)
  • XY (X Y)


  • yr (Y-Resolution)
  • Go to the previous, next section.