/*
  agif.c -- Amiga GIF picture display
  also converts GIF pictures to IFF format

  written by Timo Rossi, Finland  1988-11-25
  version 1.1

 Usage:
   agif filename	-	displays a GIF picture
   agif inputfile outputfile -	displays GIF picture 'inputfile'
				and saves it in IFF file 'outputfile'
*/

#include <exec/types.h>
#include <intuition/intuition.h>

#include <stdio.h>

/*
  compiler dependent stuff...

  much faster and shorter executable
  when compiled with Aztec C (version 3.6a, 16-bit ints)
 */

/* AZTEC_C is automatically defined by the Manx Aztec C compiler */
#ifdef AZTEC_C
#include <functions.h>
extern int Enable_Abort;
void _wb_parse() {}
#endif

#ifdef LATTICE_4
#include <proto/exec.h>
#include <proto/graphics.h>
#include <proto/intuition.h>
#include <string.h>
#include <signal.h>
#endif

/* GIF stuff */

#define GIF_SIGNATURE	"GIF87a"	/* GIF file identifier */

#define IMAGE_SEPARATOR      ','
#define EXTENSION_INTRODUCER '!'
#define GIF_TERMINATOR       ';'

struct ScreenDescriptor {
  UWORD Width,Height;
  UBYTE PixelFlags;
  UBYTE Background;
  UBYTE NotUsed;
};

struct ImageDescriptor {
  UWORD Left,Top;
  UWORD Width,Height;
  UBYTE PixelFlags;
};

/*
  sizeof(struct ScreenDescriptor) or sizeof(struct Imagedescriptor)
  can't be used when reading from file because of structure alignment
  the following values are used
 */
#define SCR_DESCR_LENGTH 7
#define IMG_DESCR_LENGTH 9

/* Amiga graphics limitations */

#define MAX_PLANES 5		/* not using HAM or Halfbrite... */
#define MAX_PLANES_HIRES 4
#define MAX_COLORS (1<<MAX_PLANES)

/* global variable definitions... */

struct GfxBase *GfxBase=NULL;
struct IntuitionBase *IntuitionBase=NULL;

struct Screen *screen=NULL;
struct Window *window=NULL;

UBYTE color_map[3*MAX_COLORS];

char *fname;	/* for fatal_error() */

FILE *fp;	/* this really shouldn't be global but... */
 
/* LZW decompression stuff */

struct tabentry {
  unsigned next;
  char ch;
};

#define MAXMAX    4096
#define MAXBITS     12
#define STACKSIZE 4000

#define push(x) { \
 *stackpointer++=(x); \
 if(stackpointer>=&stack[STACKSIZE]) \
     fatal_error("stack overflow"); }

#define pop() (*--stackpointer)

/* global variables for decompressing routine */

int code_size,clear_code,nbits;
int byte_count,ch,bt,end_of_data;

#define END_CODE (clear_code+1)
#define FIRST    (clear_code+2)

struct tabentry table[MAXMAX+10];
char stack[STACKSIZE+10];

/* IFF stuff */

#define ID_FORM 0x464f524d
#define ID_ILBM 0x494c424d
#define ID_BMHD 0x424d4844
#define ID_CMAP 0x434d4150
#define ID_BODY 0x424f4459

struct BitMapHeader {
  UWORD w,h;
  WORD x,y;
  UBYTE nPlanes,masking,compression,pad1;
  UWORD transparentColor;
  UBYTE xAspect,yAspect;
  WORD pageWidth,pageHeight;
};

/* function declarations */

void display_gif();
void display_image();

void decompress_image();
int  read_code();

void fatal_error();

void open_libs();
void wait_user();
void cleanup();
void open_display();
void init_image();
void put_pixel();

void read_file();
int  read_char();
void seek_file();
void write_file();
void write_long();
void file_word_align();

void flip_word();

void save_iff();
int  pack_row();

/*
  the main program
 */
