1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
|
#! /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)
|