program gifega;

{Version 1.0, written 1/18/88-2/7/88 by Jim Griebel. This software is
experimental! USE AT YOUR OWN RISK. In the public domain. 'GIF' and
'Graphics Interchange Format' are trademarks of Compuserve, Inc., an H&R
Block Company. 'Turbo Pascal' is a trademark of Borland International.}

{Includes for external .OBJ files}

{$L nlzw}
{$L readrast}
{$L scrsave}
{$L scroll}

{Turbo standard UNITS}

uses crt,dos;

type

{Our 64,000-byte array, used for practically everything}
    RasterArray = Array [0..63999] of byte;
    RasterP = ^RasterArray;

{Except the EGA bitplanes, which are kept here}
    Bitplane = Array [0..38399] of byte;
    PlaneP = ^BitPlane;

    Palarray = Array [0..255] of byte;
    Str12 = String [12];
    Str80 = String [80];

var
    GIFFile:File of RasterArray;     {GIF input file}
    ScrFile:File of Byte;            {Output file if desired}
    GifStuff:RasterP;                {Heap array for GIF file}
    Raster:RasterP;                  {Unblocked GIF data stream}
    Raster2:RasterP;                 {More unblocked stream if needed}
    Plane0,Plane2,Plane1,Plane3:PlaneP; {EGA bitplanes, for scroll & save}
    Regs:Registers;                  {Turbo predefined variable}
    DirInfo: SearchRec;              {Turbo predefined variable}

    Width,         {Read from GIF header, image width}
    Height,        { ditto, image height}
    LeftOfs,       { ditto, image offset from left}
    TopOfs,        { ditto, image offset from top}
    RWidth,        { ditto, raster width}
    RHeight,       { ditto, raster height}
    ClearCode,     {GIF clear code}
    EOFCode,       {GIF end-of-information code}
    MaxCode,       {Decompressor limiting value for current code size}
    CurCode,       {Decompressor variable}
    OldCode,       {Decompressor variable}
    InCode,        {Decompressor variable}
    FirstFree,     {First free code, generated per GIF spec}
    Codesize,      {Size of code, computed from GIF header}
    GIFPtr,        {Array pointer used during file read}
    FreeCode,      {Next free code, used by decompressor}
    ReadMask,      {Code AND mask for current code size}
    I,J,K,         {Loop counters, what else?}
    Bitmask,       {Used during read from compressed file}
    ColorMapSize,  {Size of the colormap}
    XDir,YDir,     {used for directory write to screen}
    DirSize,       { Ditto }
    FSize          {Size of bitplanes to write to output file}
    :word;


    Interlace,     {True if interlaced image}
    NextRaster,    {True if file>64000 bytes}
    Clear,         {True during clear}
    ColorMap:      {True if colormap present}
    Boolean;

    ch:char;       {Utility}

    a,             {Utility}
    Resolution,    {Resolution, read from GIF header}
    BitsPerPixel,  {Bits per pixel, read from GIF header}
    Background,    {Background color, read from GIF header}
    InitCodeSize,  {Starting code size, used during Clear}
    FinChar,       {Decompressor variable}
    Pass,          {Used by video output if interlaced pic}
    R,G,B          {Red,Green,Blue values used during color comps}
    :byte;

    Roll:Integer;  {Scroll offset value, an integer so
                     we can tell when it goes <0}

    {The color map, read from the GIF header}
    Red,Green,Blue: array [0..255] of word;

    {The EGA palette, derived from the color map}
    Palette: PalArray;

    {Strings used for various purposes}
    FileString:Str80;
    UString:Str80;
    Homedir:Str80;

    {Holds the directory strings}
    NString:Array [0..255] of Str12;

    {An array of computed line starting values in EGA RAM, used to
     make the program marginally faster}
    LineStart:Array [0..479] of word;

Const
    {MaxCode values for differing code sizes}
    MaxCodes: Array [0..9] of Word = (4,8,16,$20,$40,$80,$100,$200,$400,$800);

    {Saves computing these values, Pascal having no exponentiation}
    PowersOf2: Array [0..8] of word=(1,2,4,8,16,32,64,128,256);

    Rastersize:Word = 64000;
    PlaneSize:Word = 38400;
    EGAPage:Word = $A000;          {EGA video RAM segment address}
    EGAHeight:Word = 350;            {Height of vanilla EGA screen}

    Nicepal: PalArray= (1,4,54,63,63,63,63,54,63,63,63,63,63,63,54,54,60,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
                        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);