void main(argc,argv)
int argc;
char *argv[];
{
#ifdef AZTEC_C
 Enable_Abort=0;		/* disable Control-C */
#endif
#ifdef LATTICE_4
 signal(SIGINT,SIG_IGN);	/* same for Lattice 4.x */
#endif

 if(argc!=2 && argc!=3)
  {
   if(argc==1)
      fprintf(stderr,"AGIF v1.1 by Timo Rossi 1988-11-25\n");
   fprintf(stderr,"Usage: agif inputfile [outputfile]\n");
   exit(20);
  }
 open_libs();
 fname=argv[1];
 display_gif(fname);

 if(argc==3)
   {
    fname=argv[2];
    save_iff(fname);
   }

 wait_user();
 cleanup();
 exit(0);
}

/*
  wait until user wants to exit

  quit when mouse pressed in the top left corner (20x20 pixel area)
  toggle screen title on/off when mouse pressed elsewhere
 */
void wait_user()
{
 struct IntuiMessage *msg;
 ULONG class;
 UWORD code;
 int x,y;
 int keepgoing=TRUE;

 ModifyIDCMP(window,MOUSEBUTTONS);
 while(keepgoing)
  {
   Wait(1L << window->UserPort->mp_SigBit);
   while(msg=(struct IntuiMessage *)GetMsg(window->UserPort))
     {
      class=msg->Class;
      code=msg->Code;
      x=msg->MouseX;
      y=msg->MouseY;
      ReplyMsg((struct Message *)msg);
      if(class==MOUSEBUTTONS && code==SELECTDOWN)
	{
	 if(x<20 && y<20) keepgoing=FALSE;
	 else	 /* toggle title */
	   ShowTitle(screen,(long)(!(screen->Flags&SHOWTITLE)));
	}
     }
  }
 ModifyIDCMP(window,NULL);
}

/*
  open graphics & intuition libraries
 */
void open_libs()
{
 if((GfxBase=(struct GfxBase *)
    OpenLibrary("graphics.library",0L))==NULL)
     {
      cleanup();
      exit(500);
     }
 if((IntuitionBase=(struct IntuitionBase *)
    OpenLibrary("intuition.library",0L))==NULL)
     {
      cleanup();
      exit(500);
     }
}

/*
  free all resources before exiting
  (note stat stdio files are closed automatically)
 */
void cleanup()
{
 if (window)	CloseWindow(window);
 if (screen)	CloseScreen(screen);

 if (GfxBase)		CloseLibrary((struct Library *)GfxBase);
 if (IntuitionBase)	CloseLibrary((struct Library *)IntuitionBase);
}

/*
  display error message, cleanup & exit
 */
void fatal_error(errmsg)
char *errmsg;
{
 fprintf(stderr,"agif: ");
 fprintf(stderr,errmsg,fname);
 putc('\n',stderr);
 cleanup();
 exit(300);
}

/*
  the main GIF-picture reading routine

  parses the structure of the GIF file, calls open_display() to open
  screen & window, calls display_image() for all images in GIF file
 */
void display_gif(name)
char *name;
{
 char id[6];
 struct ScreenDescriptor scrdesc;
 int n,c,bits_per_pixel,ncolors;
 int keepgoing=TRUE;
 UBYTE *cp;

 if((fp=fopen(name,"r"))==NULL)
   fatal_error("can't open file '%s'");

 read_file(fp,id,6);
 if(strncmp(id,GIF_SIGNATURE,6))
   fatal_error("'%s' is not a GIF file");

 read_file(fp,(char *)&scrdesc,SCR_DESCR_LENGTH);
 flip_word(&scrdesc.Width);
 flip_word(&scrdesc.Height);
 bits_per_pixel=(scrdesc.PixelFlags & 7) + 1;

 if(scrdesc.PixelFlags & 0x80) /* global color map */
   {
     ncolors= 1<<bits_per_pixel;
     if(ncolors>MAX_COLORS) ncolors=MAX_COLORS;
     read_file(fp,color_map,3*ncolors);
     cp=color_map;
   }
 else cp=NULL;   /* no global color map */

 open_display(
   scrdesc.Width,scrdesc.Height,bits_per_pixel,cp,ncolors,scrdesc.Background);

 while(keepgoing) /* loop all images in a file */
   {
     while((c=read_char(fp))!=IMAGE_SEPARATOR && c!=EXTENSION_INTRODUCER &&
	   c!=GIF_TERMINATOR); /* skip all other bytes */
     switch(c)
       {
        case IMAGE_SEPARATOR:
	 display_image(fp);
	 break;
	case EXTENSION_INTRODUCER:	/* skip extension */
	 read_char(fp); /* skip function code */
	 while(n=read_char(fp))
	   seek_file(fp,(long)n,1);
	 break;
        case GIF_TERMINATOR:
	 keepgoing=FALSE;
	 break;
       }
   }
 fclose(fp);
}

