BGI Stroked Font
A BGI Stroked Font is a vector font format created by Borland to work with their 1987 graphics API, Borland Graphics Interface. Since the format stores vector fonts, it can easily be scaled in size with little loss of fidelity. However, because the format only supports lines and not curves, scaling to larger sizes will make the characters look jagged. The font files typically have a *.CHR file extension.
After 1987, Borland's C and Pascal programming languages shipped with 10 different BGI Stroked Fonts: Bold, Euro, Goth, Lcom, Litt, Sans, Scri, Simp, Trip, and Tscr. Due to the popularity of Borland's programming languages, and their graphic API, BGI Stroked Fonts were used in hundreds of DOS video games and software.
The designers of the file format unfortunately used a file signature starting with "PK" which would later be used by PK Zip, so BGI Stroked Font files, despite having an extension of CHR, might be misidentified as zip archives if the identifier doesn't take into account the full PK Zip signature.
Format
Header
The header includes the font's name, version number, and description.
Data Type | Name | Description |
---|---|---|
char[2] | Signature | Must be 0x50 0x4B ("PK"). Probably initials of Borland founder Philippe Kahn. |
char[...] | Description | User text. Borland's official fonts start with "BGI Stroked Font" with two 0x08 characters before to hide font file signature in case of use DOS "type" command on font file, then the version number, and ends with a copyright. The description is terminated with byte 0x1A which Borland font loader code search in first 256 bytes of the file (including two bytes of Signature above). In other words this field size can be from 0 (zero - no copyright text at all) to 253 bytes long (256 - 3). |
char | Header Start | Must be 0x1A. |
UINT16LE | Header Size | The size, in bytes, of the entire header. |
char[4] | Font Name | A 4-character font internal name. |
UINT16LE | Font Size | The size of the font's data block. The Header Size + Font Size should be the size of the file. See remarks according Font Size below. |
UINT8 | Font Major | Driver Version Information major number. Must be 1 (Borland font loader also accepts anything bigger but not 0, i.e. valid if "value >= 1"). |
UINT8 | Font Minor | Driver Version Information minor number. Ignored, should be 0. |
UINT8 | Revision Major | BGI Revision Information major number. Must be 1 (Borland font loader also accepts 0 but halts on anything bigger than 1, i.e. valid if "value <= 1"). |
UINT8 | Revision Minor | BGI Revision Information minor number. Ignored, should be 0. |
char[...] | Padding | The rest of the header should be padded out to nulls based on the Header Size. All official Borland fonts use a header size of 0x80 (note that font editors may not work with fonts with different header size). This field must be at least 15 bytes long since Borland C code aligns Stroke Header to 16 byte bounday by moving Font Size bytes back inplace. See remarks according Font Size below. |
Correct Font Size field is crucial since Borland code moving font data inplace to align on 16 byte boundary based on this field value. User-made fonts with incorrect Font Size (less than it should be) and/or with Padding less than 15 bytes will be corrupted in memory and will not work properly! It doesn't matter if Stroke Header itself aligned in file to 16 byte boundary because code checks only actual memory address and it may be anything.
To calculate proper Font Size do as follows:
- Find max offset in Character Stroke Offsets table (see Stroke Header definition below).
- Add Strokes Offset to get the Stroke Data offset for that character.
- Parse Stroke Data for that character until the stop command and sum all bytes including stop command itself.
- Use result value as Font Size.
- Do not foget that this Font Size must not include Font Header size.
Stroke Header
This section contains general information about the font including how many characters are represented, the top of the highest ascender, bottom of the lowest descender, baseline, where in the file to find each character's instructions, and the width of each character.
Data Type | Name | Description |
---|---|---|
char | Stroke Check | Must be 0x2B. Indicates this is a stroked font. |
INT16LE | Character Count | Number of total characters stored in this font. |
char | Undefined | Borland's own documentation labels this as "undefined". |
char | Starting Character | The index of the first character to have a glyph associated with it. |
INT16LE | Strokes Offset | The file offset where stroke definitions begin relative from the start of this header. |
INT8 | Scan Flag | ! Borland's documentation calls this the "scan flag", but doesn't explain what it is for ("set to 1 if set is scanable"). |
INT8 | Origin to Ascender | The distance from the origin to the font's highest point. |
INT8 | Origin to Baseline | The distance from the origin to the font's baseline (typically 0). Ignored, should be 0. |
INT8 | Origin to Descender | The distance from the origin to the font's lowest point. Note, you can derive the total character height by getting the distance from the Ascender to Descender. |
char[4] | Font Name | Borland's own documentation says "four character name of font", but it's always all zero. |
char | Undefined | Borland's own documentation labels this as "undefined". |
INT16LE[Character Count] | Character Stroke Offsets | An array of the offset to the stroke definitions for each character. To get the position in the file, you must add the Header Size and the Strokes Offset to this value. |
INT8[Character Count] | Character Widths | The unscaled width of each character. |
Stroke Data | Character strokes | The stroke instructions for each character. See below for how to interpret them. |
Stroke Data
This section contains the actual instructions for drawing individual characters.
Data Type | Name | Description |
---|---|---|
BYTE | X Instruction | Instructions for drawing the character and an X value. |
BYTE | Y Instruction | Instructions for drawing the character and a Y value. |
... | Several possible instruction pairs per character. |
Encoded in each two byte pair is a single stroke instruction, and an X and Y value. The most-significant bit in each byte is an opcode, the next seven bits represent a signed value. The first byte is the X value, the second byte is the Y value. See the diagram below.
7 6 543210 ^ ^ ^ | | +----------- 6-Bit number. | +------------- Sign bit for number. +--------------- Opcode.
The two opcodes determine the instruction.
Code 1 | Code 2 | Instruction |
---|---|---|
0 | 0 | End of character stroke definition. |
0 | 1 | Borland's documentation has this labeled as "do scan," implying it's related to the Scan Flag. It's only used in their BOLD font on characters with multiple objects like the colon and equals sign. It doesn't appear to affect how the character is drawn. Borland font rendering code silently ignores this - checked in Turbo Pascal 7.1 (1997) and in Turbo C 2.01 (1988). ! More info is needed. |
1 | 0 | Move plotter to the specified X and Y coordinates. |
1 | 1 | Draw a line from the previous X and Y coordinates and move plotter to the new X and Y coordinates. |
Character Layout
The Y values for each character are stored in a unique manner. Characters are drawn relative to the baseline started at 0 with Y-axis inverted to screen axis, in other words negative values goes below baseline and positive above. See the diagram to the right.
Borland font scale size
Borland font rendering code has internal font scale table for use as the third SetTextStyle() argument.
static char chrscale[10][2] = {
{00, 00}, /* apply custom scale coefficients */
{03, 05},
{02, 03},
{03, 04},
{01, 01}, /* charsize == 4 (as is) */
{04, 03},
{05, 03},
{02, 01},
{05, 02},
{03, 01}
};
/*
How to apply:
X = (X * chrscale[charsize][0]) / chrscale[charsize][1];
Y = (Y * chrscale[charsize][0]) / chrscale[charsize][1];
Where:
charsize - font size as third argument for settextstyle()
X, Y - decoded Stroke Data coordinats
*/
To display font characters as is without any scale set font size to 4. Custom character scale coefficients can be set via SetUserCharSize() routine.
Hardcoded Borland fonts
Borland graphics library GRAPHICS.LIB (C) and GRAPH.TPU (Pascal) has hardcoded font names table for using by number as the first SetTextStyle() argument. Only first five fonts (including default BIOS 8x8 raster font) has predefined names for them. Note that Borland Pascal was shipped with BOLD.CHR font file, but since its name missing in GRAPH.TPU (that's probably an oversight) you can use it only via InstallUserFont() routine as any other user font. User fonts can be installed only if file name matches the ????.CHR pattern and four character file name without extension matches Font Name field in font Header. In other words FONT.CHR will work but MYFONT.CHR or FNT.CHR will not.
Also be aware that register(far)bgifont() function which was designed to utilize fonts linked with executable or memory buffered font files, will work only for hardcoded fonts below. This will checked internally by matching hardcoded font names against Font Name field from font Header. If you want to use any other user font file from memory or linked code - you'll need to replace Font Name in your font Header with any four character letters from hardcoded fonts. This can be done in memory to avoid confuse with the original Borland fonts. After that you can use appropriate number for font which name you're used for masking.
Number | Name | File | Predefined name constant |
---|---|---|---|
0 | - | - | DEFAULT_FONT (C) / DefaultFont (Pascal) |
1 | TRIP | TRIP.CHR | TRIPLEX_FONT (C) / TriplexFont (Pascal) |
2 | LITT | LITT.CHR | SMALL_FONT (C) / SmallFont (Pascal) |
3 | SANS | SANS.CHR | SANS_SERIF_FONT (C) / SansSerifFont (Pascal) |
4 | GOTH | GOTH.CHR | GOTHIC_FONT (C) / GothicFont (Pascal) |
5 | SCRI | SCRI.CHR | - |
6 | SIMP | SIMP.CHR | - |
7 | TSCR | TSCR.CHR | - |
8 | LCOM | LCOM.CHR | - |
9 | EURO | EURO.CHR | - |
10 | BOLD | BOLD.CHR | Font missing in Pascal GRAPH.TPU library. |
Source Code
Draw Stroke Data
/*
Copyright 2024 CTPAX-X Team
This source code licensed under the Apache License, Version 2.0 (the "License"):
https://www.apache.org/licenses/LICENSE-2.0
*/
/* draws a character:
x, y - initial screen positon
b - buffer pointer to a Stroke Data for character */
void chr_draw_char(int x, int y, unsigned char *b) {
char m, dx, dy;
int xc, yc;
/* initial position according to Stroke Header */
y += origin_to_ascender - origin_to_descender;
xc = x;
yc = y;
moveto(xc, yc);
do {
/* get mask */
m = b[0] >> 7;
m += m + (b[1] >> 7);
/* 0 - stop; 1 - scan; 2 - move; 3 - draw */
if (m > 1) {
/* bit arithmetic (note dx/dy signed 8 bit type):
if sign bit is set - expand to full signed value */
dx = (b[0] & 0x7F) | ((b[0] & 0x40) << 1);
dy = (b[1] & 0x7F) | ((b[1] & 0x40) << 1);
dy = -dy;
/* dx/dy relative to initial screen positon */
xc = x + dx;
yc = y + dy;
if (m == 2) {
moveto(xc, yc);
} else {
lineto(xc, yc);
}
}
/* next byte pair */
b += 2;
} while (m);
}
Shape Viewer
This FreeBASIC program will draw all of the characters in the specified font, and give details about the font.
ScreenRes 800, 600, 4
' Open the font file.
Open "SANS.CHR" For Binary As #1
' Verify the signature.
Dim As String Check
Check = Space(4)
Get #1, , Check
If Check <> "PK" + Chr(8) + Chr(8) Then
Print "Not a Borland CHR font."
End
End If
' Load the Description
Dim As UByte Buffer
Dim As String Description
Do
Get #1, , Buffer
If Buffer <> &h1A Then
Description = Description + Chr(Buffer)
End If
Loop Until Buffer = &h1A
' Load the header size.
Dim As UShort HeaderSize
Get #1, , HeaderSize
' Load the font id.
Dim As String FontId
FontId = Space(4)
Get #1, , FontId
' Load the data file size. (HeaderSize + DataSize should match the size of the file.)
Dim As UShort DataSize
Get #1, , DataSize
' Load the font version number.
Dim As UShort FontMajorVersion
Get #1, , FontMajorVersion
Dim As UShort FontMinorVersion
Get #1, , FontMinorVersion
' Jump to the end of the header. The rest should be nulls anyway.
Get #1, HeaderSize, Buffer
' Verify this is a BGI Stroked Font.
Dim As String StrokedFontCheck
StrokedFontCheck = Space(1)
Get #1, , StrokedFontCheck
If StrokedFontCheck <> "+" Then
Print "Not a stroked font."
End
End If
' Load the number of characters.
Dim As UShort CharacterCount
Get #1, , CharacterCount
' Skip an unknown byte.
Get #1, , Buffer
' Load the starting charcter. This is the first character id to have a glyph associated to it.
Dim As UByte StartingChar
Get #1, , StartingChar
' Load the file offset where the stroke definitions begin.
Dim As UShort StrokeDefinitionOffset
Get #1, , StrokeDefinitionOffset
' Skip an unknown byte.
Get #1, , Buffer
' Load the distance from the origin to the top of the capital letters.
Dim As Byte OriginToCapital
Get #1, , OriginToCapital
' Load the distance from the origin to the baseline.
Dim As Byte OriginToBaseline
Get #1, , OriginToBaseline
' Load the distance from the origin to the bottom of the descenders.
Dim As Byte OriginToDescender
Get #1, , OriginToDescender
' Skip remaining nulls.
Dim As Integer I
For I = 0 To 4
Get #1, , Buffer
Next I
' Load offset to characters definitions.
Dim As UShort CharacterDefinitionOffset(StartingChar To (StartingChar + CharacterCount - 1))
For I = StartingChar To (StartingChar + CharacterCount - 1)
Get #1, , CharacterDefinitionOffset(I)
Next I
' Load character widths.
Dim As UByte CharacterWidths(StartingChar To (StartingChar + CharacterCount - 1))
For I = StartingChar To (StartingChar + CharacterCount - 1)
Get #1, , CharacterWidths(I)
Next I
Dim As Integer CharacterHeight = OriginToCapital + (OriginToDescender * -1)
Print "Font Id: "; FontId
Print "Description: "; Description
Print "Total Character Height: "; CharacterHeight
Print "Max Ascender: "; OriginToCapital
Print "Max Descender: "; OriginToDescender
Print "Total Characters: "; CharacterCount
Print "Starting Character: 0x"; Hex(StartingChar)
Dim As UByte XByte, YByte, Finished
Dim As Integer XCurrent, YCurrent, XStart, YStart, XBuffer, YBuffer
Dim As Byte XNew, YNew, XOpcode, YOpcode, XSign, YSign
XBuffer = 40
YBuffer = 32
For I = StartingChar To (StartingChar + CharacterCount - 1)
' Jump to the starting offset of this character.
Get #1, CharacterDefinitionOffset(I) + StrokeDefinitionOffset + HeaderSize, Buffer
' Character position in 16x16 table.
XStart = 80 + (I Mod 16) * XBuffer + ((XBuffer - CharacterWidths(I)) \ 2)
YStart = 70 + ((I \ 16) * YBuffer) + CharacterHeight
' Draw character bounding box.
Line(XStart, YStart - OriginToCapital)-(XStart + CharacterWidths(I), YStart - OriginToDescender), 8, B
' Load all of the drawing instructions and draw the character.
Finished = 0
Do
' Load the opcode/values. Two bytes, one for X, the other for Y.
' 7 6 543210
' ^ ^ ^
' | | +------ Start of 6-bit number.
' | +-------- Sign bit for number.
' +---------- Opcode bit.
Get #1, , XByte ' First opcode and X value.
Get #1, , YByte ' Second opcode and Y value.
' Read the opcode bits.
XOpcode = Bit(XByte, 7)
YOpcode = Bit(YByte, 7)
' Clear the opcode bits.
XByte = BitReset(XByte, 7)
YByte = BitReset(YByte, 7)
' Set the values to negative if the signed bits are on.
If Bit(XByte, 6) = True Then
XByte = BitSet(XByte, 7)
End If
If Bit(YByte, 6) = True Then
YByte = BitSet(YByte, 7)
End If
XNew = XByte
YNew = YByte
' Normalize the Y position.
YNew = -YNew
If XOpcode = 0 Then
If YOpcode = 0 Then
' End of character definition. Move to the next character.
Finished = 1
Else
' Not sure what this is for. It's only used in the BOLD font when a character has
' multiple separate objects like with the colon and equal sign. However, not all
' characters with broken up objects in the font use it, and leaving it out doesn't
' appear to mess up the drawing at all.
End If
Else
If YOpcode = 0 Then
' Move Pointer to X, Y.
' This happens below since both move and draw must change the position.
Else
' Draw From Current Pointer to new X, Y.
Line(XCurrent, YCurrent)-(XStart + XNew, YStart + YNew), 15
End If
XCurrent = XStart + XNew
YCurrent = YStart + YNew
End If
Loop Until Finished = 1
Next I
Sleep
Tools
The following tools are able to work with files in this format.
Name | Platform | View images in this format? | Convert/export to another file/format? | Import from another file/format? | Access hidden data? | Edit metadata? | Notes |
---|---|---|---|---|---|---|---|
FE Stroke Font Editor | DOS | Yes | Yes | Yes | No | Yes | Borland's in-house font Stroked Font editor. |
Credits
This format was reverse engineered by TheAlmightyGuru. If you find this information helpful in a project you're working on, please give credit where credit is due. (A link back to this wiki would be nice too!)
Links
- ftp.sunet.se/mirror/archive/ftp.sunet.se/pub/simtelnet/msdos/borland - Various tools for BGI fonts.