Defining Custom Image File Formats

The mi_img_custom_format function allows registering up to 16 new file formats, in addition to the file formats already built into raylib such as jpg or tif. This requires installing six functions in raylib (test, create, open, close, read, and write the file) and passing a number of flags. This example defines a new file format foo, which is both the name of the format and the standard file extension (however, it will work with any extension, the new name merely establishes the naming convention).

The format written in this example is Jef Poskanzer's Portable Pixmap format type P6. Reading additionally supports type P3. The standard name of this format is ppm, but raylib already supports this type so a new name had to be chosen for this example.

The following code can be compiled into a shared library (DSO or DLL) and linked into raylib with mi_link_file_add, or with a link statement in the master host's rayrc startup file. For this reason the name of the "mainline" function is module_init - raylib will call this function on loading the library, so no explicit call is needed. However, normally this code will reside in the client application that raylib is linked into, not a separate library, and mi_img_custom_format may be called anywhere after initialization of raylib.

Data Structures

This step defines a data structure that maintains internal information needed during reading and writing. Many formats require such temporary data, for things like temporary scanlines for compression and decompression, size and scanline counters, and subtype fields. It is not required by mental ray, but useful for the custom format code. This data structure is normally allocated by the open and create functions, used by the read and write functions, and released by the close function. Here, it contains a flag ascii that determines what format is being read (P3 ascii or P6 binary), the number of bits per component, and a scanline buffer.

#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <string.h>
#include <shader.h>
#include <geoshader.h>
#include <mirelay.h>

typedef struct {
    miBoolean       ascii;          /* read ascii, not binary */
    int             bits;           /* source bit length per component */
    int             c;              /* last char read from file */
    miBoolean       ok;             /* error during ascii reading? */
    char            *buffer;        /* RGB scanline buffer */
} Ppm_data;

Test File Format

The test function should return miTRUE if the file is a valid PPM file. It receives the first 256 bytes of the file, thus our function can easily check if this contains a PPM header:

/*
 * Return miTRUE if <buf> is a valid ppm header
 * beginning with "P3" for ascii files and "P6" for binary files.
 */

miBoolean customimage_test_ppm(
    miImg_file * const      ifp,            /* open file info */
    char * const            buf)            /* first 256 bytes of file */
{
    return(buf[0] == 'P' && (buf[1] == '3' || buf[1] == '6'));
}

This is a very simple test function; most will try to check header data for consistency and perhaps even whether the file length is correct. False positives prevent all following custom file formats from being recognized because mental ray checks all file formats in order, with custom formats last.

Create and Open File

The following functions create and open files. The actual file is created and opened by raylib, our functions only have to write or read the header, respectively, and allocate the private data block Ppm_data. This data block is attached to ifp→data.p reserved for this purpose. Note, that memory for such data should always be allocated using MEM functions; static variables are a bad idea becaue it makes these functions non-reentrant, and may lead to difficult and unpredictable bugs. mental ray can and often does load multiple texture files concurrently.

A single custom format may handle more than one data type. For example, the create function may be changed to handle both 8 and 16 bits per pixel. In this case, it would check ifp→type to determine what kind of header to write and how large the private scanline buffer needs to be, and the write function described later would write the data in a different format. This can happen only if the typemap argument of mi_img_custom_format (see below) has more than one bit set. This method is strongly recommended, and avoids creating one custom format per data type.

/*
 * Create a ppm P6 file.
 * The header is ascii, the rest binary.
 * The ifp->data.p field is used to attach custom data.
 */

miBoolean customimage_create_ppm(
    miImg_file * const ifp)         /* open file info */
{
    Ppm_data        *data;

    fprintf((FILE *)ifp->fp.p, "P6\n# created by custom format demo\n");
    fprintf((FILE *)ifp->fp.p, "%d %d 255\n", ifp->width, ifp->height);

    data = (Ppm_data *)mi_mem_allocate(sizeof(Ppm_data));
    data->ascii  = miFALSE;
    data->buffer = (char *)mi_mem_allocate(3 * ifp->width);

    ifp->data.p  = (void *)data;

    return(miTRUE);
}

