import sys, os, os.path, re, argparse import defcon from multiprocessing import Pool from fontTools.designspaceLib import DesignSpaceDocument from ufo2ft.filters import loadFilters from datetime import datetime sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'tools'))) from common import getGitHash, getVersion OPT_EDITABLE = False # --editable def update_version(ufo): version = getVersion() buildtag, buildtagErrs = getGitHash() now = datetime.utcnow() if buildtag == "" or len(buildtagErrs) > 0: buildtag = "src" print("warning: getGitHash() failed: %r" % buildtagErrs, file=sys.stderr) versionMajor, versionMinor = [int(num) for num in version.split(".")] ufo.info.versionMajor = versionMajor ufo.info.versionMinor = versionMinor ufo.info.year = now.year ufo.info.openTypeNameVersion = "Version %d.%03d;git-%s" % (versionMajor, versionMinor, buildtag) psFamily = re.sub(r'\s', '', ufo.info.familyName) psStyle = re.sub(r'\s', '', ufo.info.styleName) # # id format: # version ";" "git-" git-tag ";" foundry-tag ";" ps_family "-" ps_style # E.g. # "4.001;git-4de559246;RSMS;Inter-DisplayThinItalic" # Note: this should match what generated by fontmake. # fix-static-display-names.py depends on this format being consistent for all fonts. # if buildtag != "src": buildtag = "git-" + buildtag ufo.info.openTypeNameUniqueID = "%d.%03d;%s;%s;%s-%s" % ( versionMajor, versionMinor, buildtag, ufo.info.openTypeOS2VendorID, psFamily, psStyle) ufo.info.openTypeHeadCreated = now.strftime("%Y/%m/%d %H:%M:%S") def ufo_set_wws(ufo): # Fix missing WWS entries for Display fonts: # See https://github.com/googlefonts/glyphsLib/issues/820 subfamily = ufo.info.styleName if subfamily.find("Display") == -1: return subfamily = subfamily[len("Display"):].strip() if subfamily == "": # "Display" -> "Regular" subfamily = "Regular" ufo.info.openTypeNameWWSFamilyName = "Inter Display" ufo.info.openTypeNameWWSSubfamilyName = subfamily def fix_opsz_range(designspace): opsz_min = 1000000 opsz_max = 0 opsz_name = '' opsz_axis = None for opsz_axis in designspace.axes: if opsz_axis.tag == "opsz": opsz_name = opsz_axis.name break for instance in designspace.instances: opsz_value = instance.location[opsz_name] if opsz_value < opsz_min: opsz_min = opsz_value if opsz_value > opsz_max: opsz_max = opsz_value opsz_axis.minimum = opsz_min opsz_axis.maximum = opsz_max return designspace def fix_wght_range(designspace): for a in designspace.axes: if a.tag == "wght": a.minimum = 100 a.maximum = 900 break return designspace def should_decompose_glyph(g): if not g.components or len(g.components) == 0: return False # Does the component have non-trivial transformation? (i.e. scaled or skewed) # Example of no transformation: (identity matrix) # (1, 0, 0, 1, 0, 0) no scale or offset # Example of simple offset transformation matrix: # (1, 0, 0, 1, 20, 30) 20 x offset, 30 y offset # Example of scaled transformation matrix: # (-1.0, 0, 0.3311, 1, 1464.0, 0) flipped x axis, sheered and offset # Matrix order: # (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs) for cn in g.components: # if g.name == 'dotmacron.lc': # print(f"{g.name} cn {cn.baseGlyph}", cn.transformation) # Check if transformation is not identity (ignoring x & y offset) m = cn.transformation if m[0] + m[1] + m[2] + m[3] != 2.0: return True return False def copy_component_anchors(font, g): # do nothing if there are no components or if g has anchors already if not g.components or len(g.anchors) > 0: return anchor_names = set() for cn in g.components: if cn.transformation[1] != 0.0 or cn.transformation[2] != 0.0: print(f"TODO: support transformations with skew ({g.name})") return cn_g = font[cn.baseGlyph] # copy_component_anchors(font, cn_g) # depth first for a in cn_g.anchors: # Check if there are multiple components with achors with the same name. # Don't copy any anchors if there are duplicate "_..." anchors if a.name in anchor_names and len(a.name) > 1 and a.name[0] == '_': return anchor_names.add(a.name) if len(anchor_names) == 0: return anchor_names.clear() for cn in g.components: for a in font[cn.baseGlyph].anchors: if a.name in anchor_names: continue anchor_names.add(a.name) a2 = defcon.Anchor(glyph=g, anchorDict=a.copy()) m = cn.transformation # (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs) a2.x += m[4] * m[0] a2.y += m[5] * m[3] g.appendAnchor(a2) def copy_anchors_from_components(font, g): # We use two passes here, to deduplicate anchors which appear in several components. # Two assumptions are made: # 1. Insertion order of Python dict() is retained (true for Python >=3.7) # 2. Base components are listed first, mark/accent comonents later. # e.g. in /Ecircumflex, /E comes before /circumflexcomb, so we use "top" # anchor from /circumflexcomb rather than /E # skip certain glyphs if len(g.unicodes) == 0 or len(g.anchors) > 0 or not g.components: return add_anchors = dict() names_to_copy = set(('top', 'bottom')) for cn in g.components: checked_cn_skew = False #print(f" [{g.name}] cn {cn.baseGlyph}") cn_g = font[cn.baseGlyph] copy_anchors_from_components(font, cn_g) # depth first for a in cn_g.anchors: if a.name not in names_to_copy: continue #print(f" [{g.name}] use anchor {a.name}") m = cn.transformation # (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs) if not checked_cn_skew: checked_cn_skew = True if m[1] != 0.0 or m[2] != 0.0: #print(f"TODO: skewed components ({cn_g.name} used by {g.name})") return a2 = defcon.Anchor(glyph=g, anchorDict=a.copy()) a2.x += m[4] * m[0] a2.y += m[5] * m[3] add_anchors[a.name] = a2 for a in add_anchors.values(): #print(f" [{g.name}] append anchor {a.name}") g.appendAnchor(a) def find_glyphs_to_decompose(designspace_source): glyph_names = set() # print("find_glyphs_to_decompose inspecting %r" % designspace_source.name) font = defcon.Font(designspace_source.path) for g in font: # copy_anchors_from_components(font, g) if should_decompose_glyph(g): glyph_names.add(g.name) font.save(designspace_source.path) return list(glyph_names) def set_ufo_filter(ufo, **filter_dict): filters = ufo.lib.setdefault("com.github.googlei18n.ufo2ft.filters", []) for i in range(len(filters)): if filters[i].get("name") == filter_dict["name"]: filters[i] = filter_dict return filters.append(filter_dict) def del_ufo_filter(ufo, name): filters = ufo.lib.get("com.github.googlei18n.ufo2ft.filters") if not filters: return for i in range(len(filters)): if filters[i].get("name") == name: filters.pop(i) return def update_source_ufo(ufo_file, glyphs_to_decompose): print(f"update {os.path.basename(ufo_file)}") ufo = defcon.Font(ufo_file) update_version(ufo) set_ufo_filter(ufo, name="decomposeComponents", include=glyphs_to_decompose) # decompose now, up front, instead of later when compiling fonts if not OPT_EDITABLE: preFilters, postFilters = loadFilters(ufo) for filter in preFilters: filter(ufo) for filter in postFilters: filter(ufo) # del_ufo_filter(ufo, "decomposeComponents") del ufo.lib["com.github.googlei18n.ufo2ft.filters"] ufo_set_wws(ufo) # Fix missing WWS entries for Display fonts ufo.save(ufo_file) def update_sources(designspace): with Pool() as p: sources = [source for source in designspace.sources] # sources = [s for s in sources if s.name == "Inter Thin"] # DEBUG glyphs_to_decompose = set() for glyph_names in p.map(find_glyphs_to_decompose, sources): glyphs_to_decompose.update(glyph_names) glyphs_to_decompose = list(glyphs_to_decompose) # print("glyphs marked to be decomposed: %s" % ', '.join(glyphs_to_decompose)) source_files = list(set([s.path for s in sources])) p.starmap(update_source_ufo, [(path, glyphs_to_decompose) for path in source_files]) return designspace def main(argv): ap = argparse.ArgumentParser(description= 'Fixup designspace and source UFOs after they are generated by fontmake from Glyphs source') ap.add_argument('--editable', action='store_true', help="Generate UFOs suitable for further editing (don't apply filters)") ap.add_argument("designspace", help="Path to designspace file") args = ap.parse_args() OPT_EDITABLE = args.editable designspace = DesignSpaceDocument.fromfile(args.designspace) designspace = fix_opsz_range(designspace) designspace = fix_wght_range(designspace) designspace = update_sources(designspace) designspace.write(args.designspace) if __name__ == '__main__': main(sys.argv)