char/iocgn-mono
iosevka variant inspired by Simple Köln-Bonn
git clone https://git.t4t.associates/char/iocgn-mono
925b071
main
1"""Render README showcases and the full encoded character set.""" 2 3import hashlib 4import json 5from pathlib import Path 6 7import aggdraw 8from fontTools .pens .basePen import BasePen 9from fontTools .pens .boundsPen import BoundsPen 10from fontTools .pens .transformPen import TransformPen 11from fontTools .ttLib import TTFont 12from PIL import Image ,ImageDraw ,ImageFont 13 14ROOT = Path (__file__ ).resolve ().parent .parent 15CODE = '''use std::collections::BTreeMap; 16 17fn parse_env(input: &str) -> BTreeMap<&str, &str> { 18input.lines() 19.map(str::trim) 20.filter(|line| !line.starts_with('#')) 21.filter_map(|line| line.split_once('=')) 22.map(|(key, value)| (key.trim(), value.trim())) 23.collect() 24} 25 26let env = parse_env("LANG=de_DE.UTF-8\\nPORT=8080"); 27assert_eq!(env.get("PORT"), Some(&"8080"));''' 28STRESS = 'minimum / maximum / memory / window / www.example.org' 29BACKGROUND ,INK ,MUTED ,RULE ,ACCENT = '#ffffff' ,'#000000' ,'#6f6f6f' ,'#d7d7d7' ,'#595959' 30MARGIN = 64 31 32 33class OutlinePen (BasePen ): 34def __init__ (self ,glyphs ): 35super ().__init__ (glyphs ) 36self .path = aggdraw .Path () 37 38def _moveTo (self ,p ): 39self .path .moveto (* p ) 40 41def _lineTo (self ,p ): 42self .path .lineto (* p ) 43 44def _curveToOne (self ,a ,b ,c ): 45self .path .curveto (* a ,* b ,* c ) 46 47def _closePath (self ): 48self .path .close () 49 50 51def code_mask (font ,rows ,size ): 52"""Rasterize (text, em, baseline[, x]) rows without shaping.""" 53supersample = 4 54glyphs ,cmap = font .getGlyphSet (),font .getBestCmap () 55mask = Image .new ('L' ,tuple (v * supersample for v in size )) 56canvas = aggdraw .Draw (mask ) 57for line ,em ,baseline ,* position in rows : 58scale = em / font ['head' ].unitsPerEm 59x = position [0 ]if position else 38.0 60pen = OutlinePen (glyphs ) 61for char in line : 62name = cmap [ord (char )] 63glyphs [name ].draw (TransformPen (pen , (scale * supersample ,0 ,0 ,- scale * supersample , 64x * supersample ,baseline * supersample ))) 65x += font ['hmtx' ][name ][0 ]* scale 66assert x < size [0 ]- 30 ,line 67canvas .path (pen .path ,aggdraw .Brush ('white' )) 68canvas .flush () 69return mask .resize (size ,Image .Resampling .LANCZOS ) 70 71 72def showcase (font ,style ,weight ): 73panel = Image .new ('RGB' , (1400 ,1008 ),BACKGROUND ) 74draw = ImageDraw .Draw (panel ) 75path = ROOT / 'build/TTF' / f'iocgnMono- { style } .ttf' 76label = ImageFont .truetype (str (path ),size = 20 ) 77size_label = ImageFont .truetype (str (path ),size = 16 ) 78draw .line ((MARGIN ,240 ,panel .width - MARGIN ,240 ),fill = RULE ,width = 2 ) 79draw .text ((MARGIN ,264 ),'01: code' ,font = label ,fill = MUTED ) 80draw .text ((1080 ,264 ),'02: distinct forms' ,font = label ,fill = MUTED ) 81draw .line ((1032 ,264 ,1032 ,852 ),fill = RULE ,width = 2 ) 82draw .text ((1080 ,500 ),'03: extended latin' ,font = label ,fill = MUTED ) 83draw .text ((1080 ,676 ),'04: sizes' ,font = label ,fill = MUTED ) 84for em ,baseline in [(16 ,742 ), (20 ,786 ), (28 ,840 )]: 85draw .text ((1080 ,baseline ),f' { em } ' ,font = size_label ,fill = MUTED ,anchor = 'ls' ) 86draw .line ((MARGIN ,876 ,panel .width - MARGIN ,876 ),fill = RULE ,width = 2 ) 87draw .text ((MARGIN ,898 ),'05: samples' ,font = label ,fill = MUTED ) 88 89rows = [('iocgn Mono' ,112 ,144 ,MARGIN ), 90 ('Aa Bb Cc / 0123456789' ,42 ,206 ,MARGIN )] 91rows += [(line ,30 ,332 + i * 42 ,MARGIN )for i ,line in enumerate (CODE .splitlines ())] 92rows += [(line ,48 ,356 + i * 54 ,1080 )for i ,line in enumerate (('0O 1Il|' ,'8B 6G 9g' ,'aopq mWw' ))] 93rows += [('ÀÉÎÕÜ ß æ œ' ,36 ,588 ,1080 ), ('āčę ł ñ ø ž' ,36 ,640 ,1080 )] 94rows += [(text ,em ,baseline ,1128 )for text ,em ,baseline in 95 [('let port = 8080;' ,16 ,742 ), ('path.join("src")' ,20 ,786 ), ('Some(&"UTF-8")' ,28 ,840 )]] 96rows += [(STRESS ,24 ,966 ,MARGIN )] 97panel .paste (INK , (0 ,0 ),code_mask (font ,rows ,panel .size )) 98return panel 99 100 101def character_sheet (fonts ,extended = False ): 102coverage = sorted (fonts [0 ].getBestCmap ()) 103assert all (sorted (font .getBestCmap ())== coverage for font in fonts ) 104coverage = [cp for cp in coverage if (cp >= 0x180 )== extended ] 105blocks = [('ascii' ,0x20 ,0x7F ), 106 ('latin-1 supplement' ,0xA0 ,0x100 ), 107 ('latin extended-a' ,0x100 ,0x180 ), 108 ('combining accents' ,0x300 ,0x370 ), 109 ('punctuation' ,0x2000 ,0x2070 ), 110 ('arrows' ,0x2190 ,0x2200 ), 111 ('mathematical symbols' ,0x2200 ,0x2300 ), 112 ('box drawing' ,0x2500 ,0x2580 ), 113 ('block elements' ,0x2580 ,0x25A0 ), 114 ('geometric indicators' ,0x25A0 ,0x2600 ), 115 ('checkboxes and stars' ,0x2600 ,0x2700 ), 116 ('checks, crosses and navigation' ,0x2700 ,0x2800 ), 117 ('braille patterns' ,0x2800 ,0x2900 ), 118 ('replacement character' ,0xFFFD ,0xFFFE )] 119sections = [(title ,points )for title ,start ,end in blocks 120if (points := [cp for cp in coverage if start <= cp < end ])] 121assert sorted (cp for _ ,points in sections for cp in points )== coverage 122columns ,cell_width ,cell_height = 16 ,92 ,108 123height = 320 + sum (72 + ((len (points )+ columns - 1 )// columns )* cell_height for _ ,points in sections ) 124panel = Image .new ('RGB' , (1600 ,height ),BACKGROUND ) 125draw = ImageDraw .Draw (panel ) 126path = ROOT / 'build/TTF/iocgnMono-Regular.ttf' 127label = ImageFont .truetype (str (path ),size = 32 ) 128small = ImageFont .truetype (str (path ),size = 24 ) 129title = 'other characters' if extended else 'ascii + latin-1 + latin extended-a' 130draw .text ((MARGIN ,48 ),f'iocgn mono / { title } ' ,font = label ,fill = ACCENT ,anchor = 'lt' ) 131draw .text ((MARGIN ,230 ),'Regular + Bold' ,font = label ,fill = MUTED ) 132sample = '→✓□ / ⣿' if extended else 'Aa / Àæ / Āčę' 133rows = [[(sample ,96 ,192 ,MARGIN )], []] 134sources = [(font .getGlyphSet (),font .getBestCmap (),48 / font ['head' ].unitsPerEm )for font in fonts ] 135y = 284 136for title ,points in sections : 137draw .line ((MARGIN ,y ,1536 ,y ),fill = RULE ,width = 2 ) 138draw .text ((MARGIN ,y + 20 ),title ,font = label ,fill = ACCENT ) 139y += 72 140for i ,cp in enumerate (points ): 141left ,top = MARGIN + (i % columns )* cell_width ,y + (i // columns )* cell_height 142draw .text ((left + cell_width / 2 ,top + 78 ),f' { cp :04X } ' ,font = small ,fill = MUTED ,anchor = 'mt' ) 143for style , (glyphs ,cmap ,scale )in enumerate (sources ): 144pen = BoundsPen (glyphs ) 145glyphs [cmap [cp ]].draw (pen ) 146center = left + (27 if style == 0 else 65 ) 147if pen .bounds : 148# Center the ink, including zero-advance marks, rather than the advance cell. 149x = center - (pen .bounds [0 ]+ pen .bounds [2 ])/ 2 * scale 150rows [style ].append ((chr (cp ),48 ,top + 58 ,x )) 151else : 152assert cp in (0x20 ,0xA0 ,0x2800 ),f'Unexpected empty glyph: U+ { cp :04X } ' 153draw .text ((center ,top + 40 ), {0x20 :'SP' ,0xA0 :'NB' ,0x2800 :'BL' }[cp ],font = small ,fill = MUTED ,anchor = 'mm' ) 154y += ((len (points )+ columns - 1 )// columns )* cell_height 155for font ,glyph_rows in zip (fonts ,rows ): 156panel .paste (INK , (0 ,0 ),code_mask (font ,glyph_rows ,panel .size )) 157legend = 'BL = blank braille' if extended else 'SP = space / NB = non-breaking space' 158draw .text ((MARGIN ,height - 42 ),legend ,font = small ,fill = MUTED ) 159return panel ,coverage 160 161 162def main (): 163output = ROOT / 'demos' 164output .mkdir (exist_ok = True ) 165manifest = {'rendering' :'unhinted vectors, 4x supersampling; fixed advances; no kerning or ligatures' , 166'em_pixels' : [112 ,48 ,42 ,36 ,30 ,28 ,24 ,20 ,16 ], 167'renderer_sha256' :hashlib .sha256 (Path (__file__ ).read_bytes ()).hexdigest (), 168'fonts' : {}} 169with TTFont (ROOT / 'build/TTF/iocgnMono-Regular.ttf' )as regular , \ 170TTFont (ROOT / 'build/TTF/iocgnMono-Bold.ttf' )as bold : 171for font ,style ,weight in [(regular ,'Regular' ,400 ), (bold ,'Bold' ,700 )]: 172path = ROOT / 'build/TTF' / f'iocgnMono- { style } .ttf' 173manifest ['fonts' ][path .name ]= hashlib .sha256 (path .read_bytes ()).hexdigest () 174showcase (font ,style ,weight ).save (output / f' { style . lower () } .png' ) 175manifest ['character_sheets' ]= {} 176for extended ,filename in ((False ,'characters.png' ), (True ,'characters-extended.png' )): 177sheet ,coverage = character_sheet ((regular ,bold ),extended = extended ) 178sheet .save (output / filename ) 179manifest ['character_sheets' ][filename ]= { 180'em_pixels' : [96 ,48 ],'codepoints' :coverage , 181'styles' : ['Regular' ,'Bold' ],'combining_marks' :'isolated outlines' } 182 (ROOT / 'build/demo-manifest.json' ).write_text (json .dumps (manifest ,indent = 2 )+ '\n' ) 183'Rendered demos/regular.png, demos/bold.png, demos/characters.png and demos/characters-extended.png.' ) 184 185 186if __name__ == '__main__' : 187main ()