summaryrefslogtreecommitdiff
path: root/misc/pylib/extractor/formats/opentype.py
diff options
context:
space:
mode:
Diffstat (limited to 'misc/pylib/extractor/formats/opentype.py')
-rwxr-xr-xmisc/pylib/extractor/formats/opentype.py806
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