/*
  display one image in GIF file
  reads image descriptor, calls decompress_image()
  to display the image
 */
void display_image()
{
 struct ImageDescriptor imdesc;

 read_file(fp,(char *)&imdesc,IMG_DESCR_LENGTH);
 flip_word(&imdesc.Left);
 flip_word(&imdesc.Top);
 flip_word(&imdesc.Width);
 flip_word(&imdesc.Height);

 if(imdesc.PixelFlags & 0x80)
   {
	/* This program cannot really handle local color maps
	   but it can skip them anyway.
	 */
     fprintf(stderr,"agif: skipping local color map\n");
     seek_file(fp,(long)(3*(1 << ( (imdesc.PixelFlags & 7) + 1))),1);
   }

 init_image(
   imdesc.Left,imdesc.Top,imdesc.Width,imdesc.Height,imdesc.PixelFlags&0x40);

 decompress_image();
}

/*
  decompresses and displays LZW-compressed image
  calls put_pixel for graphics output to screen

  note that pixels are compressed, not bytes
 */
void decompress_image()
{
 int k,fin_char;
 register int cur_code,max_code;
 register int old_code,in_code,free_code;
 register char *stackpointer=stack;

 code_size=read_char(fp);
 byte_count=read_char(fp);
 clear_code=1<<code_size;
 end_of_data=FALSE;
 bt=0;

 nbits=code_size+1;
 max_code=1<<nbits;
 free_code=FIRST;

 for(;;)  /* decompressor main loop */
   {
     cur_code=read_code();
     if(cur_code==END_CODE) return;
     if(cur_code==clear_code)
       {
	 nbits=code_size+1;
	 max_code=1<<nbits;
	 free_code=FIRST;
	 fin_char=k=old_code=cur_code=read_code();
	 put_pixel(k);
       }
     else
       {
	 in_code=cur_code;
	 if(cur_code>=free_code)
	   {
	     cur_code=old_code;
	     push(fin_char);
	   }
	 while(cur_code>=clear_code)
	   {
	     push(table[cur_code].ch);
	     cur_code=table[cur_code].next;
	   }
	 k=fin_char=cur_code;
	 push(k);

	  /* output pixels from stack in reverse order */
	 while(stackpointer>stack) put_pixel(pop());

	 table[free_code].ch=k;  /* add code */
	 table[free_code].next=old_code;
	 if(++free_code>=max_code)
	   {
	     if(nbits<MAXBITS)
	       {
		 nbits++;
		 max_code<<=1;
	       }
	   }
	 old_code=in_code;
       }
   }
}

/*
  reads one LZW-compression code from GIF-file
  note data block format and bit & byte order
 */
int read_code()
{
 register int code=0,bb,bl=nbits;

 static int masks[]={ 0x00,0x01,0x03,0x07,0x0f,0x1f,0x3f,0x7f,0xff };

 if(end_of_data) return END_CODE;

 while(bl)
  {
   if(bt==0)
    {
     ch=read_char(fp);
     byte_count--;
     if(byte_count==0)
      {
	byte_count=read_char(fp);
	if(byte_count==0)
	  {
	   end_of_data=TRUE;
	   break;
	  }
      }
     bt=8;
    }

   bb= bt < bl ? bt : bl; /* min(bt,bl) */
   code |= (((ch >> (8-bt))&masks[bb]) << (nbits-bl));
   bl-=bb;
   bt-=bb;
  }
 return code;
}

/*
  swaps the low and high bytes in a 16-bit word
  this is necessary because GIF uses lo-byte/hi-byte order.
 */
void flip_word(ptr)
UWORD *ptr;
{
 register char a,*c=(char *)ptr;

 a=c[0];c[0]=c[1];c[1]=a;
}

/*
  open screen & window
  initialize color map if necessary
 */
