/*\
 * $Id: bmptoppm.c,v 1.10 1992/11/24 19:38:17 dws Exp dws $
 * 
 * bmptoppm.c - Converts from a Microsoft Windows or OS/2 .BMP file to a
 * PPM file.
 * 
 * The current implementation is probably not complete, but it works for
 * all the BMP files I have.  I welcome feedback.
 * 
 * Copyright (C) 1992 by David W. Sanderson.
 * 
 * Permission to use, copy, modify, and distribute this software and its
 * documentation for any purpose and without fee is hereby granted,
 * provided that the above copyright notice appear in all copies and
 * that both that copyright notice and this permission notice appear
 * in supporting documentation.  This software is provided "as is"
 * without express or implied warranty.
 * 
 * $Log: bmptoppm.c,v $
 * Revision 1.10  1992/11/24  19:38:17  dws
 * Added code to verify that reading occurred at the correct offsets.
 * Added copyright.
 *
 * Revision 1.9  1992/11/17  02:15:24  dws
 * Changed to include bmp.h.
 * Eliminated need for fseek(), and therefore the need for a
 * temporary file.
 *
 * Revision 1.8  1992/11/13  23:48:57  dws
 * Made definition of Seekable() static, to match its prototype.
 *
 * Revision 1.7  1992/11/11  00:17:50  dws
 * Generalized to use bitio routines.
 *
 * Revision 1.6  1992/11/10  23:51:44  dws
 * Enhanced command-line handling.
 *
 * Revision 1.5  1992/11/08  00:38:46  dws
 * Changed some names to help w/ addition of ppmtobmp.
 *
 * Revision 1.4  1992/10/27  06:28:28  dws
 * Corrected stupid typo.
 *
 * Revision 1.3  1992/10/27  06:17:10  dws
 * Removed a magic constant value.
 *
 * Revision 1.2  1992/10/27  06:09:58  dws
 * Made stdin seekable.
 *
 * Revision 1.1  1992/10/27  05:31:41  dws
 * Initial revision
\*/

#include	"bmp.h"
#include	"ppm.h"
#include	"bitio.h"

#define	MAXCOLORS   	256

static char    *ifname;

/*
 * Utilities
 */

static int GetByte ARGS((FILE * fp));
static short GetShort ARGS((FILE * fp));
static long GetLong ARGS((FILE * fp));
static void readto ARGS((FILE *fp, unsigned long *ppos, unsigned long dst));
static void BMPreadfileheader ARGS((FILE *fp, unsigned long *ppos,
    unsigned long *poffBits));
static void BMPreadinfoheader ARGS((FILE *fp, unsigned long *ppos,
    unsigned long *pcx, unsigned long *pcy, unsigned short *pcBitCount,
    int *pclass, int *pcmapsize));
static int BMPreadcolormap ARGS((FILE *fp, unsigned long *ppos,
    unsigned short cBitCount, int class, pixval *R, pixval *G, pixval *B,
    int cmapsize));
static int
BMPreadrow(FILE *fp, 
    unsigned long * const ppos,     /* number of bytes read from fp */
    pixel * const row, const unsigned long cx, const unsigned short cBitCount, 
    const pixval R[], const pixval G[], const pixval B[]);
static pixel ** BMPreadbits ARGS((FILE *fp, unsigned long *ppos,
    unsigned long offBits, unsigned long cx, unsigned long cy,
    unsigned short cBitCount, int class, pixval *R, pixval *G, pixval *B));

static char     er_read[] = "%s: read error";

static int
GetByte(fp)
	FILE           *fp;
{
	int             v;

	if ((v = getc(fp)) == EOF)
	{
		pm_error(er_read, ifname);
	}

	return v;
}

static short
GetShort(fp)
	FILE           *fp;
{
	short           v;

	if (pm_readlittleshort(fp, &v) == -1)
	{
		pm_error(er_read, ifname);
	}

	return v;
}

static long
GetLong(fp)
	FILE           *fp;
{
	long            v;

	if (pm_readlittlelong(fp, &v) == -1)
	{
		pm_error(er_read, ifname);
	}

	return v;
}

/*
 * readto - read as many bytes as necessary to position the
 * file at the desired offset.
 */

static void
readto(fp, ppos, dst)
	FILE           *fp;
	unsigned long  *ppos;	/* pointer to number of bytes read from fp */
	unsigned long   dst;
{
	unsigned long   pos;

	if(!fp || !ppos)
		return;

	pos = *ppos;

	if(pos > dst)
		pm_error("%s: internal error in readto()", ifname);

	for(; pos < dst; pos++)
	{
		if (getc(fp) == EOF)
		{
			pm_error(er_read, ifname);
		}
	}

	*ppos = pos;
}

/*
 * BMP reading routines
 */

