| #!/usr/bin/env python3 | 
 | # | 
 | # Copyright 2013 The Flutter Authors. All rights reserved. | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | import sys, math | 
 | from operator import itemgetter | 
 | from itertools import groupby | 
 | import unicodedata | 
 | import fontforge | 
 |  | 
 | NAME = "FlutterTest" | 
 | # Turn off auto-hinting and enable manual hinting. FreeType skips auto-hinting | 
 | # if the font's family name is in a hard-coded "tricky" font list. | 
 | TRICKY_NAME = "MingLiU" | 
 | EM = 1024 | 
 | DESCENT = -EM // 4 | 
 | ASCENT = EM + DESCENT | 
 | # -143 and 20 are the underline location and width Ahem uses. | 
 | UPOS = -143 * 1000 // EM | 
 | UWIDTH = 20 * 1000 // EM | 
 |  | 
 | ### Font Metadata and Metrics | 
 |  | 
 | font = fontforge.font() | 
 | font.familyname = TRICKY_NAME | 
 | font.fullname = NAME | 
 | font.fontname = NAME | 
 |  | 
 | # This sets the relevant fields in the os2 table and hhea table. | 
 | font.ascent = ASCENT | 
 | font.descent = -DESCENT | 
 | font.upos = UPOS | 
 | font.uwidth = UWIDTH | 
 | font.hhea_linegap = 0 | 
 | font.os2_typolinegap = 0 | 
 |  | 
 | font.horizontalBaseline = ( | 
 |     ("hang", "ideo", "romn"), | 
 |     ( | 
 |         ("latn", "romn", (ASCENT, DESCENT, 0), ()), | 
 |         ("grek", "romn", (ASCENT, DESCENT, 0), ()), | 
 |         ("hani", "ideo", (ASCENT, DESCENT, 0), ()), | 
 |     ), | 
 | ) | 
 |  | 
 | ### TrueType Hinting | 
 |  | 
 | # Hints are ignored on macOS. | 
 | # | 
 | # These hints only **vertically** adjust the outlines, for better vertical | 
 | # alignment in golden tests. They don't affect the font or the glyphs' public | 
 | # metrics available to the framework, so they typically don't affect non-golden | 
 | # tests. | 
 | # | 
 | # The hinting goals are: | 
 | # | 
 | # 1. Aligning the key points on glyph outlines between glyphs, when different | 
 | #    types of glyphs are placed side by side. E.g., for a given point size, "p" | 
 | #    and "É" should never overlap vertically, and "p" and "x" should be | 
 | #    bottom-aligned. | 
 | # | 
 | # 2. Aligning the top and the bottom of the "x" glyph with the background. With | 
 | #    point size = 14, since the em square's y-extent is 3.5 px (256 * 14 / 1024) | 
 | #    below the baseline and 10.5 px above the baseline, the glyph's CBOX will be | 
 | #    "rounded out" (3.5 -> 4, 10.5 -> 11). So "x" is going to be misaligned with | 
 | #    the background by +0.5 px when rasterized without proper grid-fitting. | 
 |  | 
 | # Allocate space in cvt. | 
 | font.cvt = [0] | 
 | # gcd is used to avoid overflowing, this works for the current ASCENT and EM value. | 
 | gcd = math.gcd(ASCENT, EM) | 
 | # The control value program is for computing the y-offset (in pixels) to move | 
 | # the embox's top edge to grid. The end result will be stored to CVT entry 0. | 
 | # CVT[0] = (pointSize * ASCENT / EM) - ceil(pointSize * ASCENT / EM) | 
 | prep_program = f""" | 
 |     RTG | 
 |     PUSHW_1 | 
 |     0 | 
 |     MPS | 
 |     PUSHW_1 | 
 |     {(ASCENT << 6) // gcd} | 
 |     MUL | 
 |     PUSHW_1 | 
 |     {EM // gcd} | 
 |     DIV | 
 |     DUP | 
 |     CEILING | 
 |     SUB | 
 |     WCVTP | 
 | """ | 
 | font.setTableData("prep", fontforge.parseTTInstrs(prep_program)) | 
 |  | 
 |  | 
 | def glyph_program(glyph): | 
 |   # Shift Zone 1 by CVT[0]. In FreeType SHZ actually shifts the zone zp2 | 
 |   # points to, instead of top of the stack. That's probably a bug. | 
 |   instructions = """ | 
 |         SVTCA[0] | 
 |         PUSHB_4 | 
 |         0 | 
 |         0 | 
 |         0 | 
 |         0 | 
 |         SZPS | 
 |         MIRP[0000] | 
 |         SRP2 | 
 |         PUSHB_3 | 
 |         1 | 
 |         1 | 
 |         1 | 
 |         SZP2 | 
 |         SHZ[0] | 
 |         SZPS | 
 |     """ | 
 |  | 
 |   # Round To Grid every on-curve point, but ignore those who are on the ASCENT | 
 |   # or DESCENT line. This step keeps "p" (ascent flushed) and "É" (descent | 
 |   # flushed)'s y extents from overlapping each other. | 
 |   for index, point in enumerate( | 
 |       [p for contour in glyph.foreground for p in contour]): | 
 |     if point.y not in [ASCENT, DESCENT]: | 
 |       instructions += f""" | 
 |                 PUSHB_1 | 
 |                 {index} | 
 |                 MDAP[1] | 
 |             """ | 
 |   return fontforge.parseTTInstrs(instructions) | 
 |  | 
 |  | 
 | ### Creating Glyphs Outlines | 
 |  | 
 |  | 
 | def square_glyph(glyph): | 
 |   pen = glyph.glyphPen() | 
 |   # Counter Clockwise | 
 |   pen.moveTo((0, DESCENT)) | 
 |   pen.lineTo((0, ASCENT)) | 
 |   pen.lineTo((EM, ASCENT)) | 
 |   pen.lineTo((EM, DESCENT)) | 
 |   pen.closePath() | 
 |   glyph.ttinstrs = glyph_program(glyph) | 
 |  | 
 |  | 
 | def ascent_flushed_glyph(glyph): | 
 |   pen = glyph.glyphPen() | 
 |   pen.moveTo((0, DESCENT)) | 
 |   pen.lineTo((0, 0)) | 
 |   pen.lineTo((EM, 0)) | 
 |   pen.lineTo((EM, DESCENT)) | 
 |   pen.closePath() | 
 |   glyph.ttinstrs = glyph_program(glyph) | 
 |  | 
 |  | 
 | def descent_flushed_glyph(glyph): | 
 |   pen = glyph.glyphPen() | 
 |   pen.moveTo((0, 0)) | 
 |   pen.lineTo((0, ASCENT)) | 
 |   pen.lineTo((EM, ASCENT)) | 
 |   pen.lineTo((EM, 0)) | 
 |   pen.closePath() | 
 |   glyph.ttinstrs = glyph_program(glyph) | 
 |  | 
 |  | 
 | def not_def_glyph(glyph): | 
 |   pen = glyph.glyphPen() | 
 |   # Counter Clockwise for the outer contour. | 
 |   pen.moveTo((EM // 8, 0)) | 
 |   pen.lineTo((EM // 8, ASCENT)) | 
 |   pen.lineTo((EM - EM // 8, ASCENT)) | 
 |   pen.lineTo((EM - EM // 8, 0)) | 
 |   pen.closePath() | 
 |   # Clockwise, inner contour. | 
 |   pen.moveTo((EM // 4, EM // 8)) | 
 |   pen.lineTo((EM - EM // 4, EM // 8)) | 
 |   pen.lineTo((EM - EM // 4, ASCENT - EM // 8)) | 
 |   pen.lineTo((EM // 4, ASCENT - EM // 8)) | 
 |   pen.closePath() | 
 |   glyph.ttinstrs = glyph_program(glyph) | 
 |  | 
 |  | 
 | def unicode_range(fromUnicode, throughUnicode): | 
 |   return range(fromUnicode, throughUnicode + 1) | 
 |  | 
 |  | 
 | square_codepoints = [ | 
 |     codepoint for l in [ | 
 |         unicode_range(0x21, 0x26), | 
 |         unicode_range(0x28, 0x6F), | 
 |         unicode_range(0x71, 0x7E), | 
 |         unicode_range(0xA1, 0xC8), | 
 |         unicode_range(0xCA, 0xFF), | 
 |         [0x131], | 
 |         unicode_range(0x152, 0x153), | 
 |         [0x178, 0x192], | 
 |         unicode_range(0x2C6, 0x2C7), | 
 |         [0x2C9], | 
 |         unicode_range(0x2D8, 0x2DD), | 
 |         [0x394, 0x3A5, 0x3A7, 0x3A9, 0x3BC, 0x3C0], | 
 |         unicode_range(0x2013, 0x2014), | 
 |         unicode_range(0x2018, 0x201A), | 
 |         unicode_range(0x201C, 0x201E), | 
 |         unicode_range(0x2020, 0x2022), | 
 |         [0x2026, 0x2030], | 
 |         unicode_range(0x2039, 0x203A), | 
 |         [0x2044, 0x2122, 0x2126, 0x2202, 0x2206, 0x220F], | 
 |         unicode_range(0x2211, 0x2212), | 
 |         unicode_range(0x2219, 0x221A), | 
 |         [0x221E, 0x222B, 0x2248, 0x2260], | 
 |         unicode_range(0x2264, 0x2265), | 
 |         [ | 
 |             0x22F2, 0x25CA, 0x3007, 0x4E00, 0x4E03, 0x4E09, 0x4E5D, 0x4E8C, | 
 |             0x4E94, 0x516B, 0x516D, 0x5341, 0x56D7, 0x56DB, 0x571F, 0x6728, | 
 |             0x6C34, 0x706B, 0x91D1 | 
 |         ], | 
 |         unicode_range(0xF000, 0xF002), | 
 |     ] for codepoint in l | 
 | ] + [0x70] + [ord(c) for c in "中文测试文本是否正确"] | 
 |  | 
 | no_path_codepoints = [ | 
 |     #(codepoint, advance %) | 
 |     (0x0020, 1), | 
 |     (0x00A0, 1), | 
 |     (0x2003, 1), | 
 |     (0x3000, 1), | 
 |     (0x2002, 1 / 2), | 
 |     (0x2004, 1 / 3), | 
 |     (0x2005, 1 / 4), | 
 |     (0x2006, 1 / 6), | 
 |     (0x2009, 1 / 5), | 
 |     (0x200A, 1 / 10), | 
 |     (0xFEFF, 0), | 
 |     (0x200B, 0), | 
 |     (0x200C, 0), | 
 |     (0x200D, 0), | 
 | ] | 
 |  | 
 |  | 
 | def create_glyph(name, contour): | 
 |   glyph = font.createChar(-1, name) | 
 |   contour(glyph) | 
 |   glyph.width = EM | 
 |   return glyph | 
 |  | 
 |  | 
 | if square_codepoints: | 
 |   create_glyph("Square", square_glyph).altuni = square_codepoints | 
 | create_glyph("Ascent Flushed", ascent_flushed_glyph).unicode = 0x70 | 
 | create_glyph("Descent Flushed", descent_flushed_glyph).unicode = 0xC9 | 
 | create_glyph(".notdef", not_def_glyph).unicode = -1 | 
 |  | 
 |  | 
 | def create_no_path_glyph(codepoint, advance_percentage): | 
 |   name = "Zero Advance" if advance_percentage == 0 else ( | 
 |       "Full Advance" | 
 |       if advance_percentage == 1 else f"1/{(int)(1/advance_percentage)} Advance" | 
 |   ) | 
 |   no_path_glyph = font.createChar(codepoint, name) | 
 |   no_path_glyph.width = (int)(EM * advance_percentage) | 
 |   return no_path_glyph | 
 |  | 
 |  | 
 | for (codepoint, advance_percentage) in no_path_codepoints: | 
 |   if (codepoint in square_codepoints): | 
 |     raise ValueError(f"{hex(codepoint)} is occupied.") | 
 |   create_no_path_glyph(codepoint, advance_percentage) | 
 |  | 
 | font.generate(sys.argv[1] if len(sys.argv) >= 2 else "test_font.ttf") | 
 |  | 
 | ### Printing Glyph Map Stats | 
 |  | 
 | scripts = set() | 
 | for glyph in font.glyphs(): | 
 |   if glyph.unicode >= 0: | 
 |     scripts.add(fontforge.scriptFromUnicode(glyph.unicode)) | 
 |   for codepoint, _, _ in glyph.altuni or []: | 
 |     scripts.add(fontforge.scriptFromUnicode(codepoint)) | 
 | script_list = list(scripts) | 
 | script_list.sort() | 
 |  | 
 | print(f"|     \ Script <br />Glyph | {' | '.join(script_list)} |") | 
 | print(" | :--- " + " | :----: " * len(script_list) + "|") | 
 |  | 
 | for glyph in font.glyphs(): | 
 |   if glyph.unicode < 0 and not glyph.altuni: | 
 |     continue | 
 |   glyph_mapping = {} | 
 |   if glyph.unicode >= 0: | 
 |     glyph_mapping[fontforge.scriptFromUnicode(glyph.unicode)] = [glyph.unicode] | 
 |   for codepoint, _, _ in glyph.altuni or []: | 
 |     script = fontforge.scriptFromUnicode(codepoint) | 
 |     if script in glyph_mapping: | 
 |       glyph_mapping[script].append(codepoint) | 
 |     else: | 
 |       glyph_mapping[script] = [codepoint] | 
 |  | 
 |   codepoints_by_script = [ | 
 |       glyph_mapping.get(script, []) for script in script_list | 
 |   ] | 
 |  | 
 |   def describe_codepoint_range(codepoints): | 
 |     if not codepoints: | 
 |       return "" | 
 |     codepoints.sort() | 
 |     codepoint_ranges = [ | 
 |         list(map(itemgetter(1), group)) | 
 |         for key, group in groupby(enumerate(codepoints), lambda x: x[0] - x[1]) | 
 |     ] | 
 |     characters = [chr(c) for c in codepoints] | 
 |  | 
 |     def map_char(c): | 
 |       if c == "`": | 
 |         return "`` ` ``" | 
 |       if c == "|": | 
 |         return "`\\|`" | 
 |       if c.isprintable() and (not c.isspace()): | 
 |         return f"`{c}`" | 
 |       return "`<" + unicodedata.name(c, hex(ord(c))) + ">`" | 
 |  | 
 |     full_list = " ".join([map_char(c) for c in characters]) | 
 |     return "**codepoint(s):** " + ", ".join([ | 
 |         f"{hex(r[0])}-{hex(r[-1])}" if len(r) > 1 else hex(r[0]) | 
 |         for r in codepoint_ranges | 
 |     ]) + "<br />" + "**character(s):** " + full_list | 
 |  | 
 |   print( | 
 |       f"| {glyph.glyphname} | {' | '.join([describe_codepoint_range(l) for l in codepoints_by_script])} |" | 
 |   ) |