void open_display(width,height,depth,colors,ncolors,bg)
int width,height,depth,ncolors,bg;
UBYTE *colors;              /* NULL if default color map */
{
 int i;

 static struct NewScreen NewS={
 0,0,0,0,0,
 0,1,
 0,
 CUSTOMSCREEN,
 NULL,
 (UBYTE *)"AGIF by T.R.  --  click in top left corner to exit",
 NULL,NULL
 };

 static struct NewWindow NewW={
 0,0,0,0,
 -1,-1,
 NULL,	 /* no IDCMP yet, set MOUSEBUTTONS in wait_user() */
 BACKDROP|BORDERLESS|SMART_REFRESH|
 ACTIVATE|RMBTRAP,
 NULL,NULL,NULL,NULL,NULL,
 0,0,0,0,
 CUSTOMSCREEN
 };

 NewS.Width=NewW.Width=width;
 NewS.Height=NewW.Height=height;
 NewS.Depth=depth;

 NewS.ViewModes=0;
 if(width>380) NewS.ViewModes|=HIRES;
 if(height>=280) NewS.ViewModes|=LACE;

 if(((NewS.ViewModes&HIRES) && depth>MAX_PLANES_HIRES) || depth>MAX_PLANES)
	fatal_error("too many bitplanes");

 if((screen=OpenScreen(&NewS))==NULL)
	fatal_error("can't open screen");
 NewW.Screen=screen;
 if((window=OpenWindow(&NewW))==NULL)
	fatal_error("can't open window");
 ShowTitle(screen,FALSE);
 if(colors)
   {
    for(i=0;i<ncolors;i++)
      SetRGB4(&screen->ViewPort,(long)i,
	 (long)colors[i*3]>>4,(long)colors[i*3+1]>>4,(long)colors[i*3+2]>>4);
   }
 if(bg!=0) SetRast(window->RPort,(long)bg);
}

/*
 global variables for image output
 */
int xpos,ypos,xstart,ystart,xend,yend,lacepass,no_more_pixels;
int depth;
UBYTE bitmask;
long planepos;

UBYTE bmasks[]={ 0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01 };

/*
  pixel output initializing routine
 */
void init_image(left,top,width,height,lace)
int left,top,width,height,lace;
{
 xpos=xstart=left;
 ypos=ystart=top;
 xend=left+width;
 yend=top+height;
 /* check if image fits on screen */
 if(xend>screen->Width || yend>screen->Height)
      fatal_error("illegal image dimensions");
 lacepass = lace ? 1 : 0;
 no_more_pixels=FALSE;
 depth=screen->BitMap.Depth;
 planepos=((long)xpos>>3)+(((long)screen->BitMap.BytesPerRow)*ypos);
 bitmask=bmasks[xpos&7];
}

/*
  pixel output routine
  handles GIF interlace scan line order
  ( note that GIF interlace has nothing to do with
    Amiga interlace display mode )
  this routine does not use WritePixel(), it writes directly into
  screen display memory (much faster...)
 */
void put_pixel(c)
register int c;
{
 register int k;
 register UBYTE **plptr=&screen->BitMap.Planes[0];

  /* arrays for decoding GIF interlace */
 static int lace_incr[]={ 0,8,8,4,2 }; /* first element of array not used */
 static int lace_strt[]={ 0,0,4,2,1 }; /* 2 first elements not used */

 if(no_more_pixels) return;

 for(k=depth;k;k--,c>>=1)
  {
   if(c&1) *(planepos+(*plptr++)) |= bitmask;
   else *(planepos+(*plptr++)) &= ~bitmask;
  }

 if(!(bitmask>>=1))
  {
   bitmask=0x80;
   planepos++;
  }
 if(++xpos>=xend)
  {
   xpos=xstart;
   if(lacepass==0)
    {
      if(++ypos>=yend) no_more_pixels=TRUE;
    }
   else
    {
      if((ypos+=lace_incr[lacepass])>=yend)
       {
        if(lacepass<4) ypos=ystart+lace_strt[++lacepass];
	else no_more_pixels=TRUE;
       }
    }
   planepos=((long)xpos>>3)+(((long)screen->BitMap.BytesPerRow)*ypos);
   bitmask=bmasks[xpos&7];
  }
}

/*
 The IFF save routines
 */
