def inherit_vertical_metrics(ttFonts, family_name=None): """Inherit the vertical metrics from the same family which is hosted on Google Fonts. Args: ttFonts: a list of TTFont instances which belong to a family family_name: Optional string which allows users to specify a different family to inherit from e.g "Maven Pro". """ family_name = font_familyname( ttFonts[0]) if not family_name else family_name gf_fonts = list(map(TTFont, download_family_from_Google_Fonts(family_name))) gf_fonts = {font_stylename(f): f for f in gf_fonts} # TODO (Marc F) use Regular font instead. If VF use font which has Regular # instance gf_fallback = list(gf_fonts.values())[0] fonts = {font_stylename(f): f for f in ttFonts} for style, font in fonts.items(): if style in gf_fonts: src_font = gf_fonts[style] else: src_font = gf_fallback copy_vertical_metrics(src_font, font) if typo_metrics_enabled(src_font): font["OS/2"].fsSelection |= 1 << 7
def _append_non_fvar_axes_to_stat( ttFont, stat_table, axes_in_family_name_records, axis_reg=axis_registry ): stylename = font_stylename(ttFont) familyname = font_familyname(ttFont) style = f"{familyname} {stylename}" # {"wght": "Regular", "ital": "Roman", ...} font_axes_in_namerecords = {style_token_to_axis(t): t for t in style.split()} # Add axes to ttFont which exist across the family but are not in the # ttFont's fvar axes_missing = axes_in_family_name_records - set(stat_table) for axis in axes_missing: axis_record = { "tag": axis, "name": axis_reg[axis].display_name, "values": [], } # Add Axis Value for axis which isn't in the fvar or ttFont style # name/family name if axis not in font_axes_in_namerecords: axis_record["values"].append(_default_axis_value(axis, axis_reg)) # Add Axis Value for axis which isn't in the fvar but does exist in # the ttFont style name/family name else: style_name = font_axes_in_namerecords[axis] value = next( (i.value for i in axis_reg[axis].fallback if i.name == style_name), None, ) axis_value = _add_axis_value(style_name, value) axis_record["values"].append(axis_value) stat_table[axis] = axis_record return stat_table
def instantiate_static_fonts(self, directory, postprocessor): self.mkdir(directory, clean=True) for font in self.config["instances"]: varfont_path = os.path.join(self.config['vfDir'], font) varfont = TTFont(varfont_path) for font, instances in self.config["instances"].items(): for inst in instances: if 'familyName' in inst: family_name = inst['familyName'] else: family_name = self.config['familyName'] if "styleName" in inst: style_name = inst['styleName'] else: style_name = None static_font = gen_static_font( varfont, axes=inst["coordinates"], family_name=family_name, style_name=style_name, ) family_name = font_familyname(static_font) style_name = font_stylename(static_font) dst = os.path.join( directory, f"{family_name}-{style_name}.ttf".replace(" ", "")) static_font.save(dst) postprocessor(dst)
def css_font_classes_from_vf(ttFont, position=None): instances = ttFont["fvar"].instances nametable = ttFont["name"] family_name = font_familyname(ttFont) style_name = font_stylename(ttFont) results = [] for instance in instances: nameid = instance.subfamilyNameID inst_style = nametable.getName(nameid, 3, 1, 0x409).toUnicode() class_name = _class_name(family_name, inst_style, position) font_family = _class_name(family_name, style_name, position) font_weight = (css_font_weight(ttFont) if not "wght" in instance.coordinates else int( instance.coordinates["wght"])) font_style = "italic" if "Italic" in inst_style else "normal" font_stretch = ("100%" if not "wdth" in instance.coordinates else f"{int(instance.coordinates['wdth'])}%") font_class = CSSElement( class_name, _full_name=f"{family_name} {inst_style}", _style=inst_style, _font_path=ttFont.reader.file.name, font_family=font_family, font_weight=font_weight, font_style=font_style, font_stretch=font_stretch, ) results.append(font_class) return results
def fix_vertical_metrics(ttFonts): """Fix a family's vertical metrics based on: https://github.com/googlefonts/gf-docs/tree/main/VerticalMetrics Args: ttFonts: a list of TTFont instances which belong to a family """ src_font = next((f for f in ttFonts if font_stylename(f) == "Regular"), ttFonts[0]) # TODO (Marc F) CJK Fonts? # If OS/2.fsSelection bit 7 isn't enabled, enable it and set the typo metrics # to the previous win metrics. if not typo_metrics_enabled(src_font): src_font["OS/2"].fsSelection |= 1 << 7 # enable USE_TYPO_METRICS src_font["OS/2"].sTypoAscender = src_font["OS/2"].usWinAscent src_font["OS/2"].sTypoDescender = -src_font["OS/2"].usWinDescent src_font["OS/2"].sTypoLineGap = 0 # Set the hhea metrics so they are the same as the typo src_font["hhea"].ascent = src_font["OS/2"].sTypoAscender src_font["hhea"].descent = src_font["OS/2"].sTypoDescender src_font["hhea"].lineGap = src_font["OS/2"].sTypoLineGap # Set the win Ascent and win Descent to match the family's bounding box win_desc, win_asc = family_bounding_box(ttFonts) src_font["OS/2"].usWinAscent = win_asc src_font["OS/2"].usWinDescent = abs(win_desc) # Set all fonts vertical metrics so they match the src_font for ttFont in ttFonts: ttFont["OS/2"].fsSelection |= 1 << 7 copy_vertical_metrics(src_font, ttFont)
def _axes_in_family_name_records(ttFonts): results = set() for ttFont in ttFonts: familyname = font_familyname(ttFont) stylename = font_stylename(ttFont) results |= set(stylename_to_axes(familyname)) | set( stylename_to_axes(stylename)) return results
def css_font_faces(ttFonts, server_dir=None, position=None): """Generate @font-face CSSElements for a collection of fonts Args: ttFonts: a list containing ttFont instances server_dir: optional. A path to the root directory of the server. @font-face src urls are relative to the server's root dir. position: optional. Adds a suffix to the font-family name Returns: A list of @font-face CSSElements """ results = [] for ttFont in ttFonts: family_name = font_familyname(ttFont) style_name = font_stylename(ttFont) font_path = ttFont.reader.file.name path = (font_path if not server_dir else os.path.relpath( font_path, start=server_dir)) src = f"url({path})" font_family = _class_name(family_name, style_name, position) font_style = "italic" if font_is_italic(ttFont) else "normal" font_weight = css_font_weight(ttFont) font_stretch = WIDTH_CLASS_TO_CSS[ttFont["OS/2"].usWidthClass] if "fvar" in ttFont: fvar = ttFont["fvar"] axes = {a.axisTag: a for a in fvar.axes} if "wght" in axes: min_weight = int(axes["wght"].minValue) max_weight = int(axes["wght"].maxValue) font_weight = f"{min_weight} {max_weight}" if "wdth" in axes: min_width = int(axes["wdth"].minValue) max_width = int(axes["wdth"].maxValue) font_stretch = f"{min_width}% {max_width}%" if "ital" in axes: pass if "slnt" in axes: min_angle = int(axes["slnt"].minValue) max_angle = int(axes["slnt"].maxValue) font_style = f"oblique {min_angle}deg {max_angle}deg" font_face = CSSElement( "@font-face", src=src, font_family=font_family, font_weight=font_weight, font_stretch=font_stretch, font_style=font_style, ) results.append(font_face) return results
def fix_fvar_instances(ttFont): """Replace a variable font's fvar instances with a set of new instances that conform to the Google Fonts instance spec: https://github.com/googlefonts/gf-docs/tree/main/Spec#fvar-instances Args: ttFont: a TTFont instance """ if "fvar" not in ttFont: raise ValueError("ttFont is not a variable font") fvar = ttFont["fvar"] default_axis_vals = {a.axisTag: a.defaultValue for a in fvar.axes} stylename = font_stylename(ttFont) is_italic = "Italic" in stylename is_roman_and_italic = any(a for a in ("slnt", "ital") if a in default_axis_vals) wght_axis = next((a for a in fvar.axes if a.axisTag == "wght"), None) wght_min = int(wght_axis.minValue) wght_max = int(wght_axis.maxValue) nametable = ttFont["name"] def gen_instances(is_italic): results = [] for wght_val in range(wght_min, wght_max + 100, 100): name = (WEIGHT_VALUES[wght_val] if not is_italic else f"{WEIGHT_VALUES[wght_val]} Italic".strip()) name = name.replace("Regular Italic", "Italic") coordinates = deepcopy(default_axis_vals) coordinates["wght"] = wght_val inst = NamedInstance() inst.subfamilyNameID = nametable.addName(name) inst.coordinates = coordinates results.append(inst) return results instances = [] if is_roman_and_italic: for bool_ in (False, True): instances += gen_instances(is_italic=bool_) elif is_italic: instances += gen_instances(is_italic=True) else: instances += gen_instances(is_italic=False) fvar.instances = instances
def fix_nametable(ttFont): """Fix a static font's name table so it conforms to the Google Fonts supported styles table: https://github.com/googlefonts/gf-docs/tree/master/Spec#supported-styles Args: ttFont: a TTFont instance """ if "fvar" in ttFont: # TODO, regen the nametable so it reflects the default fvar axes # coordinates. Implement once https://github.com/fonttools/fonttools/pull/2078 # is merged. return family_name = font_familyname(ttFont) style_name = font_stylename(ttFont) update_nametable(ttFont, family_name, style_name)
def fix_nametable(ttFont): """Fix a static font's name table so it conforms to the Google Fonts supported styles table: https://github.com/googlefonts/gf-docs/tree/main/Spec#supported-styles Args: ttFont: a TTFont instance """ if "fvar" in ttFont: from fontTools.varLib.instancer.names import updateNameTable dflt_axes = {a.axisTag: a.defaultValue for a in ttFont['fvar'].axes} updateNameTable(ttFont, dflt_axes) return family_name = font_familyname(ttFont) style_name = font_stylename(ttFont) update_nametable(ttFont, family_name, style_name)
def fix_mac_style(ttFont): """Fix the head table's macStyle so it conforms to GF's supported styles table: https://github.com/googlefonts/gf-docs/tree/main/Spec#supported-styles Args: ttFont: a TTFont instance """ stylename = font_stylename(ttFont) tokens = set(stylename.split()) mac_style = 0 if "Italic" in tokens: mac_style |= 1 << 1 if "Bold" in tokens: mac_style |= 1 << 0 ttFont["head"].macStyle = mac_style
def gen_static_font( var_font, axes, family_name=None, style_name=None, keep_overlaps=False, dst=None ): """Generate a GF spec compliant static font from a variable font. Args: var_font: a variable TTFont instance family_name: font family name style_name: font style name axes: dictionary containing axis positions e.g {"wdth": 100, "wght": 400} keep_overlaps: If true, keep glyph overlaps dst: Optional. Path to output font Returns: A TTFont instance or a filepath if an out path has been provided """ if "fvar" not in var_font: raise ValueError("Font is not a variable font!") if not keep_overlaps: keep_overlaps = OverlapMode.REMOVE # if the axes dict doesn't include all fvar axes, add default fvar vals to it fvar_dflts = {a.axisTag: a.defaultValue for a in var_font["fvar"].axes} for k, v in fvar_dflts.items(): if k not in axes: axes[k] = v update_style_name = True if not style_name else False static_font = instantiateVariableFont( var_font, axes, overlap=keep_overlaps, updateFontNames=update_style_name ) if not family_name: family_name = font_familyname(static_font) if not style_name: style_name = font_stylename(static_font) # We need to reupdate the name table using our own update function # since GF requires axis particles which are not wght or ital to # be appended to the family name. See func for more details. update_nametable(static_font, family_name, style_name) fix_fs_selection(static_font) fix_mac_style(static_font) static_font["OS/2"].usWidthClass = 5 if dst: static_font.save(dst) return static_font
def css_font_class_from_static(ttFont, position=None): family_name = font_familyname(ttFont) style_name = font_stylename(ttFont) class_name = _class_name(family_name, style_name, position) font_family = class_name font_weight = css_font_weight(ttFont) font_style = "italic" if font_is_italic(ttFont) else "normal" font_stretch = WIDTH_CLASS_TO_CSS[ttFont["OS/2"].usWidthClass] return CSSElement( class_name, _full_name=f"{family_name} {style_name}", _style=style_name, _font_path=ttFont.reader.file.name, font_family=font_family, font_weight=font_weight, font_style=font_style, font_stretch=font_stretch, )
def fix_fs_selection(ttFont): """Fix the OS/2 table's fsSelection so it conforms to GF's supported styles table: https://github.com/googlefonts/gf-docs/tree/main/Spec#supported-styles Args: ttFont: a TTFont instance """ stylename = font_stylename(ttFont) tokens = set(stylename.split()) old_selection = fs_selection = ttFont["OS/2"].fsSelection # turn off all bits except for bit 7 (USE_TYPO_METRICS) fs_selection &= 1 << 7 if "Italic" in tokens: fs_selection |= 1 << 0 if "Bold" in tokens: fs_selection |= 1 << 5 # enable Regular bit for all other styles if not tokens & set(["Bold", "Italic"]): fs_selection |= 1 << 6 ttFont["OS/2"].fsSelection = fs_selection return old_selection != fs_selection
def fix_weight_class(ttFont): """Set the OS/2 table's usWeightClass so it conforms to GF's supported styles table: https://github.com/googlefonts/gf-docs/tree/main/Spec#supported-styles Args: ttFont: a TTFont instance """ old_weight_class = ttFont["OS/2"].usWeightClass stylename = font_stylename(ttFont) tokens = stylename.split() # Order WEIGHT_NAMES so longest names are first for style in sorted(WEIGHT_NAMES, key=lambda k: len(k), reverse=True): if style in tokens: ttFont["OS/2"].usWeightClass = WEIGHT_NAMES[style] return ttFont["OS/2"].usWeightClass != old_weight_class if "Italic" in tokens: ttFont["OS/2"].usWeightClass = 400 return ttFont["OS/2"].usWeightClass != old_weight_class raise ValueError( f"Cannot determine usWeightClass because font style, '{stylename}' " f"doesn't have a weight token which is in our known " f"weights, '{WEIGHT_NAMES.keys()}'")
def fix_italic_angle(ttFont): style_name = font_stylename(ttFont) if "Italic" not in style_name and ttFont["post"].italicAngle != 0: ttFont["post"].italicAngle = 0
def update_nametable(ttFont, family_name=None, style_name=None): """Update a static font's name table. The updated name table will conform to the Google Fonts support styles table: https://github.com/googlefonts/gf-docs/tree/main/Spec#supported-styles If a style_name includes tokens other than wght and ital, these tokens will be appended to the family name e.g Input: family_name="MyFont" style_name="SemiCondensed SemiBold" Output: familyName (nameID 1) = "MyFont SemiCondensed SemiBold subFamilyName (nameID 2) = "Regular" typo familyName (nameID 16) = "MyFont SemiCondensed" typo subFamilyName (nameID 17) = "SemiBold" Google Fonts has used this model for several years e.g https://fonts.google.com/?query=cabin Args: ttFont: family_name: New family name style_name: New style name """ if "fvar" in ttFont: raise ValueError("Cannot update the nametable for a variable font") nametable = ttFont["name"] # Remove nametable records which are not Win US English # TODO this is too greedy. We should preserve multilingual # names in the future. Please note, this has always been an issue. platforms = set() for rec in nametable.names: platforms.add((rec.platformID, rec.platEncID, rec.langID)) platforms_to_remove = platforms ^ set([(3, 1, 0x409)]) if platforms_to_remove: log.warning( f"Removing records which are not Win US English, {list(platforms_to_remove)}" ) for platformID, platEncID, langID in platforms_to_remove: nametable.removeNames(platformID=platformID, platEncID=platEncID, langID=langID) # Remove any name records which contain linebreaks contains_linebreaks = [] for r in nametable.names: for char in ("\n", "\r"): if char in r.toUnicode(): contains_linebreaks.append(r.nameID) for nameID in contains_linebreaks: nametable.removeNames(nameID) if not family_name: family_name = font_familyname(ttFont) if not style_name: style_name = font_stylename(ttFont) ribbi = ("Regular", "Bold", "Italic", "Bold Italic") tokens = family_name.split() + style_name.split() nameids = { 1: " ".join(t for t in tokens if t not in ribbi), 2: " ".join(t for t in tokens if t in ribbi) or "Regular", 16: " ".join(t for t in tokens if t not in list(WEIGHT_NAMES) + ['Italic']), 17: " ".join(t for t in tokens if t in list(WEIGHT_NAMES) + ['Italic']) or "Regular" } # Remove typo name if they match since they're redundant if nameids[16] == nameids[1]: del nameids[16] if nameids[17] == nameids[2]: del nameids[17] family_name = nameids.get(16) or nameids.get(1) style_name = nameids.get(17) or nameids.get(2) # create NameIDs 3, 4, 6 nameids[4] = f"{family_name} {style_name}" nameids[ 6] = f"{family_name.replace(' ', '')}-{style_name.replace(' ', '')}" nameids[3] = unique_name(ttFont, nameids) # Pass through all records and replace occurences of the old family name # with the new family name current_family_name = font_familyname(ttFont) for record in nametable.names: string = record.toUnicode() if current_family_name in string: nametable.setName( string.replace(current_family_name, family_name), record.nameID, record.platformID, record.platEncID, record.langID, ) # Remove previous typographic names for nameID in (16, 17): nametable.removeNames(nameID=nameID) # Update nametable with new names for nameID, string in nameids.items(): nametable.setName(string, nameID, 3, 1, 0x409)