/*
 * imgXBM.c --
 *
 *	A photo image file handler for XBM files.
 *
 * Written by:
 *	Jan Nijtmans
 *	CMG Oost-Nederland B.V.
 *	email: j.nijtmans@chello.nl (private)
 *	       jan.nijtmans@cmg.nl (work)
 *	url:   http://purl.oclc.org/net/nijtmans/
 *
 */
#include "tk.h"
#include "tkVMacro.h"
#include "imgInt.h"
#include <string.h>
#include <ctype.h>
#include <stdlib.h>

/* constants used only in this file */

#define MAX_BUFFER 4096

/*
 * The following data structure is used to describe the state of
 * parsing a bitmap file or string.  It is used for communication
 * between TkGetBitmapData and NextBitmapWord.
 */

#define MAX_WORD_LENGTH 100
typedef struct ParseInfo {
    MFile handle;
    char word[MAX_WORD_LENGTH+2];
				/* Current word of bitmap data, NULL
				 * terminated. */
    int wordLength;		/* Number of non-NULL bytes in word. */
} ParseInfo;

/*
 * The format record for the XBM file format:
 */

static int		ChnMatchXBM _ANSI_ARGS_((Tcl_Channel chan,
			    Tcl_Obj *fileName, Tcl_Obj *format, int *widthPtr,
			    int *heightPtr, Tcl_Interp *interp));
static int      	ObjMatchXBM _ANSI_ARGS_((Tcl_Obj *data,
		            Tcl_Obj *format, int *widthPtr, int *heightPtr, Tcl_Interp *interp));
static int		ChnReadXBM  _ANSI_ARGS_((Tcl_Interp *interp,
			    Tcl_Channel chan, Tcl_Obj *fileName,
			    Tcl_Obj *format, Tk_PhotoHandle imageHandle,
			    int destX, int destY, int width, int height,
			    int srcX, int srcY));
static int	        ObjReadXBM _ANSI_ARGS_((Tcl_Interp *interp,
			    Tcl_Obj *dataObj, Tcl_Obj *format,
              		    Tk_PhotoHandle imageHandle, int destX, int destY,
		            int width, int height, int srcX, int srcY));
static int              ChnWriteXBM _ANSI_ARGS_((Tcl_Interp *interp,
                            char *fileName, Tcl_Obj *format,
                            Tk_PhotoImageBlock *blockPtr));
static int	        StringWriteXBM _ANSI_ARGS_((Tcl_Interp *interp,
		            Tcl_Obj *format, Tk_PhotoImageBlock *blockPtr));

static int		CommonReadXBM _ANSI_ARGS_((Tcl_Interp *interp,
			    ParseInfo *parseInfo,
			    Tcl_Obj *format, Tk_PhotoHandle imageHandle,
			    int destX, int destY, int width, int height,
			    int srcX, int srcY));
static int		CommonWriteXBM _ANSI_ARGS_((Tcl_Interp *interp,
			    CONST char *fileName, Tcl_DString *dataPtr,
			    Tcl_Obj *format, Tk_PhotoImageBlock *blockPtr));

Tk_PhotoImageFormat imgFmtXBM = {
    "xbm",					/* name */
    ChnMatchXBM,	/* fileMatchProc */
    ObjMatchXBM,	/* stringMatchProc */
    ChnReadXBM,	/* fileReadProc */
    ObjReadXBM,	/* stringReadProc */
    ChnWriteXBM,	/* fileWriteProc */
    StringWriteXBM	/* stringWriteProc */
};

/*
 * Prototypes for local procedures defined in this file:
 */

static int	ReadXBMFileHeader _ANSI_ARGS_((ParseInfo *parseInfo,
			int *widthPtr, int *heightPtr));
static int	NextBitmapWord _ANSI_ARGS_((ParseInfo *parseInfoPtr));