void save_iff(name)
char *name;
{
 static UBYTE packbuf[256];
 static struct BitMapHeader BMHD;
 int i,y,numcolors,rowbytes;
 long size,pos;
 long buff[5];

 if((fp=fopen(name,"w"))==NULL)
   fatal_error("can't create file '%s'");

 rowbytes=screen->BitMap.BytesPerRow;

 BMHD.w=BMHD.pageWidth=rowbytes<<3;
 BMHD.h=BMHD.pageHeight=screen->Height;
 BMHD.nPlanes=screen->BitMap.Depth;
 BMHD.x=BMHD.y=0;
 BMHD.transparentColor=0;
 BMHD.compression=1;
 BMHD.masking=0;
  /* absolutely not correct but what is the correct aspect ratio anyway */
 BMHD.xAspect=BMHD.yAspect=1;

 numcolors=1<<BMHD.nPlanes;

 buff[0]=ID_FORM; /* form size filled later */
 buff[2]=ID_ILBM;
 buff[3]=ID_BMHD;
 buff[4]=sizeof(BMHD);
 write_file(fp,(char *)buff,4*5);
 write_file(fp,(char *)&BMHD,(int)sizeof(BMHD)); /* write BMHD */

 buff[0]=ID_CMAP;
 buff[1]=3*numcolors;
 write_file(fp,(char *)&buff,2*4);
 write_file(fp,(char *)color_map,(int)buff[1]);	 /* write CMAP */

 buff[0]=ID_BODY;
 write_file(fp,(char *)buff,2*4);

 for(y=0;y<BMHD.h;y++)				/* pack & write BODY */
  for(i=0;i<BMHD.nPlanes;i++)
     write_file(fp,(char *)packbuf,
        pack_row((UBYTE *)(screen->BitMap.Planes[i])+rowbytes*y,
	   packbuf,rowbytes));

 size=ftell(fp);

 file_word_align(fp);

 /* write FORM & BODY sizes */

 seek_file(fp,4L,0);
 write_long(fp,size-8+(size&1));	/* FORM size (always even) */

 pos=sizeof(BMHD)+3*numcolors+8*4;
 seek_file(fp,pos,0);
 write_long(fp,size-pos-4);		/* BODY size (may be odd)  */

 fclose(fp);
}

/*
 IFF CmpByteRun1 compression
 */
int pack_row(source,dest,len)
UBYTE *source,*dest;
int len;
{
 register int i,c,current=0,cnt=0,destcnt=0;
 int dumpend,prev=-2,dumpstart=0;

#define putdest(x) dest[destcnt++]=(x)

 do
  {
   do
    {
     c = current >= len ? -1 : source[current];
     current++;
     if(c==prev)
      {
       if(!cnt) dumpend=current-2;
       cnt++;
      }
    } while (c==prev);
   if(cnt)
    {
     if(!(cnt==1 && dumpstart<dumpend)) /* 2 byte run after dump */
      {
       if(dumpstart<dumpend)
	{
	 putdest(dumpend-dumpstart-1);
	 for(i=dumpstart;i<dumpend;i++) putdest(source[i]); /* dump */
	}
       putdest((UBYTE)(-cnt));
       putdest(prev);		/* run */
       dumpstart=current-1;
      }
     cnt=0;
    }
   prev=c;
  } while (c!=-1);

 if(dumpstart<len)
  {
   putdest(len-dumpstart-1);
   for(i=dumpstart;i<len;i++) putdest(source[i]); /* dump */
  }

 return destcnt;
}

/*
 file IO routines (stdio calls are used)
 */
void read_file(file,buff,len)
FILE *file;
char *buff;
int len;
{
 if(fread(buff,len,1,file)!=1)
   fatal_error("file read error");
}

int read_char(file)
FILE *file;
{
 register int c;

 if((c=getc(file))==EOF)
   fatal_error("file read error");
 return c;
}

void seek_file(file,pos,mode)
FILE *file;
long pos;
int mode;
{
 if(fseek(file,pos,mode)<0)
     fatal_error("file seek error");
}

void write_file(file,buff,len)
FILE *file;
char *buff;
int len;
{
 if(fwrite(buff,len,1,file)!=1)
   fatal_error("file write error");
}

void write_long(file,value)
FILE *file;
long value;
{
 if(fwrite((char *)&value,4,1,file)!=1)
   fatal_error("file write error");
}

void file_word_align(file)
FILE *file;
{
 if(ftell(file)&1) putc('\0',file);
}
