Adding Other Image Formats to xv

This appendix is split up into two sections, one for reading a new file format, and the other for writing a new file format. Note that you do not necessarily have to read and write a new file format. For example, xv can read PCX files, but it doesn't write them. Likewise, xv traditionally could only write PostScript files, but couldn't read them. (And technically, it still doesn't.)

For the purposes of this example, I'll be talking about the PBM/PGM/PPM code specifically. (See the file xvpbm.c for full details.)

Writing Code for Reading a New File Format

Note: Despite the wide variety of displays and file formats xv can deal with, internally it only manipulates 8-bit colormapped images or 24-bit RGB images. If you're loading an 8-bit colormapped image, such as a GIF image, no problem. If you're loading an 8-or-fewer-bits format that doesn't have a colormap (such as an 8-bit greyscale image, or a 1-bit B/W bitmap) your Load () routine will have to generate an appropriate colormap.

Make a copy of xvpbm.c , calling it something appropriate. For the rest of this appendix, mentally replace the string 'xvpbm.c' with the name of your new file.

Edit the Makefile and/or the Imakefile so that your new module will be compiled. In the Makefile , add "xvpbm.o" to the "OBJS = ..." macro definition. In the Imakefile , add "xvpbm.o" to the end of the "OBJS1 = ..." macro definition, and "xvpbm.c" to the end of the "SRCS1 = ..." macro definition.

Edit the new module.

You'll need to #include "xv.h" , of course.

The module should have one externally callable function that does the work of loading up the file. The function is called with two arguments, a filename to load, and a pointer to a PICINFO structure, like so:

/*******************************************/
int LoadPBM(fname, pinfo)
char *fname;   PICINFO *pinfo;
/*******************************************/

The file name will be the complete file name (absolute, not relative to any directory). Note: if xv is reading from stdin, don't worry about it. stdin is always automatically copied to a temporary file. The same goes for pipes and compressed files. Your Load() routine is guaranteed that it will be reading from a real file that appears to be in your file format, not a stream. This lets you use routines such as fseek() , and such.

The pinfo argument is a pointer to a PICINFO structure. This structure is used to hold the complete set of information associated with the image that will be loaded. When your Load() routine is called, the fields in this structure will all be zeroed-out. It is your function's responsibility to load up the structure appropriately, and completely. The structure is defined as:

/* info structure filled in by the LoadXXX() image reading routines */
 typedef struct {byte *pic;                  /* image data */
                 int   w, h;                 /* size */
                 int   type;                 /* PIC8 or PIC24 */
                 byte  r[256],g[256],b[256]; /* colormap, if PIC8 */
                 int   normw, normh;         /* normal size of image.
                                                normally == w,h except when
                                                doing quick load for icons*/
                 int   frmType;              /* def. Format type to save in */
                 int   colType;              /* def. Color type to save in */
                 char  fullInfo[128];        /* Format: field in info box */
                 char  shrtInfo[128];        /* short format info */
                 char *comment;              /* comment text */
                 int   numpages;             /* # of page files, if >1 */
                 char  pagebname[64];        /* basename of page files */
 } PICINFO; 

The Load() function should return '1' on success, '0' on failure.

All other information is communicated using the PICINFO structure. The fields should be setup as follows:

byte *pic;
This is an array of bytes which holds the returned image. The array is malloc() 'd by the Load() routine. The array should be w*h bytes long (for an 8-bit colormapped image) or w*h*3 bytes long (for a 24-bit RGB image). For an 8-bit image, there is one byte per pixel, which serves as an index into the returned colormap (see below). For a 24-bit image, there are three bytes per pixel, in red, green blue, order. In either case, pixels start at the top-left corner, and proceed in normal scan-line order. There is no padding of any sort at the end of a scan line.
 
int w, h;
These variables specify the width and height (in pixels) of the image that has been loaded.
 
int type;
This variable is used to tell the calling routine whether the loaded image is an 8-bit image or a 24-bit image. It must be set equal to PIC8 or PIC24 , whichever one is appropriate. These constants are defined in ' xv.h '.
 
byte r[256], g[256], b[256];
If the returned image is an 8-bit image, you must load up these variables with the colormap for the image. A given pixel value in pic maps to an RGB color through these arrays. In each array, a value of 0 means 'off', and a value of 255 means 'fully on'. Note: the arrays do not have to be completely filled. Only RGB entries for pixels that actually exist in pic need to be set. For example, if pic is known to be a B/W bitmap with pixel values of 0 and 1, you would only have to set entries '0' and '1' of the r,g,b arrays.
 
On the other hand, if the returned image is a 24-bit image, the r,g,b arrays are ignored, and you do not have to do anything with them.
 
int normw, normh;
These specify the 'normal' size of the image. Normally, they are equal to w and h, respectively. The only exception is when doing a 'quick' load for icon generation, in which case it may be possible to read a 'reduced' version of the image, sufficient for generating the tiny icon files. In such a case, w and h would reflect the (reduced) size of the image returned, and normw and normh would reflect the 'normal' image size, for use in the comments displayed in the xv visual schnauzer. Currently only the LoadJFIF() function in xvjpeg.c actually does this.
 
int frmType;
This lets you specify the Format type to automatically select when you Save a file. As such, this is only relevant if you intend to have xv write your image format as well as read it. If you are only writing an image loader, you should set this field to ' -1 '. On the other hand, if you do intend to write a Write() function for your format, you should edit xv.h , find the F_* format definitions, and add one for your format. See xvpcx.c for an example of a load-only format, or xvpbm.c for a load-and-save format.
 
int colType;
Used to determine which Colors setting should be used by default when you save a file. Since xv will use this setting no matter what format you're using, you must fill this field in appropriately regardless of whether or not you plan to have a Write() function for your format. This field should be set to F_FULLCOLOR for any type of color image, F_GREYSCALE for greyscale images, and F_BWDITHER for black-and-white 1-bit images. If in doubt, F_FULLCOLOR is always a safe choice, though it'd be nice if your module 'does the right thing'. (For instance if you read colormapped images, you should check to see if the colormap consists only of shades of grey, and set F_GREYSCALE if it does.)
 
char fullInfo[128];
This string will be shown in the Format field of the xv info window. It should be set to something like this:

Color PPM, raw format (12345 bytes)

char shrtInfo[128];
A 'short' version of the info string. This gets displayed in the info line at the bottom of the xv controls and xv info windows when the image is loaded. It should look like this:
 
512x400 PPM.
 
char *comment;
If your image file format supports some sort of comment field, and you find one in the file, you should malloc() a pointer to a null-terminated string and copy any and all comments into this field. If there are multiple comments in a file, you should concatenate them together to form one long string. This string MUST be null-terminated, as xv will expect to be able to use strlen() on it, and possibly other 'string' functions.
 
int numpages; char pagebname[64];
These two fields will only be used if your are writing a Load() function for a format that may have multiple images per file. If your format only ever has a single image per file, you don't have to worry about (or do anything with) these two fields.
 
On the other hand, if your format does do multiple images per file, and the current file has more than one image in it, then what your program should do is split the multi-image file up into a temporary collection of single-image files, which should probably live in /tmp or something. Once you've done so, you should return the number of files created in numpages , and the 'base' filename of the page files in pagebname . The files created should have a common 'base', with the page number appended. (e.g., "/tmp/xvpg12345a.1", "/tmp/xvpg12345a.2", etc., where "/tmp/xvpg12345a." is the base filename (created by the mktemp() system function)) You should also load the first file and return its image in the normal way.
 
See the LoadPS() function in xvps.c for a complete example of how this is done. Also, note that if your format supports multiple image per file, you should also pass in a ' quick ' parameter, which will tell your function to only load the first 'page' of the file. This is used by the visual schnauzer, which needs to load images when it generates icon files. To speed things up, the schnauzer tells the Load() function to only load the first page, as that's all it need to generate the icon file.

Error Handling

Non-fatal errors in your Load() routine should be handled by calling the function SetISTR(ISTR_WARNING, "%s: %s", bname, err), and returning a zero value. Where bname is the 'simple' name of your file (which can be obtained using BaseName() function in xvmisc.c ), and err should be an appropriate error string.

Non-fatal errors are considered to be errors that only affect the success of loading this one image, and not the continued success of running xv. For instance, "can't open file", "premature EOF", "garbage in file", etc. are all non-fatal errors. On the other hand, not being able to allocate memory (unsuccessful returns from malloc() ) is considered a fatal error, as it means xv is likely to run out of memory in the near future anyhow.

Fatal errors should be handled by calling FatalError(error_string) . This function prints the string to stderr , and exits the program with an error code.

Warnings (such as 'truncated file') that may need to be noted can be handled by calling SetISTR() as shown above, but continuing to return '1' from the Load() routine, signifying success.

Also, if your load routine fails for any reason, it is your responsibility to free() any pointers allocated (such as the pic field and the comment field, and return NULL in these fields). Otherwise, there'll be memory leaks whenever an image load fails.

Hooking it up to xv

Once you have written a Load() routine, you'll want to hook it up to the xv source.

Edit xv.h and add a function prototype for any global functions you've written (presumably just LoadPBM() in this case). Follow the style used for the other Load*() function declarations.

Find the RFT_* definitions and tack one on the end for your format (e.g., RFT_PBM ). This is a list of values that ' ReadFileType() ' can return. We'll be working on that soon enough.

Edit xv.c :

  1. Tell the ReadFileType() routine about your format. Add an 'else-if' case that determines if the file in question is in your format. Note that it must be possible to uniquely identify your format by reading the first 16 characters (or so) of the file. If your file format doesn't have some sort of magic number, you won't be able to conveniently hook it into xv, though you can certainly come up with some sort of kludge...
  2. Tell the ReadPicFile() routine about your format. Add another case for your format type, and have it call your Load() routine with the appropriate arguments.
  3. Hook your file up into the visual schnauzer. Edit the file xvbrowse.c

The first thing you have to do is create a 'generic' icon for your file format. Copy one of the existing ones (such as ' bits/br_pbm.xbm ') to get the size and the general 'look' correct.

#include this icon at the top of the file.

Add an appropriately-named BF_* definition to the end of the list, and increase BF_MAX appropriately.

Have the icon pixmap created in the CreateBrowse() function, by doing something like this:

bfIcons[BF_PBM] = MakePix1(br->win, br_pbm_bits,
                           br_pbm_width, br_pbm_height);

Hook your format into the scanFile() function. Find the following code:

switch (filetype) {
case RFT_GIF:      bf->ftype = BF_GIF;      break;
case RFT_PM:       bf->ftype = BF_PM;       break;

etc...

And add a case for your format. (To map RFT_* values into their corresponding BF_* values.)

Hook your format into the genIcon() function. Find the following code:

sprintf(str, "%dx%d ", pinfo.w, pinfo.h);
switch (filetype) {
  case RFT_GIF:      if (strstr(pinfo.shrtInfo, "GIF89"))
                       strcat(str,"GIF89 file");
                     else
                       strcat(str,"GIF87 file");
  break;
  case RFT_PM:       strcat(str,"PM file");  break

And add a case for your format. This generates an appropriate info string that gets put in the icon files maintained by the visual schnauzer (and displayed whenever you click on an icon in the schnauzer window).

That should do it. Consult the files xv.h, xv.c, xvbrowse.c, and xvpbm.c for any further specifics.