Skip to content

Instantly share code, notes, and snippets.

@Longor1996
Last active March 21, 2022 11:18
Show Gist options
  • Save Longor1996/7d872e37a38f104aca5d2be84f268953 to your computer and use it in GitHub Desktop.
Save Longor1996/7d872e37a38f104aca5d2be84f268953 to your computer and use it in GitHub Desktop.
Merges multiple fonts and generates a multi-channel signed-distance-field font bitmap.

You must have both FontForge and msdf-atlas-gen installed and on your PATH (the letter may also just be in the current working directory).

To use it: fontforge -lang=py -script generator.py BASEFONT FONT...

  • BASEFONT should be a regular font, like Arial, Roboto, Courier New, or anything you might get from https://www.fonts101.com/
  • FONT can be any kind of font, including icon-fonts, emoji-fonts (without colors!) and other symbol-sets.

To render text with the resulting MSDF, you can use this shader:

in vec2 texCoord;
out vec4 color;
uniform sampler2D msdf;
uniform vec4 bgColor;
uniform vec4 fgColor;
uniform float pxRange = 4; // set to distance field's pixel range

float screenPxRange() {
    vec2 unitRange = vec2(pxRange)/vec2(textureSize(msdf, 0));
    vec2 screenTexSize = vec2(1.0)/fwidth(texCoord);
    return max(0.5*dot(unitRange, screenTexSize), 1.0);
}
float median(float r, float g, float b) {
    return max(min(r, g), min(max(r, g), b));
}

void main() {
    vec3 msd = texture(msdf, texCoord).rgb;
    float sd = median(msd.r, msd.g, msd.b);
    float screenPxDistance = screenPxRange()*(sd - 0.5);
    float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
    color = mix(bgColor, fgColor, opacity);
}

Building the text geometry is a bit more involved, but uses the same principles as any other bitmap font, and thus left as an exercise to the reader.

import re
import sys
import fontforge
print(f"MSDF Multi-Font Merger & Generator! Python {sys.version}")
merged = None
#merged = fontforge.font()
#merged.fontname = "merged"
#merged.encoding = "UnicodeFull"
#merged.em = 4096
glyph_list_path = "merged.txt"
glyph_list_file = open(glyph_list_path, "w")
s = ''.join(chr(c) for c in range(sys.maxunicode+1))
ws = ''.join(re.findall(r'\s', s))
font_paths = sys.argv[1:]
for font_path in font_paths:
print(f"Loading {font_path}")
font = fontforge.open(font_path)
if font.encoding != "UnicodeFull":
font.encoding = "UnicodeFull"
print(f"Selecting {font_path}")
available = 0
if merged is not None:
merged.selection.none()
for glyph in font.glyphs("encoding"):
if glyph.unicode == -1 or chr(glyph.unicode) in ws:
continue
glyph_list_file.write(f"{glyph.unicode}\n")
available += 1
#print(f"- {glyph}")
font.selection.select(("more",None), glyph)
if merged is not None:
merged.selection.select(("more",None), glyph)
pass
selected = sum(1 for _ in font.selection)
print(f"Selected {selected} out of {available} glyphs")
print(f"Rescaling {font_path}")
font.em = 4096 # rescale
if merged is None:
merged = font
else:
print(f"Merging {font_path}")
font.copy()
merged.paste()
pass
glyph_list_file.close()
print(f"Rebuilding Data")
merged.buildOrReplaceAALTFeatures()
#print(f"Validating")
#v = merged.validate(1)
print(f"Saving")
merged.fontname = "merged"
merged_ttf = merged.fontname + ".ttf"
merged.generate(
merged_ttf,
flags = ("opentype", "dummy-dsig")
)
print(f"Locating 'msdf-atlas-gen'")
import shutil
msdfgen = shutil.which("msdf-atlas-gen")
print(f"Found 'msdf-atlas-gen' at {msdfgen}; executing...")
merged_png = merged.fontname + ".png"
merged_json = merged.fontname + ".json"
merged_csv = merged.fontname + ".csv"
import subprocess
subprocess.run([
msdfgen,
"-font", merged_ttf,
"-charset", glyph_list_path,
"-type", "mtsdf",
"-imageout", merged_png,
"-json", merged_json,
"-csv", merged_csv,
"-potr",
"-pxrange", "4",
"-size","48",
"-minsize", "32",
"-threads", "0"
], stdout=sys.stdout, stderr=sys.stderr)
print(f"Done!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment