diff options
Diffstat (limited to 'misc/pylib/extractor/formats/opentype.py')
-rwxr-xr-x | misc/pylib/extractor/formats/opentype.py | 806 |
1 files changed, 0 insertions, 806 deletions
diff --git a/misc/pylib/extractor/formats/opentype.py b/misc/pylib/extractor/formats/opentype.py deleted file mode 100755 index 3cf0e06ec..000000000 --- a/misc/pylib/extractor/formats/opentype.py +++ /dev/null @@ -1,806 +0,0 @@ -import time -from fontTools.ttLib import TTFont, TTLibError -from fontTools.ttLib.tables._h_e_a_d import mac_epoch_diff -from fontTools.misc.textTools import num2binary -from fontTools.pens.boundsPen import ControlBoundsPen -from extractor.exceptions import ExtractorError -from extractor.tools import RelaxedInfo, copyAttr - -# ---------------- -# Public Functions -# ---------------- - -def isOpenType(pathOrFile): - try: - font = TTFont(pathOrFile) - del font - except TTLibError: - return False - return True - -def extractFontFromOpenType(pathOrFile, destination, doGlyphOrder=True, doGlyphs=True, doInfo=True, doKerning=True, customFunctions=[]): - source = TTFont(pathOrFile) - if doInfo: - extractOpenTypeInfo(source, destination) - if doGlyphs: - extractOpenTypeGlyphs(source, destination) - if doGlyphOrder: - extractGlyphOrder(source, destination) - if doKerning: - kerning, groups = extractOpenTypeKerning(source, destination) - destination.groups.update(groups) - destination.kerning.clear() - destination.kerning.update(kerning) - for function in customFunctions: - function(source, destination) - source.close() - -def extractGlyphOrder(source, destination): - glyphOrder = source.getGlyphOrder() - if len(glyphOrder): - destination.lib["public.glyphOrder"] = glyphOrder - -# ---- -# Info -# ---- - -def extractOpenTypeInfo(source, destination): - info = RelaxedInfo(destination.info) - _extractInfoHead(source, info) - _extractInfoName(source, info) - _extracInfoOS2(source, info) - _extractInfoHhea(source, info) - _extractInfoVhea(source, info) - _extractInfoPost(source, info) - _extractInfoCFF(source, info) - _extractInfoGasp(source, info) - -def _extractInfoHead(source, info): - head = source["head"] - # version - version = str(round(head.fontRevision, 3)) - versionMajor, versionMinor = version.split(".") - info.versionMajor = int(versionMajor) - info.versionMinor = int(versionMinor) - # upm - info.unitsPerEm = head.unitsPerEm - # created - format = "%Y/%m/%d %H:%M:%S" - created = head.created - created = time.gmtime(max(0, created + mac_epoch_diff)) - info.openTypeHeadCreated = time.strftime(format, created) - # lowestRecPPEM - info.openTypeHeadLowestRecPPEM = head.lowestRecPPEM - # flags - info.openTypeHeadFlags = binaryToIntList(head.flags) - # styleMapStyleName - macStyle = binaryToIntList(head.macStyle) - styleMapStyleName = "regular" - if 0 in macStyle and 1 in macStyle: - styleMapStyleName = "bold italic" - elif 0 in macStyle: - styleMapStyleName = "bold" - elif 1 in macStyle: - styleMapStyleName = "italic" - info.styleMapStyleName = styleMapStyleName - -def _extractInfoName(source, info): - records = [] - nameIDs = {} - for record in source["name"].names: - nameID = record.nameID - platformID = record.platformID - encodingID = record.platEncID - languageID = record.langID - string = record.toUnicode() - nameIDs[nameID, platformID, encodingID, languageID] = string - records.append((nameID, platformID, encodingID, languageID, - dict( - nameID=nameID, - platformID=platformID, - encodingID=encodingID, - languageID=languageID, - string=string - ) - )) - attributes = dict( - familyName=_priorityOrder(16) + _priorityOrder(1), - styleName=_priorityOrder(17) + _priorityOrder(2), - styleMapFamilyName=_priorityOrder(1), - # styleMapStyleName will be handled in head extraction - copyright=_priorityOrder(0), - trademark=_priorityOrder(7), - openTypeNameDesigner=_priorityOrder(9), - openTypeNameDesignerURL=_priorityOrder(12), - openTypeNameManufacturer=_priorityOrder(8), - openTypeNameManufacturerURL=_priorityOrder(11), - openTypeNameLicense=_priorityOrder(13), - openTypeNameLicenseURL=_priorityOrder(14), - openTypeNameVersion=_priorityOrder(5), - openTypeNameUniqueID=_priorityOrder(3), - openTypeNameDescription=_priorityOrder(10), - openTypeNamePreferredFamilyName=_priorityOrder(16), - openTypeNamePreferredSubfamilyName=_priorityOrder(17), - openTypeNameCompatibleFullName=_priorityOrder(18), - openTypeNameSampleText=_priorityOrder(20), - openTypeNameWWSFamilyName=_priorityOrder(21), - openTypeNameWWSSubfamilyName=_priorityOrder(22) - ) - for attr, priority in attributes.items(): - value = _skimNameIDs(nameIDs, priority) - if value is not None: - setattr(info, attr, value) - info.openTypeNameRecords = [record[-1] for record in sorted(records)] - -def _priorityOrder(nameID): - priority = [ - (nameID, 1, 0, 0), - (nameID, 1, None, None), - (nameID, None, None, None) - ] - return priority - -def _skimNameIDs(nameIDs, priority): - for (nameID, platformID, platEncID, langID) in priority: - for (nID, pID, pEID, lID), text in nameIDs.items(): - if nID != nameID: - continue - if pID != platformID and platformID is not None: - continue - if pEID != platEncID and platEncID is not None: - continue - if lID != langID and langID is not None: - continue - return text - -def _extracInfoOS2(source, info): - os2 = source["OS/2"] - # openTypeOS2WidthClass - copyAttr(os2, "usWidthClass", info, "openTypeOS2WidthClass") - # openTypeOS2WeightClass - copyAttr(os2, "usWeightClass", info, "openTypeOS2WeightClass") - # openTypeOS2Selection - if hasattr(os2, "fsSelection"): - fsSelection = binaryToIntList(os2.fsSelection) - fsSelection = [i for i in fsSelection if i in (1, 2, 3, 4)] - info.openTypeOS2Selection = fsSelection - # openTypeOS2VendorID - copyAttr(os2, "achVendID", info, "openTypeOS2VendorID") - ## the string could be padded with null bytes. strip those. - if info.openTypeOS2VendorID.endswith("\x00"): - r = [] - for c in reversed(info.openTypeOS2VendorID): - if r or c != "\x00": - r.insert(0, c) - info.openTypeOS2VendorID = "".join(r) - # openTypeOS2Panose - if hasattr(os2, "panose"): - panose = os2.panose - info.openTypeOS2Panose = [ - os2.panose.bFamilyType, - os2.panose.bSerifStyle, - os2.panose.bWeight, - os2.panose.bProportion, - os2.panose.bContrast, - os2.panose.bStrokeVariation, - os2.panose.bArmStyle, - os2.panose.bLetterForm, - os2.panose.bMidline, - os2.panose.bXHeight - ] - # openTypeOS2FamilyClass - # XXX info.openTypeOS2FamilyClass - if hasattr(os2, "ulUnicodeRange1"): - info.openTypeOS2UnicodeRanges = binaryToIntList(os2.ulUnicodeRange1) + binaryToIntList(os2.ulUnicodeRange2, 32) + binaryToIntList(os2.ulUnicodeRange3, 64) + binaryToIntList(os2.ulUnicodeRange4, 96) - if hasattr(os2, "ulCodePageRange1"): - info.openTypeOS2CodePageRanges = binaryToIntList(os2.ulCodePageRange1) + binaryToIntList(os2.ulCodePageRange2, 32) - copyAttr(os2, "sxHeight", info, "xHeight") - copyAttr(os2, "sCapHeight", info, "capHeight") - copyAttr(os2, "sTypoAscender", info, "ascender") - copyAttr(os2, "sTypoDescender", info, "descender") - copyAttr(os2, "sTypoAscender", info, "openTypeOS2TypoAscender") - copyAttr(os2, "sTypoDescender", info, "openTypeOS2TypoDescender") - copyAttr(os2, "sTypoLineGap", info, "openTypeOS2TypoLineGap") - copyAttr(os2, "usWinAscent", info, "openTypeOS2WinAscent") - copyAttr(os2, "usWinDescent", info, "openTypeOS2WinDescent") - if hasattr(os2, "fsType"): - info.openTypeOS2Type = binaryToIntList(os2.fsType) - copyAttr(os2, "ySubscriptXSize", info, "openTypeOS2SubscriptXSize") - copyAttr(os2, "ySubscriptYSize", info, "openTypeOS2SubscriptYSize") - copyAttr(os2, "ySubscriptXOffset", info, "openTypeOS2SubscriptXOffset") - copyAttr(os2, "ySubscriptYOffset", info, "openTypeOS2SubscriptYOffset") - copyAttr(os2, "ySuperscriptXSize", info, "openTypeOS2SuperscriptXSize") - copyAttr(os2, "ySuperscriptYSize", info, "openTypeOS2SuperscriptYSize") - copyAttr(os2, "ySuperscriptXOffset", info, "openTypeOS2SuperscriptXOffset") - copyAttr(os2, "ySuperscriptYOffset", info, "openTypeOS2SuperscriptYOffset") - copyAttr(os2, "yStrikeoutSize", info, "openTypeOS2StrikeoutSize") - copyAttr(os2, "yStrikeoutPosition", info, "openTypeOS2StrikeoutPosition") - -def _extractInfoHhea(source, info): - if "hhea" not in source: - return - hhea = source["hhea"] - info.openTypeHheaAscender = hhea.ascent - info.openTypeHheaDescender = hhea.descent - info.openTypeHheaLineGap = hhea.lineGap - info.openTypeHheaCaretSlopeRise = hhea.caretSlopeRise - info.openTypeHheaCaretSlopeRun = hhea.caretSlopeRun - info.openTypeHheaCaretOffset = hhea.caretOffset - -def _extractInfoVhea(source, info): - if "vhea" not in source: - return - vhea = source["vhea"] - info.openTypeVheaVertTypoAscender = vhea.ascent - info.openTypeVheaVertTypoDescender = vhea.descent - info.openTypeVheaVertTypoLineGap = vhea.lineGap - info.openTypeVheaCaretSlopeRise = vhea.caretSlopeRise - info.openTypeVheaCaretSlopeRun = vhea.caretSlopeRun - if hasattr(vhea, "caretOffset"): - info.openTypeVheaCaretOffset = vhea.caretOffset - -def _extractInfoPost(source, info): - post = source["post"] - info.italicAngle = post.italicAngle - info.postscriptUnderlineThickness = post.underlineThickness - info.postscriptUnderlinePosition = post.underlinePosition - info.postscriptIsFixedPitch = bool(post.isFixedPitch) - -def _extractInfoCFF(source, info): - if "CFF " not in source: - return - cff = source["CFF "].cff - info.postscriptFontName = cff.fontNames[0] - # TopDict - topDict = cff.topDictIndex[0] - info.postscriptFullName = topDict.rawDict.get("FullName", None) - info.postscriptWeightName = topDict.rawDict.get("Weight", None) - # Private - # CID doesn't have this, so safely extract. - if hasattr(topDict, "Private"): - private = topDict.Private - info.postscriptBlueValues = private.rawDict.get("BlueValues", []) - info.postscriptOtherBlues = private.rawDict.get("OtherBlues", []) - info.postscriptFamilyBlues = private.rawDict.get("FamilyBlues", []) - info.postscriptFamilyOtherBlues = private.rawDict.get("FamilyOtherBlues", []) - info.postscriptStemSnapH = private.rawDict.get("StemSnapH", []) - info.postscriptStemSnapV = private.rawDict.get("StemSnapV", []) - info.postscriptBlueFuzz = private.rawDict.get("BlueFuzz", None) - info.postscriptBlueShift = private.rawDict.get("BlueShift", None) - info.postscriptBlueScale = private.rawDict.get("BlueScale", None) - info.postscriptForceBold = bool(private.rawDict.get("ForceBold", None)) - info.postscriptNominalWidthX = private.rawDict.get("nominalWidthX", None) - info.postscriptDefaultWidthX = private.rawDict.get("defaultWidthX", None) - # XXX postscriptSlantAngle - # XXX postscriptUniqueID - -def _extractInfoGasp(source, info): - if "gasp" not in source: - return - gasp = source["gasp"] - records = [] - for size, bits in sorted(gasp.gaspRange.items()): - behavior = [] - if bits & 0x0001: - behavior.append(0) - if bits & 0x0002: - behavior.append(1) - if bits & 0x0004: - behavior.append(2) - if bits & 0x0008: - behavior.append(3) - record = dict( - rangeMaxPPEM=size, - rangeGaspBehavior=behavior - ) - records.append(record) - info.openTypeGaspRangeRecords = records - -# Tools - -def binaryToIntList(value, start=0): - intList = [] - counter = start - while value: - if value & 1: - intList.append(counter) - value >>= 1 - counter += 1 - return intList - -# -------- -# Outlines -# -------- - -def extractOpenTypeGlyphs(source, destination): - # grab the cmap - cmap = source["cmap"] - vmtx = source.get("vmtx") - vorg = source.get("VORG") - preferred = [ - (3, 10, 12), - (3, 10, 4), - (3, 1, 12), - (3, 1, 4), - (0, 3, 12), - (0, 3, 4), - (3, 0, 12), - (3, 0, 4), - (1, 0, 12), - (1, 0, 4) - ] - found = {} - for table in cmap.tables: - found[table.platformID, table.platEncID, table.format] = table - table = None - for key in preferred: - if key not in found: - continue - table = found[key] - break - reversedMapping = {} - if table is not None: - for uniValue, glyphName in table.cmap.items(): - reversedMapping[glyphName] = uniValue - # grab the glyphs - glyphSet = source.getGlyphSet() - for glyphName in glyphSet.keys(): - sourceGlyph = glyphSet[glyphName] - # make the new glyph - destination.newGlyph(glyphName) - destinationGlyph = destination[glyphName] - # outlines - pen = destinationGlyph.getPen() - sourceGlyph.draw(pen) - # width - destinationGlyph.width = sourceGlyph.width - # height and vertical origin - if vmtx is not None and glyphName in vmtx.metrics: - destinationGlyph.height = vmtx[glyphName][0] - if vorg is not None: - if glyphName in vorg.VOriginRecords: - destinationGlyph.verticalOrigin = vorg[glyphName] - else: - destinationGlyph.verticalOrigin = vorg.defaultVertOriginY - else: - tsb = vmtx[glyphName][1] - bounds_pen = ControlBoundsPen(glyphSet) - sourceGlyph.draw(bounds_pen) - if bounds_pen.bounds is None: - continue - xMin, yMin, xMax, yMax = bounds_pen.bounds - destinationGlyph.verticalOrigin = tsb + yMax - # unicode - destinationGlyph.unicode = reversedMapping.get(glyphName) - -# ------- -# Kerning -# ------- - -def extractOpenTypeKerning(source, destination): - kerning = {} - groups = {} - if "GPOS" in source: - kerning, groups = _extractOpenTypeKerningFromGPOS(source) - elif "kern" in source: - kerning = _extractOpenTypeKerningFromKern(source) - groups = {} - for name, group in groups.items(): - groups[name] = list(sorted(group)) - return kerning, groups - -def _extractOpenTypeKerningFromGPOS(source): - gpos = source["GPOS"].table - # get an ordered list of scripts - scriptOrder = _makeScriptOrder(gpos) - # extract kerning and classes from each applicable lookup - kerningDictionaries, leftClassDictionaries, rightClassDictionaries = _gatherDataFromLookups(gpos, scriptOrder) - # merge all kerning pairs - kerning = _mergeKerningDictionaries(kerningDictionaries) - # get rid of groups that have only one member - leftSingleMemberGroups = _findSingleMemberGroups(leftClassDictionaries) - rightSingleMemberGroups = _findSingleMemberGroups(rightClassDictionaries) - # filter out the single glyph groups from the kerning - kerning = _removeSingleMemberGroupReferences(kerning, leftSingleMemberGroups, rightSingleMemberGroups) - # merge groups that have the exact same member list - leftClasses, leftClassRename = _mergeClasses(leftClassDictionaries) - rightClasses, rightClassRename = _mergeClasses(rightClassDictionaries) - # search for overlapping groups and raise an error if any were found - _validateClasses(leftClasses) - _validateClasses(rightClasses) - # populate the class marging into the kerning - kerning = _replaceRenamedPairMembers(kerning, leftClassRename, rightClassRename) - # rename the groups to final names - leftClassRename = _renameClasses(leftClasses, "public.kern1.") - rightClassRename = _renameClasses(rightClasses, "public.kern2.") - # populate the final group names - kerning = _replaceRenamedPairMembers(kerning, leftClassRename, rightClassRename) - leftGroups = _setGroupNames(leftClasses, leftClassRename) - rightGroups = _setGroupNames(rightClasses, rightClassRename) - # combine the side groups - groups = {} - groups.update(leftGroups) - groups.update(rightGroups) - # done. - return kerning, groups - -def _makeScriptOrder(gpos): - """ - Run therough GPOS and make an alphabetically - ordered list of scripts. If DFLT is in the list, - move it to the front. - """ - scripts = [] - for scriptRecord in gpos.ScriptList.ScriptRecord: - scripts.append(scriptRecord.ScriptTag) - if "DFLT" in scripts: - scripts.remove("DFLT") - scripts.insert(0, "DFLT") - return sorted(scripts) - -def _gatherDataFromLookups(gpos, scriptOrder): - """ - Gather kerning and classes from the applicable lookups - and return them in script order. - """ - lookupIndexes = _gatherLookupIndexes(gpos) - seenLookups = set() - kerningDictionaries = [] - leftClassDictionaries = [] - rightClassDictionaries = [] - for script in scriptOrder: - kerning = [] - leftClasses = [] - rightClasses = [] - for lookupIndex in lookupIndexes[script]: - if lookupIndex in seenLookups: - continue - seenLookups.add(lookupIndex) - result = _gatherKerningForLookup(gpos, lookupIndex) - if result is None: - continue - k, lG, rG = result - kerning.append(k) - leftClasses.append(lG) - rightClasses.append(rG) - if kerning: - kerningDictionaries.append(kerning) - leftClassDictionaries.append(leftClasses) - rightClassDictionaries.append(rightClasses) - return kerningDictionaries, leftClassDictionaries, rightClassDictionaries - -def _gatherLookupIndexes(gpos): - """ - Gather a mapping of script to lookup indexes - referenced by the kern feature for each script. - Returns a dictionary of this structure: - { - "latn" : [0], - "DFLT" : [0] - } - """ - # gather the indexes of the kern features - kernFeatureIndexes = [index for index, featureRecord in enumerate(gpos.FeatureList.FeatureRecord) if featureRecord.FeatureTag == "kern"] - # find scripts and languages that have kern features - scriptKernFeatureIndexes = {} - for scriptRecord in gpos.ScriptList.ScriptRecord: - script = scriptRecord.ScriptTag - thisScriptKernFeatureIndexes = [] - defaultLangSysRecord = scriptRecord.Script.DefaultLangSys - if defaultLangSysRecord is not None: - f = [] - for featureIndex in defaultLangSysRecord.FeatureIndex: - if featureIndex not in kernFeatureIndexes: - continue - f.append(featureIndex) - if f: - thisScriptKernFeatureIndexes.append((None, f)) - if scriptRecord.Script.LangSysRecord is not None: - for langSysRecord in scriptRecord.Script.LangSysRecord: - langSys = langSysRecord.LangSysTag - f = [] - for featureIndex in langSysRecord.LangSys.FeatureIndex: - if featureIndex not in kernFeatureIndexes: - continue - f.append(featureIndex) - if f: - thisScriptKernFeatureIndexes.append((langSys, f)) - scriptKernFeatureIndexes[script] = thisScriptKernFeatureIndexes - # convert the feature indexes to lookup indexes - scriptLookupIndexes = {} - for script, featureDefinitions in scriptKernFeatureIndexes.items(): - lookupIndexes = scriptLookupIndexes[script] = [] - for language, featureIndexes in featureDefinitions: - for featureIndex in featureIndexes: - featureRecord = gpos.FeatureList.FeatureRecord[featureIndex] - for lookupIndex in featureRecord.Feature.LookupListIndex: - if lookupIndex not in lookupIndexes: - lookupIndexes.append(lookupIndex) - # done - return scriptLookupIndexes - -def _gatherKerningForLookup(gpos, lookupIndex): - """ - Gather the kerning and class data for a particular lookup. - Returns kerning, left clases, right classes. - The kerning dictionary is of this structure: - { - ("a", "a") : 10, - ((1, 1, 3), "a") : -20 - } - The class dictionaries have this structure: - { - (1, 1, 3) : ["x", "y", "z"] - } - Where the tuple means this: - (lookup index, subtable index, class index) - """ - allKerning = {} - allLeftClasses = {} - allRightClasses = {} - lookup = gpos.LookupList.Lookup[lookupIndex] - # only handle pair positioning and extension - if lookup.LookupType not in (2, 9): - return - for subtableIndex, subtable in enumerate(lookup.SubTable): - if lookup.LookupType == 2: - format = subtable.Format - lookupType = subtable.LookupType - if (lookupType, format) == (2, 1): - kerning = _handleLookupType2Format1(subtable) - allKerning.update(kerning) - elif (lookupType, format) == (2, 2): - kerning, leftClasses, rightClasses = _handleLookupType2Format2(subtable, lookupIndex, subtableIndex) - allKerning.update(kerning) - allLeftClasses.update(leftClasses) - allRightClasses.update(rightClasses) - elif lookup.LookupType == 9: - extSubtable = subtable.ExtSubTable - format = extSubtable.Format - lookupType = extSubtable.LookupType - if (lookupType, format) == (2, 1): - kerning = _handleLookupType2Format1(extSubtable) - allKerning.update(kerning) - elif (lookupType, format) == (2, 2): - kerning, leftClasses, rightClasses = _handleLookupType2Format2(extSubtable, lookupIndex, subtableIndex) - allKerning.update(kerning) - allLeftClasses.update(leftClasses) - allRightClasses.update(rightClasses) - # done - return allKerning, allLeftClasses, allRightClasses - -def _handleLookupType2Format1(subtable): - """ - Extract a kerning dictionary from a Lookup Type 2 Format 1. - """ - kerning = {} - coverage = subtable.Coverage.glyphs - valueFormat1 = subtable.ValueFormat1 - pairSets = subtable.PairSet - for index, leftGlyphName in enumerate(coverage): - pairSet = pairSets[index] - for pairValueRecord in pairSet.PairValueRecord: - rightGlyphName = pairValueRecord.SecondGlyph - if valueFormat1: - value = pairValueRecord.Value1 - else: - value = pairValueRecord.Value2 - if hasattr(value, "XAdvance"): - value = value.XAdvance - kerning[leftGlyphName, rightGlyphName] = value - return kerning - -def _handleLookupType2Format2(subtable, lookupIndex, subtableIndex): - """ - Extract kerning, left class and right class dictionaries from a Lookup Type 2 Format 2. - """ - # extract the classes - leftClasses = _extractFeatureClasses(lookupIndex=lookupIndex, subtableIndex=subtableIndex, classDefs=subtable.ClassDef1.classDefs, coverage=subtable.Coverage.glyphs) - rightClasses = _extractFeatureClasses(lookupIndex=lookupIndex, subtableIndex=subtableIndex, classDefs=subtable.ClassDef2.classDefs) - # extract the pairs - kerning = {} - for class1RecordIndex, class1Record in enumerate(subtable.Class1Record): - for class2RecordIndex, class2Record in enumerate(class1Record.Class2Record): - leftClass = (lookupIndex, subtableIndex, class1RecordIndex) - rightClass = (lookupIndex, subtableIndex, class2RecordIndex) - valueFormat1 = subtable.ValueFormat1 - if valueFormat1: - value = class2Record.Value1 - else: - value = class2Record.Value2 - if hasattr(value, "XAdvance") and value.XAdvance != 0: - value = value.XAdvance - kerning[leftClass, rightClass] = value - return kerning, leftClasses, rightClasses - -def _mergeKerningDictionaries(kerningDictionaries): - """ - Merge all of the kerning dictionaries found into - one flat dictionary. - """ - # work through the dictionaries backwards since - # this uses an update to load the kerning. this - # will ensure that the script order is honored. - kerning = {} - for dictionaryGroup in reversed(kerningDictionaries): - for dictionary in dictionaryGroup: - kerning.update(dictionary) - # done. - return kerning - -def _findSingleMemberGroups(classDictionaries): - """ - Find all classes that have only one member. - """ - toRemove = {} - for classDictionaryGroup in classDictionaries: - for classDictionary in classDictionaryGroup: - for name, members in list(classDictionary.items()): - if len(members) == 1: - toRemove[name] = list(members)[0] - del classDictionary[name] - return toRemove - -def _removeSingleMemberGroupReferences(kerning, leftGroups, rightGroups): - """ - Translate group names into glyph names in pairs - if the group only contains one glyph. - """ - new = {} - for (left, right), value in kerning.items(): - left = leftGroups.get(left, left) - right = rightGroups.get(right, right) - new[left, right] = value - return new - -def _mergeClasses(classDictionaries): - """ - Look for classes that have the exact same list - of members and flag them for removal. - This returns left classes, left rename map, - right classes and right rename map. - The classes have the standard class structure. - The rename maps have this structure: - { - (1, 1, 3) : (2, 3, 4), - old name : new name - } - Where the key is the class that should be - preserved and the value is a list of classes - that should be removed. - """ - # build a mapping of members to names - memberTree = {} - for classDictionaryGroup in classDictionaries: - for classDictionary in classDictionaryGroup: - for name, members in classDictionary.items(): - if members not in memberTree: - memberTree[members] = set() - memberTree[members].add(name) - # find members that have more than one name - classes = {} - rename = {} - for members, names in memberTree.items(): - name = names.pop() - if len(names) > 0: - for otherName in names: - rename[otherName] = name - classes[name] = members - return classes, rename - -def _setGroupNames(classes, classRename): - """ - Set the final names into the groups. - """ - groups = {} - for groupName, glyphList in classes.items(): - groupName = classRename.get(groupName, groupName) - # if the glyph list has only one member, - # the glyph name will be used in the pairs. - # no group is needed. - if len(glyphList) == 1: - continue - groups[groupName] = glyphList - return groups - -def _validateClasses(classes): - """ - Check to make sure that a glyph is not part of more than - one class. If this is found, an ExtractorError is raised. - """ - glyphToClass = {} - for className, glyphList in classes.items(): - for glyphName in glyphList: - if glyphName not in glyphToClass: - glyphToClass[glyphName] = set() - glyphToClass[glyphName].add(className) - conflicts = 0 - for glyphName, groupList in glyphToClass.items(): - if len(groupList) > 1: - print('Conflicting kerning classes for %s:' % glyphName) - for groupId in groupList: - group = classes[groupId] - print(' %r => %s' % (groupId, ', '.join(group))) - conflicts += 1 - if conflicts > 0: - raise ExtractorError("Kerning classes are in an conflicting state") - -def _replaceRenamedPairMembers(kerning, leftRename, rightRename): - """ - Populate the renamed pair members into the kerning. - """ - renamedKerning = {} - for (left, right), value in kerning.items(): - left = leftRename.get(left, left) - right = rightRename.get(right, right) - renamedKerning[left, right] = value - return renamedKerning - -def _renameClasses(classes, prefix): - """ - Replace class IDs with nice strings. - """ - renameMap = {} - for classID, glyphList in classes.items(): - if len(glyphList) == 0: - groupName = "%s_empty_lu.%d_st.%d_cl.%d" % (prefix, classID[0], classID[1], classID[2]) - elif len(glyphList) == 1: - groupName = list(glyphList)[0] - else: - glyphList = list(sorted(glyphList)) - groupName = prefix + glyphList[0] - renameMap[classID] = groupName - return renameMap - -def _extractFeatureClasses(lookupIndex, subtableIndex, classDefs, coverage=None): - """ - Extract classes for a specific lookup in a specific subtable. - This is relatively straightforward, except for class 0 interpretation. - Some fonts don't have class 0. Some fonts have a list of class - members that are clearly not all to be used in kerning pairs. - In the case of a missing class 0, the coverage is used as a basis - for the class and glyph names used in classed 1+ are filtered out. - In the case of class 0 having glyph names that are not part of the - kerning pairs, the coverage is used to filter out the unnecessary - glyph names. - """ - # gather the class members - classDict = {} - for glyphName, classIndex in classDefs.items(): - if classIndex not in classDict: - classDict[classIndex] = set() - classDict[classIndex].add(glyphName) - # specially handle class index 0 - revisedClass0 = set() - if coverage is not None and 0 in classDict: - for glyphName in classDict[0]: - if glyphName in coverage: - revisedClass0.add(glyphName) - elif coverage is not None and 0 not in classDict: - revisedClass0 = set(coverage) - for glyphList in classDict.values(): - revisedClass0 = revisedClass0 - glyphList - classDict[0] = revisedClass0 - # flip the class map around - classes = {} - for classIndex, glyphList in classDict.items(): - classes[lookupIndex, subtableIndex, classIndex] = frozenset(glyphList) - return classes - -def _extractOpenTypeKerningFromKern(source): - kern = source["kern"] - kerning = {} - for subtable in kern.kernTables: - if subtable.version != 0: - raise ExtractorError("Unknown kern table formst: %d" % subtable.version) - # XXX the spec defines coverage values for - # kerning direction (horizontal or vertical) - # minimum (some sort of kerning restriction) - # cross-stream (direction of the kerns within the direction of the table. odd.) - # override (if the values in this subtable should override the values of others) - # however, it is vague about how these should be stored. - # as such, we just assume that the direction is horizontal, - # that the values of all subtables are additive and that - # there are no minimum values. - kerning.update(subtable.kernTable) - return kerning |