/*\
 * $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 BMPreadrgbtable ARGS((FILE *fp, unsigned long *ppos,
    unsigned short cBitCount, int class, pixval *R, pixval *G, pixval *B,
    int cmapsize));
static int BMPreadrow ARGS((FILE *fp, unsigned long *ppos, pixel *row,
    unsigned long cx, unsigned short cBitCount, pixval *R, pixval *G, 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;
	int             class;

	cbFix = GetLong(fp);

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

		cx = GetShort(fp);
		cy = GetShort(fp);
		cPlanes = GetShort(fp);
		cBitCount = GetShort(fp);
		*pcmapsize = -1;

		break;
	case 40:
		class = C_WIN;

		cx = GetLong(fp);
		cy = GetLong(fp);
		cPlanes = GetShort(fp);
		cBitCount = GetShort(fp);

		/*
		 * We've read 16 bytes so far, need to read 24 more
		 * for the required total of 40.
		 */

		GetLong(fp);
		GetLong(fp);
		GetLong(fp);
		GetLong(fp);
		GetLong(fp);
		*pcmapsize = GetLong(fp);
		if (0 == *pcmapsize)
			*pcmapsize = -1;

		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
BMPreadrgbtable(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;

	long            ncolors = cmapsize < 0 ? (1 << cBitCount) : cmapsize;

	for (i = 0; i < ncolors; i++)
	{
		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;
}

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

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

	for (x = 0; x < cx; x++, row++)
	{
		unsigned long   v;

		if ((rc = pm_bitread(b, cBitCount, &v)) == -1)
		{
			return -1;
		}
		nbyte += rc;

		PPM_ASSIGN(*row, R[v], G[v], B[v]);
	}

	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);

	if(cBitCount > 24)
	{
		pm_error("%s: cannot handle cBitCount: %d"
			 ,ifname
			 ,cBitCount);
	}

	/*
	 * 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 = BMPreadrgbtable(ifp, &pos, cBitCount, class, R, G, B, cmapsize);

	if(rc != BMPlenrgbtable(class, cBitCount, cmapsize))
	{
		pm_message("warning: %d-byte RGB table, expected %d bytes"
			, rc
			, BMPlenrgbtable(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);
}
