"""Draws the U+2220 ANGLE mark for a BMFont sheet, matched to that sheet's own cap height and stem. Neither Philosopher nor Liberation Sans carries U+2220, and ElectricRoot's phasor labels need it. The mark is two straight strokes, so it is drawn here rather than lifted out of some third font that does have the glyph: that keeps another licence out of the tree, and stops a borrowed stroke weight sitting wrong against a face that was never cut with it. Usage: python MakeAngleGlyph.py ..\\LiberationSans\\LiberationSans-Regular-32px.fnt Writes -angle.png beside the sheet and prints the bmfc line that imports it. Needs Pillow. """ import os import sys import struct from PIL import Image from PIL import ImageDraw # BMFont's chnl field is a bitmask over the packed channels, not an index. CHANNEL_OF = {1: 2, 2: 1, 4: 0, 8: 3, 15: 3} ANGLE_CODEPOINT = 0x2220 # Sheets cut below this render 1-bit, so their mark is drawn hard-edged to match; above it the # letterforms are antialiased and so is the mark. ANTIALIAS_FLOOR = 20 SUPERSAMPLE = 4 CHAR_RECORD = 20 def readSheet(fntPath): """The sheet's requested size, line height, and every glyph record, keyed by codepoint.""" data = open(fntPath, 'rb').read() if data[:4] != b'BMF\x03': raise SystemExit(fntPath + ' is not a binary BMFont file') fields = ['x', 'y', 'width', 'height', 'xoffset', 'yoffset', 'xadvance', 'page', 'channel'] size = lineHeight = 0 glyphs = {} pos = 4 while pos < len(data): blockType = data[pos] blockSize = struct.unpack_from(' 127 else 0 longest = max(longest, run) if longest: runs.append(longest) return max(set(runs), key=runs.count) def drawAngle(capHeight, strokeWidth, antialias): """A vertex at the bottom left, one ray along the baseline and one rising to the right.""" width = max(3, int(round(capHeight * 0.95))) scale = SUPERSAMPLE if antialias else 1 canvas = Image.new('L', (width * scale, capHeight * scale), 0) draw = ImageDraw.Draw(canvas) stroke = max(1, strokeWidth * scale) right = width * scale - 1 bottom = capHeight * scale - 1 draw.line([(0, bottom), (right, bottom)], fill=255, width=stroke) draw.line([(0, bottom), (right, 0)], fill=255, width=stroke) if scale > 1: canvas = canvas.resize((width, capHeight), Image.LANCZOS) # White throughout with the drawing in alpha, which is how BMFont lays a glyph down. white = Image.new('L', canvas.size, 255) return Image.merge('RGBA', (white, white, white, canvas)) def makeAngleGlyph(fntPath): size, lineHeight, glyphs = readSheet(fntPath) capital = glyphs.get(ord('H')) if capital is None: raise SystemExit(fntPath + " has no 'H' to take the mark's proportions from") strokeWidth = stemWidthOf(fntPath, lineHeight, capital) icon = drawAngle(capital['height'], strokeWidth, antialias=size >= ANTIALIAS_FLOOR) name = os.path.basename(fntPath)[:-len('.fnt')] + '-angle.png' icon.save(os.path.join(os.path.dirname(fntPath), name)) sideBearing = max(1, int(round(capital['height'] * 0.12))) return name, icon, 'icon="%s",%d,%d,%d,%d' % ( name, ANGLE_CODEPOINT, 0, capital['yoffset'], sideBearing) if __name__ == '__main__': if len(sys.argv) != 2: raise SystemExit(__doc__) fileName, image, configLine = makeAngleGlyph(sys.argv[1]) print('wrote %s (%dx%d)' % (fileName, image.width, image.height)) print('add to the .bmfc:') print(' ' + configLine)