{Procedure declarations that bring our .OBJ externals into the program}

procedure nlzw; external;

procedure readrast; external;

procedure scrsave; external;

procedure Scroll; external;


{Go to the bottom line and clear same out}

Procedure BottomLine;

Begin
     GotoXY (2,24);
     DelLine;
End;


{End the program gracefully, giving the user back a text video mode and
returning him to the directory he started from}

Procedure Morgue;

Begin
     Textmode (15);
     ChDir (HomeDir);
     Halt;
End;

{Display the sorted directory listing}

Procedure ShowDir;

Var J:Word;

Begin

{List the filenames on the screen. If there are more than 20 then
display them in two or more columns}

     ClrScr;
     If DirSize=0 then Exit;
     XDir:=2;
     YDir:=3;
     For J:=0 to DirSize do
         Begin
              GotoXY (XDir,YDir);
              Writeln (NString [J]);
              YDir:=Succ (YDir);
              If YDir = 22 then
                 Begin
                      XDIR:=XDIR+15;
                      YDIR:=3;
                 End;
         End;

End;

Procedure DoDir;

{Get and put up a sorted directory display of eligible files only; uses
calls specific to Turbo. See the Turbo manual for a description of the
calls used and their results}

Var Sortflag: Boolean;
    I: Integer;

Begin

{Get the current directory}

     GetDir (0,UString);
     I:=0;

{Find the first matching file, if any}

     FindFirst ('*.gif',Anyfile,DirInfo);
     If DosError <> 0 then
     Begin
        Dirsize:=0;
        Exit;
     End;

{And subsequent, if any}

     NString [I]:=DirInfo.Name;
     I:=Succ (I);
     Repeat
           FindNext (DirInfo);
           If DosError =0 then
           Begin
              Nstring [I]:=DirInfo.Name;
              I:=Succ (I);
           End;
     Until (DosError=18) or (I=256);

{Now sort the filenames alphabetically}

     Repeat
     Sortflag:=False;
     If I>2 then
           For J:=0 to I-2 do
               Begin
                 If NString [J]>Nstring [J+1] then
                    Begin
                      NString [255]:=Nstring [J];
                      NString [J]:=Nstring [J+1];
                      NString [J+1]:=Nstring [255];
                      Sortflag:=True;
                    End;
               End;
     Until Sortflag=False;
     DirSize:=I-1;

End;

{Save the currently displayed image as an EGA Paint .SCR file}

Procedure SaveScreen;

Begin

{Convert the input filename to the output filename.}

     I:=Pos ('.',Filestring);
     Filestring:=Copy (Filestring,1,I);
     If (Height<480) or (EGAHeight<480) then
        Begin
             Filestring:=Filestring+'SCR';
             FSize:=28000;
        End
        Else
        Begin
            Filestring:=Filestring+'SCP';
            FSize:=38400;
        End;

{We let Turbo open the file, since we can grab the handle in the assembly
external}

     Assign (Scrfile,FileString);
     Rewrite (Scrfile);

{The assembly external handles the actual write}

     ScrSave;

