Last active
September 18, 2024 16:57
-
-
Save nicolaslegland/2ace50f4bad628f6de67a97e03b0061c to your computer and use it in GitHub Desktop.
FON renderer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>FON renderer</title> | |
<base href="https://gist.githubusercontent.com/nicolaslegland/2ace50f4bad628f6de67a97e03b0061c/raw/" /> | |
<style> | |
@font-face | |
{ | |
font-family: "Bm437_IBM_VGA_9x16.FON"; | |
src: url("Px437_IBM_VGA_9x16.ttf") format("truetype"); | |
} | |
pre | |
{ | |
display: inline-block; | |
font-family: "Bm437_IBM_VGA_9x16.FON", monospace; | |
line-height: 100%; | |
margin: 0; | |
width: 79cw; | |
} | |
pre.fon | |
{ | |
color: rgba(0, 0, 0, 0); | |
position: relative; | |
} | |
pre.fon > canvas | |
{ | |
height: 100%; | |
image-rendering: pixelated; | |
left: 0; | |
position: absolute; | |
top: 0; | |
width: 100%; | |
z-index: -1; | |
} | |
pre.ttf | |
{ | |
color: inherit; | |
} | |
pre.ttf > canvas | |
{ | |
display: none; | |
} | |
</style> | |
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> | |
<meta content="initial-scale=1.0, width=device-width" name="viewport" /> | |
</head> | |
<body> | |
<h1>FON renderer in browser</h1> | |
<h2>Reference</h2> | |
<ul> | |
<li>Preview: <a href="https://gistpreview.github.io/?2ace50f4bad628f6de67a97e03b0061c">https://gistpreview.github.io/?2ace50f4bad628f6de67a97e03b0061c</a></li> | |
<li>Source: <a href="https://gist.github.com/nicolaslegland/2ace50f4bad628f6de67a97e03b0061c">https://gist.github.com/nicolaslegland/2ace50f4bad628f6de67a97e03b0061c</a></li> | |
</ul> | |
<h2>Result</h2> | |
<label><input checked="checked" name="renderer" onchange="renderer.className='ttf';" type="radio" value="ttf" /> TTF</label> | |
<label><input name="renderer" onchange="renderer.className='fon';" type="radio" value="fon" /> FON</label> | |
<hr /> | |
<pre class="ttf" id="renderer"> | |
▄ ▄█▄ █▄ ▄ | |
▄█▀█▓ ▄▓▀▀█▀ ▀▀▀█▓▀▀ ▀▀ ▄█▀█▓▀▀▀▀▀▓▄▀██▀▀ | |
██ ██ ▀██▄▄ ▄█ ▀ ░▒ ░▒ ██ ██ ▄█▄ █▀ ██ | |
█▓▄▀██ ▄ ▀█▌▓█ ▒▓ ▒▓ █▓▄▀██ ▓█ ▀▄ █▓ | |
█▒ █▓ ██▄▓▀ ▀█▄▄█▄▓█ ▓█ █▒ █▓ ▒█ ▓█▄ ▒ | |
▀▒ ▀ ▀ █▀ ▀▒ ▀ █▀ ░ | |
</pre> | |
<hr /> | |
<a href="https://commons.wikimedia.org/wiki/File:Aa_example3.png">Aa_example3</a> using <a href="https://int10h.org/oldschool-pc-fonts/fontlist/font?ibm_vga_9x16">IBM VGA 9x16</a> | |
<script> | |
const font_list = Array.from(document.fonts); | |
for (let font_index = 0; font_index < font_list.length; ++font_index) | |
{ | |
const font_name = font_list[font_index].family.replaceAll('"', ''); | |
if ('.fon' === font_name.substring(font_name.length - 4).toLowerCase()) | |
{ | |
load_fon(font_name); | |
} | |
} | |
function load_fon(file_name) | |
{ | |
const request = new XMLHttpRequest(); | |
request.responseType = 'arraybuffer'; | |
request.onreadystatechange = function () | |
{ | |
if ((200 === this.status) && (4 === this.readyState)) | |
{ | |
const font_url = this.responseURL; | |
console.assert(font_url.substring(font_url.lastIndexOf('/') + 1) == file_name) | |
const font_data = parse_fon(this.response); | |
const pre_list = document.getElementsByTagName('pre'); | |
for (let pre_index = 0; pre_index < pre_list.length; ++pre_index) | |
{ | |
const pre_node = pre_list[pre_index]; | |
if (window.getComputedStyle(pre_node).fontFamily.replaceAll('"', '').split(', ').includes(file_name)) | |
{ | |
render_pre(pre_node, font_data); | |
break; | |
} | |
} | |
} | |
}; | |
request.open('GET', file_name, true); | |
request.send(); | |
} | |
function parse_fon(data) | |
{ | |
const buffer = new Uint8Array(data); | |
console.assert('MZ' === read_string(buffer, 0, 2), 'FON file is not a valid MZ file'); | |
const ne_offset = read_u16_le(buffer, 0x3C); | |
console.assert('NE' === read_string(buffer, ne_offset, 2), 'FON file is not a valid NE file'); | |
const table_offset = read_u16_le(buffer, ne_offset + 0x24) + ne_offset; | |
const table_shift = read_u16_le(buffer, table_offset); | |
console.assert(0x8007 == read_u16_le(buffer, table_offset + 2), 'FON file has unknown resource type'); | |
console.assert(1 == read_u16_le(buffer, table_offset + 4), 'FON file has duplicate resources'); | |
console.assert(0x8008 == read_u16_le(buffer, table_offset + 22), 'FON file has unknown resource type'); | |
console.assert(1 == read_u16_le(buffer, table_offset + 24), 'FON file has duplicate resources'); | |
const resource_offset = read_u16_le(buffer, table_offset + 30) << table_shift; | |
const resource_size = read_u16_le(buffer, table_offset + 32) << table_shift; | |
console.assert(table_offset <= resource_offset, 'FON file has out of bound resource'); | |
console.assert(resource_offset < buffer.length, 'FON file has out of bound resource'); | |
console.assert(resource_offset + resource_size < buffer.length, 'FON file has out of bound resource'); | |
console.assert('Bm437 IBM VGA 9x16' === read_string(buffer, read_u16_le(buffer, resource_offset + 0x69) + resource_offset), 'FON file has unexpected name'); | |
console.assert(0x200 === read_u16_le(buffer, resource_offset), 'FON file has an unknown version') | |
console.assert(0 === read_u16_le(buffer, resource_offset + 0x42), 'FON file is a vector font') | |
const font_height = read_u16_le(buffer, resource_offset + 0x58); | |
console.assert(16 === font_height, 'FON file has unexpected height'); | |
const character_first = buffer[resource_offset + 0x5F]; | |
const character_last = buffer[resource_offset + 0x60]; | |
const font = {}; | |
font['height'] = font_height; | |
const codepage = [0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f, 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, 0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f, 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f, 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f, 0x00c7, 0x00fc, 0x00e9, 0x00e2, 0x00e4, 0x00e0, 0x00e5, 0x00e7, 0x00ea, 0x00eb, 0x00e8, 0x00ef, 0x00ee, 0x00ec, 0x00c4, 0x00c5, 0x00c9, 0x00e6, 0x00c6, 0x00f4, 0x00f6, 0x00f2, 0x00fb, 0x00f9, 0x00ff, 0x00d6, 0x00dc, 0x00a2, 0x00a3, 0x00a5, 0x20a7, 0x0192, 0x00e1, 0x00ed, 0x00f3, 0x00fa, 0x00f1, 0x00d1, 0x00aa, 0x00ba, 0x00bf, 0x2310, 0x00ac, 0x00bd, 0x00bc, 0x00a1, 0x00ab, 0x00bb, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255d, 0x255c, 0x255b, 0x2510, 0x2514, 0x2534, 0x252c, 0x251c, 0x2500, 0x253c, 0x255e, 0x255f, 0x255a, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256c, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256b, 0x256a, 0x2518, 0x250c, 0x2588, 0x2584, 0x258c, 0x2590, 0x2580, 0x03b1, 0x00df, 0x0393, 0x03c0, 0x03a3, 0x03c3, 0x00b5, 0x03c4, 0x03a6, 0x0398, 0x03a9, 0x03b4, 0x221e, 0x03c6, 0x03b5, 0x2229, 0x2261, 0x00b1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00f7, 0x2248, 0x00b0, 0x2219, 0x00b7, 0x221a, 0x207f, 0x00b2, 0x25a0, 0x00a0]; | |
for (let character_index = character_first; character_index <= character_last; ++character_index) | |
{ | |
const character_offset = resource_offset + 0x76 + 4 * (character_index - character_first); | |
const character_width = read_u16_le(buffer, character_offset); | |
console.assert(9 === character_width, 'FON file has unexpected character width'); | |
const character_stride = (character_width + 7) >> 3; | |
const glyph_offset = resource_offset + read_u16_le(buffer, character_offset + 2); | |
const byte_count = character_stride * font_height; | |
const glyph_image = new ImageData(character_width, font_height); | |
for (let byte_index = 0; byte_count !== byte_index; ++byte_index) | |
{ | |
const byte_value = buffer[glyph_offset + byte_index]; | |
const byte_x = Math.floor(byte_index / font_height) << 3; | |
const byte_y = byte_index % font_height; | |
const shift_last = Math.min(8, character_width - byte_x); | |
for (let shift = 0; shift < shift_last; ++shift) | |
{ | |
const pixel_offset = ((byte_y * character_width) + (byte_x + shift)) << 2; | |
if (1 === ((byte_value >> (7 - shift)) & 1)) | |
{ | |
glyph_image.data[pixel_offset + 3] = 0xFF; | |
} | |
} | |
} | |
font[codepage[character_index]] = glyph_image; | |
} | |
return font; | |
} | |
function read_bits(buffer, offset, count) | |
{ | |
let bits = ''; | |
for (let shift = 7; 0 <= shift; --shift) | |
{ | |
bits += (buffer[offset] >> shift) & 1; | |
} | |
for (let shift = 7; 0 <= shift; --shift) | |
{ | |
bits += (buffer[offset + 1] >> shift) & 1; | |
} | |
return bits; | |
} | |
function read_string(buffer, offset, length = buffer.length - offset) | |
{ | |
const maximum = offset + length; | |
let result = ''; | |
for (let position = offset; position < maximum; ++position) | |
{ | |
const character = buffer[position]; | |
if (0 == character) | |
{ | |
return result; | |
} | |
result += String.fromCharCode(character); | |
} | |
return result; | |
} | |
function read_u16_le(buffer, offset) | |
{ | |
return buffer[offset] + (buffer[offset + 1] << 8); | |
} | |
function render_pre(node, font) | |
{ | |
console.assert(0 === node.getElementsByTagName('canvas').length); | |
const font_height = font['height']; | |
let x = 0; | |
let y = 0; | |
let x_maximum = 0; | |
let y_maximum = 0; | |
let text = node.textContent.replace(/\n$/, ''); | |
for (let index = 0; index < text.length; ++index) | |
{ | |
const code = text.codePointAt(index); | |
if (10 == code) | |
{ | |
x = 0; | |
y += font_height; | |
} | |
else | |
{ | |
const glyph = font[code]; | |
if (undefined === glyph) | |
{ | |
x += 9; | |
} | |
else | |
{ | |
console.assert(glyph.height == font_height, 'FON file has unexpected glyph height') | |
x += glyph.width; | |
} | |
} | |
x_maximum = Math.max(x_maximum, x); | |
y_maximum = Math.max(y_maximum, y); | |
} | |
const canvas = document.createElement('canvas'); | |
const context = canvas.getContext('2d'); | |
canvas.imageSmoothingEnabled = false; | |
canvas.width = x_maximum; | |
canvas.height = y_maximum + font_height; | |
x = 0; | |
y = 0; | |
text = node.textContent.replaceAll('\t', ' '); | |
for (let index = 0; index < text.length; ++index) | |
{ | |
const code = text.codePointAt(index); | |
if (10 == code) | |
{ | |
x = 0; | |
y += font_height; | |
} | |
else | |
{ | |
const glyph = font[code]; | |
if (undefined === glyph) | |
{ | |
x += 9; | |
} | |
else | |
{ | |
context.putImageData(glyph, x, y); | |
x += glyph.width; | |
} | |
} | |
} | |
node.appendChild(canvas); | |
node.className = 'fon'; | |
document.getElementsByName('renderer')[1].click(); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment