summaryrefslogtreecommitdiff
path: root/misc
diff options
context:
space:
mode:
authorRasmus Andersson <rasmus@notion.se>2017-11-29 04:11:48 +0300
committerRasmus Andersson <rasmus@notion.se>2017-11-29 04:11:48 +0300
commit1f684610cd33f3ca6aef0891bcd597669130b683 (patch)
tree17955cb034022e29293084eb02867d97c2114d41 /misc
parent12076e07b1daaef1653593e3c6dcc9e6baeee589 (diff)
downloadinter-1f684610cd33f3ca6aef0891bcd597669130b683.tar.xz
Adds script for verifying font files
Diffstat (limited to 'misc')
-rwxr-xr-xmisc/check-font.py34
-rwxr-xr-xmisc/pylib/extractor/License.txt21
-rwxr-xr-xmisc/pylib/extractor/README.rst50
-rwxr-xr-xmisc/pylib/extractor/__init__.py45
-rwxr-xr-xmisc/pylib/extractor/exceptions.py1
-rwxr-xr-xmisc/pylib/extractor/formats/__init__.py0
-rwxr-xr-xmisc/pylib/extractor/formats/opentype.py806
-rwxr-xr-xmisc/pylib/extractor/formats/ttx.py28
-rwxr-xr-xmisc/pylib/extractor/formats/type1.py164
-rwxr-xr-xmisc/pylib/extractor/formats/woff.py222
-rwxr-xr-xmisc/pylib/extractor/requirements.txt2
-rwxr-xr-xmisc/pylib/extractor/tools.py32
12 files changed, 1405 insertions, 0 deletions
diff --git a/misc/check-font.py b/misc/check-font.py
new file mode 100755
index 000000000..e7c432cad
--- /dev/null
+++ b/misc/check-font.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# encoding: utf8
+from __future__ import print_function
+import os, sys
+from argparse import ArgumentParser
+from multiprocessing import Pool
+import extractor, defcon
+
+
+def check_font(filename):
+ print('check %s' % filename)
+ ufo = defcon.Font()
+ extractor.extractUFO(filename, ufo, doGlyphs=True, doInfo=True, doKerning=True)
+
+
+def main(argv=None):
+ opts = ArgumentParser(description='Check')
+
+ opts.add_argument(
+ 'fontFiles', metavar='<file>', type=str, nargs='+',
+ help='Font files (otf, ttf, woff, woff2, pfa, pfb, ttx)')
+
+ args = opts.parse_args(argv)
+
+ if len(args.fontFiles) == 1:
+ check_font(args.fontFiles[0])
+ else:
+ p = Pool(8)
+ p.map(check_font, args.fontFiles)
+ p.terminate()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/misc/pylib/extractor/License.txt b/misc/pylib/extractor/License.txt
new file mode 100755
index 000000000..c564676ab
--- /dev/null
+++ b/misc/pylib/extractor/License.txt
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2010 Type Supply LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. \ No newline at end of file
diff --git a/misc/pylib/extractor/README.rst b/misc/pylib/extractor/README.rst
new file mode 100755
index 000000000..7dd9ab196
--- /dev/null
+++ b/misc/pylib/extractor/README.rst
@@ -0,0 +1,50 @@
+|Travis Build Status| |PyPI Version| |Python Versions|
+
+
+UFO Extractor
+=============
+
+Tools for extracting data from font binaries into UFO objects.
+
+Features
+--------
+
+Import data into a `Defcon <https://github.com/typesupply/defcon>`__ ``Font``
+instance:
+
+.. code:: python
+
+ >>> import extractor
+ >>> import defcon
+ >>> ufo = defcon.Font()
+ >>> extractor.extractUFO("/path/to/MyFont.ttf", ufo)
+ >>> ufo.save("/path/to/MyFont.ufo")
+
+Supported input formats:
+
+- CFF or TrueType-flavored OpenType fonts (``*.otf``, ``*.ttf``)
+- `FontTools <https://github.com/fonttools/fonttools>`__ TTX files
+ (``*.ttx``)
+- WOFF 1.0/2.0 (``*.woff``, ``*.woff2``)
+- PostScript Type1 fonts (``*.pfa``, ``*.pfb``, etc.)
+
+Installation
+------------
+
+You can install ``extractor`` with ``pip``:
+
+.. code::
+
+ $ pip install ufo-extractor
+
+Note that, for historical reasons, the package is listed on the
+`Python Package Index <https://travis-ci.org/typesupply/extractor>`__ under the name
+``ufo-extractor``, to disambiguate it from another package also called "extractor".
+However, the import name for the package remains ``extractor``, without prefix.
+
+
+.. |Travis Build Status| image:: https://travis-ci.org/typesupply/extractor.svg?branch=master
+ :target: https://travis-ci.org/typesupply/extractor
+.. |PyPI Version| image:: https://img.shields.io/pypi/v/ufo-extractor.svg
+ :target: https://pypi.org/project/ufo-extractor/
+.. |Python Versions| image:: https://img.shields.io/badge/python-2.7%2C%203.5-blue.svg
diff --git a/misc/pylib/extractor/__init__.py b/misc/pylib/extractor/__init__.py
new file mode 100755
index 000000000..121ae275e
--- /dev/null
+++ b/misc/pylib/extractor/__init__.py
@@ -0,0 +1,45 @@
+from extractor.exceptions import ExtractorError
+from extractor.formats.opentype import isOpenType, extractFontFromOpenType
+from extractor.formats.woff import isWOFF, extractFontFromWOFF
+from extractor.formats.type1 import isType1, extractFontFromType1
+from extractor.formats.ttx import isTTX, extractFontFromTTX
+
+
+__version__ = "0.2.1.dev0"
+
+_extractFunctions = dict(
+ OTF=extractFontFromOpenType,
+ Type1=extractFontFromType1,
+ WOFF=extractFontFromWOFF,
+ ttx=extractFontFromTTX,
+)
+
+def extractFormat(pathOrFile):
+ if isType1(pathOrFile):
+ return "Type1"
+ elif isWOFF(pathOrFile):
+ return "WOFF"
+ elif isOpenType(pathOrFile):
+ return "OTF"
+ elif isTTX(pathOrFile):
+ return "ttx"
+ return None
+
+def extractUFO(pathOrFile, destination, doGlyphs=True, doInfo=True, doKerning=True, format=None, customFunctions={}):
+ if format is None:
+ format = extractFormat(pathOrFile)
+ if format not in _extractFunctions:
+ raise ExtractorError("Unknown file format.")
+ func = _extractFunctions[format]
+ # wrap the extraction in a try: except: so that
+ # callers don't need to worry about lower level
+ # (fontTools, etc.) errors. if an error
+ # occurs, print the traceback for debugging and
+ # raise an ExtractorError.
+ try:
+ func(pathOrFile, destination, doGlyphs=doGlyphs, doInfo=doInfo, doKerning=doKerning, customFunctions=customFunctions.get(format, []))
+ except:
+ import sys
+ import traceback
+ traceback.print_exc(file=sys.stdout)
+ raise ExtractorError("There was an error reading the %s file." % format)
diff --git a/misc/pylib/extractor/exceptions.py b/misc/pylib/extractor/exceptions.py
new file mode 100755
index 000000000..823a88ace
--- /dev/null
+++ b/misc/pylib/extractor/exceptions.py
@@ -0,0 +1 @@
+class ExtractorError(Exception): pass
diff --git a/misc/pylib/extractor/formats/__init__.py b/misc/pylib/extractor/formats/__init__.py
new file mode 100755
index 000000000..e69de29bb
--- /dev/null
+++ b/misc/pylib/extractor/formats/__init__.py
diff --git a/misc/pylib/extractor/formats/opentype.py b/misc/pylib/extractor/formats/opentype.py
new file mode 100755
index 000000000..3cf0e06ec
--- /dev/null
+++ b/misc/pylib/extractor/formats/opentype.py
@@ -0,0 +1,806 @@
+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
diff --git a/misc/pylib/extractor/formats/ttx.py b/misc/pylib/extractor/formats/ttx.py
new file mode 100755
index 000000000..fac4a98a0
--- /dev/null
+++ b/misc/pylib/extractor/formats/ttx.py
@@ -0,0 +1,28 @@
+from extractor.formats.opentype import extractOpenTypeInfo, extractOpenTypeGlyphs, extractOpenTypeKerning
+
+def isTTX(pathOrFile):
+ from fontTools.ttLib import TTFont, TTLibError
+ try:
+ font = TTFont()
+ font.importXML(pathOrFile)
+ del font
+ except TTLibError:
+ return False
+ return True
+
+def extractFontFromTTX(pathOrFile, destination, doGlyphs=True, doInfo=True, doKerning=True, customFunctions=[]):
+ from fontTools.ttLib import TTFont, TTLibError
+ source = TTFont()
+ source.importXML(pathOrFile)
+ if doInfo:
+ extractOpenTypeInfo(source, destination)
+ if doGlyphs:
+ extractOpenTypeGlyphs(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()
diff --git a/misc/pylib/extractor/formats/type1.py b/misc/pylib/extractor/formats/type1.py
new file mode 100755
index 000000000..ca3f76741
--- /dev/null
+++ b/misc/pylib/extractor/formats/type1.py
@@ -0,0 +1,164 @@
+from fontTools.t1Lib import T1Font, T1Error
+from fontTools.agl import AGL2UV
+from fontTools.misc.psLib import PSInterpreter
+from fontTools.misc.transform import Transform
+from extractor.tools import RelaxedInfo
+
+# specification: http://partners.adobe.com/public/developer/en/font/T1_SPEC.PDF
+
+# ----------------
+# Public Functions
+# ----------------
+
+def isType1(pathOrFile):
+ try:
+ font = T1Font(pathOrFile)
+ del font
+ except T1Error:
+ return False
+ return True
+
+def extractFontFromType1(pathOrFile, destination, doGlyphs=True, doInfo=True, doKerning=True, customFunctions=[]):
+ source = T1Font(pathOrFile)
+ destination.lib["public.glyphOrder"] = _extractType1GlyphOrder(source)
+ if doInfo:
+ extractType1Info(source, destination)
+ if doGlyphs:
+ extractType1Glyphs(source, destination)
+ if doKerning:
+ # kerning extraction is not supported yet.
+ # in theory, it could be retried from an AFM.
+ # we need to find the AFM naming rules so that we can sniff for the file.
+ pass
+ for function in customFunctions:
+ function(source, destination)
+
+def extractType1Info(source, destination):
+ info = RelaxedInfo(destination.info)
+ _extractType1FontInfo(source, info)
+ _extractType1Private(source, info)
+ _extractType1FontMatrix(source, info)
+
+# ----
+# Info
+# ----
+
+def _extractType1FontInfo(source, info):
+ sourceInfo = source["FontInfo"]
+ # FontName
+ info.postscriptFontName = source["FontName"]
+ # version
+ version = sourceInfo.get("version")
+ if version is not None:
+ # the spec says that version will be a string and no formatting info is given.
+ # so, only move forward if the string can actually be parsed.
+ try:
+ # 1. convert to a float
+ version = float(version)
+ # 2. convert it back to a string
+ version = "%.3f" % version
+ # 3. split.
+ versionMajor, versionMinor = version.split(".")
+ # 4. convert.
+ versionMajor = int(versionMajor)
+ versionMinor = int(versionMinor)
+ # 5. set.
+ info.versionMajor = int(versionMajor)
+ info.versionMinor = int(versionMinor)
+ except ValueError:
+ # couldn't parse. leve the object with the default values.
+ pass
+ # Notice
+ notice = sourceInfo.get("Notice")
+ if notice:
+ info.copyright = notice
+ # FullName
+ fullName = sourceInfo.get("FullName")
+ if fullName:
+ info.postscriptFullName = fullName
+ # FamilyName
+ familyName = sourceInfo.get("FamilyName")
+ if familyName:
+ info.familyName = familyName
+ # Weight
+ postscriptWeightName = sourceInfo.get("Weight")
+ if postscriptWeightName:
+ info.postscriptWeightName = postscriptWeightName
+ # ItalicAngle
+ info.italicAngle = sourceInfo.get("ItalicAngle")
+ # IsFixedPitch
+ info.postscriptIsFixedPitch = sourceInfo.get("isFixedPitch")
+ # UnderlinePosition/Thickness
+ info.postscriptUnderlinePosition = sourceInfo.get("UnderlinePosition")
+ info.postscriptUnderlineThickness = sourceInfo.get("UnderlineThickness")
+
+def _extractType1FontMatrix(source, info):
+ # units per em
+ matrix = source["FontMatrix"]
+ matrix = Transform(*matrix).inverse()
+ info.unitsPerEm = int(round(matrix[3]))
+
+def _extractType1Private(source, info):
+ private = source["Private"]
+ # UniqueID
+ info.openTypeNameUniqueID = private.get("UniqueID", None)
+ # BlueValues and OtherBlues
+ info.postscriptBlueValues = private.get("BlueValues", [])
+ info.postscriptOtherBlues = private.get("OtherBlues", [])
+ # FamilyBlues and FamilyOtherBlues
+ info.postscriptFamilyBlues = private.get("FamilyBlues", [])
+ info.postscriptFamilyOtherBlues = private.get("FamilyOtherBlues", [])
+ # BlueScale/Shift/Fuzz
+ info.postscriptBlueScale = private.get("BlueScale", None)
+ info.postscriptBlueShift = private.get("BlueShift", None)
+ info.postscriptBlueFuzz = private.get("BlueFuzz", None)
+ # StemSnapH/V
+ info.postscriptStemSnapH = private.get("StemSnapH", [])
+ info.postscriptStemSnapV = private.get("StemSnapV", [])
+ # ForceBold
+ info.postscriptForceBold = bool(private.get("ForceBold", None))
+
+# --------
+# Outlines
+# --------
+
+def extractType1Glyphs(source, destination):
+ glyphSet = source.getGlyphSet()
+ for glyphName in sorted(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
+ # synthesize the unicode value
+ destinationGlyph.unicode = AGL2UV.get(glyphName)
+
+# -----------
+# Glyph order
+# -----------
+
+class GlyphOrderPSInterpreter(PSInterpreter):
+
+ def __init__(self):
+ PSInterpreter.__init__(self)
+ self.glyphOrder = []
+ self.collectTokenForGlyphOrder = False
+
+ def do_literal(self, token):
+ result = PSInterpreter.do_literal(self, token)
+ if token == "/FontName":
+ self.collectTokenForGlyphOrder = False
+ if self.collectTokenForGlyphOrder:
+ self.glyphOrder.append(result.value)
+ if token == "/CharStrings":
+ self.collectTokenForGlyphOrder = True
+ return result
+
+def _extractType1GlyphOrder(t1Font):
+ interpreter = GlyphOrderPSInterpreter()
+ interpreter.interpret(t1Font.data)
+ return interpreter.glyphOrder
diff --git a/misc/pylib/extractor/formats/woff.py b/misc/pylib/extractor/formats/woff.py
new file mode 100755
index 000000000..b641232f5
--- /dev/null
+++ b/misc/pylib/extractor/formats/woff.py
@@ -0,0 +1,222 @@
+from xml.sax.saxutils import quoteattr
+from fontTools.ttLib import TTFont, TTLibError
+from extractor.tools import RelaxedInfo
+from extractor.formats.opentype import extractOpenTypeInfo, extractOpenTypeGlyphs, extractOpenTypeKerning
+
+try:
+ from xml.etree import cElementTree as ElementTree
+except ImportError:
+ from xml.etree import ElementTree
+
+# ----------------
+# Public Functions
+# ----------------
+
+def isWOFF(pathOrFile):
+ flavor = None
+ try:
+ font = TTFont(pathOrFile)
+ flavor = font.flavor
+ del font
+ except TTLibError:
+ return False
+ return flavor in ("woff", "woff2")
+
+def extractFontFromWOFF(pathOrFile, destination, doGlyphs=True, doInfo=True, doKerning=True, customFunctions=[]):
+ source = TTFont(pathOrFile)
+ if doInfo:
+ extractWOFFInfo(source, destination)
+ if doGlyphs:
+ extractWOFFGlyphs(source, destination)
+ if doKerning:
+ kerning, groups = extractWOFFKerning(source, destination)
+ destination.groups.update(groups)
+ destination.kerning.clear()
+ destination.kerning.update(kerning)
+ for function in customFunctions:
+ function(source, destination)
+ source.close()
+
+# ----------------
+# Specific Imports
+# ----------------
+
+def extractWOFFInfo(source, destination):
+ info = RelaxedInfo(destination.info)
+ info.woffMajorVersion = source.flavorData.majorVersion
+ info.woffMinorVersion = source.flavorData.minorVersion
+ _extractWOFFMetadata(source.flavorData, info)
+ return extractOpenTypeInfo(source, destination)
+
+def extractWOFFGlyphs(source, destination):
+ return extractOpenTypeGlyphs(source, destination)
+
+def extractWOFFKerning(source, destination):
+ return extractOpenTypeKerning(source, destination)
+
+# --------
+# Metadata
+# --------
+
+def _extractWOFFMetadata(source, destination):
+ if source.metaData is None:
+ return
+ metadata = ElementTree.fromstring(source.metaData)
+ for element in metadata:
+ if element.tag == "uniqueid":
+ _extractWOFFMetadataUniqueID(element, destination)
+ elif element.tag == "vendor":
+ _extractWOFFMetadataVendor(element, destination)
+ elif element.tag == "credits":
+ _extractWOFFMetadataCredits(element, destination)
+ elif element.tag == "description":
+ _extractWOFFMetadataDescription(element, destination)
+ elif element.tag == "license":
+ _extractWOFFMetadataLicense(element, destination)
+ elif element.tag == "copyright":
+ _extractWOFFMetadataCopyright(element, destination)
+ elif element.tag == "trademark":
+ _extractWOFFMetadataTrademark(element, destination)
+ elif element.tag == "licensee":
+ _extractWOFFMetadataLicensee(element, destination)
+ elif element.tag == "extension":
+ _extractWOFFMetadataExtension(element, destination)
+
+def _extractWOFFMetadataUniqueID(element, destination):
+ destination.woffMetadataUniqueID = _extractWOFFMetadataDict(element, ("id",))
+
+def _extractWOFFMetadataVendor(element, destination):
+ attributes = ("name", "url", "dir", "class")
+ record = _extractWOFFMetadataDict(element, attributes)
+ destination.woffMetadataVendor = record
+
+def _extractWOFFMetadataCredits(element, destination):
+ attributes = ("name", "url", "role", "dir", "class")
+ credits = []
+ for subElement in element:
+ if subElement.tag == "credit":
+ record = _extractWOFFMetadataDict(subElement, attributes)
+ credits.append(record)
+ destination.woffMetadataCredits = dict(credits=credits)
+
+def _extractWOFFMetadataDescription(element, destination):
+ description = _extractWOFFMetadataDict(element, ("url",))
+ textRecords = _extractWOFFMetadataText(element)
+ if textRecords:
+ description["text"] = textRecords
+ destination.woffMetadataDescription = description
+
+def _extractWOFFMetadataLicense(element, destination):
+ license = _extractWOFFMetadataDict(element, ("url", "id"))
+ textRecords = _extractWOFFMetadataText(element)
+ if textRecords:
+ license["text"] = textRecords
+ destination.woffMetadataLicense = license
+
+def _extractWOFFMetadataCopyright(element, destination):
+ copyright = {}
+ textRecords = _extractWOFFMetadataText(element)
+ if textRecords:
+ copyright["text"] = textRecords
+ destination.woffMetadataCopyright = copyright
+
+def _extractWOFFMetadataTrademark(element, destination):
+ trademark = {}
+ textRecords = _extractWOFFMetadataText(element)
+ if textRecords:
+ trademark["text"] = textRecords
+ destination.woffMetadataTrademark = trademark
+
+def _extractWOFFMetadataLicensee(element, destination):
+ destination.woffMetadataLicensee = _extractWOFFMetadataDict(element, ("name", "dir", "class"))
+
+def _extractWOFFMetadataExtension(element, destination):
+ extension = _extractWOFFMetadataDict(element, ("id",))
+ for subElement in element:
+ if subElement.tag == "name":
+ if "names" not in extension:
+ extension["names"] = []
+ name = _extractWOFFMetadataExtensionName(subElement)
+ extension["names"].append(name)
+ elif subElement.tag == "item":
+ if "items" not in extension:
+ extension["items"] = []
+ item = _extractWOFFMetadataExtensionItem(subElement)
+ extension["items"].append(item)
+ extensions = []
+ if destination.woffMetadataExtensions:
+ extensions.extend(destination.woffMetadataExtensions)
+ destination.woffMetadataExtensions = extensions + [extension]
+
+def _extractWOFFMetadataExtensionItem(element):
+ item = _extractWOFFMetadataDict(element, ("id",))
+ for subElement in element:
+ if subElement.tag == "name":
+ if "names" not in item:
+ item["names"] = []
+ name = _extractWOFFMetadataExtensionName(subElement)
+ item["names"].append(name)
+ elif subElement.tag == "value":
+ if "values" not in item:
+ item["values"] = []
+ name = _extractWOFFMetadataExtensionValue(subElement)
+ item["values"].append(name)
+ return item
+
+def _extractWOFFMetadataExtensionName(element):
+ name = _extractWOFFMetadataDict(element, ("dir", "class"))
+ language = _extractWOFFMetadataLanguage(element)
+ if language is not None:
+ name["language"] = language
+ name["text"] = _flattenWOFFMetadataString(element)
+ return name
+
+def _extractWOFFMetadataExtensionValue(element):
+ return _extractWOFFMetadataExtensionName(element)
+
+# support
+
+def _extractWOFFMetadataDict(element, attributes):
+ record = {}
+ for attribute in attributes:
+ value = element.attrib.get(attribute)
+ if value is not None:
+ record[attribute] = value
+ return record
+
+def _extractWOFFMetadataText(element):
+ records = []
+ attributes = ("dir", "class")
+ for subElement in element:
+ record = _extractWOFFMetadataDict(subElement, attributes)
+ # text
+ record["text"] = _flattenWOFFMetadataString(subElement)
+ # language
+ language = _extractWOFFMetadataLanguage(subElement)
+ if language is not None:
+ record["language"] = language
+ records.append(record)
+ return records
+
+def _extractWOFFMetadataLanguage(element):
+ language = element.attrib.get("{http://www.w3.org/XML/1998/namespace}lang")
+ if language is None:
+ language = element.attrib.get("lang")
+ return language
+
+def _flattenWOFFMetadataString(element, sub=False):
+ text = element.text.strip()
+ for subElement in element:
+ text += _flattenWOFFMetadataString(subElement, sub=True)
+ if element.tail:
+ text += element.tail.strip()
+ if sub:
+ attrib = ["%s=%s" % (key, quoteattr(value)) for key, value in element.attrib.items()]
+ attrib = " ".join(attrib)
+ if attrib:
+ start = "<%s %s>" % (element.tag, attrib)
+ else:
+ start = "<%s>" % (element.tag)
+ end = "</%s>" % (element.tag)
+ text = start + text + end
+ return text
diff --git a/misc/pylib/extractor/requirements.txt b/misc/pylib/extractor/requirements.txt
new file mode 100755
index 000000000..f87221064
--- /dev/null
+++ b/misc/pylib/extractor/requirements.txt
@@ -0,0 +1,2 @@
+fonttools==3.3.1
+ufoLib==2.0.0
diff --git a/misc/pylib/extractor/tools.py b/misc/pylib/extractor/tools.py
new file mode 100755
index 000000000..f0750d7c3
--- /dev/null
+++ b/misc/pylib/extractor/tools.py
@@ -0,0 +1,32 @@
+from ufoLib import fontInfoAttributesVersion3, validateFontInfoVersion3ValueForAttribute
+
+
+class RelaxedInfo(object):
+
+ """
+ This object that sets only valid info values
+ into the given info object.
+ """
+
+ def __init__(self, info):
+ self._object = info
+
+ def __getattr__(self, attr):
+ if attr in fontInfoAttributesVersion3:
+ return getattr(self._object, attr)
+ else:
+ return super(RelaxedInfo, self).__getattr__(attr)
+
+ def __setattr__(self, attr, value):
+ if attr in fontInfoAttributesVersion3:
+ if validateFontInfoVersion3ValueForAttribute(attr, value):
+ setattr(self._object, attr, value)
+ else:
+ super(RelaxedInfo, self).__setattr__(attr, value)
+
+
+def copyAttr(src, srcAttr, dest, destAttr):
+ if not hasattr(src, srcAttr):
+ return
+ value = getattr(src, srcAttr)
+ setattr(dest, destAttr, value)