Variable-Width Font System
Unit: VGAFont
A sprite sheet-based variable-width font renderer for high-quality text display in VGA Mode 13h.
Table of Contents
Overview
VGAFONT.PAS provides a professional variable-width font rendering system for DOS games. Fonts are defined as sprite sheets (PCX images) with XML metadata describing character positions and widths.
Key Features
Variable-width characters - Proportional fonts for better readability
Sprite sheet-based - Use existing PCX image format
XML metadata - Character positions and widths defined externally
Efficient rendering - Uses
PutImageRectfor fast character blittingFlexible character set - Support ASCII 0-127
Undefined character handling - Skip characters not in font (width=0)
Use Cases
Game dialogue - Variable-width text looks more professional
Menu systems - Better visual appearance than monospace
HUD elements - Score displays, labels, etc.
Cutscenes - Subtitle text rendering
Credits screens - Large decorative fonts
Comparison to VGAPRINT
Feature |
VGAPRINT.PAS |
VGAFONT.PAS |
|---|---|---|
Font type |
Fixed-width (8×8) |
Variable-width |
Font source |
Embedded in code |
External PCX + XML |
Character sizes |
All 8×8 |
Custom per character |
Visual quality |
Monospace (retro) |
Proportional (modern) |
Setup |
None (built-in) |
Load font file |
Memory usage |
~1KB (embedded) |
~Depends on font size |
Performance |
Fast (direct pixel write) |
Fast (PutImageRect) |
Use case |
Debug text, FPS counters |
Game UI, dialogue |
When to use each:
VGAPRINT: Quick debug output, FPS counters, simple overlays
VGAFONT: Professional game UI, dialogue, menus, titles
Font Format
Sprite Sheet Layout
Fonts are stored as PCX images with characters arranged in a sprite sheet:
Example: 32-pixel tall font
┌──────────────────────────────────────┐
│ A B C D E F G H I J K ... │ Row 1 (letters)
├──────────────────────────────────────┤
│ 0 1 2 3 4 5 6 7 8 9 ! ... │ Row 2 (numbers/symbols)
├──────────────────────────────────────┤
│ ... │ Additional rows as needed
└──────────────────────────────────────┘
Each character:
- Fixed height (e.g., 32 pixels)
- Fixed right padding (e.g., 1 pixel)
- Variable width (e.g., 'i' = 8px, 'W' = 24px)
- Position defined in XML
Dimensions:
Height: Fixed for all characters (defined in XML)
Padding: Fixed for all characters (defined in XML)
Width: Variable per character (defined in XML)
Sheet size: Depends on character count and sizes
Typical: 512×64 pixels for full ASCII set
XML Metadata
Character positions and widths are defined in an XML file:
<?xml version="1.0" encoding="UTF-8"?>
<font height="32" padding="1" image="FONT.PCX" replace-color="15">
<!-- Uppercase letters -->
<char code="65" x="0" y="0" width="18" /> <!-- A -->
<char code="66" x="18" y="0" width="16" /> <!-- B -->
<char code="67" x="34" y="0" width="17" /> <!-- C -->
<!-- Lowercase letters -->
<char code="97" x="0" y="32" width="15" /> <!-- a -->
<char code="98" x="15" y="32" width="15" /> <!-- b -->
<!-- Numbers -->
<char code="48" x="200" y="0" width="14" /> <!-- 0 -->
<char code="49" x="214" y="0" width="10" /> <!-- 1 -->
<!-- Special characters -->
<char code="32" x="0" y="0" width="8" /> <!-- Space (no glyph) -->
<char code="33" x="300" y="0" width="8" /> <!-- ! -->
<!-- Characters not defined have width=0 and are skipped -->
</font>
Font Attributes:
height(required): Font height in pixels (applies to all characters)padding(optional): Horizontal spacing between characters (default: 0)image(required): Path to PCX sprite sheet file (relative to XML file)replace-color(optional): Color index to replace for PrintFontTextColored (default: 0 = disabled)
Character Attributes:
code: ASCII character code (0-127)x,y: Position in sprite sheet (pixels)width: Character width in pixels
Data Structures
TFont Type
unit VGAFont;
interface
uses VGA, GenTypes;
const
MaxChars = 128; { ASCII 0-127 }
type
TCharInfo = record
X: Integer; { X position in sprite sheet }
Y: Integer; { Y position in sprite sheet }
Width: Byte; { Character width in pixels }
Defined: Boolean; { True if character exists in font }
end;
TFont = record
Image: TImage; { Font sprite sheet }
Height: Byte; { Height of all characters }
Padding: Byte; { Right padding of all characters }
ReplaceColor: Byte; { Color to replace (0 = no replacement) }
Chars: array[0..MaxChars-1] of TCharInfo; { Character metadata }
Loaded: Boolean; { True if font successfully loaded }
end;
PFont = ^TFont;
Memory layout:
TFont structure:
- Image: TImage (Width, Height, Data pointer)
- Height: 1 byte
- Chars: 128 × TCharInfo (128 × 7 bytes = 896 bytes)
- Loaded: 1 byte
Total: ~900 bytes + image data
API Functions
LoadFont
function LoadFont(const XMLFile: string; var Font: TFont): Boolean;
Loads a font from PCX image and XML metadata files.
Parameters:
XMLFile- Path to XML metadata (e.g., ‘FONTS\MAIN.XML’). The XML file should contain animageattribute pointing to the PCX sprite sheet.Font- TFont structure to populate
Returns:
Trueif font loaded successfullyFalseon error (useGetLoadFontErrorfor details)
Example:
var
GameFont: TFont;
begin
if not LoadFont('FONTS\MAIN.XML', GameFont) then
begin
WriteLn('Error loading font: ', GetLoadFontError);
Halt(1);
end;
{ Font ready to use }
end;
Loading process:
Load PCX image into
Font.ImageParse XML file using MINIXML.PAS
Extract
heightattribute from<font>elementParse each
<char>element:Read
code,x,y,widthattributesStore in
Font.Chars[code]Set
Defined := True
Initialize undefined characters:
Set
Width := 0Set
Defined := False
Set
Font.Loaded := True
Error conditions:
PCX file not found or invalid
XML file not found or invalid
XML parsing errors (malformed XML)
Missing required attributes
Invalid attribute values (negative, out of range)
GetLoadFontError
function GetLoadFontError: string;
Returns the last error message from LoadFont.
Returns: Error description string
Example:
if not LoadFont('FONT.XML', Font) then
begin
WriteLn('Font loading failed:');
WriteLn(GetLoadFontError);
end;
Typical error messages:
"PCX file not found: FONT.PCX""XML file could not be loaded: FONT.XML""Invalid XML format""Missing height attribute in <font> element""Invalid character code: 200 (must be 0-127)""Missing required attribute: width"
FreeFont
procedure FreeFont(var Font: TFont);
Frees all resources associated with a font.
Parameters:
Font- TFont structure to free
Example:
var
GameFont: TFont;
begin
LoadFont('FONT.XML', GameFont);
{ Use font... }
FreeFont(GameFont); { Free resources before exit }
end;
Cleanup actions:
Free sprite sheet image data:
FreeImage(Font.Image)Clear character metadata
Set
Font.Loaded := False
CRITICAL:
Always call before program exit
Prevents memory leaks
Safe to call on unloaded fonts (no-op)
PrintFontText
procedure PrintFontText(
X, Y: Integer;
const Text: string;
Align: TAlign;
var Font: TFont;
FrameBuffer: PFrameBuffer
);
Renders text using a loaded font with horizontal alignment.
Parameters:
X, Y- Starting position in framebuffer (pixels). X is adjusted based on alignment.Text- String to render (ASCII 0-127 only)Align- Horizontal alignment (from GENTYPES.PAS):Align_Left(0) - X is left edge of textAlign_Center(1) - X is center of textAlign_Right(2) - X is right edge of text
Font- Loaded TFont structureFrameBuffer- Target framebuffer to draw into
Example:
var
GameFont: TFont;
ScreenBuffer: PFrameBuffer;
begin
LoadFont('FONT.XML', GameFont);
ScreenBuffer := GetScreenBuffer;
{ Draw text with different alignments }
PrintFontText(10, 50, 'Left aligned', Align_Left, GameFont, ScreenBuffer);
PrintFontText(160, 90, 'Centered', Align_Center, GameFont, ScreenBuffer);
PrintFontText(310, 130, 'Right aligned', Align_Right, GameFont, ScreenBuffer);
end;
Rendering process:
Initialize cursor X position
For each character in text:
Get ASCII code
Look up character info in
Font.Chars[code]If
Defined = FalseorWidth = 0, skip characterOtherwise, call
PutImageRectto draw character:PutImageRect( Font.Image, { Source sprite sheet } CharRect, { Source rectangle } CursorX, Y, { Destination position } True, { Transparent } FrameBuffer { Target buffer } );
Advance cursor:
CursorX := CursorX + Width
Return final cursor position (for chaining)
Character handling:
{ Example character rendering logic }
for i := 1 to Length(Text) do
begin
CharCode := Ord(Text[i]);
if CharCode > 127 then Continue; { Skip extended ASCII }
CharInfo := Font.Chars[CharCode];
if (not CharInfo.Defined) or (CharInfo.Width = 0) then Continue;
{ Draw character }
SourceRect.X := CharInfo.X;
SourceRect.Y := CharInfo.Y;
SourceRect.Width := CharInfo.Width;
SourceRect.Height := Font.Height;
PutImageRect(
Font.Image,
SourceRect,
CursorX, Y,
True, { Transparent (color 0 = transparent) }
FrameBuffer
);
CursorX := CursorX + CharInfo.Width + Font.Padding;
end;
Transparency:
Uses
PutImageRectwithTransparent := TrueColor 0 (black) is treated as transparent
Font sprite sheets should use color 0 for background
PrintFontTextColored
procedure PrintFontTextColored(
X, Y: Integer;
const Text: string;
Color: Byte;
Align: TAlign;
var Font: TFont;
FrameBuffer: PFrameBuffer
);
Renders colored text using a loaded font with horizontal alignment and runtime color replacement.
Parameters:
X, Y- Starting position in framebuffer (pixels). X is adjusted based on alignment.Text- String to render (ASCII 0-127 only)Color- Color to use for replacement (0-255)Align- Horizontal alignment (Align_Left, Align_Center, Align_Right)Font- Loaded TFont structure (must have ReplaceColor > 0)FrameBuffer- Target framebuffer to draw into
How it works:
If
Font.ReplaceColoris 0, callsPrintFontTextinstead (no color replacement)If
Font.ReplaceColor> 0, renders text pixel-by-pixel:Color 0 (transparent) is skipped
Pixels matching
Font.ReplaceColorare replaced withColorAll other pixels are drawn as-is
Example:
var
GameFont: TFont;
ScreenBuffer: PFrameBuffer;
begin
{ Font XML has replace-color="15" attribute }
LoadFont('FONT.XML', GameFont);
ScreenBuffer := GetScreenBuffer;
{ Draw text in different colors }
PrintFontTextColored(160, 50, 'Red', 12, Align_Center, GameFont, ScreenBuffer);
PrintFontTextColored(160, 90, 'Green', 10, Align_Center, GameFont, ScreenBuffer);
PrintFontTextColored(160, 130, 'Blue', 9, Align_Center, GameFont, ScreenBuffer);
end;
Performance note:
Color replacement uses pixel-by-pixel rendering (slower than PrintFontText)
Only use when you need dynamic text coloring
Set
ReplaceColorto 0 in XML if color replacement is not needed
GetTextWidth
function GetTextWidth(const Text: string; var Font: TFont): Integer;
Calculates the pixel width of a text string when rendered with the font.
Parameters:
Text- String to measure (ASCII 0-127 only)Font- Loaded TFont structure
Returns:
Total width in pixels (including padding between characters)
Example:
var
Width: Integer;
begin
Width := GetTextWidth('Hello World', GameFont);
WriteLn('Text width: ', Width, ' pixels');
end;
Use GetTextWidth with alignment:
{ Manual centering (alternative to Align_Center parameter) }
var
TextWidth, X: Integer;
begin
TextWidth := GetTextWidth('Hello', GameFont);
X := (320 - TextWidth) div 2; { Calculate center X }
PrintFontText(X, 100, 'Hello', Align_Left, GameFont, BackBuffer);
{ Or simply use Align_Center: }
PrintFontText(160, 100, 'Hello', Align_Center, GameFont, BackBuffer);
end;
XML Schema
Font Element
<font height="HEIGHT" padding="PADDING" image="IMAGE.PCX" replace-color="COLOR">
<!-- Character definitions -->
</font>
Attributes:
height(required): Integer, 1-255Height of all characters in pixels
All characters in font have same height
padding(optional): Integer, 0-255 (default: 0)Horizontal spacing between characters in pixels
Applied after each character (right padding)
NOT for vertical spacing (use Font.Height for line spacing)
image(required): StringPath to PCX sprite sheet file (relative to XML file)
Example: “FONT.PCX” or “../IMAGES/FONT.PCX”
replace-color(optional): Integer, 0-255 (default: 0)Color index to replace when using PrintFontTextColored
Set to 0 to disable color replacement
Example: Use 15 (white) as placeholder color in font image
Child elements:
One or more
<char>elements
Character Element
<char code="CODE" x="X" y="Y" width="WIDTH" />
Attributes:
code(required): Integer, 0-127ASCII character code
Must be unique (no duplicate codes)
x(required): Integer, 0-65535X position in sprite sheet (pixels)
Left edge of character glyph
y(required): Integer, 0-65535Y position in sprite sheet (pixels)
Top edge of character glyph
width(required): Integer, 0-255Character width in pixels
Can be 0 for space characters
Example:
<char code="65" x="0" y="0" width="18" /> <!-- 'A' -->
Complete Example
<?xml version="1.0" encoding="UTF-8"?>
<font height="32" padding="1" image="FONT.PCX" replace-color="15">
<!-- Uppercase A-Z -->
<char code="65" x="0" y="0" width="18" /> <!-- A -->
<char code="66" x="18" y="0" width="16" /> <!-- B -->
<char code="67" x="34" y="0" width="17" /> <!-- C -->
<!-- ... Z -->
<!-- Lowercase a-z -->
<char code="97" x="0" y="32" width="15" /> <!-- a -->
<char code="98" x="15" y="32" width="15" /> <!-- b -->
<!-- ... z -->
<!-- Numbers 0-9 -->
<char code="48" x="200" y="0" width="14" /> <!-- 0 -->
<char code="49" x="214" y="0" width="10" /> <!-- 1 -->
<char code="50" x="224" y="0" width="14" /> <!-- 2 -->
<!-- ... 9 -->
<!-- Special characters -->
<char code="32" x="0" y="0" width="8" /> <!-- Space -->
<char code="33" x="300" y="0" width="8" /> <!-- ! -->
<char code="46" x="308" y="0" width="6" /> <!-- . -->
<char code="58" x="314" y="0" width="6" /> <!-- : -->
<!-- Extended special characters -->
<char code="44" x="320" y="0" width="6" /> <!-- , -->
<char code="63" x="326" y="0" width="14" /> <!-- ? -->
</font>
Usage Examples
Basic Text Rendering
program FontTest;
uses VGA, VGAFont, PCX;
var
GameFont: TFont;
ScreenBuffer: PFrameBuffer;
Running: Boolean;
begin
InitVGA;
ScreenBuffer := GetScreenBuffer;
{ Load font }
if not LoadFont('FONTS\MAIN.XML', GameFont) then
begin
DoneVGA;
WriteLn('Error: ', GetLoadFontError);
Halt(1);
end;
{ Clear screen }
ClearFrameBuffer(BackBuffer);
{ Draw text with alignment }
PrintFontText(10, 10, 'Hello, World!', Align_Left, GameFont, ScreenBuffer);
PrintFontText(160, 50, 'Centered!', Align_Center, GameFont, ScreenBuffer);
PrintFontText(310, 90, 'Right', Align_Right, GameFont, ScreenBuffer);
ReadLn;
{ Cleanup }
FreeFont(GameFont);
DoneVGA;
end.
Multiple Fonts
var
TitleFont: TFont; { Large decorative font }
NormalFont: TFont; { Regular game text }
SmallFont: TFont; { Small UI text }
begin
{ Load different fonts for different purposes }
LoadFont('FONTS\TITLE.XML', TitleFont);
LoadFont('FONTS\NORMAL.XML', NormalFont);
LoadFont('FONTS\SMALL.XML', SmallFont);
{ Use appropriate font for each element }
PrintFontText(160, 50, 'XICLONE', Align_Center, TitleFont, BackBuffer);
PrintFontText(160, 100, 'Press ENTER to start', Align_Center, NormalFont, BackBuffer);
PrintFontText(10, 190, 'v1.0', Align_Left, SmallFont, BackBuffer);
{ Cleanup all fonts }
FreeFont(TitleFont);
FreeFont(NormalFont);
FreeFont(SmallFont);
end;
Text Wrapping (Advanced)
procedure PrintWrappedText(
X, Y, MaxWidth: Integer;
const Text: string;
var Font: TFont;
FrameBuffer: PFrameBuffer
);
var
Words: array[0..99] of string;
WordCount, i: Integer;
Line: string;
LineWidth: Integer;
CursorY: Integer;
begin
{ Split text into words }
WordCount := SplitWords(Text, Words);
CursorY := Y;
Line := '';
for i := 0 to WordCount - 1 do
begin
{ Try adding word to current line }
if Line = '' then
TestLine := Words[i]
else
TestLine := Line + ' ' + Words[i];
LineWidth := GetTextWidth(TestLine, Font);
if LineWidth > MaxWidth then
begin
{ Line too long - print current line and start new one }
if Line <> '' then
begin
PrintFontText(X, CursorY, Line, Align_Left, Font, FrameBuffer);
CursorY := CursorY + Font.Height + 4; { Line spacing }
end;
Line := Words[i];
end
else
Line := TestLine;
end;
{ Print remaining line }
if Line <> '' then
PrintFontText(X, CursorY, Line, Align_Left, Font, FrameBuffer);
end;
Font Creation Workflow
Step 1: Design Font in GrafX2
Create new image:
Width: 512 pixels (or as needed)
Height: 64-128 pixels (depends on character count)
Colors: 256 (indexed palette)
Draw characters:
Fixed height per character (e.g., 32 pixels)
Variable width (proportional spacing)
Use color 0 (black) for transparent background
Leave padding between characters for clarity
Layout example:
Row 1: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Row 2: a b c d e f g h i j k l m n o p q r s t u v w x y z Row 3: 0 1 2 3 4 5 6 7 8 9 ! ? . , : ; ' "
Save as PCX:
File → Export → PCX format (8-bit indexed color)
Save to
DATA\FONTS\MYFONT.PCX
Step 2: Generate XML Metadata
Create MYFONT.XML:
<?xml version="1.0" encoding="US-ASCII"?>
<font height="32" padding="1" image="MYFONT.PCX" replace-color="15">
<char code="65" x="0" y="0" width="18" /> <!-- A -->
<!-- Add all characters... -->
</font>
Step 3: Test Font
program TestFont;
uses VGA, VGAFont;
var
Font: TFont;
Buffer: PFrameBuffer;
begin
InitVGA;
Buffer := CreateFrameBuffer;
if not LoadFont('FONTS\MYFONT.XML', Font) then
begin
DoneVGA;
WriteLn('Error: ', GetLoadFontError);
Halt(1);
end;
ClearFrameBuffer(Buffer);
{ Test all printable ASCII with different alignments }
PrintFontText(10, 10, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', Align_Left, Font, Buffer);
PrintFontText(160, 50, 'abcdefghijklmnopqrstuvwxyz', Align_Center, Font, Buffer);
PrintFontText(310, 90, '0123456789 !?.,:;', Align_Right, Font, Buffer);
RenderFrameBuffer(Buffer);
ReadLn;
FreeFont(Font);
FreeFrameBuffer(Buffer);
DoneVGA;
end.
Performance Considerations
Rendering Speed
Character rendering cost:
Single character (16×32 pixels):
- PutImageRect call: ~0.5ms on 286
- Memory copied: 512 bytes
Text line "SCORE: 12345" (12 characters):
- Total time: ~6ms on 286
- 166 FPS if only drawing text
Typical HUD (5 text lines):
- Total time: ~30ms on 286
- Still achieves 30+ FPS
Optimization tips:
Cache static text - Don’t re-render every frame
Use dirty rectangles - Only redraw changed text
Pre-render common strings - “SCORE:”, “LEVEL:”, etc.
Avoid text in inner loops - Render UI once per frame max
Memory Usage
Per font:
TFont structure: ~900 bytes
Sprite sheet (512×64, 256 colors): 32KB
Total per font: ~33KB
Multiple fonts (Title + Normal + Small): ~100KB
Still plenty of room in 640KB conventional memory
Comparison:
VGAPRINT (embedded): 1KB
VGAFONT (typical): 33KB per font
Trade-off: Quality vs. memory
XML Parsing Performance
Loading time:
Parse XML (128 characters): ~50ms on 286
Load PCX image: ~100ms on 286
Total load time: ~150ms (done once at startup)
Acceptable for:
- Game initialization
- Level loading
- Not acceptable for: Every frame rendering
Best practice:
Load fonts at startup
Keep loaded for entire game session
Don’t reload fonts during gameplay
Error Handling
Error Categories
File errors:
PCX file not found
XML file not found
File read errors
XML errors:
Invalid XML format (malformed)
Missing root element
Missing required attributes
Invalid attribute values
Data errors:
Character code out of range (not 0-127)
Negative dimensions
Duplicate character codes
Example Error Messages
"PCX file not found: FONTS\MAIN.PCX"
"XML file could not be loaded: FONTS\MAIN.XML"
"Invalid XML format"
"Missing root <font> element"
"Missing height attribute in <font> element"
"Invalid height: -5 (must be 1-255)"
"Missing required attribute: width"
"Invalid character code: 200 (must be 0-127)"
"Invalid width: -10 (must be 0-255)"
"Duplicate character code: 65"
Conclusion
VGAFONT.PAS provides a professional variable-width font system that:
✅ Advantages:
Better visual appearance than monospace fonts
Flexible character sizes (proportional spacing)
External font definitions (easy to modify)
Efficient rendering (PutImageRect)
Multiple fonts support
⚠️ Considerations:
Requires external PCX + XML files
Higher memory usage than VGAPRINT
Slower loading (XML parsing)
More complex setup
Best for:
Game UI, menus, dialogues
Professional-looking text
Games with high production values
Use VGAPRINT for:
Debug output, FPS counters
Quick prototypes
Minimal memory footprint