Text

Table of Contents:
What's text good for?
The Font Structure
Aquiring a font
A brief tangent on optimization
1 Bit per pixel fonts
An example program


What's text good for?

Text: In it's many shapes and forms, it has provided us (humans) with the capability to communicate with millions of other people without having to spend years writing it out by hand. Since the invention of the printing press, everything from junk mail and political propaganda to literary masterpieces have been printed. With text, the ability to communicate is greatly enhanced and the availability of different fonts (typefaces) have made communicating emotions possible.

To extend the GraphPro library for fonts, we have to introduce a new data type: The Font. We also have to create some routines to manipulate the fonts and to draw them to the screen. The goal of the fonts in GraphPro is to add a flexible way to print to the screen, while being simple enough to use and, of course, is very fast. With these goals in mind, we will plunge into a new topic: Fonts.


The Font Structure

From now on, a "Font" references a structure in memory. This structure holds all of the pertinent details about the font and a pointer to the actual font data. Because we want to conserve memory but be as flexible as possible, there are a number of options that apply to fonts.

  • The characters of a font can be any height and width.
  • The font can be composed of 1, 2, 4, or 8 bits of color information per pixel.
  • A font does not have to define all 256 ASCII characters.
  • When printing, a background color can be specified or a transparent background can be used.

    Because of all of these options, the FontType structure (Shown below) has many fields. Some of the fields are not specified by the user or an editor, as they are strictly used to speed the routines up. Here is the FontType structure:

      FontType = RECORD
        Data : ScreenBufferPtr;        { Pointer to an array of bytes     }
        Height,                        { Size of a single character       }
        Width,
        Depth,                         { Depth in bits                    }
        FirstChar,                     { First char in font               }
        LastChar,                      { Last char in font                }
        Background,                    { 0 = clear                        }
        CharSize   : BYTE;             { Size of each character in bytes  }
        Size       : WORD;             { Size of the font in memory       }
      END;
    
    Here is a short description of how each of the fields are used and why they are important:

    The Data Field
    The Data field is simply a pointer into an array in memory that holds the actual font data. It is defined as the ScreenBufferPtr simply because this is an array of bytes. It has no relation to the screen buffer.

    The Height and Width Fields
    The Height and Width fields specify the dimensions of the characters. These fields do not directly specify the amount of memory that the font uses. They specify how many pixels are processed by the text routines and the spacing of the characters.

    The Depth Field
    The Depth field specifies how many bits of color data are used by each pixel in the font. This is variable so that a tradeoff between the size of the font in memory and the quality of the font can be changed. For example, the fonts that are used in the text modes are all one bit fonts. Although these look okay on a high resolution display, they look disapointing at low resolutions. With 16 colors (4 bits), however, anti-aliasing can be done to make a font be smoother and easier on the eyes. With a full 256 colors (8 bits), fonts that have every color in the palette are possible and multicolor characters are an easy accomplishment. Here are two strings, printed in a normal font and in an anti-aliased font:

    Although the details are hard to see at high resolution, when the resolution is decreased by scaling, the difference is obvious:

    Note: This is not simply blurring the font. To anti-alias a font the correct way, you have to take a vector font and find the amount of a pixel that is actually filled by the letter. If only half of the pixel is filled, then the pixel is lit with one half of the intensity. This simulates higher resolution which makes prolonged exposure to the font easier on the eyes.

    The FirstChar and LastChar Fields
    These two fields are used to specify the range of characters that appear in the font file or in memory. Some ASCII codes are not needed in graphics mode (Such as the line drawing characters and the control codes). Because of this, any memory allocated for those would be a waste. If a character out of range gets printed, it is simply ignored.

    The Background Field
    The Background field is set during the execution of a program. It specifies whether or not to use a background color and if so, what color to use. If it is set to zero, then the background is visible behind the text. If it is non-zero, a box is drawn around the text that covers up the background. Here is an example:

    The CharSize Field
    The CharSize field specifies how many bytes are used by each character in a font. This is something that is precalculated when a font is loaded, so it is not stored in a font file. The number of bytes taken up by a character is always an integer number, because each character is padded with blank bits at the end if they are needed. This means that each character starts out at the beginning of a byte which means that spliting bytes to get to a character is not necessary. The formula for the number of bytes taken per character is:

      CharSize := (Width*Height*Depth+7) DIV 8;    { 8 bits per byte }
    
    The Size Field
    The Size field specifies the total size of the memory allocated to the data field of the font. It is simply:

      Size := CharSize*(LastChar-FirstChar+1)
    


    Aquiring a font

    Where are we going to get all of the beautiful fonts for use with these routines? There is two answers to that question... The first is that you can draw them all out. The second is that we can grab the fonts that are already on your system: Hiding in the BIOS. If you have a VGA card (You do if you are using these routines...), then you have three fonts that are built into the ROM on your computer. Their purpose is to define the characters shapes in text mode. They are also used to draw the text in graphics mode when you use the WRITE or WRITELN procedures, which in turn call on the BIOS. All three fonts are single bit fonts, and all are 8 pixels wide. Here are the three fonts and their use:

    • 8x8 - This font was originally used in the text mode on CGA systems. It is still commonly used in low resolution graphics modes. On a VGA, this font is used to achieve 50 line mode.
    • 8x14 - This font was introduced with the EGA graphics boards. It increased the resolution of the text in text mode - back then it was a big thing. Using this font on a VGA monitor, you can achieve 28 line mode.
    • 8x16 - This font is currently the standard used with VGA text modes, again a noticable improvement over 14 point fonts.
    All we have to do is get a pointer to these fonts in memory and copy them into a Font structure. That way, we don't have to draw out all of our fonts for a quick demonstration... Although they don't look as good as 2, 4, or 8 bit fonts, they are functional.

    If you have an interrupt reference, look up Interrupt 10, Service 11. It allows you to get access to the character sets used by the BIOS. You can modify them so that your text mode font is different, and you can also get a pointer to the fonts with subfunction 30h. This is front door to the fonts that come with your computer.

    This interrupt requires a code in BH that specifies which font to get. It then returns the font pointer in ES:BP. Because it trashed the BP register, we have to be careful about accessing variable on the stack. Other than that though, this is a pretty straight-forward procedure.

    The BIOS requires the following codes, passed in BH, to access the font sets:

    • 8x8 = 3
    • 8x14 = 2
    • 8x16 = 6
    Don't even bother to ask me how they came up with these numbers... it is beyond me. With these numbers, we can create a procedure that takes a height and returns the BIOS font in a Font structure. With this Font, we can draw text to the screen!
    PROCEDURE LoadBIOSFont(VAR Font : FontType; Height : BYTE);
    VAR Segm, Offs : WORD;
    BEGIN
      IF Font.Data <> NIL THEN
        FreeFontMem(Font);        { Release previously used font         }
    
      Font.Height     := Height;  { Fill out all of the default settings }
      Font.Width      := 8;
      Font.Depth      := 1;
      Font.CharSize   := (Font.Width*Font.Depth*Font.Height+7) SHR 3;
      Font.FirstChar  := 0;
      Font.LastChar   := 255;
    
      CASE Height OF              { Select the appropriate code.         }
      8  : Height := 3;
      14 : Height := 2;
      16 : Height := 6;
      ELSE EXIT;
      END;
    
      GetFontMem(Font);           { Allocate memory for the font         }
      ASM
        push BP                   { Save Base Pointer                    }
        mov BH, Height
        mov AX, 1130h             { Function 11h, Subfuntion 30h         } 
        int 10h                   { Interrupt 10h                        }
        mov AX, BP
        pop BP
        mov Offs, AX              { Store the pointer                    }
        mov Segm, ES
      END;
      Move(PTR(Segm, Offs)^, Font.Data^, Font.Size); { Copy the data!    }
    END;
    


    A brief tangent on optimization

    Now we have the structures, the font in memory, and the groundwork necessary to display a font. All we need now is the actual routines to display the font on the screen... Which IS the topic of this article.

    The way I see it, there are many ways to do things. For example, when coding a comparison and a loop together, there are two equally correct ways to code it: (This is mildly pseudo code...)

    PROCEDURE ClearOrFillArray(Array : ArrayType; Clear : BOOLEAN);
    VAR I : SuperLongInteger;
    BEGIN
      FOR I := 0 TO 10000000 DO
        IF Clear THEN
          Array[I] := 0
        ELSE
          Array[I] := 1;
    END;
    
    This hypothetical procedure merely goes through an array one element at a time and either sets or clears the element. This could also be coded like this:

    PROCEDURE ClearOrFillArray(Array : ArrayType; Clear : BOOLEAN);
    VAR I : SuperLongInteger;
    BEGIN
        IF Clear THEN
          FOR I := 0 TO 10000000 DO
            Array[I] := 0
        ELSE
          FOR I := 0 TO 10000000 DO
            Array[I] := 1;
    END;
    
    They are both correct, but one is a heck of a lot faster. (No, I do not want to use FillChar! :) This little tangent serves to illustrate why we are going to break our font routines into separate routines for each font depth. For each pixel, we could do something like this in our inner loop:

      Data := Data SHL Font.Depth;
      BitsLeft := BitsLeft - Font.Depth;
      Color := Data MOD (1 SHL Font.Depth);
    
    Or we could do something like this way outside of our loops:

    CASE Font.Depth OF
      1 : WriteG1(Font, X, Y, Text, Color);
      2 : WriteG2(Font, X, Y, Text, Color);
      4 : WriteG4(Font, X, Y, Text, Color);
      8 : WriteG8(Font, X, Y, Text);
    
    Gee, which would be faster? :) Because of this, I have chosen to write seperate routines for each of the color depths, which is actually pretty easy. Lets start with one bit color:


    1 Bit per pixel fonts

    To draw a font to the screen, we have to have three nested loops operating in each other. Here is the basic idea of what we are trying to do: (Again, pseudo code, this time it's slightly weirder...)

    #1.  FOR EachCharacterInString 
    #2.    FOR EachLineInCharacter
    #3.      FOR EachPixelInLine
    #4.        IF Pixel THEN SetPixel(Pixel);
    
    Of course, there is a lot of details that are needed to fill in between the steps. Here is what has to happen:

  • In between steps #1 and #2, we need to verify that the character selected is in the range of characters that the font describes.
  • After that, we need to generate a pointer to the data for the particular character we are drawing.

  • In between steps #3 and #4, we have to keep track of the bits that we are using for the pixel data. We will be working with partial bytes (bits) so we have to be careful.
  • To check to see if we should draw a pixel or not, we simply AND the it we want to check with 128 (to get the high bit), if it is zero, then we draw the background, if not we draw the font.

    With all of these goals in mind, I created the following procedure. It fulfulls all of what we have been looking for (It is just for 1 bit fonts though...), but is not as fast as we would like. In future articles, we will optimize this and add support for other color depths... Anyhoo:

    PROCEDURE WriteG1(Font : FontType; X, Y : INTEGER; Text : STRING; Color : BYTE);
    VAR
      C       : BYTE;       { ASCII code of current character     }
      CurChar : INTEGER;    { Index into Text                     }
      FontPtr : WORD;       { Index into the font data...         }
      Xc, Yc  : INTEGER;    { X & Y Counts                        }
      FontData,             { The bit data                        }
      BitsLeft  : BYTE;     { The number of bits left in FontData }
    BEGIN
      FOR CurChar := 1 TO LENGTH(Text) DO       { Step over each char in string }
      BEGIN
        C := ORD(Text[CurChar]);                { Grab the Ascii code of char   }
        IF (C <= Font.LastChar) AND (C >= Font.FirstChar) THEN  { Range check   }
        BEGIN
          FontPtr := (C-Font.FirstChar)*Font.CharSize;   { Generate ptr to data }
          BitsLeft := 0;
          FOR Yc := Y TO Y+Font.Height-1 DO              { Step over each pixel }
          BEGIN                                          { in the Y direction   }
            FOR Xc := X TO X+Font.Width-1 DO             { Step over each pixel }
            BEGIN                                        { In the X direction   }
              IF BitsLeft = 0 THEN
              BEGIN
                FontData := Font.Data^[FontPtr];         { Refill FontData when }
                INC(FontPtr);                            { it gets empty        }
                BitsLeft := 8;
              END;
              IF (FontData AND 128) = 128 THEN           { Draw a pixel?        }
                SetPixel(Xc, Yc, Color)
              ELSE IF (Font.Background > 0) THEN         { Draw the background? }
                SetPixel(Xc, Yc, Font.Background);
    
              FontData := FontData SHL 1;                { Next bit please!     }
              DEC(BitsLeft);
            END;
          END;
        END;
        INC(X, Font.Width);                              { Step to the right for }
      END;                                               { the next character    }
    END;
    
    Now that we have a working text drawing procedure, we can go on to make our stunning demo of the power of the Font!! {Insert sound effect here}


    Example Program

    This stuningly clever (well, maybe not...) example shows how to use the Background property and how to select fonts. The other main purpose of the example is to coalate all of the source code up 'til now into one place... This also shows how to animate with text...

    -----------------] Example Starts here [-----------------
    PROGRAM Fonts;
    USES CRT,       { Need the Keypressed function }
         GraphPro;
    
    TYPE
      FontType = RECORD
        Data : ScreenBufferPtr;        { Pointer to an array of bytes     }
        Height,                        { Size of a single character       }
        Width,
        Depth,                         { Depth in bits                    }
        FirstChar,                     { First char in font               }
        LastChar,                      { Last char in font                }
        CharSize,                      { Size of each character in bytes  }
        Background : BYTE;             { 0 = clear                        }
        Size       : WORD;             { Size of the font in memory       }
      END;
    
    PROCEDURE GetFontMem(VAR Font : FontType);
    BEGIN
      Font.Size := (Font.Width*Font.Depth*Font.Height*
                   (Font.LastChar-Font.FirstChar+1) + 7) DIV 8;
      GetMem(Font.Data, Font.Size);
    END;
    
    PROCEDURE FreeFontMem(VAR Font : FontType);
    BEGIN
     FreeMem(Font.Data, Font.Size);
     Font.Data := NIL;
    END;
    
    PROCEDURE LoadBIOSFont(VAR Font : FontType; Height : BYTE);
    VAR Segm, Offs : WORD;
    BEGIN
      IF Font.Data <> NIL THEN
        FreeFontMem(Font);        { Release previously used font         }
    
      Font.Height     := Height;  { Fill out all of the default settings }
      Font.Width      := 8;
      Font.Depth      := 1;
      Font.CharSize   := (Font.Width*Font.Depth*Font.Height+7) SHR 3;
      Font.FirstChar  := 0;
      Font.LastChar   := 255;
    
      CASE Height OF              { Select the appropriate code.         }
      8  : Height := 3;
      14 : Height := 2;
      16 : Height := 6;
      ELSE EXIT;
      END;
    
      GetFontMem(Font);           { Allocate memory for the font         }
      ASM
        push BP                   { Save Base Pointer                    }
        mov BH, Height
        mov AX, 1130h             { Function 11h, Subfuntion 30h         } 
        int 10h                   { Interrupt 10h                        }
        mov AX, BP
        pop BP
        mov Offs, AX              { Store the pointer                    }
        mov Segm, ES
      END;
      Move(PTR(Segm, Offs)^, Font.Data^, Font.Size); { Copy the data!    }
    END;
    
    PROCEDURE WriteG1(Font : FontType; X, Y : INTEGER; Text : STRING; Color : BYTE);
    VAR
      C       : BYTE;       { ASCII code of current character     }
      CurChar : INTEGER;    { Index into Text                     }
      FontPtr : WORD;       { Index into the font data...         }
      Xc, Yc  : INTEGER;    { X & Y Counts                        }
      FontData,             { The bit data                        }
      BitsLeft  : BYTE;     { The number of bits left in FontData }
    BEGIN
      FOR CurChar := 1 TO LENGTH(Text) DO       { Step over each char in string }
      BEGIN
        C := ORD(Text[CurChar]);                { Grab the Ascii code of char   }
        IF (C <= Font.LastChar) AND (C >= Font.FirstChar) THEN  { Range check   }
        BEGIN
          FontPtr := (C-Font.FirstChar)*Font.CharSize;   { Generate ptr to data }
          BitsLeft := 0;
          FOR Yc := Y TO Y+Font.Height-1 DO              { Step over each pixel }
          BEGIN                                          { in the Y direction   }
            FOR Xc := X TO X+Font.Width-1 DO             { Step over each pixel }
            BEGIN                                        { In the X direction   }
              IF BitsLeft = 0 THEN
              BEGIN
                FontData := Font.Data^[FontPtr];         { Refill FontData when }
                INC(FontPtr);                            { it gets empty        }
                BitsLeft := 8;
              END;
              IF (FontData AND 128) = 128 THEN           { Draw a pixel?        }
                SetPixel(Xc, Yc, Color)
              ELSE IF (Font.Background > 0) THEN         { Draw the background? }
                SetPixel(Xc, Yc, Font.Background);
    
              FontData := FontData SHL 1;                { Next bit please!     }
              DEC(BitsLeft);
            END;
          END;
        END;
        INC(X, Font.Width);                              { Step to the right for }
      END;                                               { the next character    }
    END;
    
    
    VAR
      Font : FontType;
      X, Y, DX, DY : INTEGER;
      Text : STRING;
    BEGIN
      InitGraph;
      ClrScr(1);
      LoadBIOSFont(Font, 8);
      WRITEG1(Font, 0, 0, 'This is size 8', 14);
      LoadBIOSFont(Font, 14);
      WRITEG1(Font, 0, 8, 'This is the BIOS''s 14 point font', 14);
    
      LoadBIOSFont(Font, 16);
      Font.Background := 14;
      WRITEG1(Font, 0, 22, 'This is a 16 point font inverted', 1);
    
      Font.Background := 0;
      WriteG1(Font, 0, 50, 'Press  to continue', 15);
      READLN;
    
      Font.Background := 13;
      ClrScr(Font.Background);
      X := 0; Y := 0; DX := 1; DY := 1;
      Text := ' Press any key to continue! ';
      WHILE Not Keypressed Do
      BEGIN
        X := X + DX;
        Y := Y + DY;
        IF (X = 0) OR (X = (319-8*LENGTH(Text))) THEN DX := -DX; { Bounce } 
        IF (Y = 0) OR (Y = (199-Font.Height)) THEN DY := -DY;    { Bounce }
        WriteG1(Font, X, Y, Text, 14);
      END;
      CloseGraph;
    END.
    
    -----------------] Example Ends here [-----------------


  • Created by Chris Lattner