Appendix E. Implementing Your Own Image File Format

IFL supports a wide variety of image formats, including .tiff, .rgb, .rgba, .jpeg, and .gif. (For a complete list of supported file formats, see “Supported IFL Image File Formats”.) IFL is extensible, however, so that you can easily add support for additional file formats. You do that by

  1. deriving your file format class from iflFile and iflFormat

  2. implementing your derived class

  3. adding your file format to the file format database, ifl_database

The file format supplied with IFL, FIT, provides the sample code described throughout this chapter that demonstrates how you can extend IFL to implement your own file format. The code for the FIT format is also available in the software distribution in

/usr/share/src/ifl/src/iflFITFile.c++

Although the C++ code might differ slightly from the excerpts shown in this chapter, the functionality remains the same.

This chapter describes how to add and implement your own image file format.

Deriving and Implementing Your Image File Format Class

iflFile is an abstract, base class that you use to derive your image file format class. Every iflFile object is an image file format class, such as iflTIFFFile (.tiff) and iflFITFile (.fit). iflFile does not have a public constructor or destructor, so you cannot use iflFile directly.

In your new image file format class, you need to provide functions that

  • create a new file or open an existing one

  • read data from a file into the cache, one page at a time, decompressing it if necessary

  • write data from the cache into a file, one page at a time, compressing it if necessary

  • close a file

  • allow your format to be registered

To accomplish these tasks, your derived class will typically use the following inherited member functions of iflFile that open, create, and close the file, flush the buffer, and parse the file name:

  • open()

  • create()

  • close()

  • flush()

  • parseFileName()

These functions (and a class destructor) are the minimum number of functions your class must provide. Very likely, your class will provide more capabilities including, perhaps, your own version of reset() (declared in ilLink and ilImage) to handle altered parameters properly.

The remainder of this section describes how the iflFile methods implement these necessary tasks.

Opening an Existing File

You can specify a file by a filename, a file descriptor, or both. If both are specified, the file descriptor is used to open the file. In this case, the filename is stored for use with error messages and the iflFile::getfileName() method.

The iflFile::open() method is defined as follows:

iflFile* open(fileDesc, filename, mode, format, status);

where fileDesc is the file descriptor.

The name of the file, filename, can be followed by an index to specify sub-images using the following syntax:

filename:index

For more information about changing the index after a file is open, see “Functions that Manipulate the Image Index”.

The mode argument specifies the read-write permissions set on the file. The two valid mode are read-only, O_RDONLY, and read-write, O_RDWR.

The format argument specifies the file format for the opened file. If you set this argument to NULL, the usual implementation, the file format is deduced from the file's contents, in particular, its magic number. You can, however, use the format argument to specify a file format.

The status argument is set to an error value if the open() method fails. If you have not implemented error messages, you should set this argument to NULL. If the method fails, the return value of the method is NULL.

If the open() method succeeds, an iflFile* pointer is returned to a derived class of iflFile, such as iflTIFFFile. The object created with open() can then be manipulated by the methods in the derived class. It is the application's responsibility to deallocate the object using the iflFile::close() method, for example,

newFileObject->close();

The iflFITFile file format uses two constructors to open a file. The first constructor uses just the filename to open the file, the second uses the file descriptor:

static iflFile* open(const char* filename, int mode = O_RDONLY,     iflStatus* status = NULL);
static iflFile* open(int fileDesc, const char* filename, 
    int mode = O_RDONLY, iflFormat* format = NULL, 
    iflStatus* status = NULL);

Example E-1 shows how ilFITFile.c++ implements opening a file.

Example E-1. Opening a File


iflStatus iflFITFile::openFile()
{
    int fd_opened_here = 0;
    iflStatus status;

    if (fd < 0) {
        assert(filename != NULL);
        fd = (iflStatus)::open(filename, accessmode);
        if (fd < 0)
            return iflStatusEncode(iflOPENFAILED, iflSubDomainUNIX, ::oserror());
        fd_opened_here = 1;
    }

    needHeader = 0;     // must be initialized for destructor!
    dataWritten = 1;    // so extensions can't be reserved

    // read the header
    if ((status = readHeader()) != iflOKAY) {
        if (fd_opened_here) {
            (void)::close(fd);
            fd = -1;
        }
        return status;
    }

    // fill in other info
    compression = iflNoCompression;
    calcPageParams();

    return iflOKAY;
}