static void
BMPreadfileheader(fp, ppos, poffBits)
	FILE           *fp;
	unsigned long  *ppos;	/* number of bytes read from fp */
	unsigned long  *poffBits;
{
	unsigned long   cbSize;
	unsigned short  xHotSpot;
	unsigned short  yHotSpot;
	unsigned long   offBits;

	if (GetByte(fp) != 'B')
	{
		pm_error("%s is not a BMP file", ifname);
	}
	if (GetByte(fp) != 'M')
	{
		pm_error("%s is not a BMP file", ifname);
	}

	cbSize = GetLong(fp);
	xHotSpot = GetShort(fp);
	yHotSpot = GetShort(fp);
	offBits = GetLong(fp);

	*poffBits = offBits;

	*ppos += 14;
}

static void
BMPreadinfoheader(fp, ppos, pcx, pcy, pcBitCount, pclass, pcmapsize)
	FILE           *fp;
	unsigned long  *ppos;	/* number of bytes read from fp */
	unsigned long  *pcx;
	unsigned long  *pcy;
	unsigned short *pcBitCount;
	int            *pclass;
	int	       *pcmapsize;
{
	unsigned long   cbFix;
	unsigned short  cPlanes;

	unsigned long   cx;
	unsigned long   cy;
	unsigned short  cBitCount;
    unsigned long   compression;
	int             class;

	cbFix = GetLong(fp);

	switch (cbFix)
	{
	case 12:
		class = C_OS2;

		cx = GetShort(fp);
		cy = GetShort(fp);
		cPlanes = GetShort(fp);
		cBitCount = GetShort(fp);
        /* I actually don't know if the OS/2 BMP format allows
           cBitCount > 8 or if it does, what it means, but ppmtobmp
           creates such BMPs, more or less as a byproduct of creating
           the same for Windows BMP, so we interpret cBitCount > 8 the
           same as for Windows.
        */
        if (cBitCount <= 8)
            *pcmapsize = 1 << cBitCount;
        else if (cBitCount == 24)
            *pcmapsize = 0;
        /* There is a 16 bit truecolor format, but we don't know how the
           bits are divided among red, green, and blue, so we can't handle it.
           */
        else
            pm_error("Unrecognized bits per pixel in BMP file header: %d",
                     cBitCount);

		break;
	case 40:
		class = C_WIN;

		cx = GetLong(fp);
		cy = GetLong(fp);
		cPlanes = GetShort(fp);
		cBitCount = GetShort(fp);
        if (cBitCount <= 8)
            *pcmapsize = 1 << cBitCount;
        else if (cBitCount == 24)
            *pcmapsize = 0;
        /* There is a 16 bit truecolor format, but we don't know how the
           bits are divided among red, green, and blue, so we can't handle it.
           */
        else
            pm_error("Unrecognized bits per pixel in BMP file header: %d",
                     cBitCount);
 
		compression = GetLong(fp);
        if (compression != 0) 
            pm_error("Input is compressed.  This program doesn't handle "
                     "compressed images.  Compression type code = %d",
                     compression);

        /* And read the rest of the junk in the 40 byte header */
		GetLong(fp);
		GetLong(fp);
		GetLong(fp);
		GetLong(fp);
        /* The code used to get *pcmapsize from the following word
           (# of important colors).  This appears to be a bug.  2000.06.08 BJH
           */
		GetLong(fp);

		break;
	default:
		pm_error("%s: unknown cbFix: %d", ifname, cbFix);
		break;
	}

	if (cPlanes != 1)
	{
		pm_error("%s: don't know how to handle cPlanes = %d"
			 ,ifname
			 ,cPlanes);
	}

	switch (class)
	{
	case C_WIN:
		pm_message("Windows BMP, %dx%dx%d"
			   ,cx
			   ,cy
			   ,cBitCount);
		break;
	case C_OS2:
		pm_message("OS/2 BMP, %dx%dx%d"
			   ,cx
			   ,cy
			   ,cBitCount);
		break;
	}

#ifdef DEBUG
	pm_message("cbFix: %d", cbFix);
	pm_message("cx: %d", cx);
	pm_message("cy: %d", cy);
	pm_message("cPlanes: %d", cPlanes);
	pm_message("cBitCount: %d", cBitCount);
#endif

	*pcx = cx;
	*pcy = cy;
	*pcBitCount = cBitCount;
	*pclass = class;

	*ppos += cbFix;
}

/*
 * returns the number of bytes read, or -1 on error.
 */
static int
BMPreadcolormap(FILE *fp, 
                unsigned long *ppos, /* number of bytes read from fp */
                unsigned short cBitCount, 
                int class, pixval *R, pixval *G, pixval *B, int cmapsize)
{
	int             i;
	int		nbyte = 0;

	for (i = 0; i < cmapsize; i++)
	{
        /* There is a document that says the bytes or ordered R,G,B,Z,
           but in practice it appears to be the following instead:
           */
		B[i] = (pixval) GetByte(fp);
		G[i] = (pixval) GetByte(fp);
		R[i] = (pixval) GetByte(fp);
		nbyte += 3;

		if (class == C_WIN)
		{
			(void) GetByte(fp);
			nbyte++;
		}
	}

	*ppos += nbyte;
	return nbyte;
}

