@CHAPTER TITLE = Working with PCX Files @FIRSTPAR = The Picture file format used by ZSoft for their PC Paintbrush paint program has become one of the more popular formats around. I personally took an interest in it originally because I needed to get diagrams into Ventura Publisher; which not only accepts .pcx files, but will also expand and contract them sensibly. The format turned out to be easy to work with. The files can be loaded and written quickly, using a set of simple functions. Screen update is fast, and the screen can easily be panned around a large picture. Drawing operations for multi-color formats aren't as fast, but that's due to the data structure that I've chosen. I'd like to start by describing PCX pictures in general terms. The Header and Color palette are complicated enough to deserve some more detail. I'll finish with a description of the modules that I've written to generate, write, read and display PCX pictures. You'll probably want to make many improvements to them, but I'm confidant that they'll get a PCX related project off the ground for you. @MAJOR HEADING = The Basic Structure A PCX file consists of a 128 byte header, followed by one or more compressed picture planes. In memory, each plane is basically a bit map, with each bit usually corresponding to a pixel on the screen. The number of bits per pixel, pixels per byte, bytes per line, lines per plane and planes per picture are all stored in the header. On the EGA board, there are four planes (see figure 1.) Bit (X,Y) in each plane corresponds to pixel (X,Y) on the screen. The binary value of the four (X,Y) bits in each plane is used to index into the color palette. On the CGA & HERC boards, only 1 plane is used. For the 720x348 HERC and 640x200 CGA modes, each bit in the plane corresponds to a single pixel on the screen. Using the 320x200 by 4 color CGA mode, there is again only one plane, but each pixel is made up of two adjacent bits. It's possible to make a 1000x1000 by 2 color drawing which can be displayed on a CGA board 640x200 dots at a time, then show it on a Hercules board 720x348 dots at a time, but a picture isn't always so portable. A 320x200 CGA picture has to be translated before it can be shown in another mode. As you can imagine, drawing the picture on the screen is easy <197> all that's needed is a block move of each line of the picture to the VRAM on the board. The trade off is that there has to be some interpretation of the header information, and the pictures don't look quite the same from one board to another due to changes in dot width and height. @MAJOR HEADING = The Header The structure for the header looks like this: @LISTING = typedef struct { unsigned char red, green, blue; } TRIPLET; typedef struct { char maker, version, code, bpp; int x1, y1, x2, y2, hres, vres; TRIPLET triple[16]; char vmode, nplanes; int bpl; char __unused[128-68]; } PCXHDR; The fields maker, version, code and vmode all currently have fixed values <197> 10, 5, 1, & 0. They represent the code for the program that generated the picture, its version number and the type of compression that was applied to the picture planes; vmode is currently ignored; and __unused is reserved for future expansion. hres and vres represent the number of dots, horizontal and vertical, on the video board to be used. nplanes is the number of picture planes used, bpp is the number of bits per pixel and bpl is the number of bytes that are used to store a horizontal line of the picture. (x1,y1)-(x2,y2) refer to the upper left and lower right corners of the picture. For a standard 640x350x16 EGA picture, we would get the following: hres=640, vres=350, x1=0, y1=0, x2=639, y2=349, nplanes=4, bpp=1 and bpl=80. It's interesting to note that PC PaintBrush seems to take these numbers very seriously. If a picture is made after PaintBrush is set up to use a HERC board, it won't even be recognized as a picture file if PaintBrush is then set up to use an EGA board. That's a bit picky considering that a HERC picture is black and white, and an EGA board supports those colors. So, if you plan to use the pictures that you generate on some existing program, be careful to model your picture after one of the various video board modes. To make a picture with a different size, only (x2, y2) and bpl would have to change; and the easiest way to add colors would be to add planes. Although the software that I've written only supports a few of the standard configurations, it'll probably be easy to modify. @MAJOR HEADING = The Color Palette The palette information is stored in the array of 16 triples. It's the only part of the PCX format that's really tricky <197> being totally board dependent. Perhaps Shannon at ZSoft can send Dave a letter explaining it better. Here's the way I understand it: The IBM EGA adapter has 16 single byte color registers, each of which holds a two bit intensity value of from 0 to 3 for each color gun (Red, Green, and Blue.) The intensity values are stored as pairs of bits, like this: "xxRGBrgb". The first two bits, 7 & 6, are ignored. The next three bits are the high bits of the intensity values for the Red, Green & Blue guns. The last three bits are the low bits of the same intensity values. This allows each of the 16 registers to contain one of 64 possible color combinations. The PCX header has a 16 element palette table, one element per color register. Each element is a three byte structure (TRIPLET) which holds an intensity value for each color gun. To set the color for any given palette register, the Red, Green & Blue intensity bytes are set with the desired intensity for that color multiplied by 85. (I have no idea why we multiply by 85.) To set the EGA palette register, we take the intensity value, divide by 85, twiddle the bits, then take the easy route to the palette register by calling the BIOS Set Palette function. The CGA board has a Color Select Register at I/O location 0x3d9. The bit configuration for this register is "xxCAIRGB". The first two bits are ignored. The C bit selects one of two pre-defined palettes, called Color Sets by IBM. Set 1 is Background, Green, Red & Brown. Set 2 is Background, Cyan, Magenta & White. The Background color is one of 16 colors formed using the IRGB bits. I is an intensity bit, R, G & B are enables for the Red, Green and Blue color guns, respectively. If the A bit is set, it "Selects (an) Alternate, Intensified set of colors in Grapics Mode" (see ref. 1.) To me, this translates into 4 color sets and one of 16 background colors. Storage of the CGA palette settings in the PCX header is simple: The first byte of the first element in the palette table contains a background color, multiplied by 16. The first byte of the second element in the palette table apparently contains a three bit value, multiplied by 32. (I haven't been able to figure out what to do with the third bit.) At any rate, there are 8 such settings in the PCX headers palette table, but it seems to me that only one of them can be in effect at a time. There are probably many more palette formats for other boards, but I don't know of any documentation for them. In the code that accompanies this article, the pcx_set_palette() function has a load_palette flag which must be set in order to get the routine to tinker with the palette registers. I didn't bother much with the palette for a number of reasons; the first one being that the palette is automatically set each time a graphics mode is selected; another being that Ventura Publisher ignores it. @MAJOR HEADING = Compressing a Plane When in a file, each plane in the picture is compressed. The compression is very simple; designed simply to take advantage of the fact that a byte value will often repeat. The read byte algorithm works like this: Read a byte. If the two high order bits are not set ((byte & 0xc0) != 0xc0), then return the byte. If the two high order bits are set ((byte & 0xc0) == 0xc0), then the remaining bits represent a repeat count (count = byte & 0x3f) and the next byte is the value that repeats. As such, the repeat count can never be greater than 0x3f, or 63. The write byte algorithm is obviously the opposite operation. @MAJOR HEADING = The Software Originally, what I wanted to do was copy schematics into Ventura. I sat in front of the system for many sleepless nights decoding DXF files, only to find in the end that the GEM files Ventura creates from the DXF files don't properly scale text. To make a long story short, I wrote most of a DXF to PCX converter, then (balking at the prospect of having to write a font editor, create fonts, and write the code to scale them,) scrapped the idea <197> it was less time consuming to just chop the schematics into small blocks. In the mean time, I'd written a (simple) board independent graphics package, several test routines, and a slide show program. As you've just guessed, text is not implemented. Everything was compiled using Manx Aztec 'C 86, V. 4.10a. With the exception of the code that handles flicker on the CGA board, it should compile without complaint on most systems. Note, however, that it'll only work in the large data model; not only because of the huge amount of RAM that it uses, but because of the way I accessed the VRAM. There isn't enough space to list all the code, so I'll document the software here, and you can download the files from one of the following sources: The file "$$$PCX.ARC" will be on the MicroC BBS long before you see this; and I'll put it in one of the IBM forums on Compu-Serve, in thanks to the many people who put together the EGA.ARC file (see ref. 2.) Finally, there'll be a copy in the LISTINGS/IBM.ARC section of BIX. @MAJOR HEADING = The Functions Basically, the functions can be divided into three groups. The lowest level is the board level driver modules. There's one for the HERC, EGA and CGA boards. To make it easy to use, the PCX module is set up as just another video board. Each module has the same collection of functions, each with the same name, although the function name is prefixed with the board name to keep them distinct. The next level is what I call the VGR modules, for Video GRaphics. It's a simple interface between the driver modules and the next higher level. This level is based on a bunch of macros to allow access to the driver level while still being able to switch drivers at run-time. Using these macros, object draw functions can be written which are independent of the low level drivers. So far, I've put in Bressenham's line drawing algorithm and an inefficient but functional irregular polygon fill. There's a circle drawing algorithm in the May '83 issue of Doctor Dobbs Journal which is likely to go in next. The highest level is the Picture level. The functions at this level operate on the following picture structure: @LISTING = typedef struct { PCXHDR hdr; char **rows[4]; } PCXPIC; (I feel I should warn you that the following few paragraphs are exceptionally boring. If you're not interested in how I chose to handle the pointer and bit twiddling details, skip to the next heading...) The PCXHDR is obvious enough, but the rows array deserves some explanation. Figure 2 shows how I visualize it when drawing a 640x350x16 EGA-type picture. A plane is a pointer to an array of rows, each of which is a pointer to an array of characters. That array of characters contains the bits which constitute a horizontal line of pixels. This approach is relatively inefficient in that it forces several 32 bit pointer operations, but it's easy to work with. As you can see, there's currently a hardwired limit of 4 planes, and the arrays are dynamically allocated. For all but the 320x200x4 CGA modes, we can read the bit at position (x,y) on a given plane using the following expression: @LISTING = !!(pcx_cpic<197>>>rows[plane][y][x>>>>3] & (0x80 >>>> (x & 7))); The global variable pcx_cpic is defined in the PCX module as a pointer to a PCXPIC picture (cpic == current picture) It's used to tell the pixel routines what picture to work on. For the HERC board, or the 640x200x2 CGA mode, plane is always zero; the 640x350x16 EGA mode requires that the operation be repeated once for each plane. Since rows[plane] is a pointer to an array of rows, rows[plane][y] is a pointer to the Yth row. All that's needed now is the byte to look at in the row, and the bit in that byte. The low order three bits of x (x & 7) select the bit, and the remaining bits of x (x >>>> 3) become the byte offset for the row. The above expression would return 1 if the bit at (x,y) is set, or zero if it's not. The 320x200x4 CGA mode is slightly more complicated since two adjacent bits are used to select a color. The expression to return the color of the pixel at location (x,y) in that mode would be: @LISTING = i = (x & 0x03) <<<< 1; color = (pcx<197>>>rows[0][y][x>>>>2] & (0xc0 >>>> i)) >>>> (6<197>i); Since each byte represents 4 pixels, x>>>>2 is used as the byte offset for the row, and x & 0x03 is the number of the pixel in the byte. Multiplying that number by 2 gives i, the number of bits from bit 7 that we skip over to get to the first of the two bits that select the color of the pixel. 0xc0 >>>> i makes a mask for those bits. The big difference between this expression and the previous one is that we need a two bit result, so we can't use the !! operator to clean up after the bit-wise AND. Shifting to the right 6<197>i bits does the job. The only difference between the two expressions above and the ones actually used in the EGA and CGA modules is that there aren't any planes. The EGA board uses a plane register to enable each plane and all four planes use the same memory locations. The HERC board has two pages, which can be thought of as unrelated planes. I visualized each horizontal row of pixels as being an array of columns, so the arrays are called columns instead of rows. The array of pointers to each row is dynamically allocated. The pointers are initialized with sequential row addresses for the EGA board, and scattered addresses for the HERC and CGA boards. Although the pointer operations and bit twiddling never get more complicated than that, some of the expressions get a bit dense, and aren't much fun to read. Fortunately, all the low level operations are already implemented; you can just throw them into your library and use them. With that said, let's get down to function names and parameter lists... @MAJOR HEADING = Using the Functions Note that the declarations here don't match the ones in the modules. Many functions that don't return anything are declared as returning int. Most parameters to most of the functions are not range-checked. @PROTO = void allocf(char *p); void map_not(int *map,int len); These two come out of my library. allocf() checks the pointer p to see if it's NULL. If so, it doesn't do anything. If not, it calls free(). If free() returns an error code, an error message is produced, and the program quits. map_not() inverts len ints in the bit-map pointed at by map. @PROTO = void cga_movmem(char far *s,char far *d,int n); void cga_peekb(char far *p); void cga_pokeb(char far *p,char b); These functions are the same as movmem(), peekb() and pokeb(); except that they wait for a horizontal retrace to start on the CGA board before accessing it, to avoid flicker. The cga_movmem() function is a straight block move, so you'll have to check for overlapping blocks if you plan to move a source to a destination which are both in the CGA VRAM. @PROTO = void ega_select_plane(int plane); int pcx_select_plane(int plane); The EGA VRAM holds 4 planes, each of them mapped into the same memory space. ega_select_plane() is used to enable one or more planes. If it's passed a negative number, the absolute value of that number is used as an enable mask, ie: if plane == <197>0x0f, all 4 planes are simultaneously enabled. This is handy when clearing the screen since a single setmem() call can clear all four planes! If plane is not negative, then it must be from 0 to 3, and only that plane is enabled. pcx_select_plane() selects a plane for use by the VGR_ROW() macro when drawing on a PCX picture. @PROTO = void ega_set_palette(char reg,char red,char green,char blue); Sets one of the 16 EGA palette registers. reg can have a value of from 0 to 15. red, green and blue are the desired intensities for each color gun, for that register. They must each have a value of from 0 to 3. @PROTO = int herc_set_page(int page); The HERC driver will default to page 0, but either of the two pages can be selected using this function. @PROTO = int *pcx_init(void); cga_init(void); ega_init(void); herc_init(void); int VGR_HRES, VGR_VRES, VGR_NCOLORS, VGR_NBPL; /* #bytes per line */ The _init() routines initialize all static local variables in a module, allocate any required space (if it hasn't been allocated by a previous _init() call,) and set up the VGR module to use the module that was INITed. The VGR module, among other things, holds the four ints shown above. The HERC and EGA modules always set these values, as they're constants. The PCX module will set them only if pcx_cpic is non-null, therefore pointing to a valid PCXPIC picture. The CGA module sets them after a VGR_MODE() call. The _init() functions each return OK or ERROR, with ERROR usually meaning that an out of memory condition occurred. @PROTO = int VGR_MODE(int m); Once the VGR module has been set up, using the appropriate *_init() function, the "mode" must be set. Since the _init() call set up the array of pointers to functions in the VGR module, it's possible to use the #defined macros in vgr.h. VGR_MODE() is one of those macros. m must be one of the following: @LISTING = MODE_TEXT0 <197> text, 80x25 MODE_APA0 <197> APA, 640x350x16 MODE_APA1 <197> APA, 720x348x2 MODE_APA2 <197> APA, 640x200x2 MODE_APA3 <197> APA, 320x200x4 When drawing on a PCX picture, the mode is only used to tell the PCX module if it's producing a one or two bit per pixel picture; so available PCX modes are effectively MODE_APA0 or MODE_APA3. Available EGA modes are MODE_TEXT0 or MODE_APA0; for the CGA it's MODE_TEXT0, MODE_APA2 or MODE_APA3; and for HERC it's MODE_TEXT0 or MODE_APA1. ERROR or OK is returned, with ERROR indicating an invalid mode number. @PROTO = void VGR_CLEAR() void VGR_SET(int x,int y,int c) void VGR_CLR(int x,int y) void VGR_XOR(int x,int y,int c) int VGR_GET(int x,int y) void VGR_ROW(int r,char far *p,int n) void VGR_MOVE(char far *s,char far *d,int n) void VGR_PEEKB(char far *p) void VGR_POKEB(char far *p,int b) These VGR_ macros allow access to the board independent functions. _CLEAR clears the screen. _SET sets the color of the pixel at (x,y) to c. Note that if c has a value of zero, it has the effect of "clearing" the pixel. It's a good idea to not confuse the two operations since the HERC module ignores the value of c. _CLR sets the color of the pixel to zero. _XOR does a bit-wise exclusive or of the color of the pixel with c. Again, the HERC module ignores c. _GET returns the color of the pixel. _ROW moves n bytes from location p to the VRAM row r. _MOVE, _PEEKB & _POKEB either call movmem(), peekb() & pokeb(), or cga_movmem(), cga_peekb() & cga_pokeb(); depending on the driver module in use. @PROTO = void vgr_fill(int x,int y,int color); void vgr_line(int x1,int y1,int x2,int y2,int color); void vgr_rectangle(int x1,int y1,int x2,int y2,int color); void vgr_point(int x2,int y2,int color); vgr_line(), _rectangle() and _fill() do what they sound like. vgr_point() does one of two things. If color is -1, the values (x2,y2) are copied to the local static values (x1,y1). If color is not -1, vgr_point() calls vgr_line(), then copies (x2,y2) to (x1,y1). This makes it possible to draw lines between a series of points. @PROTO = int vgr_get_board(void); int vgr_mode(char mode); vgr_get_board() attempts to figure out what kind of video graphics board is in use. The return value is one of TYPE_UNKNOWN, TYPE_EGA, TYPE_CGA, TYPE_MDA or TYPE_HERC. These values are defined in vgr.h. vgr_mode() calls the BIOS set graphic mode function; INT 0x10, AH==0. See ref. 2, page A-46 for more info on that BIOS call. @PROTO = PCXPIC *pcx_init_pic(int hres,int vres,int nplanes); void pcx_free_pic(PCXPIC *pic); void pcx_invert_pic(PCXPIC *pic); pcx_init_pic() dynamically allocates space for a PCXPIC structure, and returns a pointer to it. hres & vres are the number of dots horizontally and vertically; nplanes is the number of planes in the picture. All arrays are dynamically allocated, and a pointer to the result is returned. If an out of memory condition occurs, any allocated arrays are de-allocated, and NULL is returned. pcx_free_pic() is used to free up all the space allocated for a picture. pcx_invert_pic() inverts all the bits in each row of the picture. @PROTO = int pcx_read_pic(PCXPIC *pic,FILE *fp); int pcx_getc(int *c,int *n,FILE *fp,int maxn); int pcx_write_pic(PCXPIC *pic,FILE *fp); int pcx_xputc(int c,FILE *fp); int pcx_putc(int c,int n,FILE *fp); These functions operate on a file which has been opened by one of the STDIO buffered file open functions; fp is the file pointer returned by the open call. pic is a pointer to a PCXPIC picture structure. All these functions return OK or ERROR on failure. pcx_read_pic() loads a picture from the file. The pic structure can either be statically allocated, or dynamically allocated using the pcx_init_pic(0,0,0) call. pcx_read_pic() automatically allocates whatever arrays it needs to get the picture in. pcx_write_pic() writes the picture to the file. pcx_getc() reads a character or pair of characters from the file. The character is placed in the integer pointed at by c, and a repeat count of one or more is placed in the integer pointed at by n. maxn is the maximum repeat count that the calling function wants to receive. pcx_write_pic() calls pcx_xputc(), which counts the number of times it receives a given byte value, then calls pcx_putc() with that number. If pcx_xputc() is called with c == -1, the buffered byte value and count are passed immediately (flushed) to pcx_putc(). pcx_putc() writes the byte c to the file, using a repeat count of n. n may have a value of up to 32767; pcx_putc() will recursively call itself with n<<=63 until the correct total has been written to the file. @PROTO = void pcx_showpic(PCXPIC *pic,int hoffs,int voffs,int load_palette_flg); This function uses VGR_ROW to copy rows from the pic picture to the video board driver module in use. hoffs is an offset from the start of each row in pic. voffs is an offset which is applied to the row number. These two offsets allow the screen to be panned around a large picture. The load_palette_flg, when set, allows pcx_showpic() to set the hardware palette registers from the PCXPIC palette table. Consider the palette functions to be unreliable. @MAJOR HEADING = The Test Routine As I'm writing this, PCX.EXE is bug free and running. It's essentially a test routine which excercises the various modules. If executed without command line parameters, it tries to figure out what kind of video board is available. If a CGA/HERC/EGA board is found, it creates a simple test picture (see figure 3,) displays it, then returns to DOS. If it's invoked with a board name, CGA (640x200x2), EGA, HERC or CGA2 (320x200x4); it creates a picture for that board, and tries to display it on that adapter. If it's invoked with a board name followed by a file name, the picture is saved, using the specified file name, after it's been displayed. PCXSHOW.EXE, the slide show program, is in need of some upgrading <197> it was written before the latest improvements to the PCX/VGR modules. It'll be ready before you see this, and will be included with the other files. @MAJOR HEADING = Bibliography @BIBN = 1 @BIB = The IBM Personal Computer XT Technical Reference Manual, April 1983 revision. Part Number 1502237. @BIBN = 2 @BIB = The file EGA.ARC, which was found somewhere on Compu-Serve, probably in one of the IBM forums. @BIBN = 3 @BIB = ZSoft Technical Reference Manual. ZSoft Corporation, 1950 Spectrum Circle, Suite A-495, Marietta, GA 30067. Tel. (404) 428-0008. Many thanks to Shannon. @BIBN = 4 @BIB = A Hercules Primer, by Larry Fogg, MicroCornucopia, Jan-Feb 1988, page 26. @BIBN = 5 @BIB = Tidbits, by Gary Entsminger, MicroCornucopia, Jan-Feb '88, page 84. @BIBN = 6 @BIB = Language Connections, by Gary Entsminger, Turbo Technix, Jan-Feb '88, page 136. @BIBN = 7 @BIB = MicroEMACS by Dan Laurence & others. Public domain text editor, with 'C sources. Available on BIX in LISTINGS/IBM.ARC.