GTA 1 File Formats:  FON

Back to main page

Description

The "FON" files are files that contain a sequence of images with a constant height.
They are used for Fonts and the moving parts in the cut scenes.

Format

FON file {
    Header {
        byte numberOfPictures;
        byte heightOfAllPictures;
    }

    Picture[numberOfPictures] {
        byte widthOfPicture;
        byte[heightOfAllPictures*widthOfPicture] rawIndexedPictureData
    }
    
    Palette {
        Color[256] {
            byte red;
            byte green;
            byte blue;
        }
    }
}

PHP Script to extract FON files to a single PNG file

<?php


function convertFON($filename)
{
    
$outputDir = dirname($filename).'/';

    
$data = file_get_contents($filename);
    if (
$data === false) {
        die(
"Fehler beim Lesen der Datei.");
    }

    
$offset = 0;

    
$numberOfPictures = ord($data[$offset++]);
    
$height = ord($data[$offset++]);

    
$pictures = [];
    
$totalWidth = 0;

    
// Bilder lesen
    
for ($i = 0; $i < $numberOfPictures; $i++) {
        
$width = ord($data[$offset++]);
        
$size = $width * $height;

        
$pixelData = substr($data, $offset, $size);
        
$offset += $size;

        
$pictures[] = [
            
'width' => $width,
            
'data' => $pixelData
        
];

        
$totalWidth += $width;
    }

    
// Palette lesen
    
$palette = [];
    for (
$i = 0; $i < 256; $i++) {
        
$r = ord($data[$offset++]);
        
$g = ord($data[$offset++]);
        
$b = ord($data[$offset++]);

        
$palette[] = [$r, $g, $b];
    }

    
// === Kombiniertes Bild erstellen ===
    
$combinedData = str_repeat("\x00", $totalWidth * $height);

    
$currentX = 0;

    foreach (
$pictures as $pic) {
        for (
$y = 0; $y < $height; $y++) {
            
$srcOffset = $y * $pic['width'];
            
$dstOffset = $y * $totalWidth + $currentX;

            
$row = substr($pic['data'], $srcOffset, $pic['width']);

            for (
$x = 0; $x < $pic['width']; $x++) {
                
$combinedData[$dstOffset + $x] = $row[$x];
            }
        }

        
$currentX += $pic['width'];
    }

    
// Output-Ordner
    
if (!is_dir($outputDir)) {
        
mkdir($outputDir, 0777, true);
    }

    
$name = pathinfo($filename, PATHINFO_FILENAME);

    
// === BMP speichern ===
    //$bmpData = createBMP($totalWidth, $height, $combinedData, $palette);
    //file_put_contents("$outputDir/{$name}_FON.bmp", $bmpData);

    // === PNG speichern === (via GD)
    
savePNG("$outputDir/{$name}_FON.png", $totalWidth, $height, $combinedData, $palette);

    echo 
"Done: {$name}_FON.png\n";
}


function 
createBMP($width, $height, $pixelData, $palette)
{
    
$rowSize = ($width + 3) & ~3; // auf 4 Byte ausrichten
    
$pixelArraySize = $rowSize * $height;
    
$paletteSize = 256 * 4;
    
$fileSize = 14 + 40 + $paletteSize + $pixelArraySize;

    
$bmp = '';

    
// === BMP HEADER ===
    
$bmp .= "BM"; // Signature
    
$bmp .= pack('V', $fileSize);
    
$bmp .= pack('v', 0);
    
$bmp .= pack('v', 0);
    
$bmp .= pack('V', 14 + 40 + $paletteSize);

    
// === DIB HEADER ===
    
$bmp .= pack('V', 40); // Header size
    
$bmp .= pack('V', $width);
    
$bmp .= pack('V', $height);
    
$bmp .= pack('v', 1); // planes
    
$bmp .= pack('v', 8); // bpp
    
$bmp .= pack('V', 0); // compression
    
$bmp .= pack('V', $pixelArraySize);
    
$bmp .= pack('V', 2835); // X ppm
    
$bmp .= pack('V', 2835); // Y ppm
    
$bmp .= pack('V', 256); // colors used
    
$bmp .= pack('V', 0);

    
// Palette (BGR0)
    
foreach ($palette as [$r, $g, $b]) {
        
$bmp .= chr($b) . chr($g) . chr($r) . chr(0);
    }

    
// Pixel (bottom-up)
    
for ($y = $height - 1; $y >= 0; $y--) {
        
$row = substr($pixelData, $y * $width, $width);
        
$bmp .= $row;

        
$padding = $rowSize - $width;
        if (
$padding > 0) {
            
$bmp .= str_repeat("\x00", $padding);
        }
    }

    return 
$bmp;
}