static int
BMPreadrow(FILE *fp, 
    unsigned long * const ppos,     /* number of bytes read from fp */
    pixel * const row, const unsigned long cx, const unsigned short cBitCount, 
    const pixval R[], const pixval G[], const pixval B[])
{
/*
 * returns the number of bytes read, or -1 on error.
 */
	BITSTREAM       b;
	unsigned        nbyte = 0;
	int             rc;
	unsigned int    col;

	if ((b = pm_bitinit(fp, "r")) == (BITSTREAM) 0)
	{
		return -1;
	}

	for (col = 0; col < cx; col++)
	{
		unsigned long   v;

        /* There is a document that gives a much different format for
           24 bit BMPs.  But this seems to be the de facto standard.
        */
		if ((rc = pm_bitread(b, cBitCount, &v)) == -1)
		{
			return -1;
		}
		nbyte += rc;

        if (cBitCount <= 8)
            /* It's colormapped, so look up v in the colormap */
            PPM_ASSIGN(row[col], R[v], G[v], B[v]);
        else if (cBitCount == 24)
            /* It's truecolor, so just unpack v */
            PPM_ASSIGN(row[col], 
                       (v >>  0) & 0xff,
                       (v >>  8) & 0xff,
                       (v >> 16) & 0xff);

	}

	if ((rc = pm_bitfini(b)) != 0)
	{
		return -1;
	}

	/*
	 * Make sure we read a multiple of 4 bytes.
	 */
	while (nbyte % 4)
	{
		GetByte(fp);
		nbyte++;
	}

	*ppos += nbyte;
	return nbyte;
}

static pixel **
BMPreadbits(FILE *fp, 
            unsigned long *ppos, /* number of bytes read from fp */
            unsigned long offBits, unsigned long cx, unsigned long cy, 
            unsigned short cBitCount, int class, 
            pixval *R, pixval *G, pixval *B)
{
	pixel         **pixels;	/* output */
	long            y;

	readto(fp, ppos, offBits);

	pixels = ppm_allocarray(cx, cy);

	/*
	 * The picture is stored bottom line first, top line last
	 */

	for (y = cy - 1; y >= 0; y--)
	{
		int rc;
		rc = BMPreadrow(fp, ppos, pixels[y], cx, cBitCount, R, G, B);

		if(rc == -1)
		{
			pm_error("%s: couldn't read row %d"
				 ,ifname
				 ,y);
		}
		if(rc%4)
		{
			pm_error("%s: row had bad number of bytes: %d"
				 ,ifname
				 ,rc);
		}
	}

	return pixels;
}

int
main(argc, argv)
	int             argc;
	char          **argv;
{
	FILE           *ifp = stdin;
	char           *usage = "[bmpfile]";
	int             argn;

	int             rc;
	unsigned long	pos = 0;

	unsigned long   offBits;

	unsigned long   cx;
	unsigned long   cy;
	unsigned short  cBitCount;
	int             class;
	int		cmapsize;

	pixval          R[MAXCOLORS];	/* reds */
	pixval          G[MAXCOLORS];	/* greens */
	pixval          B[MAXCOLORS];	/* blues */

	pixel         **pixels;


	ppm_init(&argc, argv);

	/*
	 * Since this command takes no flags, produce an error message
	 * if the user tries to give any.
	 * This is friendlier than if the command were to say
	 * 'no such file: -help'.
	 */

	argn = 1;
	while (argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0')
	{
		pm_usage(usage);
		++argn;
	}

	if (argn < argc)
	{
		ifname = argv[argn];
		ifp = pm_openr(ifname);
		++argn;
	}
	else
	{
		ifname = "standard input";
		ifp = stdin;
	}

	if (argn != argc)
	{
		pm_usage(usage);
	}

	BMPreadfileheader(ifp, &pos, &offBits);
	BMPreadinfoheader(ifp, &pos, &cx, &cy, &cBitCount, &class, &cmapsize);

	if(offBits != BMPoffbits(class, cBitCount, cmapsize))
	{
		pm_message("warning: offBits is %d, expected %d"
			, offBits
			, BMPoffbits(class, cBitCount, cmapsize));
	}

	rc = BMPreadcolormap(ifp, &pos, cBitCount, class, R, G, B, cmapsize);

	if(rc != BMPlencolormap(class, cBitCount, cmapsize))
	{
		pm_message("warning: %d-byte RGB table, expected %d bytes"
			, rc
			, BMPlencolormap(class, cBitCount, cmapsize));
	}


	pixels = BMPreadbits(ifp, &pos, offBits, cx, cy
			, cBitCount, class, R, G, B);

	if(pos != BMPlenfile(class, cBitCount, cmapsize, cx, cy))
	{
		pm_message("warning: read %d bytes, expected to read %d bytes"
			, pos
			, BMPlenfile(class, cBitCount, cmapsize, cx, cy));
	}

	pm_close(ifp);
	ppm_writeppm(stdout, pixels, cx, cy, (pixval) (MAXCOLORS-1), 0);
	pm_close(stdout);

	exit(0);
}

