Categories
Amiga Bitmap Fonts

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 signify FontContents or 0x0F02 for TFontContents. For simplicity we’ll ignore this for now and always assume that our font entries are in FontContents 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.