From 8e9e6da034a4fe06a0e211d34e8ead73dda6d9eb Mon Sep 17 00:00:00 2001 From: Giovanni Mascellani Date: Fri, 20 Jan 2023 13:02:22 +0100 Subject: [PATCH] fonts: Rewrite font merging script based on fonttools. The current script based on fontforge seems to have a few problems. The generated fonts show glitches, for example in Cyberpunk 2077 for Thai and in FIFA 22 for Arabic. I don't precisely know what is the problem, and it might be that the real bug is in the rendering code rather than in the merging script. But since this seems to work better overall, I'm sticking with it. CW-Bug-Id: #20302 --- Makefile.in | 8 +- fonts/scripts/merge.py | 250 ++++++++++++++++++++++++++++++++++++ fonts/scripts/mergefonts.pe | 29 ----- 3 files changed, 254 insertions(+), 33 deletions(-) create mode 100755 fonts/scripts/merge.py delete mode 100644 fonts/scripts/mergefonts.pe diff --git a/Makefile.in b/Makefile.in index 1401531a..e4a9c952 100644 --- a/Makefile.in +++ b/Makefile.in @@ -818,17 +818,17 @@ $(simsun.ttc): $(simsun.ttf) $(nsimsun.ttf) $(msgothic.ttc): $(msgothic.ttf) $(mspgothic.ttf) $(msuigothic.ttf) afdko otf2otc -o $@ $^ -$(micross.ttf): $(noto_sans.ttf) $(noto_sans_arabic.ttf) $(noto_sans_armenian.ttf) $(noto_sans_bengali.ttf) $(noto_sans_coptic.ttf) \ +$(micross.ttf): $(FONTS)/scripts/merge.py $(noto_sans.ttf) $(noto_sans_arabic.ttf) $(noto_sans_armenian.ttf) $(noto_sans_bengali.ttf) $(noto_sans_coptic.ttf) \ $(noto_sans_georgian.ttf) $(noto_sans_gujarati.ttf) $(noto_sans_hebrew.ttf) $(noto_sans_khmer.ttf) $(noto_sans_tamil.ttf) \ $(noto_sans_thaana.ttf) $(noto_sans_thai.ttf) - $(FONTFORGE) -script $(MERGEFONTSSCRIPT) $(noto_sans.ttf) $(noto_sans_arabic.ttf) $(noto_sans_armenian.ttf) $(noto_sans_bengali.ttf) \ + $(FONTS)/scripts/merge.py $(noto_sans.ttf) $(noto_sans_arabic.ttf) $(noto_sans_armenian.ttf) $(noto_sans_bengali.ttf) \ $(noto_sans_coptic.ttf) $(noto_sans_georgian.ttf) $(noto_sans_gujarati.ttf) $(noto_sans_hebrew.ttf) $(noto_sans_khmer.ttf) \ $(noto_sans_tamil.ttf) $(noto_sans_thaana.ttf) $(noto_sans_thai.ttf) "MicrosoftSansSerif" "Microsoft Sans Serif" "Regular" $(micross.ttf) -$(nirmala.ttf): $(noto_sans.ttf) $(noto_sans_bengaliui.ttf) $(noto_sans_devanagariui.ttf) $(noto_sans_gujaratiui.ttf) $(noto_sans_gurmukhiui.ttf) \ +$(nirmala.ttf): $(FONTS)/scripts/merge.py $(noto_sans.ttf) $(noto_sans_bengaliui.ttf) $(noto_sans_devanagariui.ttf) $(noto_sans_gujaratiui.ttf) $(noto_sans_gurmukhiui.ttf) \ $(noto_sans_kannadaui.ttf) $(noto_sans_malayalamui.ttf) $(noto_sans_meeteimayek.ttf) $(noto_sans_olchiki.ttf) $(noto_sans_oriyaui.ttf) \ $(noto_sans_sinhalaui.ttf) $(noto_sans_sorasompeng.ttf) $(noto_sans_tamilui.ttf) $(noto_sans_teluguui.ttf) - $(FONTFORGE) -script $(MERGEFONTSSCRIPT) $(noto_sans.ttf) $(noto_sans_bengaliui.ttf) $(noto_sans_devanagariui.ttf) $(noto_sans_gujaratiui.ttf) \ + $(FONTS)/scripts/merge.py $(noto_sans.ttf) $(noto_sans_bengaliui.ttf) $(noto_sans_devanagariui.ttf) $(noto_sans_gujaratiui.ttf) \ $(noto_sans_gurmukhiui.ttf) $(noto_sans_kannadaui.ttf) $(noto_sans_malayalamui.ttf) $(noto_sans_meeteimayek.ttf) $(noto_sans_olchiki.ttf) \ $(noto_sans_oriyaui.ttf) $(noto_sans_sinhalaui.ttf) $(noto_sans_sorasompeng.ttf) $(noto_sans_tamilui.ttf) $(noto_sans_teluguui.ttf) \ "NirmalaUI" "Nirmala UI" "Regular" $(nirmala.ttf) diff --git a/fonts/scripts/merge.py b/fonts/scripts/merge.py new file mode 100755 index 00000000..0acb77d2 --- /dev/null +++ b/fonts/scripts/merge.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 + +# Based on merge_noto.py and merge_fonts.py from the nototools +# (https://github.com/googlefonts/nototools), with the following +# copyright notice: + +# Copyright 2014-2017 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The font name changing logic is taken from +# https://github.com/chrissimpkins/fontname.py/blob/master/fontname.py, +# with the following copyright notice: + +# Copyright 2019 Christopher Simpkins +# MIT License + +"""Merges a number of Noto fonts and then sets a given name to the +result. + +""" + +import sys +import tempfile +import os + +from fontTools import merge +from fontTools import ttLib +from fontTools.ttLib.tables import otTables + +def read_line_metrics(font): + metrics = { + "ascent": font["hhea"].ascent, + "descent": font["hhea"].descent, + "usWinAscent": font["OS/2"].usWinAscent, + "usWinDescent": font["OS/2"].usWinDescent, + "sTypoAscender": font["OS/2"].sTypoAscender, + "sTypoDescender": font["OS/2"].sTypoDescender, + "sxHeight": font["OS/2"].sxHeight, + "sCapHeight": font["OS/2"].sCapHeight, + "sTypoLineGap": font["OS/2"].sTypoLineGap, + } + return metrics + + +def set_line_metrics(font, metrics): + font["hhea"].ascent = metrics["ascent"] + font["hhea"].descent = metrics["descent"] + font["OS/2"].usWinAscent = metrics["usWinAscent"] + font["OS/2"].usWinDescent = metrics["usWinDescent"] + font["OS/2"].sTypoAscender = metrics["sTypoAscender"] + font["OS/2"].sTypoDescender = metrics["sTypoDescender"] + font["OS/2"].sxHeight = metrics["sxHeight"] + font["OS/2"].sCapHeight = metrics["sCapHeight"] + font["OS/2"].sTypoLineGap = metrics["sTypoLineGap"] + +def has_gsub_table(fontfile): + font = ttLib.TTFont(fontfile) + return "GSUB" in font + +SCRIPT_TO_OPENTYPE_SCRIPT_TAG = { + # Retrieved from Opentype 1.9 delta specs. Prerelease scripttags used out of necessity. https://docs.microsoft.com/en-us/typography/opentype/spec/scripttags + "Carian": "cari", + "CypriotSyllabary": "cprt", + "CyproMinoan": "cpmn", + "Deseret": "dsrt", + "Glagolitic": "glag", + "EgyptianHieroglyphs": "egyp", + "ImperialAramaic": "armi", + "LinearA": "lina", + "LinearB": "linb", + "Lisu": "lisu", + "Lycian": "lyci", + "Lydian": "lydi", + "Ogham": "ogam", + "OldItalic": "ital", + "OldPersian": "xpeo", + "OldSouthArabian": "sarb", + "OldTurkic": "orkh", + "OldSogdian": "sogo", + "OldNorthArabian": "narb", + "OldHungarian": "hung", + "Osmanya": "osma", + "Phoenician": "phnx", + "SumeroAkkadianCuneiform": "xsux", + "Ugaritic": "ugar", + "OlChiki": "olck", + "TaiLe": "tale", + "Cuneiform": "xsux", + "Cypriot": "cprt", + "Runic": "runr", + "Shavian": "shaw", + "Vai": "vai ", + "Yi": "yi ", + "AnatolianHieroglyphs": "hluw", + "Bamum": "bamu", + "ByzantineMusic": "byzm", + "Gothic": "goth", + "ImperialAramaic": "armi", + "InscriptionalPahlavi": "phli", + "InscriptionalParthian": "prti", + "Khojki": "khoj", + "MathematicalAlphanumericSymbols": "math", + "MeroiticCursive": "merc", + "MeroiticHieroglyphs": "mero", + "MusicalSymbols": "musc", + "Palmyrene": "palm", + "Rejang": "rjng", + "Samaritan": "samr", + "Carian": "cari", + "Ahom": "ahom", + "Adlam": "adlm", + "Dogra": "dogr", + "Lisu": "lisu", + "Mandaean": "mand", + "Manichaean": "mani", + "Tifinagh": "tfng", + "Wancho": "wcho", + "Yezidi": "yezi", + "Cherokee": "cher", + "Chorasmian": "chrs", + "PahawhHmong": "hmng", + "Phagspa": "phag", + "Sundanese": "sund", + "WarangCiti": "wara", + "SylotiNagri": "sylo", + "PsalterPahlavi": "phlp", + "CaucasianAlbanian": "aghb", + "Medefaidrin": "medf", + "MeiteiMayek": "mtei", + "MendeKikakui": "mend", + "Mro": "mroo", + "Multani": "mult", + "Nabataean": "nbat", + "Nandinagari": "nand", + "Newa": "newa", + "NewTaiLue": "talu", + "Nushu": "nshu", + "NyiakengPuachueHmong": "hmnp", + "OldPermic": "perm", + "SoraSompeng": "sora", + "Soyombo": "soyo", + "SylotiNagri": "sylo", + "Tagbanwa": "tagb", + "Tagalog": "tglg", + "Takri": "takr", + "TaiTham": "lana", + "TaiViet": "tavt", + "Tangut": "tang", + "Thaana": "thaa", + "UgariticCuneiform": "ugar", + "ZanabazarSquare": "zanb", + "SignWriting": "sgnw", + "OldUyghur": "ougr", + "Tangsa": "tnsa", + "Toto": "toto", + "Vithkuqi": "vith", + "Duployan": "dupl", + "Hatran": "hatr", + # These last two would only merge using the long script name including the 'NotoSerif' part + "NotoSerifYezidi": "yezi", + "NotoSerifNyiakengPuachueHmong": "hmnp", +} + +def get_opentype_script_tag(fontfile): + fontfile = os.path.basename(fontfile) + if fontfile.startswith("NotoSans"): + fontfile = fontfile[8:] + fontfile = fontfile[: fontfile.index("-")] + return SCRIPT_TO_OPENTYPE_SCRIPT_TAG[fontfile] + +def add_gsub_to_font(fontfile): + """Adds an empty GSUB table to a font.""" + font = ttLib.TTFont(fontfile) + gsub_table = ttLib.getTableClass("GSUB")("GSUB") + gsub_table.table = otTables.GSUB() + gsub_table.table.Version = 1.0 + gsub_table.table.ScriptList = otTables.ScriptList() + gsub_table.table.ScriptCount = 1 + gsub_table.table.LookupList = otTables.LookupList() + gsub_table.table.LookupList.LookupCount = 0 + gsub_table.table.LookupList.Lookup = [] + gsub_table.table.FeatureList = otTables.FeatureList() + gsub_table.table.FeatureList.FeatureCount = 0 + gsub_table.table.LookupList.FeatureRecord = [] + + script_record = otTables.ScriptRecord() + script_record.ScriptTag = get_opentype_script_tag(fontfile) + script_record.Script = otTables.Script() + script_record.Script.LangSysCount = 0 + script_record.Script.LangSysRecord = [] + + default_lang_sys = otTables.DefaultLangSys() + default_lang_sys.FeatureIndex = [] + default_lang_sys.FeatureCount = 0 + default_lang_sys.LookupOrder = None + default_lang_sys.ReqFeatureIndex = 65535 + script_record.Script.DefaultLangSys = default_lang_sys + + gsub_table.table.ScriptList.ScriptRecord = [script_record] + + font["GSUB"] = gsub_table + + target_file = tempfile.gettempdir() + "/" + os.path.basename(fontfile) + font.save(target_file) + return target_file + +def main(): + output_filename = sys.argv[-1] + weight = sys.argv[-2] + font_name = sys.argv[-3] + ps_name = sys.argv[-4] + input_filenames = sys.argv[1:-4] + + # Add a GSUB table to the fonts that do not have one + for index, filename in enumerate(input_filenames): + if not has_gsub_table(filename): + input_filenames[index] = add_gsub_to_font(filename) + + # Merge the fonts together + merger = merge.Merger() + font = merger.merge(input_filenames) + + # Use the line metrics defined by the first font + metrics = read_line_metrics(ttLib.TTFont(input_filenames[0])) + set_line_metrics(font, metrics) + + # Rename the result + for record in font['name'].names: + if record.nameID == 1: + record.string = font_name + elif record.nameID == 4: + record.string = "{} {}".format(font_name, weight) + elif record.nameID == 6: + record.string = "{}-{}".format(ps_name, weight.replace(' ', '')) + + font.save(output_filename) + +if __name__ == '__main__': + main() diff --git a/fonts/scripts/mergefonts.pe b/fonts/scripts/mergefonts.pe deleted file mode 100644 index 500c7380..00000000 --- a/fonts/scripts/mergefonts.pe +++ /dev/null @@ -1,29 +0,0 @@ -if ($argc < 6) - Error ("Expected arguments - Font1, Font2, Font3, ..., FontName, FullName, FontWeight, OutputFilename") -endif - -FONTNAME = ToString($argv[$argc - 4]) -FULLNAME = ToString($argv[$argc - 3]) -FONTWEIGHT = ToString($argv[$argc - 2]) -SRCFONTNAME = ToString($argv[1]) -DESTFONTNAME = ToString($argv[$argc - 1]) - -Open(SRCFONTNAME) - -i = 2 -while (i < $argc - 4) - MergeFonts($argv[i]) - i = i + 1 -endloop - -SetFontNames(FONTNAME, FULLNAME, FULLNAME, FONTWEIGHT, "", "1.0.0") -Generate(DESTFONTNAME) -Close() - -Open(DESTFONTNAME) -Reencode("unicode") -SelectWorthOutputting() -SelectInvert() -BuildAccented() -Generate(DESTFONTNAME) -Close()