char/iocgn-mono
iosevka variant inspired by Simple Köln-Bonn
git clone https://git.t4t.associates/char/iocgn-mono
9f76a66
main
1"""Build a ten-degree oblique from the compatible upright weight masters.""" 2 3from contextlib import ExitStack 4import hashlib 5import json 6from math import radians ,tan 7from pathlib import Path 8 9from fontTools .misc .roundTools import otRound 10from fontTools .misc .transform import Transform 11from fontTools .pens .boundsPen import BoundsPen 12from fontTools .pens .transformPen import TransformPen 13from fontTools .ttLib import TTFont 14 15from masters import ARROWS ,CELL_GRAPHICS ,MASTERS 16 17ROOT = Path (__file__ ).resolve ().parent .parent 18ANGLE = 10 19QUOTE_SPREAD = 16 20SHEAR = tan (radians (ANGLE )) 21 22 23def bounds (glyph_sets ,name ,transform ): 24boxes = [] 25for glyphs in glyph_sets : 26pen = BoundsPen (glyphs ) 27glyphs [name ].draw (TransformPen (pen ,transform )) 28if pen .bounds : 29boxes .append (pen .bounds ) 30if boxes : 31return (min (b [0 ]for b in boxes ),min (b [1 ]for b in boxes ), 32max (b [2 ]for b in boxes ),max (b [3 ]for b in boxes )) 33 34 35def name_oblique (font ,style ,weight ): 36subfamily = 'Oblique' if style == 'Regular' else f' { style } Oblique' 37legacy_family = 'iocgn Mono' if weight in (400 ,700 )else f'iocgn Mono { style } ' 38legacy_style = 'Bold Italic' if weight == 700 else 'Italic' 39postscript = f'iocgnMono- { subfamily . replace ( " " , "" ) } ' 40version = font ['head' ].fontRevision 41values = {1 :legacy_family ,2 :legacy_style ,3 :f' { postscript } - { version :.3f } ' , 424 :f'iocgn Mono { subfamily } ' ,6 :postscript ,16 :'iocgn Mono' , 4317 :subfamily ,21 :'iocgn Mono' ,22 :subfamily ,25 :'iocgnMonoOblique' } 44for record in font ['name' ].names : 45if record .nameID in values : 46font ['name' ].setName (values [record .nameID ],record .nameID , 47record .platformID ,record .platEncID ,record .langID ) 48for name_id ,value in values .items (): 49font ['name' ].setName (value ,name_id ,3 ,1 ,0x409 ) 50# Legacy Windows families use Italic for style linking, even for an oblique design. 51font ['OS/2' ].fsSelection = (font ['OS/2' ].fsSelection & ~ 0x61 )| 0x201 | (0x20 if weight == 700 else 0 ) 52font ['head' ].macStyle = 2 | (1 if weight == 700 else 0 ) 53font ['post' ].italicAngle = - ANGLE 54font ['hhea' ].caretSlopeRise = 1000 55font ['hhea' ].caretSlopeRun = otRound (1000 * SHEAR ) 56font ['hhea' ].caretOffset = 0 57 58 59def main (): 60output = ROOT / 'build/oblique' 61output .mkdir (parents = True ,exist_ok = True ) 62paths = [ROOT / 'build/variable' / f' { style } .ttf' for style ,_ in MASTERS ] 63masters = [output / f' { style } .ttf' for style ,_ in MASTERS ] 64report = {'angle' :ANGLE ,'quote_spread' :QUOTE_SPREAD ,'narrowed' : {},'accent_shifts' : {},'sources' : { 65str (path .relative_to (ROOT )):hashlib .sha256 (path .read_bytes ()).hexdigest () 66for path in [Path (__file__ ),* paths ]}} 67with ExitStack ()as stack : 68fonts = [stack .enter_context (TTFont (path ,recalcTimestamp = False ))for path in paths ] 69glyph_sets = [font .getGlyphSet ()for font in fonts ] 70order = fonts [0 ].getGlyphOrder () 71cmap = fonts [0 ].getBestCmap () 72# A little extra gap keeps double quotes distinct under small-size grayscale rendering. 73for font in fonts : 74quote = font ['glyf' ][cmap [ord ('"' )]] 75start = 0 76for end in quote .endPtsOfContours : 77xs = [quote .coordinates [i ][0 ]for i in range (start ,end + 1 )] 78shift = QUOTE_SPREAD if (min (xs )+ max (xs ))/ 2 > 250 else - QUOTE_SPREAD 79for i in range (start ,end + 1 ): 80x ,y = quote .coordinates [i ] 81quote .coordinates [i ]= (x + shift ,y ) 82start = end + 1 83punctuation = {name for cp ,name in cmap .items ()if chr (cp )in "'\"`.,:;!?()[]{}<>/\\|*~_^+-=" or 0x2010 <= cp <= 0x203A } 84upright_symbols = {cmap [cp ]for cp in CELL_GRAPHICS | ARROWS } 85transforms = {} 86for name in order : 87# Preserve arrow directions and terminal cell joins under oblique selection. 88if name in upright_symbols : 89transforms [name ]= Transform () 90continue 91pivot = 262.5 92if name in punctuation : 93box = bounds ([glyph_sets [1 ]],name ,Transform ()) 94if box : 95pivot = (box [1 ]+ box [3 ])/ 2 96scale = 1.0 97# Bound y's overhang so its diagonal stays broad but clears a preceding underscore. 98left = - 40 if name == cmap [ord ('y' )]else 14 99transform = Transform (scale ,0 ,SHEAR ,1 , (1 - scale )* 250 - SHEAR * pivot ,0 ) 100box = bounds (glyph_sets ,name ,transform ) 101if box and box [2 ]- box [0 ]> 486 - left : 102lo ,hi = 0.0 ,1.0 103for _ in range (24 ): 104scale = (lo + hi )/ 2 105transform = Transform (scale ,0 ,SHEAR ,1 , (1 - scale )* 250 - SHEAR * pivot ,0 ) 106box = bounds (glyph_sets ,name ,transform ) 107if box [2 ]- box [0 ]<= 486 - left : 108lo = scale 109else : 110hi = scale 111scale = lo 112transform = Transform (scale ,0 ,SHEAR ,1 , (1 - scale )* 250 - SHEAR * pivot ,0 ) 113box = bounds (glyph_sets ,name ,transform ) 114report ['narrowed' ][name ]= scale 115if box : 116shift = max (left - box [0 ],min (0 ,486 - box [2 ])) 117transform = Transform (* transform [:4 ],transform .dx + shift ,0 ) 118transforms [name ]= transform 119accented = {name for name in order if fonts [0 ]['glyf' ][name ].isComposite ()} 120# An accented letter must keep exactly the same base as its unaccented form. 121for name in accented : 122base = fonts [0 ]['glyf' ][name ].components [0 ].glyphName 123transforms [name ]= transforms [base ] 124report ['narrowed' ].pop (name ,None ) 125for font in fonts : 126for name in order : 127glyph = font ['glyf' ][name ] 128transform = transforms [name ] 129if glyph .isComposite (): 130for component in glyph .components : 131child ,original = component .getComponentInfo () 132matrix = transform .transform (original ).transform (transforms [child ].inverse ()) 133component .transform = ((matrix .xx ,matrix .xy ), (matrix .yx ,matrix .yy )) 134component .x ,component .y = otRound (matrix .dx ),otRound (matrix .dy ) 135# The conjugated offsets are in parent coordinates. 136component .flags = (component .flags & ~ 0x0800 )| 0x1000 137elif glyph .numberOfContours : 138glyph .coordinates .transform (((transform .xx ,transform .xy ), (transform .yx ,transform .yy ))) 139glyph .coordinates .translate ((transform .dx ,transform .dy )) 140glyph .coordinates .toInt () 141# Apply each glyph's optical correction to both sides of mark attachment. 142for lookup in font ['GPOS' ].table .LookupList .Lookup : 143for table in lookup .SubTable : 144if lookup .LookupType == 4 : 145groups = ((table .MarkCoverage .glyphs ,table .MarkArray .MarkRecord ,'MarkAnchor' ), 146 (table .BaseCoverage .glyphs ,table .BaseArray .BaseRecord ,'BaseAnchor' )) 147elif lookup .LookupType == 6 : 148groups = ((table .Mark1Coverage .glyphs ,table .Mark1Array .MarkRecord ,'MarkAnchor' ), 149 (table .Mark2Coverage .glyphs ,table .Mark2Array .Mark2Record ,'Mark2Anchor' )) 150else : 151raise ValueError (f'Unsupported positioning lookup: { lookup . LookupType } ' ) 152for names ,records ,field in groups : 153for name ,record in zip (names ,records ): 154anchors = getattr (record ,field ) 155if not isinstance (anchors ,list ): 156anchors = [anchors ] 157for anchor in anchors : 158if anchor : 159anchor .XCoordinate ,anchor .YCoordinate = map (otRound ,transforms [name ].transformPoint ( 160 (anchor .XCoordinate ,anchor .YCoordinate ))) 161glyph_sets = [font .getGlyphSet ()for font in fonts ] 162for name in accented : 163count = len (fonts [0 ]['glyf' ][name ].components ) 164for index in range (1 ,count ): 165boxes = [] 166for font ,glyphs in zip (fonts ,glyph_sets ): 167child ,matrix = font ['glyf' ][name ].components [index ].getComponentInfo () 168boxes .append (bounds ([glyphs ],child ,matrix )) 169left ,right = min (b [0 ]for b in boxes ),max (b [2 ]for b in boxes ) 170shift = otRound (max (14 - left ,min (0 ,486 - right ))) 171if shift : 172report ['accent_shifts' ][name ]= shift 173for font in fonts : 174font ['glyf' ][name ].components [index ].x += shift 175for font ,path , (style ,weight )in zip (fonts ,masters ,MASTERS ): 176for name in order : 177glyph = font ['glyf' ][name ] 178glyph .recalcBounds (font ['glyf' ]) 179font ['hmtx' ][name ]= (font ['hmtx' ][name ][0 ],getattr (glyph ,'xMin' ,0 )) 180name_oblique (font ,style ,weight ) 181font .save (path ) 182report ['masters' ]= {path .name :hashlib .sha256 (path .read_bytes ()).hexdigest ()for path in masters } 183 (output / 'manifest.json' ).write_text (json .dumps (report ,indent = 2 ,sort_keys = True )+ '\n' ) 184'Prepared four ten-degree oblique weight masters.' ) 185 186 187if __name__ == '__main__' : 188main ()