program gifslow;

{Written 1/16/88-1/19/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.}

{This is a short simple GIF reader/displayer for the EGA, adapted from
GIFREAD, an earlier effort targeted on the Hercules. No provision is made
for saving files or for scrolling in this program, which is intended as an
example. This is the ultraslow version, pure high level}


uses crt,dos;

type

    RasterArray = Array [0..63999] of byte;
    RasterP = ^RasterArray;

var
    GifFile:File of RasterArray;  {The input file}
    GifStuff:RasterP;   {The heap array to hold it, raw}
    Raster:RasterP;     {The raster data stream, unblocked}
    Raster2:RasterP;    {More raster data stream if needed}
    Regs:Registers;     {Turbo's predefined record}

    Byteoffset,         {Computed byte position in RASTER array}
    Bitoffset           {Bit offset of next code in RASTER array}
    :LongInt;

    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}
    OutCount,   {Decompressor output 'stack count'}
    MaxCode,    {Decompressor limiting value for current code size}
    Code,       {Value returned by ReadCode}
    CurCode,    {Decompressor variable}
    OldCode,    {Decompressor variable}
    InCode,     {Decompressor variable}
    FirstFree,  {First free code, generated per GIF spec}
    FreeCode,   {Decompressor, next free slot in hash table}
    GIFPtr,     {Array pointers used during file read}
    RasterPtr,
    XC,YC,      {Screen X and Y coords of current pixel}
    Pindex,     {Index into screen save array}
    ReadMask,   {Code AND mask for current code size}
    I           {Loop counter, what else?}
    :word;


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

    ch           {Utility}
    :char;

    a,              {Utility}
    Resolution,     {Resolution, read from GIF header}
    BitsPerPixel,   {Bits per pixel, read from GIF header}
    Background,     {Background color, read from GIF header}
    ColorMapSize,   {Length of color map, from GIF header}
    CodeSize,       {Code size, read from GIF header}
    InitCodeSize,   {Starting code size, used during Clear}
    FinChar,        {Decompressor variable}
    Pass,           {Used by video output if interlaced pic}
    BitMask,        {AND mask for data size}
    R,G,B
    :byte;


    {The hash table used by the decompressor}
    Prefix: Array [0..4095] of word;
    Suffix: Array [0..4095] of byte;

    {An output array used by the decompressor}
    Outcode:Array [0..1024] of byte;

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

    {The EGA palette, derived from the color map}
    Palette: Array [0..255] of byte;

    {Strings to hold the filenames}
    FileString:String [80];


Const

    MaxCodes: Array [0..9] of Word = (4,8,16,$20,$40,$80,$100,$200,$400,$800);

    CodeMask:Array [1..4] of byte= (1,3,7,15);

    PowersOf2: Array [0..8] of word=(1,2,4,8,16,32,64,128,256);

    Masks: Array [0..9] of Integer = (7,15,$1f,$3f,$7f,$ff,$1ff,$3ff,$7ff,$fff);

    Rastersize:Word = 64000;


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