/*
 * Open a ppm file for reading.
 * Allocate all data structures and interpret the header.
 * The header is always ascii, the rest is either ascii or binary.
 */

miBoolean customimage_open_ppm(
    miImg_file * const ifp,         /* open file info */
    char * const       buf)         /* first 256 bytes of file */
{
    Ppm_data        *data;          /* file information */
    int             c;              /* current char from input file */
    int             field[3];       /* header numbers: width, height, 255*/
    int             nfield = 0;     /* next number to read, 0..2, 3=done */

    fgetc((FILE *)ifp->fp.p);                       /* skip magic no */
    fgetc((FILE *)ifp->fp.p);
    fgetc((FILE *)ifp->fp.p);
    for (;;) {
            if (c == '#')
                    do c = fgetc((FILE *)ifp->fp.p);
                    while (c != '\n' && c != EOF);

            else if (c == '\n' && nfield == 3)
                    break;

            else if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
                    c = fgetc((FILE *)ifp->fp.p);

            else if (isdigit(c)) {
                    field[nfield] = 0;
                    do {
                            field[nfield] = field[nfield] * 10 + c - '0';
                            c = fgetc((FILE *)ifp->fp.p);
                    } while (isdigit(c));
                    nfield++;
            } else {
                    mi_img_custom_format_error(ifp, miIMG_ERR_DECODE, 0);
                    return(miFALSE);
            }
    }

    ifp->width  = field[0];
    ifp->height = field[1];
    ifp->comp   = 3;
    ifp->bits   = 8;

    data = (Ppm_data *)mi_mem_allocate(sizeof(Ppm_data));
    data->ascii  = buf[1] == '3';

    if (field[2] > 255) {
            if (!data->ascii || field[2] > 65535) {
                    mi_img_custom_format_error(ifp, miIMG_ERR_SUBTYPE, 0);
                    return(miFALSE);
            } else
                    data->bits = 16;
    } else
            data->bits = 8;

    data->buffer = (char *)mi_mem_allocate(3 * ifp->width);
    ifp->data.p = (void *)data;

    return(miTRUE);
}

Close File

Closing the actual file on disk is done by raylib. This function exists only to release the allocated private data upon close. It is good practice to clear the pointer afterwards to prevent releasing the pointer more than once.

/*
 * Close the ppm file.
 * Release all data structures that have been allocated by
 * the open and create routines. This routine is also called
 * when an open or create failed, to clean up things that
 * have already been successfully allocated.
 */

miBoolean customimage_close_ppm(
    miImg_file * const ifp)
{
    if (ifp->data.p) {
            mi_mem_release((char *)((Ppm_data *)ifp->data.p)->buffer);
            mi_mem_release((char *)ifp->data.p);

            ifp->data.p = 0;
    }

    return(miTRUE);
}

Read File

The read function reads the next scanline into the line buffer maintained by raylib. Its operation depends on whether the open function has recognized a P3 or P6 header, and has set the private ascii flag accordingly. 16-bit ascii data is scaled back to 8 bits.

/*
 * Read one integer from the file; used by the ascii format.
 * Set the error flag if something unexpected is read.
 */

static int read_int(
    miImg_file * const  ifp)        /* open file */
{
    int             n = 0;          /* returned int */
    miBoolean       minus;          /* shouldn't happen */
    Ppm_data        *data = (Ppm_data *)ifp->data.p;
    int             c = data->c;

    while (c == ' ' || c == '\t' || c == '\n' || c == '\r')
            c = fgetc((FILE *)ifp->fp.p);
    if (minus = c == '-')
            c = fgetc((FILE *)ifp->fp.p);
    if (!isdigit(c)) {
            mi_img_custom_format_error(ifp, miIMG_ERR_DECODE, 0);
            data->ok = miFALSE;
    }
    while (isdigit(c)) {
            n = n * 10 + c - '0';
            c = fgetc((FILE *)ifp->fp.p);
    }
    if (data->bits > 8)
            n >>= 8;
    data->c = c;

    return(minus ? 0 : n > 255 ? 255 : n);
}

