#! /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)