Creating a New Image File

You can create a new file by specifying many of the same values you used in the open() method. You can create a new file using a filename, a file descriptor, or both. If both are specified, the file descriptor is used to create the file and the filename is stored for use with error messages and the iflFile::getfileName() method.

The iflFile::create() method is defined as follows:

iflFile* create(fileDesc, filename, source, config, format, status);

All of the arguments in the create() method have the same mean as those described for the open() method. Only the source and config arguments are different.

The config argument is a structure defined in iflFileConfig that specifies a wide range of file characteristics, including the file's

  • dimensions

  • data type

  • dimension order

  • color model

  • orientation

  • compression

  • page dimensions

If any of these characteristics are not given, the source argument is used to define them. If the source value is not defined, the characteristics default to the preferred values for the file format.

The source argument points at an iflFile object. If any of the file's characteristics are not defined in the config structure, the characteristics are set to be the same as those of the source object.

If the create() method succeeds, an iflFile pointer is returned to a derived class of iflFile, such as iflFITFile. If the method fails, the method returns NULL and you can use the status argument to set an error value.

Example E-2 shows how ilFITFile.c++ implements creating a file.

Example E-2. Creating a File


iflStatus iflFITFile::createFile()
{
    // validate the c page size
    if (pageSize.c == 0)
        pageSize.c = order==iflSeparate? 1:size.c;
    else if (order != iflSeparate && pageSize.c != size.c)
        return iflStatusEncode(iflBADPARAMS);

    if (fd < 0) {
        assert(filename != NULL);
        fd = ::open(filename, accessmode|O_CREAT|O_TRUNC, 0666);
        if (fd < 0)
            return iflStatusEncode(iflOPENFAILED, iflSubDomainUNIX, ::oserror());
    } else {
        (void)::ftruncate(fd, (off_t)0);
    }

    dataOffset = userOffset = sizeof(FIThead02);

    // flag the header/data as not written
    needHeader = 1;
    dataWritten = 0;

    calcPageParams();

    scaleMinValue = iflDataMin(dtype);
    scaleMaxValue = iflDataMax(dtype);

    return iflOKAY;
}

Closing a File

Whether you open or create a new file object, you must write a destructor that terminates it. This destructor needs to:

  • finish writing out any modified pages of image data to disk

  • close the file and release the file descriptor

  • free any temporary buffers that were allocated

You use the iflFile::close() member function to close files, defined as follows:

iflStatus close(int flags = 0);

where flags can be set to IFL_CLOSE_DISCARD which means that iflFile::flush() is not automatically called so that buffered file data is not flushed when the file object is closed.

The close() method performs the following tasks:

  • flushes any buffered file data (unless the IFL_CLOSE_DISCARD flag is set)

  • closes the file

  • destroys the file object

The close() method automatically calls the iflFile::flush() and iflFile::closeFile() methods to carry out these tasks. Even if any of these three methods returns an error, the above tasks are performed.


Note: The file descriptor is closed even if it is opened prior to the original iflFile::open() or iflFile::create() call. To keep a file descriptor open, use dup() on the file descriptor before closing the file and then pass the duplicated file descriptor to an open() or create() method.

The following code shows how you might implement a destructor for a file format:

iflFileFormat::~iflFileFormat() {FileFormat->close();}

Example E-3 shows how ilFITFile.c++ implements closing a file.

Example E-3. Closing a File


iflStatus iflFITFile::closeFile()
{
    assert(fd >= 0);

    if (::close(fd) != 0)
        return iflStatusEncode(iflCLOSEFAILED, iflSubDomainUNIX, ::oserror());

    return iflOKAY;
}

Flushing the Buffer