/*
 * Read one RGB scanline from the file, either in ascii or binary format.
 */

miBoolean customimage_read_ppm(
    miImg_file * const  ifp,        /* open file */
    miImg_line * const  line)       /* ptrs to component buffers */
{
    Ppm_data                *data = (Ppm_data *)ifp->data.p;
    register char           *p = data->buffer;
    register miUchar        *r = miIMG_LINEACCESS(line, miIMG_R);
    register miUchar        *g = miIMG_LINEACCESS(line, miIMG_G);
    register miUchar        *b = miIMG_LINEACCESS(line, miIMG_B);
    register int            x;

    data->ok = miTRUE;
    if (data->ascii) {
            data->c = '\n';
            for (x=ifp->width; x; x--) {
                    *r++ = read_int(ifp);
                    *g++ = read_int(ifp);
                    *b++ = read_int(ifp);
            }
    } else {
            if (fread(p, ifp->width, 3, (FILE *)ifp->fp.p) != 3) {
                    mi_img_custom_format_error(ifp, miIMG_ERR_READ, errno);
                    return(miFALSE);
            }
            for (x=ifp->width; x; x--) {
                    *r++ = *p++;
                    *g++ = *p++;
                    *b++ = *p++;
            }
    }

    return(data->ok);
}

Write File

Writing a scanline of pixel data is very similar, except that only one format (binary P6) is supported here.

/*
 * Rrite one RGB scanline to the file.
 */

miBoolean customimage_write_ppm(
    miImg_file * const  ifp,        /* open file */
    miImg_line * const  line)       /* ptrs to component buffers */
{
    Ppm_data                *data = (Ppm_data *)ifp->data.p;
    register unsigned char  *q = (unsigned char *)data->buffer;
    register miUchar        *r = miIMG_LINEACCESS(line, miIMG_R);
    register miUchar        *g = miIMG_LINEACCESS(line, miIMG_G);
    register miUchar        *b = miIMG_LINEACCESS(line, miIMG_B);
    register int            x;

    for (x=ifp->width; x; x--, q+=3) {
            q[0] = *r++;
            q[1] = *g++;
            q[2] = *b++;
    }
    if (fwrite(data->buffer, ifp->width, 3, (FILE *)ifp->fp.p) != 3) {
            mi_img_custom_format_error(ifp, miIMG_ERR_WRITE, errno);
            return(miFALSE);
    }

    return(miTRUE);
}

Register File Format

Finally, all the custom functions need to be registered with raylib. After registering, the format is available and supported just like any builtin format. The mi_img_custom_format function can fail only if an illegal format or a null name is passed.

/*
 * Install the functions in raylib.
 * The name "module_init" was chosen so this file can be compiled
 * to a library and linked into raylib. raylib will always call
 * module_init in a library it has just loaded.
 */

DLLEXPORT void module_init(void)
{
    if (!mi_img_custom_format(miIMG_FORMAT_CUSTOM_0,
		customimage_test_ppm,
		customimage_create_ppm,
		customimage_open_ppm,
                    customimage_close_ppm,
                    customimage_read_ppm,
                    customimage_write_ppm,
                    miTRUE,               /* top line first */
                    "foo",                /* standard extension */
                    1 << miIMG_TYPE_RGB,  /* allowed types */
                    miIMG_TYPE_RGB,       /* preferred data type*/
                    0))                   /* always 0 */
            mi_error("internal error: failed to create foo image format");
}
Copyright © 1986, 2015 NVIDIA ARC GmbH. All rights reserved.