/*
 *----------------------------------------------------------------------
 *
 * ObjMatchXBM --
 *
 *	This procedure is invoked by the photo image type to see if
 *	a datastring contains image data in XBM format.
 *
 * Results:
 *	The return value is >0 if the first characters in data look
 *	like XBM data, and 0 otherwise.
 *
 * Side effects:
 *	none
 *
 *----------------------------------------------------------------------
 */
static int
ObjMatchXBM(data, format, widthPtr, heightPtr, interp)
    Tcl_Obj *data;		/* The data supplied by the image */
    Tcl_Obj *format;		/* User-specified format string, or NULL. */
    int *widthPtr, *heightPtr;	/* The dimensions of the image are
				 * returned here if the file is a valid
				 * raw XBM file. */
    Tcl_Interp *interp;
{
    ParseInfo parseInfo;

    ImgFixObjMatchProc(&interp, &data, &format, &widthPtr, &heightPtr);

    parseInfo.handle.data = ImgGetStringFromObj(data, &parseInfo.handle.length);
    parseInfo.handle.state = IMG_STRING;

    return ReadXBMFileHeader(&parseInfo, widthPtr, heightPtr);
}


/*
 *----------------------------------------------------------------------
 *
 * ChnMatchXBM --
 *
 *	This procedure is invoked by the photo image type to see if
 *	a channel contains image data in XBM format.
 *
 * Results:
 *	The return value is >0 if the first characters in channel "chan"
 *	look like XBM data, and 0 otherwise.
 *
 * Side effects:
 *	The access position in chan may change.
 *
 *----------------------------------------------------------------------
 */

static int
ChnMatchXBM(chan, fileName, format, widthPtr, heightPtr, interp)
    Tcl_Channel chan;		/* The image channel, open for reading. */
    Tcl_Obj *fileName;		/* The name of the image file. */
    Tcl_Obj *format;		/* User-specified format object, or NULL. */
    int *widthPtr, *heightPtr;	/* The dimensions of the image are
				 * returned here if the file is a valid
				 * raw XBM file. */
    Tcl_Interp *interp;
{
    ParseInfo parseInfo;

    ImgFixChanMatchProc(&interp, &chan, &fileName, &format, &widthPtr, &heightPtr);

    parseInfo.handle.data = (char *) chan;
    parseInfo.handle.state = IMG_CHAN;

    return ReadXBMFileHeader(&parseInfo, widthPtr, heightPtr);
}


/*
 *----------------------------------------------------------------------
 *
 * CommonReadXBM --
 *
 *	This procedure is called by the photo image type to read
 *	XBM format data from a file or string and write it into a
 *	given photo image.
 *
 * Results:
 *	A standard TCL completion code.  If TCL_ERROR is returned
 *	then an error message is left in interp->result.
 *
 * Side effects:
 *	The access position in file f is changed (if read from file)
 *	and new data is added to the image given by imageHandle.
 *
 *----------------------------------------------------------------------
 */

typedef struct myblock {
    Tk_PhotoImageBlock ck;
    int dummy; /* extra space for offset[3], if not included already
		  in Tk_PhotoImageBlock */
} myblock;

#define block bl.ck

