/* pennovate2bmp - version 0.1
 * Seb Wills 2005
 * Quick, hacky program to convert from the PDB files which Pennovate's 
 * PalmOS note-taking application (http://www.pennovate.com) saves (when
 * you 'save to handheld') into regular .bmp files.
 * Note that if you save to external memory card
 * from Pennovate then I believe it saves bmp directly. This program
 * is useful if you retrieve the pdb files direct from your handheld's
 * internal memory.
 * 
 * Notes:
 * 
 * 1. I output bmp because that's how the data is stored in the pdb file,
 *    not because I think it's a sensible format to use.
 * 2. The bmp file is flipped vertically, as a result of the way data is stored.
 *    I could flip it back in this program, but since I'm going to want to
 *    feed the output through 'convert' (ImageMagick) to get it into a more
 *    sensible format in any case, I'll just use the '-flip' convert option
 *    at that stage.
 * 
 * I used http://www.fortunecity.com/skyscraper/windows/364/bmpffrmt.html
 * and http://www.daubnet.com/formats/BMP.html for info about bmp format,
 * and reverse-engineered the Pennovate database file
 * with help from http://www.nicholson.com/rhn/palm/pdb.txt
 * 
 * Essentially, the input is a PDB file containing a number of "records", each
 * containing a block of binary data. Assembling together the data payloads from
 * all the records gives you a block of data consisting of 20 bytes of header,
 * followed by the image data in the same format used for the image data part
 * of BMP files (except that the rows are not in reverse order as they are in
 * bmp files).
 * 
 * Seb Wills 2005
 * Freely distributable and modifiable, but if you improve it I would appreciate
 * you sending me a copy of your improvements.
 */

#include <stdio.h>


unsigned int read_twobytes_be(FILE *stream);
unsigned int read_fourbytes_be(FILE *stream);
void check_header(FILE *in, char *str);
void make_bmp_header(unsigned char *d, unsigned int *hdrsize, unsigned int width, unsigned int height);

int bmp_bytes_per_row(width,height)
{
   /* returns the number of bytes in a "row" of bmp data for a 2 colour bmp */
   int rowlen;
   rowlen=width/8; /* 8 bits(pixels) per byte */
   if(rowlen % 4 !=0)
     {
	rowlen=rowlen+ 4-(rowlen % 4); /* pad out to multiple of 4 bytes */
     }
   return rowlen;
}

   

int main(int argc, char **argv) 
{
   
   FILE *in, *out;
   char fourc[5];
   int numrecords,i;
   unsigned int *offsets;
   unsigned int width, height, hdrsize;
   unsigned char cptr[32*1024];
   unsigned int count, bytecount, imagebytes,rowlen;
   
   
   if (argc < 3) 
     {
	fprintf(stderr,"\nUsage: %s filename.pdb outfile.bmp\n\
\n\
Converts Pennovate's note format (as saved to handheld as .pdb) to a\n\
(vertical mirror-image) .bmp file.\
\n\
Can use - instead of filename(s) for stdin/stdout.\n\n", argv[0]);
	exit(1);
     }
   
   if (strcmp(argv[1],"-")==0) 
     {
	in=stdin;
     }
   else 
     {
	if((in=fopen(argv[1],"rb"))==NULL)
	  {
	     fprintf(stderr,"Error opening file %s.",argv[1]);
	     exit(1);
	  }
	
     }

   if (strcmp(argv[2],"-")==0) 
     {
	out=stdout;
     }
   else 
     {
	if((out=fopen(argv[2],"wb"))==NULL)
	  {
	     fprintf(stderr,"Error opening file %s.",argv[2]);
	     exit(1);
	  }
	
     }
   

   
   /* read header stuff */
   safe_fseek(in, 0x40, SEEK_SET);
   fread(fourc,4,1,in);
   fourc[4]=0;
   if(strcmp(fourc,"PNVT") != 0) 
     {
	fprintf(stderr,"Creator seems to be %s; expecting PNVT. Aborting.",fourc);
	exit(1);
     }
   
   safe_fseek(in, 0x4c, SEEK_SET);
   numrecords=read_twobytes_be(in);
   fprintf(stderr,"Contains %d records.\n",numrecords);

   offsets=(unsigned int *) calloc(numrecords,sizeof(unsigned int));
   if(offsets==NULL) 
     {
	fprintf(stderr,"calloc failed.");
	exit(1);
     }
   
   /* already at correct seek position for first record entry */
   for(i=0; i< numrecords; i++) 
     {
	offsets[i]=read_fourbytes_be(in);
	safe_fseek(in,4,SEEK_CUR);
	fprintf(stderr,"Block %d at 0x%x\n",i+1,offsets[i]);
     }
   
   /* seek to first data block */
   safe_fseek(in,offsets[0],SEEK_SET);
   /* each record starts with DBLK and then four other bytes. The
    * remaining bytes until the start of the next record are data */
   check_header(in,"DBLK");
   safe_fseek(in,4,SEEK_CUR); /* skip four unknown */
   
   /* first two dwords in the data are width and height */
   width=read_twobytes_be(in);
   height=read_twobytes_be(in);
   fprintf(stderr,"Image size: %dx%d\n",width,height);
   
   safe_fseek(in,16,SEEK_CUR); /* unknown remainder of header */
   
   /* now at start of image data */

   fprintf(stderr,"Writing bmp header and data from record 1...\n");
   make_bmp_header(cptr, &hdrsize, width,height);

   count=fwrite(cptr, 1, hdrsize, out);
   fprintf(stderr,"Wrote %d bytes\n",count);
   if(count !=hdrsize) 
     {
	fprintf(stderr,"fwrite didn't work!\n");
	exit(1);
     }
   
   
   /* write remainder of first record  (the first chunk of actual bmp image data */
   if(offsets[1]-offsets[0] > 24) 
     {
	count=fread(cptr, 1, offsets[1]-offsets[0]-28, in);
	fwrite(cptr, 1,  offsets[1]-offsets[0]-28, out);
	if(count != offsets[1]-offsets[0]-28)
	  {
	     fprintf(stderr,"fwrite failed!\n");
	     exit(1);
	  }
	bytecount=count;
     }
   

   /* write other records */
   for(i=1;i<numrecords; i++) 
     { 
	fprintf(stderr,"Writing data from record %d...\n",i+1);
	check_header(in,"DBLK");
	safe_fseek(in,4,SEEK_CUR); /* skip four unknown */
	count=fread(cptr,1,offsets[i+1]-offsets[i]-8,in);
	if(i!=numrecords-1 && count != offsets[i+1]-offsets[i]-8)
	  {
	     fprintf(stderr,"File ended unexpectedly in record %i\n",i+1);
	     exit(1);
	  }
	fwrite(cptr,1,count,out);
	bytecount+=count;
     }
   
   /* round up to multiple of 4 bytes */
   fprintf(stderr,"Total image data bytes in pdb: %d.\n",bytecount);
   imagebytes=height*bmp_bytes_per_row(width,height);
   fprintf(stderr,"%d data bytes required in bmp file.\n",imagebytes);
   if(bytecount != imagebytes)
     {
	fprintf(stderr,"Padding with %d bytes\n", imagebytes-bytecount);
	for(;bytecount<imagebytes;bytecount++)
	  {
	     fwrite("\x00", 1, 1 , out);
	  }
	
     }
   
   fclose(in);
   fclose(out);
   
   fprintf(stderr,"All done!\nSuggest you use 'convert -flip' next\n",argv[2]);
   
   return 0;
   
}

   
   int safe_fseek(FILE *stream, long offset, int whence) 
{
   int retval;
   if(retval=fseek(stream,offset,whence)==-1)
     {
	fprintf(stderr,"Error seeking to offset %ld. Exciting.\n",offset);
	exit(3);
     }
   return retval;
}