{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. This is 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;



{Mimic reading in the raster data. Unblock it into a single large array
to save having to do this as we go, which makes life a lot simpler for
the rest of the program. We cope here with files larger than 64000 bytes by
doing another read from the input file, and by creating a second RASTER
array if necessary to hold the excess unblocked data}

Procedure ReadRaster;

Var BlockLength:Byte;
    I,IOR:Integer;

Begin
   RasterPtr:=0;
   Repeat
   BlockLength:=Getbyte;
     For I:=0 to Blocklength-1 do
       Begin
         If Gifptr = RasterSize then
            Begin
                 {$I-}
                 Read (GIFFile,GIFStuff^);
                 {$I+}
                 IOR:=IOResult;
                 GIFPtr:=0;
            End;
         If not Nextraster then
                  Raster^[RasterPtr]:=Getbyte else
                         Raster2^[RasterPtr]:=Getbyte;
         RasterPtr:=Succ (RasterPtr);
         If RasterPtr=RasterSize then
         Begin
            NextRaster:=True;
            Rasterptr:=0;
            AllocMem (Raster2);
         End;
       End;
   Until Blocklength=0;
End;


{Fetch the next code from the raster data stream. The codes can be any
length from 3 to 12 bits, packed into 8-bit bytes, so we have to maintain
our location in the Raster array as a BIT offset. We compute the byte offset
into the raster array by dividing this by 8, pick up three bytes, compute
the bit offset into our 24-bit chunk, shift to bring the desired code to
the bottom, then mask it off and return it. If the unblocked raster data
overflows the original RASTER array, we switch to the second one}

Procedure ReadCode;

Var RawCode:LongInt;
    A,B:Word;


Begin
     ByteOffset:=BitOffset div 8;

{Pick up our 24-bit chunk}

     A:=Raster^[Byteoffset]+(256*Raster^[ByteOffset+1]);
     If CodeSize>=8 then
     Begin
     B:=Raster^[Byteoffset+2];
     RawCode:=A+(65536*B);
     End
     Else Rawcode:=A;

{Doing the above calculation as a single statement, i.e.
Rawcode:=Raster^[Byteoffset]+(256*Raster^[Byteoffset+1])+
         (65536*Raster[Byteoffset+2])
sometimes returns incorrect results. This may or may not be a bug.}


     RawCode:=RawCode shr (BitOffset mod 8);
     Code:=RawCode and ReadMask;

{Cope with overflow of the first RASTER array}

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

     BitOffset:=BitOffset+CodeSize;

End;


Procedure AddToPixel (Index:Byte);


Begin

     Regs.AH:=12;
     Regs.AL:=Index;
     Regs.CX:=XC;
     Regs.DX:=YC;
     Intr ($10,Regs);

{Update the X-coordinate, and if it overflows, update the Y-coordinate}

     XC:=Succ (XC);
     If XC=Width then

{If a non-interlaced picture, just increment YC to the next scan line. If
it's interlaced, deal with the interlace as described in the GIF spec. Put
the decoded scan line out to the screen if we haven't gone past the bottom
of it}

        Begin

        XC:=0;
        If not Interlace then YC:=Succ (YC) else
            Begin
               Case Pass of
               0: Begin
                  YC:=YC+8;
                  If YC>=Height then
                  Begin
                     Pass:=Succ(Pass);
                     YC:=4;
                  End;
                  End;
               1: Begin
                  YC:=YC+8;
                  If YC>=Height then
                     Begin
                       Pass:=Succ(Pass);
                       YC:=2;
                     End;
                  End;
               2: Begin
                  YC:=YC+4;
                  If YC>=Height then
                     Begin
                          Pass:=Succ(Pass);
                          YC:=1;
                     End;
                  End;
               3: Begin
                  YC:=YC+2;
                  End;
               End;    {Case}
            End;  {If interlace}
        End;

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;

Begin


{Set EGA graphics mode}

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

{Set the palette}

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

End;


{Determine the palette value corresponding to the GIF colormap intensity
value.}

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

Var Local:Byte;

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

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

Procedure ReInitialize;
Begin
     XC:=0;          {X and Y screen coords back to home}
     YC:=0;
     Pass:=0;        {Interlace pass counter back to 0}
     Bitoffset:=0;   {Point to the start of the raster data stream}
     GIFPtr:=0;      {Mock file read pointer back to 0}
End;

{React to GIF clear code, or reset GIF decompression values back to their
initial state when restarting.}

Procedure DoClear;

    Begin
      CodeSize:=InitCodeSize;
      MaxCode:=MaxCodes [CodeSize-2];
      FreeCode:=FirstFree;
      ReadMask:=Masks [CodeSize-3];
    End;

Begin    {the main program}

{Initialize a bunch of variables}

     ReInitialize;         {Initialize common vars}
     Nextraster:=False;    {Over 64000 flag off}

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

     AllocMem (Raster);
     AllocMem (GIFStuff);

{Prompt the user for the filename}

     Write ('Filename: ');
     Readln (Filestring);


{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
             Writeln ('Error opening file ',FileString,'. Press any key ');
             Readln;
             Exit;
        End;

{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}

     Writeln ('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}

I:=IOResult;

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

     FileString:='';
     for i:=1 to 6 do
     Begin
         FileString:=FileString+chr(Getbyte);
     End;
     If FileString<>'GIF87a' then
        Begin
             Writeln ('Not a GIF file, or header read error. Press any key ');
             Readln;
             Exit;
        End;

{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;
     Write ('Colors: ',I);
     BitMask:=CodeMask [BitsPerPixel];
     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;
     Writeln;
     Palette [16]:=Background;
     End;

{Now read in values from the image descriptor}

     B:=Getbyte;  {skip image seperator}
     Leftofs:=Getword;
     Topofs:=Getword;
     Width:=Getword;
     Writeln ('Width: ',Width);
     Height:=Getword;
     Writeln ('Height: ',Height);
     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 is 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:=Masks [Codesize-3];

{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}

     Writeln ('Unblocking');
     ReadRaster;

{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, and if the user wants to save,
reclaim it for the Picture array}

     FreeMem (GIFStuff,RasterSize);
     OutCount:=0;

{Set up the EGA}

     InitEGA;

{Decompress the file, continuing until you see the GIF EOF code. One
obvious enhancement is to add checking for corrupt files here.}

   Repeat

     {Get the next code from the raster array}

          ReadCode;

          If Code <> EOFCode then
          Begin

     {Clear code sets everything back to its initial value, then reads
      the immediately subsequent code as uncompressed data.}

            If Code = ClearCode then
               Begin
                 DoClear;
                 ReadCode;
                 CurCode:=Code;
                 OldCode:=Code;
                 FinChar:=Code and BitMask;
                 AddToPixel (FinChar);
               End
               Else

     {If not a clear code, then must be data: save same as CurCode and InCode}

               Begin
                CurCode:=Code;
                InCode:=Code;

     {If greater or equal to FreeCode, not in the hash table yet; repeat
      the last character decoded}

                If Code>=FreeCode then
                  Begin
                    CurCode:=OldCode;
                    OutCode [OutCount]:=FinChar;
                    OutCount:=Succ (OutCount);
                  End;

     {Unless this code is raw data, pursue the chain pointed to by CurCode
     through the hash table to its end; each code in the chain puts its
     associated output code on the output queue.}

                If CurCode>BitMask then
                   Repeat
                     OutCode [OutCount]:=Suffix [CurCode];
                     OutCount:=Succ (OutCount);
                     CurCode:=Prefix [CurCode];
                   Until CurCode<=BitMask;

      {The last code in the chain is treated as raw data.}

               FinChar:=CurCode and BitMask;
               OutCode [OutCount]:=FinChar;
               OutCount:=Succ (OutCount);

     {Now we put the data out to the using routine. It's been stacked
      LIFO, so deal with it that way}

               For I:=OutCount-1 downto 0 do
                    AddToPixel (Outcode [I]);

      {Make darned sure OutCount gets set back to start}

               OutCount:=0;

      {Build the hash table on-the-fly. No table is stored in the file.}

               Prefix [FreeCode]:=OldCode;
               Suffix [FreeCode]:=FinChar;
               OldCode:=InCode;

     {Point to the next slot in the table. If we exceed the current MaxCode
      value, increment the code size unless it's already 12. If it is, do
      nothing: the next code decompressed better be CLEAR}

               FreeCode:=Succ (FreeCode);
               If FreeCode>=MaxCode then
                Begin
                  If CodeSize < 12 then
                  Begin
                     CodeSize:=Succ (CodeSize);
                     MaxCode:=MaxCode*2;
                     ReadMask:=Masks [CodeSize-3];
                  End;
                End;
               End {not Clear};

               If Keypressed then
                  Begin
                       Ch:=Readkey;
                       If Ch=#27 then
                          Begin
                               Textmode (15);
                               Exit;
                          End;
                  End;
            End; {not EOFCode}
       Until Code=EOFCode;

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

    {Read one key, then pack it in}

    Ch:=Readkey;

    Textmode (15);                 {Back to text}
    Close (GifFile);
    FreeMem (Raster,RasterSize);

End.