static int
CommonReadXBM(interp, parseInfo, format, imageHandle, destX, destY,
	width, height, srcX, srcY)
    Tcl_Interp *interp;		/* Interpreter to use for reporting errors. */
    ParseInfo *parseInfo;
    Tcl_Obj *format;		/* User-specified format string, or NULL. */
    Tk_PhotoHandle imageHandle;	/* The photo image to write into. */
    int destX, destY;		/* Coordinates of top-left pixel in
				 * photo image to be written to. */
    int width, height;		/* Dimensions of block of photo image to
				 * be written to. */
    int srcX, srcY;		/* Coordinates of top-left pixel to be used
				 * in image being read. */
{
    myblock bl;
    int fileWidth, fileHeight;
    int numBytes, row, col, value, i;
    unsigned char *data, *pixelPtr;
    char *end;

    ReadXBMFileHeader(parseInfo, &fileWidth, &fileHeight);

    if ((srcX + width) > fileWidth) {
	width = fileWidth - srcX;
    }
    if ((srcY + height) > fileHeight) {
	height = fileHeight - srcY;
    }
    if ((width <= 0) || (height <= 0)
	|| (srcX >= fileWidth) || (srcY >= fileHeight)) {
	return TCL_OK;
    }

    Tk_PhotoExpand(imageHandle, destX + width, destY + height);

    numBytes = ((fileWidth+7)/8)*32;
    block.width = fileWidth;
    block.height = 1;
    block.pixelSize = 4;
    block.offset[0] = 0;
    block.offset[1] = 1;
    block.offset[2] = 2;
    block.offset[3] = 3;

    data = (unsigned char *) ckalloc((unsigned) numBytes);
    block.pixelPtr = data + srcX*4;
    for (row = 0; row < srcY + height; row++) {
	pixelPtr = data;
        for (col = 0; col<(numBytes/32); col++) {
	    if (NextBitmapWord(parseInfo) != TCL_OK) {
		ckfree((char *) data);
		return TCL_ERROR;
	    }
	    value = (int) strtol(parseInfo->word, &end, 0);
	    if (end == parseInfo->word) {
	    	ckfree((char *) data);
	    	return TCL_ERROR;
	    }
	    for (i=0; i<8; i++) {
	        *pixelPtr++ = 0;
	        *pixelPtr++ = 0;
	        *pixelPtr++ = 0;
	        *pixelPtr++ = (value & 0x1)? 255:0;
	  	value >>= 1;
	    }
	}
	if (row >= srcY) {
	    ImgPhotoPutBlock(imageHandle, &block, destX, destY++, width, 1);
	}
    }
    ckfree((char *) data);
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * ChnReadXBM --
 *
 *	This procedure is called by the photo image type to read
 *	XBM format data from a channel and write it into a given
 *	photo image.
 *
 * Results:
 *	A standard TCL completion code.  If TCL_ERROR is returned
 *	then an error message is left in interp->result.
 *
 * Side effects:
 *	The access position in channel chan is changed, and new data is
 *	added to the image given by imageHandle.
 *
 *----------------------------------------------------------------------
 */

static int
ChnReadXBM(interp, chan, fileName, format, imageHandle, destX, destY,
	width, height, srcX, srcY)
    Tcl_Interp *interp;		/* Interpreter to use for reporting errors. */
    Tcl_Channel chan;		/* The image channel, open for reading. */
    Tcl_Obj *fileName;		/* The name of the image file. */
    Tcl_Obj *format;		/* User-specified format object, or NULL. */
    Tk_PhotoHandle imageHandle;	/* The photo image to write into. */
    int destX, destY;		/* Coordinates of top-left pixel in
				 * photo image to be written to. */
    int width, height;		/* Dimensions of block of photo image to
				 * be written to. */
    int srcX, srcY;		/* Coordinates of top-left pixel to be used
				 * in image being read. */
{
    ParseInfo parseInfo;

    parseInfo.handle.data = (char *) chan;
    parseInfo.handle.state = IMG_CHAN;

    return CommonReadXBM(interp, &parseInfo, format, imageHandle,
		destX, destY, width, height, srcX, srcY);
}

/*
 *----------------------------------------------------------------------
 *
 * ObjReadXBM --
 *
 *	This procedure is called by the photo image type to read
 *	XBM format data from a string and write it into a given
 *	photo image.
 *
 * Results:
 *	A standard TCL completion code.  If TCL_ERROR is returned
 *	then an error message is left in interp->result.
 *
 * Side effects:
 *	New data is added to the image given by imageHandle.
 *
 *----------------------------------------------------------------------
 */

static int
ObjReadXBM(interp, data, format, imageHandle, destX, destY,
	width, height, srcX, srcY)
    Tcl_Interp *interp;		/* Interpreter to use for reporting errors. */
    Tcl_Obj *data;
    Tcl_Obj *format;		/* User-specified format string, or NULL. */
    Tk_PhotoHandle imageHandle;	/* The photo image to write into. */
    int destX, destY;		/* Coordinates of top-left pixel in
				 * photo image to be written to. */
    int width, height;		/* Dimensions of block of photo image to
				 * be written to. */
    int srcX, srcY;		/* Coordinates of top-left pixel to be used
				 * in image being read. */
{
    ParseInfo parseInfo;
    parseInfo.handle.data = ImgGetStringFromObj(data, &parseInfo.handle.length);
    parseInfo.handle.state = IMG_STRING;

    return CommonReadXBM(interp, &parseInfo, format, imageHandle,
		destX, destY, width, height, srcX, srcY);
}

/*
 *----------------------------------------------------------------------
 *
 * ReadXBMFileHeader --
 *
 *	This procedure reads the XBM header from the beginning of a
 *	XBM file and returns information from the header.
 *
 * Results:
 *	The return value is 1 if file "f" appears to start with a valid
 *      XBM header, and 0 otherwise.  If the header is valid,
 *	then *widthPtr and *heightPtr are modified to hold the
 *	dimensions of the image and *numColors holds the number of
 *	colors and byteSize the number of bytes used for 1 pixel.
 *
 * Side effects:
 *	The access position in f advances.
 *
 *----------------------------------------------------------------------
 */

#define UCHAR(c) ((unsigned char) (c))

/*
 *----------------------------------------------------------------------
 *
 * NextBitmapWord --
 *
 *	This procedure retrieves the next word of information (stuff
 *	between commas or white space) from a bitmap description.
 *
 * Results:
 *	Returns TCL_OK if all went well.  In this case the next word,
 *	and its length, will be availble in *parseInfoPtr.  If the end
 *	of the bitmap description was reached then TCL_ERROR is returned.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
NextBitmapWord(parseInfoPtr)
    ParseInfo *parseInfoPtr;		/* Describes what we're reading
					 * and where we are in it. */
{
    char *dst, buf;
    int num;

    parseInfoPtr->wordLength = 0;
    dst = parseInfoPtr->word;

    for (num=ImgRead(&parseInfoPtr->handle,&buf,1); isspace(UCHAR(buf)) || (buf == ',');
	    num=ImgRead(&parseInfoPtr->handle,&buf,1)) {
	if (num == 0 || buf == EOF) {
	    return TCL_ERROR;
	}
    }
    for ( ; !isspace(UCHAR(buf)) && (buf != ',') && (num != 0);
	    num=ImgRead(&parseInfoPtr->handle,&buf,1)) {
	if (UCHAR(buf) < ' ' || UCHAR(buf) > '~') {
	    return TCL_ERROR;
	}
	*dst = buf;
	dst++;
	parseInfoPtr->wordLength++;
	if (num == 0 || parseInfoPtr->wordLength > MAX_WORD_LENGTH) {
	    return TCL_ERROR;
	}
    }

    if (parseInfoPtr->wordLength == 0) {
	return TCL_ERROR;
    }
    parseInfoPtr->word[parseInfoPtr->wordLength] = 0;
    return TCL_OK;
}

static int
ReadXBMFileHeader(pi, widthPtr, heightPtr)
    ParseInfo *pi;
    int *widthPtr, *heightPtr;	/* The dimensions of the image are
				 * returned here. */
{
    int width, height, hotX, hotY;
    char *end;

    /*
     * Parse the lines that define the dimensions of the bitmap,
     * plus the first line that defines the bitmap data (it declares
     * the name of a data variable but doesn't include any actual
     * data).  These lines look something like the following:
     *
     *		#define foo_width 16
     *		#define foo_height 16
     *		#define foo_x_hot 3
     *		#define foo_y_hot 3
     *		static char foo_bits[] = {
     *
     * The x_hot and y_hot lines may or may not be present.  It's
     * important to check for "char" in the last line, in order to
     * reject old X10-style bitmaps that used shorts.
     */

    width = 0;
    height = 0;
    hotX = -1;
    hotY = -1;
    while (1) {
	if (NextBitmapWord(pi) != TCL_OK) {
	    return 0;
	}
	if ((pi->wordLength >= 6) && (pi->word[pi->wordLength-6] == '_')
		&& (strcmp(pi->word+pi->wordLength-6, "_width") == 0)) {
	    if (NextBitmapWord(pi) != TCL_OK) {
		return 0;
	    }
	    width = strtol(pi->word, &end, 0);
	    if ((end == pi->word) || (*end != 0)) {
		return 0;
	    }
	} else if ((pi->wordLength >= 7) && (pi->word[pi->wordLength-7] == '_')
		&& (strcmp(pi->word+pi->wordLength-7, "_height") == 0)) {
	    if (NextBitmapWord(pi) != TCL_OK) {
		return 0;
	    }
	    height = strtol(pi->word, &end, 0);
	    if ((end == pi->word) || (*end != 0)) {
		return 0;
	    }
	} else if ((pi->wordLength >= 6) && (pi->word[pi->wordLength-6] == '_')
		&& (strcmp(pi->word+pi->wordLength-6, "_x_hot") == 0)) {
	    if (NextBitmapWord(pi) != TCL_OK) {
		return 0;
	    }
	    hotX = strtol(pi->word, &end, 0);
	    if ((end == pi->word) || (*end != 0)) {
		return 0;
	    }
	} else if ((pi->wordLength >= 6) && (pi->word[pi->wordLength-6] == '_')
		&& (strcmp(pi->word+pi->wordLength-6, "_y_hot") == 0)) {
	    if (NextBitmapWord(pi) != TCL_OK) {
		return 0;
	    }
	    hotY = strtol(pi->word, &end, 0);
	    if ((end == pi->word) || (*end != 0)) {
		return 0;
	    }
	} else if ((pi->word[0] == 'c') && (strcmp(pi->word, "char") == 0)) {
	    while (1) {
		if (NextBitmapWord(pi) != TCL_OK) {
		    return 0;
		}
		if ((pi->word[0] == '{') && (pi->word[1] == 0)) {
		    goto getData;
		}
	    }
	} else if ((pi->word[0] == '{') && (pi->word[1] == 0)) {
	    return 0;
	}
    }

getData:
    *widthPtr = width;
    *heightPtr = height;
    return 1;
}


/*
 *----------------------------------------------------------------------
 *
 * ChnWriteXBM
 *
 *	Writes a XBM image to a file. Just calls CommonWriteXBM
 *      with appropriate arguments.
 *
 * Results:
 *	Returns the return value of CommonWriteXBM
 *
 * Side effects:
 *	A file is (hopefully) created on success.
 *
 *----------------------------------------------------------------------
 */
static int
ChnWriteXBM(interp, fileName, format, blockPtr)
    Tcl_Interp *interp;
    char *fileName;
    Tcl_Obj *format;
    Tk_PhotoImageBlock *blockPtr;
{
    return CommonWriteXBM(interp, fileName, (Tcl_DString *)NULL, format, blockPtr);
}


/*
 *----------------------------------------------------------------------
 *
 * StringWriteXBM
 *
 *	Writes a XBM image to a string. Just calls CommonWriteXBM
 *      with appropriate arguments.
 *
 * Results:
 *	Returns the return value of CommonWriteXBM
 *
 * Side effects:
 *	The Tcl_DString dataPtr is modified on success.
 *
 *----------------------------------------------------------------------
 */
static int
StringWriteXBM(interp, format, blockPtr)
    Tcl_Interp *interp;
    Tcl_Obj *format;
    Tk_PhotoImageBlock *blockPtr;
{
    int result;
    Tcl_DString data;
    Tcl_DString *dataPtr;

    ImgFixStringWriteProc(&data, &interp, &dataPtr, &format, &blockPtr);

    result = CommonWriteXBM(interp, (CONST char *) NULL, dataPtr, format, blockPtr);

    if ((result == TCL_OK) && (dataPtr == &data)) {
	Tcl_DStringResult(interp, dataPtr);
    }
    return result;
}


/*
 * Yes, I know these macros are dangerous. But it should work fine
 */
#define WRITE(buf) { if (chan) Tcl_Write(chan, buf, -1); else Tcl_DStringAppend(dataPtr, buf, -1);}

/*
 *----------------------------------------------------------------------
 *
 * CommonWriteXBM
 *
 *	This procedure writes a XBM image to the file filename
 *      (if filename != NULL) or to dataPtr.
 *
 * Results:
 *	Returns TCL_OK on success, or TCL_ERROR on error.
 *
 * Side effects:
 *	varies (see StringWriteXBM and ChnWriteXBM)
 *
 *----------------------------------------------------------------------
 */
static int
CommonWriteXBM(interp, fileName, dataPtr, format, blockPtr)
    Tcl_Interp *interp;
    CONST char *fileName;
    Tcl_DString *dataPtr;
    Tcl_Obj *format;
    Tk_PhotoImageBlock *blockPtr;
{
    Tcl_Channel chan = (Tcl_Channel) NULL;
    char buffer[256];
    unsigned char *pp;
    int x, y, value, mask;
    int sep = ' ';
    int alphaOffset;
    char *p = NULL;
    static CONST char header[] =
"#define %s_width %d\n\
#define %s_height %d\n\
static char %s_bits[] = {\n";

    alphaOffset = blockPtr->offset[0];
    if (alphaOffset < blockPtr->offset[1]) alphaOffset = blockPtr->offset[1];
    if (alphaOffset < blockPtr->offset[2]) alphaOffset = blockPtr->offset[2];
    if (++alphaOffset < blockPtr->pixelSize) {
	alphaOffset -= blockPtr->offset[0];
    } else {
	alphaOffset = 0;
    }


    /* open the output file (if needed) */
    if (fileName) {
      chan = Tcl_OpenFileChannel(interp, (char *) fileName, "w", 0644);
      if (!chan) {
	return TCL_ERROR;
      }
    }

    /* compute image name */
    if (chan) {
	p = strrchr(fileName, '/');
	if (p) {
	    fileName = p+1;
	}
	p = strrchr(fileName, '\\');
	if (p) {
	    fileName = p+1;
	}
	p = strrchr(fileName, ':');
	if (p) {
	    fileName = p+1;
	}
	p = strchr(fileName, '.');
	if (p) {
	    *(char *)p = 0;
	}
    } else {
        fileName = "unknown";
    }

    sprintf(buffer, header, fileName, blockPtr->width, fileName,
	    blockPtr->height, fileName);
    if (p) {
	*p = '.';
    }
    WRITE(buffer);

    /* write image itself */
    pp = blockPtr->pixelPtr + blockPtr->offset[0];
    sep = ' ';
    for (y = 0; y < blockPtr->height; y++) {
	value = 0;
	mask  = 1;
	for (x = 0; x < blockPtr->width; x++) {
	    if (!alphaOffset || pp[alphaOffset]) {
		value |= mask;
	    } else {
		/* make transparent pixel */
	    }
	    pp += blockPtr->pixelSize;
	    mask <<= 1;
	    if (mask >= 256)
             {
	      sprintf(buffer,"%c 0x%02x",sep,value);
	      WRITE(buffer);
              value = 0;
	      mask = 1;
	      sep = ',';
             }
	}
	if (mask != 1) {
	      sprintf(buffer,"%c 0x%02x",sep, value);
	      WRITE(buffer);
	}

	if (y == blockPtr->height - 1) {
	    WRITE("};\n");
	} else {
	    WRITE(",\n");
	    sep = ' ';
	}
    }

    /* close the channel */
    if (chan) {
	Tcl_Close(interp, chan);
    }
    return TCL_OK;
}
