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;

etc...

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.

Adding Code for Writing a New File Format

Note: Despite the wide variety of displays and file formats xv deals with, internally it only manipulates either 8-bit colormapped images or 24-bit RGB images. Your Write() routine must be prepared to take either sort of image, and convert it (if necessary) to the image format that your file format dictates.

If you haven't already done so (if/when you created the Load() function):

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 writing the file. The function is called with a large number of arguments, described below. The function should return '0' if everything succeeded, and '-1' on failure.

/*******************************************/
int
WritePBM(fp,pic,ptype,w,h,rmap,gmap,bmap,numcols,colorstyle,raw,comment)
     FILE *fp;
     byte *pic;
     int   ptype, w,h;
     byte *rmap, *gmap, *bmap;
     int   numcols, colorstyle, raw;
     char *comment;
/*******************************************/
file *fp;

This is a pointer to an already- fopen() 'd stream. Your function should neither open nor close this stream, as that all gets handled elsewhere in xvdir.c .

byte *pic;

This points to the image data that will be written. In the case of a PIC8 image, pic will point to a w*h long array of bytes, one byte per pixel, starting at the top-left, and proceeding in normal scan-line order. There is no padding of any sort at the end of the lines.

In the case of a PIC24 image, pic will point to a w*h*3 long array of bytes. There are three bytes per pixel, in red, green, blue order. The pixels start at the top-left, and proceed in normal scan line order. There is no padding of any sort at the end of the lines.

int ptype, w, h;

These variables describe the format of pic . ptype can be set to either PIC8 or PIC24 . w and h are the width and height of the image, in pixels.

byte *rmap, *gmap, *bmap;
int   numcols;

These pointers point to the colormap associated with the image. They are only relevant when ptype is PIC8, meaning that pic is an 8-bit per pixel colormapped image. These arrays will each be numcols entries long, with a maximum length of 256. Do not attempt to access entries >= numcols , as the colormaps are not necessarily 256 entries long. You are guaranteed that pixel values found in pic will be within the range [0..numcols-1], so you don't have to check each pixel value. Also, do not attempt to access these arrays at all if ptype is PIC24, as these pointers will probably be NULL in that case.

int colorstyle;

The Colors choice selected in the xv save window. It can be either F_FULLCOLOR , F_GREYSCALE , or F_BWDITHER . It will not be F_REDUCED . If the user selects Reduced Color in the xv save window, the appropriate image will be computed, and you'll be given that image, and colorstyle will be set to F_FULLCOLOR .

Likewise, if the user has selected B/W Dithered in the xv save window, an appropriate black-and-white image will have been generated before your Write() routine is called, so you won't have to worry about that. Such an image will be a PIC8 image, with a 2-entry colormap. It is up to you to decide which of the two colors should be written as black, and which should be written as white. You should do this by comparing the values of
MONO(rmap[0],gmap[0],bmap[0])
and
MONO(rmap[1],gmap[1],bmap[1]) .
Whichever value is smaller is the darker of the two, and should be written as black.

int raw;

This is a value passed in specifically for the WritePBM() function, as PBM has two closely-related subformats (raw, and ascii) both of which are written by this one function. Your function won't need this, nor should it be passed in to your function.

char *comment;

This will point to a zero-terminated character string which contains the comments that should be written into the image file. Note that this string can be of any length, and it may contain any number of lines

(separated by ' \\n ' characters). If your image format supports comments in the file, you should write this information to the file. If it doesn't, you should just ignore this variable. Also, this variable may be NULL , (signifying 'no comments'), in which case it should not be used.

You may pass more parameters, since you're going to be adding the call to this function later on. For example, in my PBM code, I pass one more parameter, 'raw' (whether to save the file as 'raw' or 'ascii') to handle two very similar formats. (Rather than having to write WritePBMRaw () and WritePBMAscii () functions.)

Write the function as you deem appropriate. See xvpbm.c for an example of a Write() routine that writes different formats for 1-bit per pixel images, 8-bit per pixel images, and 24-bit per pixel images, based on ptype and colorstyle .

Note: If your file format can only handle 8-bit images, and ptype is set to PIC24 , you will have to call Conv24to8() to convert the 24-bit image into an 8-bit colormapped image that you can write to the file. See xvgifwr.c for an example of how this is done.

That done, edit ' xv.h ' and add a function declaration for your new function. Search for ' WritePBM() ' in the file for a sample declaration to copy.

Also find the block that begins with:

#define F_GIF       0
#define F_JPEG      ( 0 + F_JPGINC)

and add a definition for your format. Note that it'll be easiest to tack it on at the end.

These numbers must be contiguous, as they are used as indices into the fmtMB menu in xvdir.c .

Edit ' xvdir.c '. This is the module that controls the xv save window.

Add another format name, in the appropriate position, to the saveFormats[] string array.

In the function DoSave() , find the following block:

switch (fmt) {
  case F_GIF:
    rv = WriteGIF(fp, thepic, ptype, w, h, rp, gp, bp, nc, col,
picComments);
  break;
  case F_PM:
    rv = WritePM (fp, thepic, ptype, w, h, rp, gp, bp, nc, col,
picComments);
  break;

and add a case for your function.

Finally, if your format has a common 3 or 4 letter filename suffix (like, ".gif", ".jpg", etc.), you should modify the changeSuffix() routine in xvdir.c so that it recognizes your suffix, and puts your suffix on when someone selects your format.

And It's just that easy!

Writing Complex Formats

Okay, maybe it's not that easy...

If your format requires some additional information to specify how the file should be saved (such as the 'quality' setting in JPEG, or position/size parameters in PostScript), then your task is somewhat more difficult. You'll have to create some sort of pop-up dialog box to get the additional information that you want. You'll also have to change the way your Write() function gets called (as it will now get called from your pop-up dialog box). (Though, if you only feel like doing a quick hack, you can probably just

use the GetStrPopUp() function to get a one-line character string from the user, and avoid the complication of writing your own dialog box.)

This is not recommended for anyone who doesn't understand Xlib programming. Frankly, it's not recommended for those who do , either, but they at least stand some chance of success.

The more adventurous types who wish to pursue this should take a look at the xvjpeg.c code, which implements an extremely simple pop-up dialog. A considerably more complicated dialog box is implemented in xvps.c . In addition to writing a module like these for your format, you'll also have to add the appropriate hooks to the DoSave() function (in xvdir.c ) and the HandleEvent() function (in xvevent.c ). ' grep PS *.c ' will be helpful in finding places where the xvps.c module is called.


This document was extracted from the PostScript documentation and formatted by Roy Johnson. Much of the process was automated, and may therefore have introduced errors. Corrections are appreciated.