# 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](#overview)
- [Comparison to VGAPRINT](#comparison-to-vgaprint)
- [Font Format](#font-format)
- [Data Structures](#data-structures)
- [API Functions](#api-functions)
- [XML Schema](#xml-schema)
- [Usage Examples](#usage-examples)
- [Font Creation Workflow](#font-creation-workflow)
- [Performance Considerations](#performance-considerations)
- [Implementation Details](#implementation-details)
- [Error Handling](#error-handling)
---
## 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 `PutImageRect` for fast character blitting
- **Flexible 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
```
**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
```pascal
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
```pascal
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 an `image` attribute pointing to the PCX sprite sheet.
- `Font` - TFont structure to populate
**Returns:**
- `True` if font loaded successfully
- `False` on error (use `GetLoadFontError` for details)
**Example:**
```pascal
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:**
1. Load PCX image into `Font.Image`
2. Parse XML file using MINIXML.PAS
3. Extract `height` attribute from `` element
4. Parse each `` element:
- Read `code`, `x`, `y`, `width` attributes
- Store in `Font.Chars[code]`
- Set `Defined := True`
5. Initialize undefined characters:
- Set `Width := 0`
- Set `Defined := False`
6. 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
```pascal
function GetLoadFontError: string;
```
Returns the last error message from `LoadFont`.
**Returns:** Error description string
**Example:**
```pascal
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 element"`
- `"Invalid character code: 200 (must be 0-127)"`
- `"Missing required attribute: width"`
---
### FreeFont
```pascal
procedure FreeFont(var Font: TFont);
```
Frees all resources associated with a font.
**Parameters:**
- `Font` - TFont structure to free
**Example:**
```pascal
var
GameFont: TFont;
begin
LoadFont('FONT.XML', GameFont);
{ Use font... }
FreeFont(GameFont); { Free resources before exit }
end;
```
**Cleanup actions:**
1. Free sprite sheet image data: `FreeImage(Font.Image)`
2. Clear character metadata
3. Set `Font.Loaded := False`
**CRITICAL:**
- Always call before program exit
- Prevents memory leaks
- Safe to call on unloaded fonts (no-op)
---
### PrintFontText
```pascal
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 text
- `Align_Center` (1) - X is center of text
- `Align_Right` (2) - X is right edge of text
- `Font` - Loaded TFont structure
- `FrameBuffer` - Target framebuffer to draw into
**Example:**
```pascal
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:**
1. Initialize cursor X position
2. For each character in text:
- Get ASCII code
- Look up character info in `Font.Chars[code]`
- If `Defined = False` or `Width = 0`, skip character
- Otherwise, call `PutImageRect` to draw character:
```pascal
PutImageRect(
Font.Image, { Source sprite sheet }
CharRect, { Source rectangle }
CursorX, Y, { Destination position }
True, { Transparent }
FrameBuffer { Target buffer }
);
```
- Advance cursor: `CursorX := CursorX + Width`
3. Return final cursor position (for chaining)
**Character handling:**
```pascal
{ 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 `PutImageRect` with `Transparent := True`
- Color 0 (black) is treated as transparent
- Font sprite sheets should use color 0 for background
---
### PrintFontTextColored
```pascal
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:**
1. If `Font.ReplaceColor` is 0, calls `PrintFontText` instead (no color replacement)
2. If `Font.ReplaceColor` > 0, renders text pixel-by-pixel:
- Color 0 (transparent) is skipped
- Pixels matching `Font.ReplaceColor` are replaced with `Color`
- All other pixels are drawn as-is
**Example:**
```pascal
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 `ReplaceColor` to 0 in XML if color replacement is not needed
---
### GetTextWidth
```pascal
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:**
```pascal
var
Width: Integer;
begin
Width := GetTextWidth('Hello World', GameFont);
WriteLn('Text width: ', Width, ' pixels');
end;
```
**Use GetTextWidth with alignment:**
```pascal
{ 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
```xml
```
**Attributes:**
- `height` (required): Integer, 1-255
- Height 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): String
- Path 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 `` elements
---
### Character Element
```xml
```
**Attributes:**
- `code` (required): Integer, 0-127
- ASCII character code
- Must be unique (no duplicate codes)
- `x` (required): Integer, 0-65535
- X position in sprite sheet (pixels)
- Left edge of character glyph
- `y` (required): Integer, 0-65535
- Y position in sprite sheet (pixels)
- Top edge of character glyph
- `width` (required): Integer, 0-255
- Character width in pixels
- Can be 0 for space characters
**Example:**
```xml
```
---
### Complete Example
```xml
```
---
## Usage Examples
### Basic Text Rendering
```pascal
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
```pascal
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)
```pascal
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
1. **Create new image:**
- Width: 512 pixels (or as needed)
- Height: 64-128 pixels (depends on character count)
- Colors: 256 (indexed palette)
2. **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
3. **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 ! ? . , : ; ' "
```
4. **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
```
---
### Step 3: Test Font
```pascal
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:**
1. **Cache static text** - Don't re-render every frame
2. **Use dirty rectangles** - Only redraw changed text
3. **Pre-render common strings** - "SCORE:", "LEVEL:", etc.
4. **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 element"
"Missing height attribute in 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