{Close it, and we're done}

     Close (Scrfile);
End;

{This procedure checks to be sure we've got enough heap for the array
we're trying to allocate, then allocates same. If there isn't enough
heap available, we exit with an error}

Procedure AllocMem (Var P:RasterP);

Var ASize:Longint;

Begin
     ASize:=MaxAvail;
     If ASize<RasterSize then
        Begin
             Textmode (15);
             Writeln ('Insufficient memory available!');
             Halt;
        End
        Else
        Getmem (P,RasterSize);
End;

{Same as AllocMem, but for a bitplane-sized array}

Procedure AllocPlane (Var P:PlaneP);

Var ASize:LongInt;

Begin
     ASize:=Maxavail;
     If ASize<PlaneSize then
        Begin
             Textmode (15);
             Writeln ('Insufficient memory available!');
             Halt;
        End
        Else GetMem (P,PlaneSize);

End;


{Mimics a file read of a single byte, reading from the input record rather
than the file itself. If you wish to change back to a file of byte rather
than using the faster read of the record, you can modify this routine to
read directly from the file. That's simpler but slower}

Function Getbyte:Byte;
  Begin
       If GIFPtr=RasterSize then Exit;
       Getbyte:=GIFStuff^[GIFPtr];
       GIFPtr:=Succ(GIFPtr);
  End;

{Reads two bytes, to get a word value}

Function Getword:Word;

Var A,B:Byte;

Begin
     A:=Getbyte;
     B:=Getbyte;
     Getword:=A+(256*B);
End;

{During READRAST, reach out and get the rest of the file if it is
larger than 64000 bytes}

Procedure ReadMore;

Var IOR:Integer;

Begin
     {$I-}
     Read (GIFFile,GIFStuff^);
     {$I+}
     IOR:=IOResult;
End;

{During the file decompress cycle, readjust the RASTER arrays if the
original file read was larger than 64000 bytes.}

Procedure MoveUp (var Bitoffset:Longint);

Var Byteoffset:Longint;

        Begin
             Byteoffset:=Bitoffset div 8;
             Move (Raster^[Byteoffset],Raster^[0],RasterSize-Byteoffset);
             Move (Raster2^[0],Raster^[RasterSize-Byteoffset],63000);
             Bitoffset:=Bitoffset mod 8;
             FreeMem (Raster2,RasterSize);
             Nextraster:=False;
        End;


Procedure SetPal (Pal:PalArray);


Begin
     Regs.AX:=$1002;
     Regs.DX:=Ofs (Pal);
     Regs.ES:=Seg (Pal);
     Intr ($10,Regs);
End;


{Use the BIOS functions to set up the EGA. This avoids dependence on Turbo's
GRAPH package and the necessity to keep .BGI files with the executable}

Procedure InitEGA;

Var LocPal:PalArray;

Begin

{Set EGA graphics mode}

   Regs.AX:=$0010;
   Intr ($10,Regs);

{Set the palette}

   LocPal:=Palette;
   LocPal [16]:=Background;

   SetPal (LocPal);

{Compute line starting values for the assembly-language displayer}

    For I:=0 to 479 do
        LineStart [I]:=I*80;

{Enable Set/Reset on all the EGA bitplanes}
    Port [$3CE]:=1;
    Port [$3CF]:=15;

End;


{Derive the EGA palette value corresponding to the GIF colormap intensity
value for a given color.}

Procedure DetColor (Var PValue:Byte;MapValue:Byte);

Begin
        PValue:=MapValue div 64;
        If PValue=1 then PValue:=2 else
           If PValue=2 then PValue:=1;
End;

{A crude attempt to deal with 256-color pics. Works. Badly. Dot-dithering,
anyone?}

Procedure AdjustBigPal;


Var ColPtr,Cindex,I,J,X:Word;

Begin

     For I:=16 to ColorMapSize-1 do
         Begin
              Colptr:=63;
              For J:=0 to 15 do
              Begin
                  If Palette [I]>Palette[J] then
                     X:=Palette [I]-Palette[J] else X:=Palette [J]-Palette[I];
                  If (X< Colptr) then
                     Begin
                          Colptr:=X;
                          Cindex:=J;
                     End;
              End;
              Palette [I]:=Cindex;
         End;
     End;



{Set the key variables to their necessary initial values.}


Procedure ReInitialize;
Begin
     Pass:=0;        {Interlace pass counter back to 0}
     GIFPtr:=0;      {Mock file read pointer back to 0}
     Nextraster:=False; {Start by claiming file <64000 bytes}
End;

{React to GIF clear code by resetting GIF decompression values back to their
initial state.}

Procedure DoClear;

    Begin
      CodeSize:=InitCodeSize;
      MaxCode:=MaxCodes [CodeSize-2];
      FreeCode:=FirstFree;
      ReadMask:=(1 shl CodeSize)-1;
    End;


Begin    {the main program}

{First get the home directory so that we can reset to it on exit, and get
the eligible files in that directory}

Getdir (0,HomeDir);
DoDir;

Repeat   {giant loop reruns whole program till user bails}

{Initialize a bunch of variables}

     ReInitialize;         {Initialize common vars}


{Get memory for the raster data array, and the input file data array}

     AllocMem (Raster);
     AllocMem (GIFStuff);
     SetPal (NicePal);
     Textcolor (2);

 Repeat {Get GIF file}
  Repeat {Get good file}
    Repeat {Get good filename}

     ShowDir;         {Show eligible files}

{Prompt the user for the filename}

     Bottomline;
     Write ('Filename (ENTER exits,\ changes dir): ');
     Readln (Filestring);
     If Filestring = '' then Morgue; {bail on null string}
     If FileString ='\' then         {change dir on single '\' char}
        Begin
             Bottomline;
             Write ('New directory name: ');
             Readln (Ustring);
             {$I-}
             Chdir (Ustring);
             {$I+}
             I:=IOResult;
             If I<>0 then
                Begin
                     Bottomline;
                     Textcolor (1);
                     Writeln ('Error changing to directory ',Ustring);
                     Textcolor (2);
                End;
             DoDir;                   {Relist directory in new dir}
        End;
    Until FileString <> '\';    {Got good filename}

    If Pos ('.',Filestring)=0 then Filestring:=Filestring+'.gif';


    {Open the file}

    {$I-}
     Assign (GIFFile,FileString);
     Reset (GIFFile);
    {$I+}

    {Cope with I/O error should one occur}

     I:=IOResult;
     If I<>0 then
        Begin
             Textcolor (1);
             Bottomline;
             Write ('Error opening file ',FileString,'. Press any key ');
             Readln;
             Textcolor (2);
        End;

  Until I=0;    {Got good file}

{Read in the GIF file. Reading it as one big hunk rather than N bytes results
in far faster disk I/O; see user notes. Error checking is turned off in
order to avoid 'attempt to read past EOF' errors. If the file does not exist,
this will be detected at RESET}

     BottomLine;
     Write ('Reading . . . ');
{$I-}
     Read (GIFFile,GIFStuff^);
{$I+}

{Note that 4.0 requires this assignment, or else if an error results (as it
will if the file is smaller than 64000 bytes) no I/O will be allowed for
the remainder of the run. If the file is >64000 bytes, then the rest
of it will be read during ReadRast}

I:=IOResult;

{Deal with the GIF header. Start by checking the GIF tag to make sure this
is a GIF file}

     UString:='';
     for I:=1 to 6 do
     Begin
         UString:=UString+chr(Getbyte);
     End;
     If UString<>'GIF87a' then
        Begin
             Textcolor (1);
             Bottomline;
             Write (UString);
             Write ('Not a GIF file, or header read error. Press any key ');
             Textcolor (2);
             Readln;
        End;

 Until UString = 'GIF87a';  {Get GIF file}

{Get variables from the GIF screen descriptor}

     RWidth:=Getword;         {The raster width and height}
     RHeight:=Getword;

     {Get the packed byte immediately following and decode it}
     B:=Getbyte;
     If B and $80=$80 then Colormap:=True else Colormap:=False;
     Resolution:=(B and $70 shr 5)+1;
     BitsPerPixel:=(B and 7)+1;
     If BitsPerPixel=1 then I:=2 else I:=1 shl BitsPerPixel;
     Bitmask:=(1 shl BitsPerPixel)-1;

     Background:=Getbyte;
     B:=Getbyte;         {Skip byte of 0's}

{Compute size of colormap, and read in the global one if there. Compute
values to be used when we set up the EGA palette}

     ColorMapSize:=1 shl BitsPerPixel;
     If Colormap then
     Begin
     For I:=0 to ColorMapSize-1 do
     Begin
         Red [I]:=Getbyte;
         Green [I]:=Getbyte;
         Blue [I]:=Getbyte;
         DetColor (R,Red[I]);
         DetColor (G,Green [I]);
         DetColor (B,Blue [I]);
         Palette [I]:=B and 1+(2*(G and 1))+(4*(R and 1))+(8*(B div 2))+(16*(G div 2))+(32*(R div 2));
     End;
     If ColorMapSize>16 then AdjustBigPal; {Hack at 256-color pics}
     End;

{Now read in values from the image descriptor}

     B:=Getbyte;  {skip image seperator}
     Leftofs:=Getword;    {Left offset, not used here}
     Topofs:=Getword;     {Top offset, not used here}
     Width:=Getword;      {Width, not used here}
     Height:=Getword;     {Height, not used here}
     A:=Getbyte;
     If A and $40=$40 then Interlace:=True else Interlace:=False;


{Note that we ignore the possible existence of a local color map. I've yet
to encounter an image that had one, and the spec says it's defined for
future use. This could lead to an error reading some files}

{Start reading the raster data. First we get the intial code size}

     Codesize:=Getbyte;

{Compute decompressor constant values, based on the code size}

     ClearCode:=PowersOf2 [Codesize];
     EOFCode:=ClearCode+1;
     FirstFree:=ClearCode+2;
     FreeCode:=FirstFree;

{The GIF spec has it that the code size used to compute the above values is
the code size given in the file, but the code size used in compression/
decompression is the code size given in the file plus one.}

     Codesize:=Succ (Codesize);
     InitCodeSize:=Codesize;
     Maxcode:=Maxcodes [Codesize-2];
     ReadMask:=(1 shl Codesize)-1;

{Read the raster data. Here we just transpose it from the GIF array to the
Raster array, turning it from a series of blocks into one long data stream,
which makes life much easier for ReadCode. This too is now assembly}

     Writeln ('Unblocking . . . ');
     ReadRast;

{Get ready to do the actual read/display. Free up the heap used by the
GIF array since we don't need it any more}

     FreeMem (GIFStuff,RasterSize);

{Set up the EGA}

     InitEGA;

{Preset the CLEAR flag to off}

     Clear:=False;

{Decompress and display}

     nlzw;

{Get rid of the heap used by the Raster array, and close the GIF file}

   FreeMem (Raster,RasterSize);
   Close (GIFFile);

{Now grab the bitplanes from EGA memory, to be used when scrolling and
saving the file. We take advantage of the fact that although only 28000
bytes of EGA memory can be used for display, the extra memory to store
a 640x480 pic is there and working and will contain the excess scan
lines}

   AllocPlane (Plane0);
   AllocPlane (Plane1);
   AllocPlane (Plane2);
   AllocPlane (Plane3);

{Set read map select register to the plane to be read, then simply move
the plane to the corresponding array}

   Port [$3CE]:=4;
   Port [$3CF]:=0;
   Move (Mem [EGAPage:0],Plane0^,PlaneSize);
   Port [$3CE]:=4;
   Port [$3CF]:=1;
   Move (Mem [EGAPage:0],Plane1^,PlaneSize);
   Port [$3CE]:=4;
   Port [$3CF]:=2;
   Move (Mem [EGAPage:0],Plane2^,PlaneSize);
   Port [$3CE]:=4;
   Port [$3CF]:=3;
   Move (Mem [EGAPage:0],Plane3^,PlaneSize);

{If the picture is larger than my el cheapo EGA can handle, scroll it}

   If Height>EGAHeight then
   Begin

   {Wake up the user}

     Write (^G); {signals whole picture decoded}

   {Scroll offset value to top of image}

     Roll:=0;

   {Then get the scrolling keys. HOME and END move to top & bottom of
    pic. Up & Down arrow move picture in smaller increments. The values
    used here are based on the size difference between a 350 and 480 line
    EGA pic, i.e. 130 scan lines.}

     Repeat
     Ch:=Readkey;

   {We recognize special keys by their being preceded with 0}

     If (Ch=#0) and (Keypressed) then
        Begin
           Ch:=Readkey;
           Case Ch of
                #71: Roll:=0;
                #72: Begin
                     Roll:=Roll-800;
                     If Roll<0 then Roll:=0;
                     End;
                #79: Roll:=10400;
                #80: Begin
                     Roll:=Roll+800;
                     If Roll>10400 then Roll:=10400;
                     End;
           End;
           Scroll;
     End;
   Until (Ch=#27) and (not Keypressed); {ESC gets us out}
   End;

       {Get one key, then exit}

       If Ch<>#27 then    {Do this only if we didn't scroll}
       Begin
          Write (^G);
          Repeat
           Ch:=Readkey;
          Until Ch=#27;
       End;

       Textmode (15);     {Back to text}
       SetPal (NicePal);
       Writeln ('Press space bar to save as a .SCR file, or any other key to continue');
       Ch:=Readkey;
       If Ch=' ' then SaveScreen;
       FreeMem (Plane0,PlaneSize);
       FreeMem (Plane1,PlaneSize);
       FreeMem (Plane2,PlaneSize);
       FreeMem (Plane3,PlaneSize);

   Until Ch=#0;

End.                      {Finis.}