The iflFile::flush() method is a virtual function that displays any buffered data associated with an iflFile object. It is automatically called by iflFile::close() unless the environment variable, IFL_CLOSE_DISCARD, is set. In this case, the data in the buffer is flushed but not displayed.

You might like to call flush() before closing an image file if, for example, you want to optimize memory space or system performance.

If flush() succeeds, it returns iflOKAY; if not, it returns an appropriate iflStatus error value.

Example E-4 shows how ilFITFile.c++ implements flushing a buffer.

Example E-4. Flushing a Buffer


iflStatus
iflFITFile::flush()
{
    // update the header if necessary
    if (needHeader) {
        iflStatus status = writeHeader();
        if (status != iflOKAY) return status;
        needHeader = 0;
    }
    return iflOKAY;
}

Parsing the File Name

iflFile::parseFileName() is a static class member function. It is used to parse a file name for IFL. IFL file names have the following syntax:

<name-of-file>[#<format-name>][:<sub-image-index>]
    [%<format-specific-args>]

This function is called automatically by the iflFile::open() member function. It is defined as follows:

static char* parseFileName(const char* fullname, 
    char** formatName=NULL, int* index=NULL, char** formatArgs=NULL);

The return value is the actual filename and must be deleted by the user. The sub-image index can be returned using index if it is non-NULL. If an index is not present in the filename, -1 is returned. The format name can be returned using formatName if it is non-NULL. If no format name is present in the filename, NULL is returned. The format-specific argument string can be returned using formatArgs if it is non-NULL. If format-specific arguments are not present in the filename. NULL is returned.

Reading and Writing Formatted Data

In addition to providing functions that open and close files, you need to override getPage(), setPage(), getTile(), and getPage() to read and write image data in your file format. These functions are declared as follows:

virtual iflStatus getPage(void* data, int x, int y, int z, int c,
                     int nx, int ny, int nz, int nc);
virtual iflStatus setPage(const void* data, int x, int y, int z, int c,
                     int nx, int ny, int nz, int nc);
iflStatus getTile(int x, int y, int z, int nx, int ny, int nz,
                     void *data, const iflConfig* config=NULL);
iflStatus setTile(int x, int y, int z, int nx, int ny, int nz,
                     const void *data, const iflConfig* config=NULL);

The data argument for getPage() is a pointer to an already allocated, page-sized buffer into which data is read. This buffer must be large enough to hold a page of data. For setPage(), data is a pointer to a page-sized chunk of memory that contains data that will be written to disk. These functions are described in greater detail in the following sections.

These member functions are used by an IFL application to read image data from an image file into memory, or to write image data from memory to an image file.

Standard practice is to use getTile(), setTile() functions. They enable reading and writing of arbitrary rectangular regions, tiles, with an arbitrary datatype, dimension ordering, and orientation.

Optimized applications may use the lower-level getPage(), setPage() functions. The specified regions in these function are the file's natural pages.

Using getTile()

This member function reads an arbitrary rectangular region from the image file into memory. The portions of the memory buffer corresponding to the area outside of the rectangular boundaries of the source file image are left undisturbed.

The arguments, x, y, z, nx, ny, nz, specify the origin and size of the desired tile within the source image file, in the orientation indicated in the config parameter. The data argument specifies the address of the memory buffer into which the data should be placed. The config argument describes the configuration of the memory buffer. Its orientation affects the interpretation of x, y, z, nx, ny, nz.

A successful call to getTile() returns iflOKAY. If an error occurs, an iflStatus error value is returned. In an error condition, the buffer's contents are undefined.

Using setTile()

This member function writes an arbitrary rectangular region from a memory buffer into the image file. The portions of the memory buffer corresponding to area outside the boundaries of the source file image are ignored.

The arguments x, y, z, nx, ny, nz specify the origin and size of the target tile within the destination image file, in the orientation indicated in the config parameter. The data argument specifies the address of the memory buffer containing the data to be written. The config argument describes the configuration of the memory buffer. Its orientation affects the interpretation of x, y, z, nx, ny, nz.

A successful call to setTile() returns iflOKAY. If an error occurs, an iflStatus error value is returned describing the error. In an error condition, the buffer's contents are undefined.


Note: setTile() may make calls to the subclass's getPage() and setPage() functions in order to write a tile that is not aligned with the file's pages.


Using getPage()

This virtual member function reads a page of image data from the image file. The data argument specifies the address of the memory buffer into which the data should be placed. The arguments x, y, z, nx, ny, nz specify the origin and size of the desired page within the source image file. The caller must guarantee that nx, ny, nz are the image's page size and that x, y, z are a multiple of the page size. No checking is done by the function.

A successful call to getPage() returns iflOKAY. If an error occurs, an iflStatus error value is returned describing the error. In an error condition, the buffer's contents are undefined.


Note: If the caller makes multiple, concurrent calls to getPage() or setPage(), it needs to set i/o callbacks.

getPage() is required to surround code that must be executed atomically by calls to beginFileIO().

Using setPage()

This virtual member function writes a page of image data from the image file.

The data argument specifies the address of the memory buffer containing the data to be written. The arguments x, y, z, nx, ny, nz specify the origin and size of the desired page within the source image file. The caller must guarantee that nx, ny, nz are the image's page size and that x, y, z are a multiple of the page size; no checking is done by the function.

successful call to setPage() returns iflOKAY. If an error occurs, an iflStatus error value is returned describing the error. In an error condition, the buffer's contents are undefined.

Example E-5 shows how ilFITFile.c++ implements reading and writing data.

Example E-5. Reading and Writing Data in the FIT Format


iflStatus
iflFITFile::getPage(void* data, int x, int y, int z, int c, int,int,int,int)
{
    iflStatus returnval = iflOKAY;
    beginFileIO();
    lseek (fd, pageOffset(x,y,z,c), SEEK_SET);
    int sts = read(fd, data, pageSizeBytes);
    if (sts == -1)
        returnval = iflStatusEncode(iflREADFAILED, iflSubDomainUNIX, ::oserror());
    endFileIO();

    return returnval;
}

iflStatus
iflFITFile::setPage(const void* data, int x,int y,int z,int c,int,int,int,int)
{
    iflStatus returnval = iflOKAY;
    beginFileIO();
    lseek (fd, pageOffset(x,y,z,c), SEEK_SET);
    int sts = write(fd, data, pageSizeBytes);
    if (sts == -1)
        returnval = iflStatusEncode(iflWRITEFAILED, iflSubDomainUNIX, ::oserror());
    endFileIO();

    dataWritten = 1;

    return returnval;
}

Functions that Manipulate the Image Index

An image file can contain more than one image, depending on the file format. For example, the TIFF and GIF formats allow a file to contain any number of unrelated images, and the Kodak Photo CD Image Pac (PCD) and JFIF formats allow access to multiple resolutions of the same image. You can use image operations and queries on each image in a file by using an iflFile object's current image index.

The functions you can use to manipulate an image index are

  • getNumImg()

  • getCurrentImg()

  • setCurrentImg()

The application can change the index by calling the object's setCurrentImg() method. The current index and total number of images in the file can be queried by calling the getCurrentImg() or getNumImg() method, respectively. The initial index may also be set by specifying an index with the filename argument to iflFile::open().


Note: These operations are meaningful even if the file format does not support multiple images per file. In that case, getNumImgs() returns 1, getCurrentImg() returns 0, and setCurrentImg(i) will succeed only if i == 0.

The following sections describe these functions in greater detail.

Using getNumImgs()

This virtual member function returns the number of images contained in the image file.

The function is defined as follows:

virtual int getNumImgs();

Using getCurrentImg()

This virtual member function returns the iflFile's current image index. The first image in the file is number zero. The second image in a file is number one, and so on.

The function is defined as follows:

virtual int getCurrentImg();

Using setCurrentImg()

This virtual member function sets the current image index to the specified value.

If the operation is successful, the function returns iflOKAY and the image index is changed. If the argument given in the function is out of bounds of the images in the file, the function returns iflStatusEncode(iflFILEFINDEXOOB) and the image index is left unchanged.

If the operation fails for some other reason, an ifl error is returned using the iflError() mechanism. If the program continues, the file's image index will be in an unknown state.

The function is defined as follows:

virtual iflStatus setCurrentImg(int i);

Adding Images to Image Files

If an image file has write permissions and the file format supports the addition of images, an application can use appendImg() to append an image to an image file. When the function succeeds, the new image in the file is given an index number and the index marker is updated to that of the added image.

The function is defined as follows:

virtual iflStatus appendImg(iflFile* source, iflFileConfig* fc=NULL);

The dimensions, datatype, dimensionorder, colormodel, compression, and pagedims parameters specified in the iflFileConfig structure are treated the same as in the create() static member function.

If the operation is successful, the function returns iflOKAY. If the operation fails, an ifl error is returned using the iflError() mechanism. and the file's contents and the object's current index will be in an unknown state.

Deriving an Image File Format from iflFormat

An iflFormat object describes the capabilities of an image file format. Usually, there is only one such object per file format. Functions whose return values are of type iflFormat, such as iflFile::getFormat(), generally return a pointer to the static iflFormat object of the derived class.

iflFormat is an abstract base class. Every iflFormat object is actually a file-format-specific subclass, such as iflTIFFFormat.

Deriving Subclasses

A derived class must define all of the following pure virtual functions found in iflFormat:

accessModeIsSupported()

randomAccessWriteIsSupported()

typeIsSupported()

getPreferredType()

orderIsSupported()

getPreferredOrder()

colorModelIsSupported()

getPreferredOrientation()

compressionIsSupported()

getPreferredColorModel()

sizeIsSupported()

getPreferredCompression()

pagingIsSupported()

getPreferredPageSize()

pageSizeIsSupported()

newfileobj()

randomAccessReadIsSupported()

 

Defining all of these functions fully characterizes the derived file format. newfileobj() tells iflFile::open() how to create an iflFile object in the derived format.

Each image file format subclass must declare a single static object of its derived type. Because the base class constructor is invoked automatically when the DSO is opened (because of the static object declaration) the derived format is placed on the global format list as part of its initialization. The following code shows you a sample declaration:

static iflFITFormat theFITformat;

If you install ifl_dev.sw.gifts, you can check the source code provided in /usr/share/src/ifl for examples of deriving from iflFormat.

Virtual Function Descriptions

This section describes all of the virtual functions listed in “Deriving Subclasses”.

Table E-1. iflFormat's Virtual Functions

Function

Description

accessModeIsSupported()

Tells whether the given access mode (which must be one of O_RDONLY, O_WRONLY, or O_RDWR) is supported by the subclass

typeIsSupported()

Tells whether the type is supported by the image file format.

orderIsSupported()

Tells whether the order is supported by the image file format.

colorModelIsSupported()

Tells whether the color model is supported by the image file format.

compressionIsSupported()

Tells whether the compression is supported by the image file format.

sizeIsSupported()

Tells whether the given image size x, y, z, c (which the caller must guarantee are all positive values) is supported by the image file format.

pagingIsSupported()

Tells whether the image file format supports any page size other than the entire image size.

pageSizeIsSupported()

Tells whether the image file format supports the given page size pWidth, pHeight, pz, pc for an image whose size is iWidth, iHeight, iz, ic, which must be a supported image size.

randomAccessWriteIsSupported()

Tells whether the iflFile subclass supports random access reads or writes (as opposed to requiring that reads or writes occur sequentially).

getPreferredType()

Returns the image file format's preferred type attribute value. The returned value must be one of the supported values.

getPreferredOrder()

Returns the image file format's preferred order attribute value. The returned value must be one of the supported values.

getPreferredOrientation()

Returns the image file format's preferred orientation attribute value. The returned value must be one of the supported values.

getPreferredColorModel()

Returns the image file format's preferred color model attribute value. The returned value must be one of the supported values.

getPreferredCompression()

Returns the image file format's preferred compression attribute value. The returned value must be one of the supported values.

getPreferredPageSize()

Returns the image file format's preferred page size attribute value. The returned value must be one of the supported values.

newfileobj()

Used by the static functions iflFile::open() and iflFile::create() to construct an object of the derived class. The implementation should only return a new object of the desired subclass of iflFile.

randomAccessWriteIsSupported()

Tells whether the value of an image attribute is supported by the image file format.

The following section shows you a code excerpt that defines all of these virtual functions.

Sample Code for Virtual Function Definitions

Example E-6 shows an excerpt from iflFITFile.c++ that demonstrates how the virtual functions in iflFormat are defined.

Example E-6. Defining Virtual Functions for Your Image File Format


class iflFITFormat : public iflFormat {
public:

    iflFITFormat() : iflFormat(“FIT”) {} 
    int typeIsSupported(iflDataType) { return 1; }
    int orderIsSupported(iflOrder) { return 1; }
    int orientationIsSupported(iflOrientation) { return 1; }
    int colorModelIsSupported(iflColorModel) { return 1; }
    int compressionIsSupported(iflCompression cmp)
        { return cmp == iflNoCompression; }
    int sizeIsSupported(int,int,int,int, iflOrientation)
        { return 1;}
    int pagingIsSupported() { return 1; }
    int pageSizeIsSupported(int,int,int,int, int,int,int,int,         iflOrientation)
        { return 1; }
    int randomAccessReadIsSupported() { return 1; }
    int randomAccessWriteIsSupported() { return 1; }
    virtual iflDataType getPreferredType() { return iflUChar; }
    virtual iflOrder getPreferredOrder() { return iflInterleaved; }
    iflOrientation getPreferredOrientation() 
        { return iflLowerLeftOrigin; }
    virtual iflColorModel getPreferredColorModel(int nc)
        { return nc == 0? iflRGB : iflColorModelFromChans(nc); }
    virtual iflCompression getPreferredCompression()
        { return iflNoCompression; }
    void getPreferredPageSize(int, int, int, int sc,
        int& pWidth, int& pHeight, int& pz, int& pc, iflOrientation)
        { pWidth = 128; pHeight = 128; pz = 1; pc = sc; }
    iflFile* newfileobj() { return new iflFITFile; }
};
// This static object declaration causes the file format to be 
// automatically registered when the DSO containing this code is 
// dlopen'ed.

static iflFITFormat theFITformat;

Registering an Image File Format

To register your file format with IFL, you must create a dynamic shared object (DSO). A DSO allows programs that use IFL to recognize your new file format without relinking to those programs. The DSO contains the code for both your file format and the registration object.

Each image file format subclass must declare a single static object of its derived type. The base class constructor is invoked automatically when the DSO is opened (because of the static object declaration). When the constructor is called, the derived format is placed on the global format list as part of its initialization.

This following static object declaration for iflTIFFFormat causes the file format to be automatically registered when the DSO containing this code is dlopen'ed.

static iflTIFFFormat theTIFFFormat;

Using the File Format Database

To add support for a new image file format, you create a Dynamic Shared Object (DSO) that implements the format and adds an entry in the IFL file format database. IFL uses a text database file to determine what file formats IFL can use. The database is normally located in the file, usr/lib/ifl/ifl_database, but this location may be changed using the IFL_DATABASE environment variable.

You can add your own file formats to the database manually by using the following procedure:

  1. Create a file of image format descriptions using the following format:

    format YourFormat
        match       		ushort(0) == 0x01da || ushort(0) == 0xda01
        description 		“Your New Format”
        dso			         libiflYOURS.so
        subsystem   		“ifl_eoe.sw.c++”
        suffixes    		.yrfmt
    format YourAlternateFormat
        match 	ulong(0) == 0x49492a00 || ulong(0) == 0x4d4d002a
        description    	“Your Alternate Format”
        dso            			libiflALT.so
        subsystem      		“ifl_eoe.sw.c++”
        suffixes       		.altfmt
    

  2. In the file you created in step one, use the #include directive to include the file ifl/src/ifl_database, which defines all ifl-supported image file formats.

  3. Set the IFL_DATABASE environment variable to point at the file created in step one.