unsigned int read_twobytes_be(FILE *stream)
{
   /* read a two-byte big-endian word, and store natively */
   unsigned char c;
   unsigned int result;
   fread(&c,1,1,stream);
   result=((unsigned int) c)<<8;
   fread(&c,1,1,stream);
   result=result | ((unsigned int) c);
   return result;
}

unsigned int read_fourbytes_be(FILE *stream)
{
   int i;
   unsigned char c;
   unsigned int result=0;
   for (i=0;i<4;i++)
     {
	fread(&c,1,1,stream);
	result = (result << 8) | (unsigned int) c;
     }
   return result;
}


void check_header(FILE *in, char *str)
{
   /* checks that the stream in has str at the current seek position. Reads strlen(str) characters. */
   char c[32];
   
   if (strlen(str)>31) 
     {
	fprintf(stderr,"arg too long in check_header.\n");
	exit(1);
     }
   
   fread(c,1,strlen(str),in);
   c[strlen(str)]=0;
   if (strcmp(c,str) != 0) 
     {
	fprintf(stderr,"Didn't find expected header string '%s'\n",str);
	exit(1);
     }
}

void make_bmp_header(unsigned char *d, unsigned int *hdrsize, unsigned int width, unsigned int height)
{
   /* makes a suitable header for a BMP file for a 2-color bmp file */
   /* This assumes we're running on a Little Endian architecture */
   
   int i;
   unsigned int filesize;
   unsigned int rowlen;
   
   *hdrsize=54+4*2;  /* 2 colour table entries */
   
   rowlen=bmp_bytes_per_row(width,height);

   filesize=*hdrsize+height*rowlen;

   if (sizeof(unsigned short int)!=2) 
     {
	fprintf(stderr,"Code requires platform/compiler with unsigned short int being 2 bytes!");
	exit(1);
     }
   
   for(i=0;i<*hdrsize;i++) 
     {
	d[i]=0; /* most values need be zero, so initialise everything that way */
     }
   
   d[0]='B';
   d[1]='M';
   *((unsigned int *) (d+2)) = filesize;
   *((unsigned int *) (d+10)) = *hdrsize; /* offset to bitmap data */
   *((unsigned int *) (d+14)) = 40; /* BITMAPINFOHEADER size */
   *((unsigned int *) (d+18)) = width;
   *((unsigned int *) (d+22)) = height;
   *((unsigned short int *) (d+26)) = 1; /* number of planes */
   *((unsigned int *) (d+28)) = 1; /* bits per pixel */
   *((unsigned int *) (d+46)) = 2; /* number of colours */
   /* now colour table */
   /* Colour 0 is white for Pennovate: */
   d[54]=255;
   d[55]=255;
   d[56]=255;
   /*bytes at offsets 58-60 are already zero, for the colour 1 RGB*/

}

