Font that can be used for validating baseline alignments. sajidanwar.com/misc/baseline-diagnostic-font/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 248 lines 13 kB view raw
1import re 2from font import Font, FontBaseline, FontBaselineStyle, FontGlyph, FontGlyphKind, build_baselines_font 3from jinja2 import Environment, FileSystemLoader 4from textwrap import dedent, indent 5from typing import Dict, List 6 7AUTHOR = "Sajid Anwar" 8 9def main(): 10 glyphs = [ 11 FontGlyph("x", FontGlyphKind.PAIR_LAYOUT, ["x-height", "alphabetic"]), 12 FontGlyph("χ", FontGlyphKind.PAIR_LABELED, ["x-height", "alphabetic"]), 13 FontGlyph("B", FontGlyphKind.PAIR_LAYOUT, ["cap-height", "alphabetic"]), 14 FontGlyph("β", FontGlyphKind.PAIR_LABELED, ["cap-height", "alphabetic"]), 15 FontGlyph("", FontGlyphKind.PAIR_LAYOUT, ["ideographic-over", "ideographic-under"]), 16 FontGlyph("", FontGlyphKind.PAIR_LABELED, ["ideographic-over", "ideographic-under"]), 17 FontGlyph("", FontGlyphKind.PAIR_LAYOUT, ["ideographic-face-over", "ideographic-face-under"]), 18 FontGlyph("", FontGlyphKind.PAIR_LABELED, ["ideographic-face-over", "ideographic-face-under"]), 19 FontGlyph("", FontGlyphKind.PAIR_LAYOUT, ["hanging", "alphabetic"]), 20 FontGlyph("", FontGlyphKind.PAIR_LABELED, ["hanging", "alphabetic"]), 21 FontGlyph("+", FontGlyphKind.PAIR_LAYOUT, ["math", "alphabetic"]), 22 FontGlyph("±", FontGlyphKind.PAIR_LABELED, ["math", "alphabetic"]), 23 FontGlyph("", FontGlyphKind.EMBOX_FILLED), 24 FontGlyph("", FontGlyphKind.EMBOX_OUTLINE), 25 ] 26 27 fonts = [] 28 fonts.append(Font( 29 name="BaselineDiagnostic", 30 description=(dedent("""\ 31 Font that can be used for validating baseline alignments. Given the embedded 32 text in the font, this should be used with very large font sizes. There are 33 two glyphs in the font.""")), 34 baselines=[ 35 FontBaseline("ascent", 800, "OS/2", "sTypoAscender", None, None), 36 FontBaseline("ascent", 800, "hhea", "ascent", None, None), 37 FontBaseline("ascent", 800, "vhea", "ascent", None, None), 38 FontBaseline("ideographic-over", 750, "BASE", "idtp", "IDEOGRAPHIC-OVER", FontBaselineStyle.SOLID), 39 FontBaseline("hanging", 650, "BASE", "hang", "HANGING", FontBaselineStyle.SOLID), 40 FontBaseline("ideographic-face-over", 650, "BASE", "icft", "IDEO-FACE-OVER", None), 41 FontBaseline("cap-height", 550, "OS/2", "sCapHeight", "CAP-HEIGHT", FontBaselineStyle.SOLID), 42 FontBaseline("math", 450, "BASE", "math", "MATH", FontBaselineStyle.SOLID), 43 FontBaseline("central", 350, None, None, "CENTRAL", FontBaselineStyle.SOLID), 44 FontBaseline("em-middle", 300, None, None, None, FontBaselineStyle.DASHED), 45 FontBaseline("x-height", 250, "OS/2", "sxHeight", "X-HEIGHT", FontBaselineStyle.SOLID), 46 FontBaseline("x-middle", 150, None, None, "X-MIDDLE", FontBaselineStyle.SOLID), 47 FontBaseline("alphabetic", 50, "BASE", "romn", "ALPHABETIC", FontBaselineStyle.SOLID), 48 FontBaseline("ideographic-face-under", 50, "BASE", "icfb", "IDEO-FACE-UNDER", None), 49 FontBaseline("zero", 0, None, None, None, FontBaselineStyle.DASHED), 50 FontBaseline("ideographic-under", -50, "BASE", "ideo", "IDEOGRAPHIC-UNDER", FontBaselineStyle.SOLID), 51 FontBaseline("descent", -200, "OS/2", "sTypoDescender", None, None), 52 FontBaseline("descent", -200, "hhea", "descent", None, None), 53 FontBaseline("descent", -200, "vhea", "descent", None, None), 54 ], 55 glyphs=glyphs, 56 )) 57 fonts.append(Font( 58 name="BaselineDiagnosticAlphabeticZero", 59 description=(dedent("""\ 60 Same as the "BaselineDiagnostic" font, but uses the common alphabetic baseline 61 of 0. This also results in the x-middle baseline being at 125.""")), 62 baselines=[ 63 FontBaseline("ascent", 800, "OS/2", "sTypoAscender", None, None), 64 FontBaseline("ascent", 800, "hhea", "ascent", None, None), 65 FontBaseline("ascent", 800, "vhea", "ascent", None, None), 66 FontBaseline("ideographic-over", 750, "BASE", "idtp", "IDEOGRAPHIC-OVER", FontBaselineStyle.SOLID), 67 FontBaseline("hanging", 650, "BASE", "hang", "HANGING", FontBaselineStyle.SOLID), 68 FontBaseline("ideographic-face-over", 650, "BASE", "icft", "IDEO-FACE-OVER", None), 69 FontBaseline("cap-height", 550, "OS/2", "sCapHeight", "CAP-HEIGHT", FontBaselineStyle.SOLID), 70 FontBaseline("math", 450, "BASE", "math", "MATH", FontBaselineStyle.SOLID), 71 FontBaseline("central", 350, None, None, "CENTRAL", FontBaselineStyle.SOLID), 72 FontBaseline("em-middle", 300, None, None, None, FontBaselineStyle.DASHED), 73 FontBaseline("x-height", 250, "OS/2", "sxHeight", "X-HEIGHT", FontBaselineStyle.SOLID), 74 FontBaseline("x-middle", 125, None, None, "X-MIDDLE", FontBaselineStyle.SOLID), 75 FontBaseline("alphabetic", 0, "BASE", "romn", None, FontBaselineStyle.DASHED), 76 FontBaseline("ideographic-face-under", 50, "BASE", "icfb", "IDEO-FACE-UNDER", None), 77 FontBaseline("zero", 0, None, None, None, None), 78 FontBaseline("ideographic-under", -50, "BASE", "ideo", "IDEOGRAPHIC-UNDER", FontBaselineStyle.SOLID), 79 FontBaseline("descent", -200, "OS/2", "sTypoDescender", None, None), 80 FontBaseline("descent", -200, "hhea", "descent", None, None), 81 FontBaseline("descent", -200, "vhea", "descent", None, None), 82 ], 83 glyphs=glyphs, 84 )) 85 86 write_font_files(fonts) 87 write_font_stylesheet(fonts) 88 write_font_html(fonts) 89 write_font_readme(fonts) 90 write_font_license() 91 92 93def write_font_files(fonts: List[Font]): 94 for font in fonts: 95 build_baselines_font(font, f'dist/{font.name}.ttf') 96 97 98def write_font_stylesheet(fonts: List[Font]): 99 out_path = "dist/baseline-diagnostic-font.css" 100 with open(out_path, "w") as f: 101 for font in fonts: 102 template = dedent(''' 103 @font-face {{ 104 /** 105 {description} 106 */ 107 font-family: "{name}"; 108 src: url('./{name}.ttf') format('opentype'); 109 }} 110 111 :root {{ 112 /** 113 * Variables representing the positions of the given baselines/metrics from the top 114 * of the em-box as a percentage of the em-height. The top of the em-box (ascent) has 115 * a position of 0, and the bottom of the em-box (descent) has a position of 1. 116 */ 117 {variables} 118 }} 119 ''') 120 description = indent(font.description, ' * ') 121 positions: Dict[str, int] = {} 122 ascent = None 123 descent = None 124 125 for baseline in font.baselines: 126 if baseline.id in positions and positions[baseline.id] != baseline.position: 127 raise ValueError(f"Baseline metric {baseline.id} has different position values") 128 if baseline.id == 'ascent': 129 ascent = baseline.position 130 if baseline.id == 'descent': 131 descent = -1 * baseline.position 132 positions[baseline.id] = baseline.position 133 134 if not ascent or not descent: 135 raise ValueError(f"Required ascent / descent but got {ascent} / {descent}") 136 137 variables = [] 138 for baseline, position in positions.items(): 139 variables.append( 140 "--{font_name}-{baseline}: calc(1 - ({position} + {descent}) / {height});".format( 141 font_name=dashing(font.name), 142 baseline=baseline, 143 position=position, 144 descent=descent, 145 height=(ascent + descent), 146 ) 147 ) 148 149 f.write(template.format( 150 name=font.name, 151 description=description, 152 variables=indent('\n'.join(variables), ' '), 153 )) 154 print(f"Wrote stylesheet at {out_path}") 155 156 157def prepare_template_data(fonts: List[Font]) -> dict: 158 font = fonts[0] 159 font_az = fonts[1] 160 161 dfn_tooltips = { 162 'central': 'Computed at halfway between ideographic-under and ideographic-over', 163 'em-middle': 'Computed at halfway between ascent and descent', 164 'x-middle': 'Computed at halfway between alphabetic and x-height', 165 'zero': 'Zero coordinate', 166 } 167 168 seen: Dict[str, dict] = {} 169 baseline_table = [] 170 for b in font.baselines: 171 if b.id not in seen: 172 entry = {'id': b.id, 'position': b.position, 'base': '', 'os2': '', 'hhea': '', 'tooltip': dfn_tooltips.get(b.id)} 173 seen[b.id] = entry 174 baseline_table.append(entry) 175 if b.table == 'BASE': seen[b.id]['base'] = b.name 176 elif b.table == 'OS/2': seen[b.id]['os2'] = b.name 177 elif b.table == 'hhea': seen[b.id]['hhea'] = b.name 178 179 az_positions = {} 180 for b in font_az.baselines: 181 if b.id not in az_positions: 182 az_positions[b.id] = b.position 183 az_diffs = [{'id': e['id'], 'position': az_positions[e['id']]} 184 for e in baseline_table 185 if e['id'] in az_positions and az_positions[e['id']] != e['position']] 186 az_diffs.sort(key=lambda baseline: baseline['id']) 187 188 pair_map: Dict[tuple, dict] = {} 189 pair_order = [] 190 for g in font.glyphs: 191 if g.baseline_ids: 192 key = tuple(g.baseline_ids) 193 if key not in pair_map: 194 pair_map[key] = {'ids': list(key), 'layout': None, 'labeled': None} 195 pair_order.append(key) 196 glyph_data = {'char': g.char, 'codepoint': f'U+{ord(g.char):04X}'} 197 if g.kind == FontGlyphKind.PAIR_LAYOUT: pair_map[key]['layout'] = glyph_data 198 elif g.kind == FontGlyphKind.PAIR_LABELED: pair_map[key]['labeled'] = glyph_data 199 200 def embox_data(kind): 201 g = next((g for g in font.glyphs if g.kind == kind), None) 202 return {'char': g.char, 'codepoint': f'U+{ord(g.char):04X}'} if g else None 203 204 return { 205 'font_name': font.name, 206 'font_az_name': font_az.name, 207 'baseline_table': baseline_table, 208 'az_diffs': az_diffs, 209 'pairs': [pair_map[k] for k in pair_order], 210 'embox_filled': embox_data(FontGlyphKind.EMBOX_FILLED), 211 'embox_outline': embox_data(FontGlyphKind.EMBOX_OUTLINE), 212 } 213 214 215def _jinja_env(): 216 return Environment(loader=FileSystemLoader('templates'), trim_blocks=True, lstrip_blocks=True) 217 218 219def write_font_html(fonts: List[Font]): 220 out_path = "dist/index.html" 221 data = prepare_template_data(fonts) 222 html = _jinja_env().get_template('index.html.jinja').render(**data) 223 with open(out_path, 'w') as f: 224 f.write(html) 225 print(f"Wrote HTML at {out_path}") 226 227 228def write_font_readme(fonts: List[Font]): 229 out_path = "dist/README.md" 230 data = prepare_template_data(fonts) 231 md = _jinja_env().get_template('README.md.jinja').render(**data) 232 with open(out_path, 'w') as f: 233 f.write(md) 234 print(f"Wrote README at {out_path}") 235 236 237def write_font_license(): 238 out_path = "dist/LICENSE.md" 239 md = _jinja_env().get_template('LICENSE.md.jinja').render(author=AUTHOR) 240 with open(out_path, 'w') as f: 241 f.write(md) 242 print(f"Wrote OFL 1.1 license at {out_path}") 243 244def dashing(value: str): 245 return re.sub(r'(?<!^)(?=[A-Z])', '-', value).lower() 246 247if __name__ == "__main__": 248 main()