Skip to content

Instantly share code, notes, and snippets.

@jleedev
Last active August 23, 2024 15:26
Show Gist options
  • Save jleedev/b8a7de8d4be1bbade5feded0ea83f8b9 to your computer and use it in GitHub Desktop.
Save jleedev/b8a7de8d4be1bbade5feded0ea83f8b9 to your computer and use it in GitHub Desktop.
sdf font preview
import io
from pathlib import Path
import sys
import unicodedata
from google.protobuf.message_factory import GetMessages
from google.protobuf.descriptor_pb2 import FileDescriptorSet
glyphs_pb = b'\n\xe3\x02\n\x0cglyphs.proto\x12\x0bllmr.glyphs"\x9d\x01\n\x05glyph\x12\x0e\n\x02id\x18\x01 \x02(\rR\x02id\x12\x16\n\x06bitmap\x18\x02 \x01(\x0cR\x06bitmap\x12\x14\n\x05width\x18\x03 \x02(\rR\x05width\x12\x16\n\x06height\x18\x04 \x02(\rR\x06height\x12\x12\n\x04left\x18\x05 \x02(\x11R\x04left\x12\x10\n\x03top\x18\x06 \x02(\x11R\x03top\x12\x18\n\x07advance\x18\x07 \x02(\rR\x07advance"a\n\tfontstack\x12\x12\n\x04name\x18\x01 \x02(\tR\x04name\x12\x14\n\x05range\x18\x02 \x02(\tR\x05range\x12*\n\x06glyphs\x18\x03 \x03(\x0b2\x12.llmr.glyphs.glyphR\x06glyphs"?\n\x06glyphs\x12.\n\x06stacks\x18\x01 \x03(\x0b2\x16.llmr.glyphs.fontstackR\x06stacks*\x05\x08\x10\x10\x80@B\x02H\x03'
message_classes = GetMessages(FileDescriptorSet.FromString(glyphs_pb).file)
glyph = message_classes["llmr.glyphs.glyph"]
fontstack = message_classes["llmr.glyphs.fontstack"]
glyphs = message_classes["llmr.glyphs.glyphs"]
"""
Numbers are cribbed from mapbox/tiny-sdf/blob/main/index.html:
Default parameters: scale=128, halo=0.55, gamma=2
White is drawn with a buffer of <halo> +/- u_gamma
Black is drawn with a buffer of 0.75 +/- u_gamma
where u_gamma = <gamma> * 1.4142 / <scale>
When the glyphs are generated, the signed distance is computed, negative is
inside and positive is outside, 0 is on the contour. This is biased by
adding 0.25 and encoded as an unsigned byte, 0-255.
The fragment shader then reads the signed distance value from the texture,
and computes:
float dist = texture2D(u_texture, v_texcoord).r;
float alpha = smoothstep(u_buffer - u_gamma, u_buffer + u_gamma, dist);
gl_FragColor = vec4(u_color.rgb, alpha * u_color.a);
"""
def clamp(x, minVal, maxVal):
return min(max(x, minVal), maxVal)
def smoothstep(edge0, edge1, x):
t = clamp((x - edge0) / (edge1 - edge0), 0, 1)
return t * t * (3 - 2 * t)
def mix(x, y, a):
return x * (1 - a) + y * a
def point(n):
assert n in range(256)
dist = n / 255
base = 0.8
ga = 0.0221
alpha_wh = smoothstep(0.55 - ga, 0.55 + ga, dist)
alpha_bl = smoothstep(0.75 - ga, 0.75 + ga, dist)
gr = mix(base, mix(1, 0, alpha_bl), alpha_wh)
r = int(gr * 255)
g = r
b = r
s = "\033\13348;2;%d;%d;%dm" % (r, g, b)
s += " "
s += "\033\1330m"
return s
def gray(mem):
out = io.StringIO()
for row in mem.tolist():
for b in row:
out.write(point(b))
out.write("\n")
return out.getvalue()
def show_font(path):
files = list(Path(path).glob("*.pbf"))
files.sort(key=lambda p: int(p.stem.split("-")[0]))
for f in files:
print(f)
show_pbf_file(f)
def name(c):
try:
return unicodedata.name(c)
except ValueError:
return None
def show_pbf_file(f):
for stack in glyphs.FromString(f.read_bytes()).stacks:
print(stack.name, stack.range)
for g in stack.glyphs:
if not g.bitmap:
continue
pr = type(g)()
pr.CopyFrom(g)
pr.ClearField("bitmap")
buf = memoryview(g.bitmap).cast("B", ((g.height + 6, g.width + 6)))
pic = gray(buf)
print(f"U+{g.id:04X}", name(chr(g.id)))
print(repr(pr).replace('\n', ' '))
print(pic)
sys.stdout.flush()
def main(argv):
args = argv[1:]
if not args:
print("Specify path to PBF font stack")
return 1
for arg in args:
show_font(arg)
if __name__ == "__main__":
sys.exit(main(sys.argv))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment