#! /usr/bin/python3 # SPDX-License-Identifier: CC0-1.0 # This is a script to check the backward compatibility of fonts, and # specifically of Bedstead. It takes two font files as arguments and # checks whether upgrades from the first to the second will break # anything. It should fail on anything that would require a change of # major version number. # # Things that are checked: # # Glyph names. Existing PostScript files that refer to specific glyph # names should continue to work. The script checks that all glyph # names in the old font still exist, but not that they look right. # # Code points. Anything using a font through the 'cmap' table should # continue to work. That means that any 'cmap' that exists should # continue to exist, and that any mapping through any 'cmap' should # continue to work. As with glyph names, the script checks for the # existence of mappings, but not their content. # # Names. Applications use a font's name to refer to it. This script # checks that the subset of names for which this seems reasonable # match between the old and new fonts. # # Vendor ID. At least some versions of GNU Emacs like to use the # OS/2.achVendID field to select fonts. So if that changes, Emacs # might not find your favourite font. # # Some 'GSUB' lookups. For some features, the OpenType specification # says that the various outputs of an alternate lookup should match # across fonts in a family. This suggests that it's reasonable to # depend on the behaviour of particular inputs to these lookups, and # that they should thus be consistent within major versions. Unlike # for most features above, we'd like to check that the semantics of # the chosen glyphs haven't changed. This is made tricky by the fact # the Bedstead 3.251 changed some glyph names while keeping the same # semantics, and also changed the shape of some glyphs. Our approach # is to look up both old and new glyph names in the new font and check # that they have the same outline there. The only feature currently # handled by this is 'aalt' (Access All Alternates). from argparse import ArgumentParser from fontTools import ttLib from sys import exit parser = ArgumentParser() parser.add_argument("old") parser.add_argument("new") cfg = parser.parse_args() ttold = ttLib.TTFont(cfg.old) ttnew = ttLib.TTFont(cfg.new) failed = False def fail(msg): failed = True print(f"FAIL: {msg}") if not (set(ttold.getGlyphOrder()) <= set(ttnew.getGlyphOrder())): fail("Glyphs vanished: " f"{set(ttold.getGlyphOrder()) - set(ttnew.getGlyphOrder())!r}") for cmapold in ttold['cmap'].tables: cmapnew = ttnew['cmap'].getcmap(cmapold.platformID, cmapold.platEncID) if cmapnew == None: fail("No cmap in new font for " f"{(cmapold.platformID,cmapold.platEncID)}") elif not (set(cmapold.cmap.keys()) <= set(cmapnew.cmap.keys())): fail("Code points vanished from " f"{(cmapold.platformID,cmapold.platEncID)}: " f"{set(cmapold.cmap.keys()) - set(cmapnew.cmap.keys())!r}") # Names that might be used to specify fonts. specnames = { 1, # Font family name 2, # Font subfamily name 4, # Full font name 6, # PostScript FontName 16, # Typographic family name 17, # Typographic subfamily name 18, # Compatible full name 20, # PostScript CID findfont name 21, # WWS family name 22, # WWS subfamily name 25, # Variations PostScript name prefix } for oldname in ttold['name'].names: if oldname.nameID in specnames: newname = ttnew['name'].getName(oldname.nameID, oldname.platformID, oldname.platEncID, oldname.langID) if newname == None: fail("No name in new font for " f"nameID={oldname.nameID}, platformID={oldname.platformID}, " f"platEncID={oldname.platEncID}, langID={oldname.langID}") if newname.string != oldname.string: fail("Name mismatch for " f"nameID={oldname.nameID}, platformID={oldname.platformID}, " f"platEncID={oldname.platEncID}, langID={oldname.langID}") if ttold['OS/2'].achVendID != ttnew['OS/2'].achVendID: fail("Vendor ID mismatch") def feat_to_dict(gsub, tag): # Assertions in this function trap various unhandled cases. feature_records = [f for f in gsub.table.FeatureList.FeatureRecord if f.FeatureTag == tag] assert(len(feature_records) == 1) for fr in feature_records: assert(len(fr.Feature.LookupListIndex) == 1) for llix in fr.Feature.LookupListIndex: lookup = gsub.table.LookupList.Lookup[llix] assert(lookup.LookupType == 3) assert(len(lookup.SubTable) == 1) for st in lookup.SubTable: return st.alternates def charstring(glyphname): return ttnew['CFF '].cff[0].CharStrings[glyphname].bytecode for feat in ['aalt']: oldalt = feat_to_dict(ttold['GSUB'], feat) newalt = feat_to_dict(ttnew['GSUB'], feat) for k in sorted(set(oldalt.keys()) & set(newalt.keys())): if ([charstring(x) for x in newalt[k][:len(oldalt[k])]] != [charstring(x) for x in oldalt[k]]): fail(f"new '{feat}' lookup for {k} is not a prefix of old one") if failed: exit(1)