Amiga Bitmap Fonts, Part 2: The Font Contents File
In Part 1 of our exploration of Amiga bitmap fonts, we identified the structure of a font on disk. We’ll now take a look at one of the files in more detail - the font contents file, suffixed .font
.
To identify the structure of this file we turn again to the Amiga ROM Kernel Reference Manual: Libraries and the chapter on graphics and text. In the section named ‘Composition of a Bitmap Font on Disk’ we have some answers. We have descriptions of the font contents file, and the numeric files in the font folder - or font descriptor files as they are called here.
In that section, the font contents file is described using Amiga assembly language. I’m not too hot on that, but let’s see if we can interpret what’s going on regardless:
struct FontContentsHeader {
UWORD fch_FileID; /* FCH_ID */
UWORD fch_NumEntries; /* the number of FontContents elements */
struct FontContents fch_FC[]; /* or struct TFontContents fch_TFC[]; */
};
#define MAXFONTPATH 256
So our font contents file is basically a struct
- a collection of related data joined together into one big blob. It consists of some header information and a list of font entries. Each font entry in the list corresponds to an available font size - i.e. one of the font descriptor files.
In more detail:
- The first two bytes, or
UWORD
, of the file contains the ‘File ID’. It is used to identify the format of the font entries in this file. For bitmap fonts this can be either 0x0F00 to signifyFontContents
or 0x0F02 forTFontContents
. For simplicity we’ll ignore this for now and always assume that our font entries are inFontContents
format. - The next
UWORD
contains the number of entries, corresponding to the number of available sizes in the font - and the number of font descriptor files. - Finally we have an array of
FontContents
structs, which we’ll examine next.
Font contents entries
Our FontContents
struct looks like this in assembly language:
struct FontContents {
char fc_FileName[MAXFONTPATH];
UWORD fc_YSize;
UBYTE fc_Style;
UBYTE fc_Flags;
};
Here we can see:
- The first 256 characters contains the font file path.
- The next
UWORD
contains the font size. - Then we have some style information. This is stored as a set of bits and denotes whether the font:
- is underlined
- is bold
- is italic
- is extended
- And finally a set of flags (or bits, if you prefer) to denote whether the font:
- is built into the ROM
- was loaded from disk
- is designed to be printed from from right to left
- is designed for a Hires screen
- is designed for a Lores Interlaced screen
- has character widths that are not constant
- was explicitly designed at the given size rather than constructed from a scalable outline.
We now have all the information needed to interrogate a typical file and extract the data into a more readable format.
Enter Node.js
I’m a web developer by trade, so my natural inclination is to crack open Visual Studio Code and start writing Node.js scripts. Let’s see if we can read a file and grab the information we need.
If you want to join in, you’ll need to have Node.js installed and have a text editor or integrated development environment (IDE) available. Having a Git client will also be useful to you as I’ll be sharing my code examples - and some of my old fonts - at https://github.com/smugpie/amiga-bitmap-font-tools.
Let’s make a start by setting some constants and reading our file in from disk. We’ll output any information we find as a JSON file as this can be read by many different languages and systems for maximum interoperability.
const fs = require("fs");
const path = require("path");
const MAXFONTPATH = 256;
const FONTENTRYSIZE = MAXFONTPATH + 4;
const fontFile = fs.readFileSync(
path.join(__dirname, "../fonts/webcleaner/WebBold.font")
);
We now have our font file, fontFile
, read in as a Buffer
. From this buffer we can examine individual bytes, words and strings to get the information we need. For example, we know that the File ID is stored in bytes 0 and 1 of our file so we can create a function to extract that from our buffer:
const getFileContentsType = (fontFile) => {
const fileContentsType = fontFile.readUInt16BE(0);
const fileContentsFormats = {
0xf00: "FontContents",
0xf02: "TFontContents",
0xf03: "Scalable",
};
return fileContentsFormats[fileContentsType] || "Unknown";
};
Similarly we can retrieve the number of entries by reading the font file buffer at bytes 2 and 3:
const getNumberOfEntries = (fontFile) => fontFile.readUInt16BE(2);
And we can put both of these values in a fontData
variable:
const fontData = {
fontContentsFormat: getFileContentsType(fontFile),
numberOfEntries: getNumberOfEntries(fontFile),
entries: {},
};
Extracting font entries
We now need to work through the list of font entries. Before we do, we’ll create some helper functions to extract the information stored as bits.
const bitIsSet = (value, bit) => {
return !!(value & (2 ** bit));
};
const expandStyle = (style) => ({
value: style,
normal: style === 0,
underlined: bitIsSet(style, 0),
bold: bitIsSet(style, 1),
italic: bitIsSet(style, 2),
extended: bitIsSet(style, 3),
colorfont: bitIsSet(style, 6),
tagged: bitIsSet(style, 7),
});
const expandFlags = (flags) => ({
value: flags,
rom: bitIsSet(flags, 0),
disk: bitIsSet(flags, 1),
reversed: bitIsSet(flags, 2),
tallDot: bitIsSet(flags, 3),
wideDot: bitIsSet(flags, 4),
proportional: bitIsSet(flags, 5),
designed: bitIsSet(flags, 6),
removed: bitIsSet(flags, 7),
});
Finally, we’ll work our way through our font entries and dig out all the information we need:
for (let offset = 4; offset < fontFile.byteLength; offset += FONTENTRYSIZE) {
const fontEntry = fontFile.slice(offset, offset + FONTENTRYSIZE);
const fontNameAndPointSize = fontEntry.slice(0, MAXFONTPATH);
const [fontName, pointSize] = fontNameAndPointSize
.toString("utf-8")
.replace(/0/g, "")
.split("/");
const ySize = fontEntry.readUInt16BE(MAXFONTPATH);
const style = fontEntry.readUInt8(MAXFONTPATH + 2);
const flags = fontEntry.readUInt8(MAXFONTPATH + 3);
fontData.entries[pointSize] = {
fontName,
pointSize,
ySize,
flags: expandFlags(flags),
style: expandStyle(style),
};
}
console.log(JSON.stringify(fontData, null, 2));
And that should do it! All the code is at https://github.com/smugpie/amiga-bitmap-font-tools/blob/main/node/readFontContents.js
so you can
cd node
npm i
node ./readFontContents.js
And, fingers crossed, you’ll see some font information in JSON format.
{
"fontContentsFormat": "FontContents",
"numberOfEntries": 6,
"entries": {
"14": {
"fontName": "WebBold",
"pointSize": "14",
"ySize": 14,
"flags": {
"value": 98,
"rom": false,
"disk": true,
"reversed": false,
"tallDot": false,
"wideDot": false,
"proportional": true,
"designed": true,
"removed": false
},
"style": {
"value": 0,
"normal": true,
"underlined": false,
"bold": false,
"italic": false,
"extended": false,
"colorfont": false,
"tagged": false
}
},
... other entries deleted for brevity
}
}
Great! The system works. But there’s nothing particularly exciting here. There’s no actual character data for starters. For that we’ll need to delve into the numeric files in the font folder - the font descriptor files. That’s where we’ll be heading in the comparatively more exciting Part 3.