diff --git a/sources/build.sh b/sources/build.sh new file mode 100755 index 0000000..ccfd699 --- /dev/null +++ b/sources/build.sh @@ -0,0 +1,40 @@ +cp WorkSans-Upright.glyphs WorkSans-UprightBuild.glyphs + +# Add bracket layers to build version +# python2 tools/fixBrackets.py WorkSans-UprightBuild.glyphs + +fontmake -o variable -g WorkSans-UprightBuild.glyphs + +mv variable_ttf/WorkSans-VF.ttf WorkSans-VF.ttf + +rm -rf WorkSans-UprightBuild.glyphs +rm -rf master_ufo +rm -rf variable_ttf + +python ./addItalics.py WorkSans-VF.ttf + +gftools fix-nonhinting WorkSans-VF.ttf WorkSans-VF.ttf +gftools fix-dsig --autofix WorkSans-VF.ttf +gftools fix-gasp WorkSans-VF.ttf +ttfautohint WorkSans-VF.ttf WorkSans-VF-Hinted.ttf --verbose + +python ./gftools-fix-vf-meta.py WorkSans-VF-Hinted.ttf + +## move font into folder of dist/, with timestamp, then fontbake the font + python3 ./distdate-and-fontbake.py WorkSans-VF-Hinted.ttf + rm -rf variable_ttf + +# ttx WorkSans-VF.ttf + +# rm -rf WorkSans-VF.ttf +# rm -rf WorkSans-Upright-VF-backup-fonttools-prep-gasp.ttf + +# cat WorkSans-Upright-VF.ttx | tr '\n' '\r' | sed -e "s,.*<\/STAT>,$(cat tools/patch1.xml | tr '\n' '\r')," | tr '\r' '\n' > WorkSans-Upright-VF-STAT.ttx + +# rm -rf WorkSans-Upright-VF.ttx + +# ttx WorkSans-Upright-VF-STAT.ttx + +# rm -rf WorkSans-Upright-VF-STAT.ttx + +# mv WorkSans-Upright-VF-STAT.ttf WorkSans-UprightBeta-VF.ttf \ No newline at end of file diff --git a/sources/tools/addItalics.py b/sources/tools/addItalics.py new file mode 100755 index 0000000..a05e49d --- /dev/null +++ b/sources/tools/addItalics.py @@ -0,0 +1,26 @@ +import os +import sys +import fontTools +from fontTools.ttLib import TTFont +from fontTools.varLib.featureVars import addFeatureVariations +from fontTools.ttx import makeOutputFileName + + +inputTTF = sys.argv[1] + +# f = TTFont(fontPath) +f = TTFont(inputTTF) + +condSubst = [ + # A list of (Region, Substitution) tuples. + ([{"wght": (0.158, 0.564)}], {"a": "a.swsh"}), + ([{"wght": (0.868, 1.0)}], {"dollar": "dollar.201_230"}), +] + +addFeatureVariations(f, condSubst) + +# newFontPath = fontPath.split(".")[0] + "-italic.ttf" +# f.save(fontPath) +extension = os.path.splitext(inputTTF)[1] +outputTTF = makeOutputFileName(inputTTF, '', extension) +f.save(outputTTF) \ No newline at end of file diff --git a/sources/tools/gftools-fix-vf-meta.py b/sources/tools/gftools-fix-vf-meta.py new file mode 100755 index 0000000..5c9530a --- /dev/null +++ b/sources/tools/gftools-fix-vf-meta.py @@ -0,0 +1,330 @@ +""" +Fontmake can only generate a single variable font. It cannot generate a +family of variable fonts, that are related to one another. + +This script will update the nametables and STAT tables so a family +which has more than one variable font will work correctly in desktop +applications. + +It will also work on single font VF families by creating a better STAT +table. + +TODO make script work on VFs which have multiple axises. We'll need to +change the axis array format to v4 (we're using v1), +https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4 + +Atm, the script will work well for single axis fonts and families which +have a single vf for Roman and another for Italic/Condensed, both using the wght +axis (covers 95% of GF cases). +""" +from argparse import ArgumentParser +from fontTools.ttLib import TTFont, newTable +from fontTools.ttLib.tables import otTables + + +OS_2_WEIGHT_CLASS = { + 'Thin': 250, + 'ExtraLight': 275, + 'Light': 300, + 'Regular': 400, + '': 400, + 'Medium': 500, + 'SemiBold': 600, + 'Bold': 700, + 'ExtraBold': 800, + 'Black': 900, +} + + +def _parse_styles(stylename): + bold, italic = False, False + if 'Italic' in stylename: + italic = True + bold = False + if 'Bold' == stylename or 'Bold Italic' == stylename: + bold = True + return bold, italic + + +def set_fsselection(style, fsselection,): + bold, italic = _parse_styles(style) + + mask = 0b1100001 + fsselection = (fsselection | mask) ^ mask + + if bold: + fsselection |= 0b100000 + else: + fsselection |= 0b1000000 + if italic: + # unset Reg bit + fsselection = (fsselection | 0b1000000) ^ 0b1000000 + fsselection |= 0b1 + return fsselection + + +def set_mac_style(stylename, macstyle): + bold, italic = _parse_styles(stylename) + + mask = ~0b11 + bold_bit = 0b1 if bold else 0b0 + italic_bit = 0b10 if italic else 0b0 + + macstyle = (macstyle | mask) ^ mask + macstyle |= (bold_bit + italic_bit) + return macstyle + + +def set_weight_class(stylename): + weight = stylename.replace('Italic', '').replace(' ', '') + return OS_2_WEIGHT_CLASS[weight] + + +def fonts_are_same_family(ttfonts): + """Check fonts have the same preferred family name or family name""" + family_names = [] + for ttfont in ttfonts: + pref_family_name = ttfont['name'].getName(16, 3, 1, 1033) + family_name = ttfont['name'].getName(1, 3, 1, 1033) + name = pref_family_name if pref_family_name else family_name + family_names.append(name.toUnicode()) + if len(set(family_names)) != 1: + return False + return True + + +def fix_nametable(ttfont): + """Cleanup nametable issues caused by fontmake""" + table = ttfont['name'] + family_name = table.getName(1, 3, 1, 1033).toUnicode() + + # Remove style from family name. Often happens for Italic fonts + # Family Name Light --> Family Name + for style in (' Regular', ' Light', ' Bold'): + if style in family_name: + table.setName(family_name.replace(style, ''), 1, 3, 1, 1033) + # Remove preferred family name and prefered style + idx = 0 + while idx < len(table.names) - 1: + if table.names[idx].nameID in [16, 17]: + table.names.pop(idx) + idx = 0 + else: + idx += 1 + + # Update existing nameids based on the varfont's default style + default_style = _get_vf_default_style(ttfont) + family_name = table.getName(1, 3, 1, 1033).toUnicode() + table.setName(default_style, 2, 3, 1, 1033) + + fullname = '{} {}'.format(family_name, default_style) + table.setName(unicode(fullname), 4, 3, 1, 1033) + + psname = '{}-{}'.format(family_name, default_style.replace(' ', '')) + table.setName(unicode(psname), 6, 3, 1, 1033) + + # uniqueid basedon fontmake output version;vendorid;psname + font_version = format(ttfont['head'].fontRevision, '.3f') + vendor = ttfont['OS/2'].achVendID + uniqueid = '{};{};{}'.format(font_version, vendor, psname) + table.setName(unicode(uniqueid), 3, 3, 1, 1033) + + +def fix_bits(ttfont): + """Set fsSelection, macStyle and usWeightClass to correct values. + + The values must be derived from the default style. By default, the + Regular instance's values are used""" + dflt_style = _get_vf_default_style(ttfont) + ttfont['OS/2'].fsSelection = set_fsselection( + dflt_style, ttfont['OS/2'].fsSelection + ) + ttfont['OS/2'].usWeightClass = set_weight_class(dflt_style) + ttfont['head'].macStyle = set_mac_style( + dflt_style, ttfont['head'].macStyle + ) + + +def create_stat_table(ttfont): + """Atm, Fontmake is only able to produce a basic stat table. Because of + this, we'll create a STAT using the font's fvar table.""" + stat = newTable('STAT') + stat.table = otTables.STAT() + stat.table.Version = 0x00010002 + + # # Build DesignAxisRecords from fvar + stat.table.DesignAxisRecord = otTables.AxisRecordArray() + stat.table.DesignAxisRecord.Axis = [] + + stat_axises = stat.table.DesignAxisRecord.Axis + + # TODO (M Foley) add support for fonts which have multiple + # axises e.g Barlow + if len(ttfont['fvar'].axes) > 1: + raise Exception('VFs with more than one axis are currently ' + 'not supported.') + + for idx, axis in enumerate(ttfont['fvar'].axes): + append_stat_axis(stat, axis.axisTag, axis.axisNameID) + + # Build AxisValueArrays for each namedInstance from fvar namedInstances + stat.table.AxisValueArray = otTables.AxisValueArray() + stat.table.AxisValueArray.AxisValue = [] + + for idx, instance in enumerate(ttfont['fvar'].instances): + append_stat_record(stat, 0, instance.coordinates.values()[0], instance.subfamilyNameID) + + # Set ElidedFallbackNameID + stat.table.ElidedFallbackNameID = 2 + ttfont['STAT'] = stat + + +def _get_vf_types(ttfonts): + styles = [] + for ttfont in ttfonts: + styles.append(_get_vf_type(ttfont)) + return styles + + +def _get_vf_type(ttfont): + style = ttfont['name'].getName(2, 3, 1, 1033).toUnicode() + return 'Italic' if 'Italic' in style else 'Roman' + + +def _get_vf_default_style(ttfont): + """Return the name record string of the default style""" + default_fvar_val = ttfont['fvar'].axes[0].defaultValue + + name_id = None + for inst in ttfont['fvar'].instances: + if inst.coordinates['wght'] == default_fvar_val: + name_id = inst.subfamilyNameID + return ttfont['name'].getName(name_id, 1, 0, 0).toUnicode() + + +def add_other_vf_styles_to_nametable(ttfont, text_records): + """Each nametable in a font must reference every font in the family. + Since fontmake doesn't append the other families to the nametable, + we'll do this ourselves. Skip this step if these records already + exist.""" + found = set() + for name in ttfont['name'].names[:-len(text_records)-1:-1]: + found.add(name.toUnicode()) + leftover = set(text_records) - found + + if leftover: + nameid = ttfont['name'].names[-1].nameID + 1 + for record in leftover: + ttfont['name'].setName(unicode(record), nameid, 3, 1, 1033) + nameid += 1 + + +def get_custom_name_record(ttfont, text): + """Return a name record by text. Record ID must be greater than 255""" + for record in ttfont['name'].names[::-1]: + if record.nameID > 255: + rec_text = record.toUnicode() + if rec_text == text: + return record + return None + + +def append_stat_axis(stat, tag, namerecord_id): + """Add a STAT axis if the tag does not exist already.""" + has_tags = [] + axises = stat.table.DesignAxisRecord.Axis + for axis in axises: + has_tags.append(axis.AxisTag) + + if tag in has_tags: + raise Exception('{} has already been declared in the STAT table') + + axis_record = otTables.AxisRecord() + axis_record.AxisTag = tag + axis_record.AxisNameID = namerecord_id + axis_record.AxisOrdering = len(axises) + axises.append(axis_record) + + +def append_stat_record(stat, axis_index, value, namerecord_id, linked_value=None): + records = stat.table.AxisValueArray.AxisValue + axis_record = otTables.AxisValue() + axis_record.Format = 1 + axis_record.ValueNameID = namerecord_id + axis_record.Value = value + axis_record.AxisIndex = axis_index + + axis_record.Flags = 0 + if linked_value: + axis_record.Format = 3 + axis_record.LinkedValue = linked_value + records.append(axis_record) + + +def get_stat_axis_index(ttfont, axis_name): + axises = ttfont['STAT'].table.DesignAxisRecord.Axis + available_axises = [a.AxisTag for a in axises] + for idx, axis in enumerate(axises): + if axis.AxisTag == axis_name: + return idx + raise Exception('{} is not a valid axis. Font has [{}] axises'.format( + axis_name, available_axises) + ) + + +def set_stat_for_font_in_family(ttfont, family_styles): + """Based on examples from: + https://docs.microsoft.com/en-us/typography/opentype/spec/stat""" + font_type = _get_vf_type(ttfont) + # See example 5 + if font_type == 'Roman' and 'Italic' in family_styles: + name_record = get_custom_name_record(ttfont, 'Italic') + append_stat_axis(ttfont['STAT'], 'ital', name_record.nameID) + + name_record = get_custom_name_record(ttfont, 'Roman') + axis_idx = get_stat_axis_index(ttfont, 'ital') + append_stat_record(ttfont['STAT'], axis_idx, 0, name_record.nameID, linked_value=1.0) + + elif font_type == 'Italic' and 'Roman' in family_styles: + name_record = get_custom_name_record(ttfont, 'Italic') + append_stat_axis(ttfont['STAT'], 'ital', name_record.nameID) + + name_record = get_custom_name_record(ttfont, 'Italic') + axis_idx = get_stat_axis_index(ttfont, 'ital') + append_stat_record(ttfont['STAT'], axis_idx, 1.0, name_record.nameID) + + +def harmonize_vf_families(ttfonts): + """Make sure the fonts which are part of a vf family reference each other + in both the nametable and STAT table. For examples see: + https://docs.microsoft.com/en-us/typography/opentype/spec/stat + + """ + family_styles = _get_vf_types(ttfonts) + for ttfont in ttfonts: + add_other_vf_styles_to_nametable(ttfont, family_styles) + set_stat_for_font_in_family(ttfont, family_styles) + + +def main(): + parser = ArgumentParser() + parser.add_argument('fonts', nargs='+', + help='All fonts within a font family must be included') + args = parser.parse_args() + font_paths = args.fonts + ttfonts = [TTFont(p) for p in font_paths] + if not fonts_are_same_family(ttfonts): + raise Exception('Fonts have different family_names: [{}]'.format( + ', '.join(map(os.path.basename, ttfonts)) + )) + + map(fix_nametable, ttfonts) + map(fix_bits, ttfonts) + map(create_stat_table, ttfonts) + harmonize_vf_families(ttfonts) + + for path, ttfont in zip(font_paths, ttfonts): + ttfont.save(path + '.fix') + +if __name__ == '__main__': + main()