From 79d900f1588f1e813dc3c7e46dcbb5ce2cb67a04 Mon Sep 17 00:00:00 2001 From: Mitchell Riedstra Date: Fri, 26 Dec 2025 16:42:08 -0500 Subject: Initial --- mnicmp/src/code/makefont.py | 226 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100755 mnicmp/src/code/makefont.py (limited to 'mnicmp/src/code/makefont.py') diff --git a/mnicmp/src/code/makefont.py b/mnicmp/src/code/makefont.py new file mode 100755 index 0000000..a167e4d --- /dev/null +++ b/mnicmp/src/code/makefont.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# make a font loosely based on the DECwriter 7×7 dot matrix printer +# scruss - 2017-02 +# (this is meant for Python 2.x, btw) + +import json +import codecs +import fontforge +import math +import psMat + +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# Please - if you change anything in this code +# or the related decwriter.json data, you must +# change the fnt_name value. A unique name is +# about the only way people and computers can +# tell fonts apart. +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +fnt_name = 'FIXME_mnicmp' + +# dot radius: for round dots, 50 is about right, 30 is light. +# square dots are a bit heavier than round +# star and diamond dots are a bit lighter than round +r = 50 + +# italic angle: 0.0 for none; 12.08° looks nice +# italic=0.0 +italic=0.0 + +# bold? +bold=False + +# dot shape: Square, Diamond or Star. +# Anything else gets you plain old Round +shape='Round' + +# not much to change after here +# unless you want to break stuff +################################# + +# coordinates of centres of dots: +# eg lower left dot is at (340, 250) +xvals = ( 340, 393, 447, 500, 553, 607, 660 ) +yvals = ( 750, 667, 583, 500, 417, 333, 250 ) + +# matrix translations: +# italic 1: shift LL corner to origin +mat_origin=psMat.translate(-xvals[0], -yvals[6]) +# italic 2: skew by italic angle +mat_skew=psMat.skew(math.pi * italic / 180.0) # it likes radians +# italic 3: restore from origin to LL corner +mat_restore=psMat.translate(xvals[0], yvals[6]) +# bold: double-strike shift is half point diameter +mat_bold=psMat.translate(r,0) + +# a 'magic' value for approximating a circle with Bézier segments +magic = 4.0 / 3.0 * (math.sqrt(2) - 1) +# diamonds are just circles with relaxed control points +if (shape is "Diamond"): + magic = magic / 2.0 +# don't be tempted to make magic too large or you end up +# with blocky yet frilly fonts that look disturbingly intestinal. + +# parameters for stars +star=(3.0+math.sqrt(5))/2.0 +inner=r/star +# segment angle (72°), radians +seg=math.pi * (360.0/5.0)/180.0 + +# read dot structure from JSON file +# resulting structure is a hash/dictionary of dot bitmap arrays +# against unicode character: +# +# chars = { +# ... +# u'A': [u'...#...', +# u'..#.#..', +# u'.#...#.', +# u'#.....#', +# u'#.#.#.#', +# u'#.....#', +# u'#.....#'], +# ... +# } +# +# Everything that's a '#' counts as a dot; everything else is ignored +# If you want to be true to the DECwriter way, you can't have adjacent +# dots printed, but that's not enforced by this decoder + +with open('decwriter.json') as data_file: + chars = json.load(data_file) + +font = fontforge.font(em=1000, encoding='UnicodeFull', ascent=800, + descent=200, design_size=12.0, is_quadratic=False, + fontname=fnt_name) +# helps with glyph naming (usually) +fontforge.loadNamelist('glyphlist.txt') + +# try to make a glyph for every char in json +for uch in chars: + glyph = font.createChar(ord(uch)) + pts=[] + print '*** ', ord(uch), ': ' + yline = 0 + # go through glyph bitmap, placing dots where we find #s + for li in chars[uch]: + cy = yvals[yline] + # only encode to prevent Python 2 from grousing about utf-8 + a_li = li.encode('ascii') + xcol = 0 + for b in list(a_li): + cx = xvals[xcol] + if b == '#': + # we have a pixel at cx, cy, so add it to the list + pts.append(fontforge.point(cx,cy,False)) + xcol = xcol + 1 + yline = yline + 1 + + # get the glyph's layer to draw on + lyr = glyph.layers[glyph.activeLayer] + # now transform the points and place contours in layer + for p in pts: + # italicize! + if (italic > 0.0): + p.transform(mat_origin) + p.transform(mat_skew) + p.transform(mat_restore) + cx=p.x + cy=p.y + c = fontforge.contour() + # draw a printer dot at (cx, cy) using chosen shape + if (shape is "Square"): + # + # Draw a dot by drawing a square of side 2r + # + # move to start position + c.moveTo(cx + r, cy + r) + # draw the outline + c.lineTo(cx + r, cy - r) + c.lineTo(cx - r, cy - r) + c.lineTo(cx - r, cy + r) + c.lineTo(cx + r, cy + r) + elif (shape is "Star"): + # + # Draw a 5 pointed star! + # + # move to start position (vertical; 90°) + c.moveTo(cx, cy + r) + for k in range(5): + angle=math.pi/2 + (k+1)*seg + inangle=angle-seg/2.0 + c.lineTo(cx + inner * math.cos(inangle), + cy + inner * math.sin(inangle)) + c.lineTo(cx + r * math.cos(angle), + cy + r * math.sin(angle)) + # I drew this anticlockwise, so fix it (ahem) + c.reverseDirection() + else: + # Draw a printer dot by approximating a circle + # (default; also draws diamonds if magic is low) + # move to start position + c.moveTo(cx + r, cy) + # cubic sector 1: from 0° to 270°, clockwise + c.cubicTo((cx + r, cy - magic * r), + (cx + magic * r, cy - r), + (cx, cy - r)) + # cubic sector 2: from 270° to 180°, clockwise + c.cubicTo((cx - magic * r, cy - r), + (cx - r, cy - magic * r), + (cx - r, cy)) + # cubic sector 3: from 180° to 90°, clockwise + c.cubicTo((cx - r, cy + magic * r), + (cx - magic * r, cy + r), + (cx, cy + r)) + # cubic sector 4: from 90° to 0°, clockwise + c.cubicTo((cx + magic * r, cy + r), + (cx + r, cy + magic * r), + (cx + r, cy)) + # ensure path is closed (important!) + c.closed = True + lyr += c + + # do double-strike effect on glyph if bold + if (bold is True): + new_lyr=lyr.dup() + new_lyr.transform(mat_bold) + lyr += new_lyr + # update the glyph layers with our drawing + glyph.layers[glyph.activeLayer] = lyr + # some auto cleanups on each glyph to avoid manual work later + # fix overlapping paths + glyph.removeOverlap() + # seems you have to set this too if you deliberately overlap + glyph.unlinkRmOvrlpSave=True + # add curve extrema (it's a font convention) + glyph.addExtrema() + # round all coordinates to integers + glyph.round() + # add PS hints, because we can + glyph.autoHint() + + +# poor old space, always left to the end ... +space=font.createChar(ord(' ')) +space.left_side_bearing = 90 +space.right_side_bearing = 90 +space.width = 600 + +# one last blat through all the glyphs to set monospace parameters +for g in font.glyphs(): + g.left_side_bearing = 90 + g.right_side_bearing = 90 + g.width = 600 + +# these need to restated for some reason, +# and even then they don't always stick in FontForge +font.encoding = 'UnicodeFull' +font.fontname = fnt_name +font.design_size=12.0 +# italic angle is negative for $reasons_i_dont_understand +font.italicangle=-italic + +# save it and exit +font.save(fnt_name + '.sfd') -- cgit v1.2.3