Most computer fonts (like .ttf or .otf) are vector-based. They store mathematical formulas (Bézier curves) that describe the shape of a letter. This allows them to scale infinitely without losing quality, but it requires a significant amount of processing power to render those shapes into pixels on a screen.
A VLW (Video Logic Font) file is different. It is a bitmap font.
When you convert a TTF to VLW, you are essentially taking a "snapshot" of the font at a specific size. The file stores every single character as a grid of pixels (bits).
lv_font_conv --font myfont.ttf --size 24
--bpp 4 --format vlw
--range 0x20-0x7F,0x40E-0x4FF
--output myfont_24.vlw
ttf to vlw converter
This method gives you absolute control over kerning, compression, and symbol ranges.
There are several reasons why you might need to convert TTF to VLW:
import struct from fontTools.ttLib import TTFont from fontTools.pens.boundsPen import BoundsPen from fontTools.pens.pointPen import PointToSegmentPen import numpy as npdef ttf_to_vlw(ttf_path, vlw_path, point_size=64, codepoints=None): ttf = TTFont(ttf_path) upm = ttf['head'].unitsPerEm scale = point_size / upm Most computer fonts (like
# Default codepoints if none given: ASCII printable if codepoints is None: codepoints = list(range(32, 127)) glyph_data = [] for cp in codepoints: gid = ttf.getBestCmap().get(cp) if gid is None: continue glyph = ttf['glyf'][gid] # Extract metrics advance = ttf['hmtx'][gid][0] * scale xmin, ymin, xmax, ymax = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax # Flatten contours (simplified: use glyph.draw() + LineTo collector) points = flatten_glyph_outlines(glyph, ttf, scale) glyph_data.append((cp, gid, advance, xmin*scale, ymin*scale, xmax*scale, ymax*scale, points)) # Write VLW file with open(vlw_path, 'wb') as f: f.write(struct.pack('>I', 0x9A33A19F)) # magic f.write(struct.pack('>i', point_size)) # Ascent/descent/leading – simplified f.write(struct.pack('>i', int(ttf['hhea'].ascent * scale))) f.write(struct.pack('>i', int(ttf['hhea'].descent * scale))) f.write(struct.pack('>i', 0)) f.write(struct.pack('>i', len(glyph_data))) # Write code points and glyph indices for cp, gid, _, _, _, _, _ in glyph_data: f.write(struct.pack('>I', cp)) for cp, gid, _, _, _, _, _ in glyph_data: f.write(struct.pack('>I', gid)) # Write offsets (after data block, simplified) # … full implementation would compute and write offsets, # then each glyph's binary blob (bounds, advance, contours, points).
The full flattening (flatten_glyph_outlines) requires a recursive subdivision of quadratic splines into line segments, respecting the pixel grid. This method gives you absolute control over kerning,
Without VLW, you would have to port the entire FreeType library (hundreds of KB) into your firmware. Using a converted VLW file reduces your renderer to a 100-line function that just draws bitmaps.
Because VLW files are bitmaps, they don't scale. If you create a font at size 12, it will look tiny on a high-resolution screen or huge on a small OLED. You usually need to generate multiple .h files for different sizes (e.g., FontSmall.h, FontLarge.h) and switch between them in your code.