function 
savePNG($filename, $width, $height, $pixelData, $palette)
{
    
$img = imagecreatetruecolor($width, $height);

    
// Palette in GD konvertieren
    
$gdPalette = [];
    foreach (
$palette as $i => [$r, $g, $b]) {
        
$gdPalette[$i] = imagecolorallocate($img, $r, $g, $b);
    }

    
// Pixel setzen
    
for ($y = 0; $y < $height; $y++) {
        for (
$x = 0; $x < $width; $x++) {
            
$index = ord($pixelData[$y * $width + $x]);
            
imagesetpixel($img, $x, $y, $gdPalette[$index]);
        }
    }

    
imagepng($img, $filename);
    
imagedestroy($img);
}


// === AUFRUF ===
$files = glob("D:\\GTADATA\\*");
foreach (
preg_grep('/\.fon$/i', $files) as $fon_file) {
    
convertFON($fon_file);
}

PHP Script to extract FON files to BMP file series

<?php

function readFON($filename, $outputDir)
{
    
$data = file_get_contents($filename);
    if (
$data === false) {
        die(
"Error reading file: $filename.");
    }

    
$offset = 0;

    
// Header lesen
    
$numberOfPictures = ord($data[$offset++]);
    
$height = ord($data[$offset++]);

    echo 
"Pictures: $numberOfPictures\n";
    echo 
"Height: $height\n";

    
$pictures = [];

    
// Bilder lesen
    
for ($i = 0; $i < $numberOfPictures; $i++) {
        
$width = ord($data[$offset++]);
        
$size = $width * $height;

        
$pixelData = substr($data, $offset, $size);
        
$offset += $size;

        
$pictures[] = [
            
'width' => $width,
            
'height' => $height,
            
'data' => $pixelData
        
];
    }

    
// Palette lesen
    
$palette = [];
    for (
$i = 0; $i < 256; $i++) {
        
$r = ord($data[$offset++]);
        
$g = ord($data[$offset++]);
        
$b = ord($data[$offset++]);

        
$palette[] = [$r, $g, $b];
    }

    
// Output-Ordner erstellen
    
if (!is_dir($outputDir)) {
        
mkdir($outputDir, 0777, true);
    }

    
// BMPs speichern
    
foreach ($pictures as $index => $pic) {
        
$bmpData = createBMP($pic['width'], $pic['height'], $pic['data'], $palette);
        
$name = strtoupper(pathinfo($filename, PATHINFO_FILENAME) . "_FON");
        
$indexFormatted = str_pad($index, 3, "0", STR_PAD_LEFT);
        
file_put_contents("$outputDir/{$name}_{$indexFormatted}.bmp", $bmpData);
    }

    echo 
"Finished!\n";
}

function 
createBMP($width, $height, $pixelData, $palette)
{
    
$rowSize = ($width + 3) & ~3; // auf 4 Byte ausrichten
    
$pixelArraySize = $rowSize * $height;
    
$paletteSize = 256 * 4;
    
$fileSize = 14 + 40 + $paletteSize + $pixelArraySize;

    
$bmp = '';

    
// === BMP HEADER ===
    
$bmp .= "BM"; // Signature
    
$bmp .= pack('V', $fileSize);
    
$bmp .= pack('v', 0);
    
$bmp .= pack('v', 0);
    
$bmp .= pack('V', 14 + 40 + $paletteSize);

    
// === DIB HEADER ===
    
$bmp .= pack('V', 40); // Header size
    
$bmp .= pack('V', $width);
    
$bmp .= pack('V', $height);
    
$bmp .= pack('v', 1); // planes
    
$bmp .= pack('v', 8); // bpp
    
$bmp .= pack('V', 0); // compression
    
$bmp .= pack('V', $pixelArraySize);
    
$bmp .= pack('V', 2835); // X ppm
    
$bmp .= pack('V', 2835); // Y ppm
    
$bmp .= pack('V', 256); // colors used
    
$bmp .= pack('V', 0);

    
// === PALETTE === (BGR0)
    
foreach ($palette as [$r, $g, $b]) {
        
$bmp .= chr($b) . chr($g) . chr($r) . chr(0);
    }

    
// === PIXELS === (BMP ist bottom-up!)
    
for ($y = $height - 1; $y >= 0; $y--) {
        
$row = substr($pixelData, $y * $width, $width);
        
$bmp .= $row;

        
// Padding
        
$padding = $rowSize - $width;
        if (
$padding > 0) {
            
$bmp .= str_repeat("\x00", $padding);
        }
    }

    return 
$bmp;
}


// === AUFRUF ===
$files = glob("D:\\GTADATA\\*");
foreach (
preg_grep('/\.fon$/i', $files) as $fon_file) {
    echo 
"File: $fon_file\n";
    
$dirname = dirname($fon_file).'\\_EXTRACT\\';
    if (!
is_dir($dirname)) mkdir($dirname);
    
readFON($fon_file, $dirname);
}

?>