mirror of
https://bjh21.me.uk/bedstead/.git
synced 2026-07-09 05:23:06 -04:00
We do this by pulling out the CFF charstring for old and new glyph names from the new font and seeing if they're the same.
135 lines
5.2 KiB
Python
Executable File
135 lines
5.2 KiB
Python
Executable File
#! /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)
|