Merge pull request #677 from fonttools/opentype-gx
Merge Opentype Variation Fonts (previously known as OpenType GX)
This commit is contained in:
commit
7740a5fe74
@ -79,6 +79,10 @@ class CFFFontSet(object):
|
||||
writer.toFile(file)
|
||||
|
||||
def toXML(self, xmlWriter, progress=None):
|
||||
xmlWriter.simpletag("major", value=self.major)
|
||||
xmlWriter.newline()
|
||||
xmlWriter.simpletag("minor", value=self.minor)
|
||||
xmlWriter.newline()
|
||||
for fontName in self.fontNames:
|
||||
xmlWriter.begintag("CFFFont", name=tostr(fontName))
|
||||
xmlWriter.newline()
|
||||
@ -155,6 +159,10 @@ class CFFWriter(object):
|
||||
log.log(DEBUG, "CFFWriter.toFile() writing to file.")
|
||||
begin = file.tell()
|
||||
posList = [0]
|
||||
cffOffSize = calcOffSize(lastPosList[-1])
|
||||
headerBytes = self.data[0]
|
||||
headerBytes = headerBytes[:-1] + bytechr(cffOffSize)
|
||||
self.data[0] = headerBytes
|
||||
for item in self.data:
|
||||
if hasattr(item, "toFile"):
|
||||
item.toFile(file)
|
||||
@ -186,43 +194,53 @@ class IndexCompiler(object):
|
||||
return items
|
||||
|
||||
def getOffsets(self):
|
||||
pos = 1
|
||||
offsets = [pos]
|
||||
for item in self.items:
|
||||
if hasattr(item, "getDataLength"):
|
||||
pos = pos + item.getDataLength()
|
||||
else:
|
||||
pos = pos + len(item)
|
||||
offsets.append(pos)
|
||||
# An empty INDEX contains only the count field.
|
||||
if self.items:
|
||||
pos = 1
|
||||
offsets = [pos]
|
||||
for item in self.items:
|
||||
if hasattr(item, "getDataLength"):
|
||||
pos = pos + item.getDataLength()
|
||||
else:
|
||||
pos = pos + len(item)
|
||||
offsets.append(pos)
|
||||
else:
|
||||
offsets = []
|
||||
return offsets
|
||||
|
||||
def getDataLength(self):
|
||||
lastOffset = self.getOffsets()[-1]
|
||||
offSize = calcOffSize(lastOffset)
|
||||
dataLength = (
|
||||
2 + # count
|
||||
1 + # offSize
|
||||
(len(self.items) + 1) * offSize + # the offsets
|
||||
lastOffset - 1 # size of object data
|
||||
)
|
||||
if self.items:
|
||||
lastOffset = self.getOffsets()[-1]
|
||||
offSize = calcOffSize(lastOffset)
|
||||
dataLength = (
|
||||
2 + # count
|
||||
1 + # offSize
|
||||
(len(self.items) + 1) * offSize + # the offsets
|
||||
lastOffset - 1 # size of object data
|
||||
)
|
||||
else:
|
||||
dataLength = 2 # count. For empty INDEX tables, this is the only entry.
|
||||
|
||||
return dataLength
|
||||
|
||||
def toFile(self, file):
|
||||
offsets = self.getOffsets()
|
||||
writeCard16(file, len(self.items))
|
||||
offSize = calcOffSize(offsets[-1])
|
||||
writeCard8(file, offSize)
|
||||
offSize = -offSize
|
||||
pack = struct.pack
|
||||
for offset in offsets:
|
||||
binOffset = pack(">l", offset)[offSize:]
|
||||
assert len(binOffset) == -offSize
|
||||
file.write(binOffset)
|
||||
for item in self.items:
|
||||
if hasattr(item, "toFile"):
|
||||
item.toFile(file)
|
||||
else:
|
||||
file.write(tobytes(item, encoding="latin1"))
|
||||
# An empty INDEX contains only the count field.
|
||||
if self.items:
|
||||
offSize = calcOffSize(offsets[-1])
|
||||
writeCard8(file, offSize)
|
||||
offSize = -offSize
|
||||
pack = struct.pack
|
||||
for offset in offsets:
|
||||
binOffset = pack(">l", offset)[offSize:]
|
||||
assert len(binOffset) == -offSize
|
||||
file.write(binOffset)
|
||||
for item in self.items:
|
||||
if hasattr(item, "toFile"):
|
||||
item.toFile(file)
|
||||
else:
|
||||
file.write(tobytes(item, encoding="latin1"))
|
||||
|
||||
|
||||
class IndexedStringsCompiler(IndexCompiler):
|
||||
@ -433,6 +451,11 @@ class FDArrayIndex(TopDictIndex):
|
||||
|
||||
compilerClass = FDArrayIndexCompiler
|
||||
|
||||
def produceItem(self, index, data, file, offset, size):
|
||||
fontDict = FontDict(self.strings, file, offset, self.GlobalSubrs)
|
||||
fontDict.decompile(data)
|
||||
return fontDict
|
||||
|
||||
def fromXML(self, name, attrs, content):
|
||||
if name != "FontDict":
|
||||
return
|
||||
@ -441,7 +464,11 @@ class FDArrayIndex(TopDictIndex):
|
||||
if isinstance(element, basestring):
|
||||
continue
|
||||
name, attrs, content = element
|
||||
fontDict.fromXML(name, attrs, content)
|
||||
try:
|
||||
fontDict.fromXML(name, attrs, content)
|
||||
except KeyError:
|
||||
"""Since fonttools used to pass a lot of fields that are not relevant in the FDArray FontDict, there are 'ttx' files in the wild that contain all these. These got in the ttx files because fonttools writes explicit values for all the TopDict default values. These are not actually illegal in the context of an FDArray FontDict - you can legally, per spec, put any arbitrary key/value pair in a FontDict - but are useless since current major company CFF interpreters ignore anything but the set listed in this file. So, we just silently skip them. An exception is Weight: this is not used by any interepreter, but some foundries have asked that this be supported in FDArray FontDicts just to preserve information about the design when the font is being inspected."""
|
||||
pass
|
||||
self.append(fontDict)
|
||||
|
||||
|
||||
@ -490,7 +517,6 @@ class FDSelect:
|
||||
def append(self, fdSelectValue):
|
||||
self.gidArray.append(fdSelectValue)
|
||||
|
||||
|
||||
class CharStrings(object):
|
||||
|
||||
def __init__(self, file, charset, globalSubrs, private, fdSelect, fdArray):
|
||||
@ -500,9 +526,11 @@ class CharStrings(object):
|
||||
for i in range(len(charset)):
|
||||
charStrings[charset[i]] = i
|
||||
self.charStringsAreIndexed = 1
|
||||
# read from OTF file: charStrings.values() are indices into charStringsIndex.
|
||||
else:
|
||||
self.charStrings = {}
|
||||
self.charStringsAreIndexed = 0
|
||||
# read from ttx file : charStrings.values() are actual charstrings
|
||||
self.globalSubrs = globalSubrs
|
||||
self.private = private
|
||||
if fdSelect is not None:
|
||||
@ -806,6 +834,8 @@ class CharsetConverter(object):
|
||||
charset = cffIExpertStrings
|
||||
elif value == 2:
|
||||
charset = cffExpertSubsetStrings
|
||||
if charset and len(charset) != parent.numGlyphs:
|
||||
charset = charset[:parent.numGlyphs]
|
||||
return charset
|
||||
|
||||
def write(self, parent, value):
|
||||
@ -843,6 +873,25 @@ class CharsetCompiler(object):
|
||||
def toFile(self, file):
|
||||
file.write(self.data)
|
||||
|
||||
def getStdCharSet(charset):
|
||||
# check to see if we can use a predefined charset value.
|
||||
charsetCode = None
|
||||
predefinedCharSets = [
|
||||
(cffISOAdobeStringCount, cffISOAdobeStrings, 0),
|
||||
(cffExpertStringCount, cffIExpertStrings, 1),
|
||||
(cffExpertSubsetStringCount, cffExpertSubsetStrings, 2) ]
|
||||
len_cs = len(charset)
|
||||
for cnt, charList, code in predefinedCharSets:
|
||||
if charsetCode != None:
|
||||
break
|
||||
if len_cs > cnt:
|
||||
continue
|
||||
charsetCode = code
|
||||
for i in range(len_cs):
|
||||
if charset[i] != charList[i]:
|
||||
charsetCode = None
|
||||
break
|
||||
return charsetCode
|
||||
|
||||
def getCIDfromName(name, strings):
|
||||
return int(name[3:])
|
||||
@ -1253,6 +1302,16 @@ topDictOperators = [
|
||||
(17, 'CharStrings', 'number', None, CharStringsConverter()),
|
||||
]
|
||||
|
||||
fontDictOperators = [
|
||||
# opcode name argument type default converter
|
||||
((12, 38), 'FontName', 'SID', None, None),
|
||||
((12, 7), 'FontMatrix', 'array', [0.001, 0, 0, 0.001, 0, 0], None),
|
||||
(4, 'Weight', 'SID', None, None),
|
||||
(18, 'Private', ('number', 'number'), None, PrivateDictConverter()),
|
||||
]
|
||||
|
||||
|
||||
|
||||
# Note! FDSelect and FDArray must both preceed CharStrings in the output XML build order,
|
||||
# in order for the font to compile back from xml.
|
||||
|
||||
@ -1298,11 +1357,14 @@ def addConverters(table):
|
||||
|
||||
addConverters(privateDictOperators)
|
||||
addConverters(topDictOperators)
|
||||
addConverters(fontDictOperators)
|
||||
|
||||
|
||||
class TopDictDecompiler(psCharStrings.DictDecompiler):
|
||||
operators = buildOperatorDict(topDictOperators)
|
||||
|
||||
class FontDictDecompiler(psCharStrings.DictDecompiler):
|
||||
operators = buildOperatorDict(fontDictOperators)
|
||||
|
||||
class PrivateDictDecompiler(psCharStrings.DictDecompiler):
|
||||
operators = buildOperatorDict(privateDictOperators)
|
||||
@ -1394,7 +1456,14 @@ class TopDictCompiler(DictCompiler):
|
||||
def getChildren(self, strings):
|
||||
children = []
|
||||
if hasattr(self.dictObj, "charset") and self.dictObj.charset:
|
||||
children.append(CharsetCompiler(strings, self.dictObj.charset, self))
|
||||
if hasattr(self.dictObj, "ROS"): # aka isCID
|
||||
charsetCode = None
|
||||
else:
|
||||
charsetCode = getStdCharSet(self.dictObj.charset)
|
||||
if charsetCode == None:
|
||||
children.append(CharsetCompiler(strings, self.dictObj.charset, self))
|
||||
else:
|
||||
self.rawDict["charset"] = charsetCode
|
||||
if hasattr(self.dictObj, "Encoding"):
|
||||
encoding = self.dictObj.Encoding
|
||||
if not isinstance(encoding, basestring):
|
||||
@ -1402,7 +1471,7 @@ class TopDictCompiler(DictCompiler):
|
||||
if hasattr(self.dictObj, "FDSelect"):
|
||||
# I have not yet supported merging a ttx CFF-CID font, as there are interesting
|
||||
# issues about merging the FDArrays. Here I assume that
|
||||
# either the font was read from XML, and teh FDSelect indices are all
|
||||
# either the font was read from XML, and the FDSelect indices are all
|
||||
# in the charstring data, or the FDSelect array is already fully defined.
|
||||
fdSelect = self.dictObj.FDSelect
|
||||
if len(fdSelect) == 0: # probably read in from XML; assume fdIndex in CharString data
|
||||
@ -1434,7 +1503,7 @@ class TopDictCompiler(DictCompiler):
|
||||
|
||||
class FontDictCompiler(DictCompiler):
|
||||
|
||||
opcodes = buildOpcodeDict(topDictOperators)
|
||||
opcodes = buildOpcodeDict(fontDictOperators)
|
||||
|
||||
def getChildren(self, strings):
|
||||
children = []
|
||||
@ -1563,23 +1632,18 @@ class TopDict(BaseDict):
|
||||
i = i + 1
|
||||
|
||||
|
||||
class FontDict(BaseDict):
|
||||
class FontDict(TopDict):
|
||||
|
||||
defaults = buildDefaults(topDictOperators)
|
||||
converters = buildConverters(topDictOperators)
|
||||
order = buildOrder(topDictOperators)
|
||||
decompilerClass = None
|
||||
defaults = buildDefaults(fontDictOperators)
|
||||
converters = buildConverters(fontDictOperators)
|
||||
order = buildOrder(fontDictOperators)
|
||||
decompilerClass = FontDictDecompiler
|
||||
compilerClass = FontDictCompiler
|
||||
|
||||
def __init__(self, strings=None, file=None, offset=None, GlobalSubrs=None):
|
||||
BaseDict.__init__(self, strings, file, offset)
|
||||
self.GlobalSubrs = GlobalSubrs
|
||||
|
||||
def getGlyphOrder(self):
|
||||
return self.charset
|
||||
TopDict.__init__(self, strings, file, offset)
|
||||
|
||||
def toXML(self, xmlWriter, progress):
|
||||
self.skipNames = ['Encoding']
|
||||
BaseDict.toXML(self, xmlWriter, progress)
|
||||
|
||||
|
||||
|
2
Lib/fontTools/feaLib/testdata/Attach.ttx
vendored
2
Lib/fontTools/feaLib/testdata/Attach.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<AttachList>
|
||||
<Coverage>
|
||||
<Glyph value="a"/>
|
||||
|
2
Lib/fontTools/feaLib/testdata/GPOS_1.ttx
vendored
2
Lib/fontTools/feaLib/testdata/GPOS_1.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/GPOS_2.ttx
vendored
2
Lib/fontTools/feaLib/testdata/GPOS_2.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/GPOS_2b.ttx
vendored
2
Lib/fontTools/feaLib/testdata/GPOS_2b.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/GPOS_3.ttx
vendored
2
Lib/fontTools/feaLib/testdata/GPOS_3.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
4
Lib/fontTools/feaLib/testdata/GPOS_4.ttx
vendored
4
Lib/fontTools/feaLib/testdata/GPOS_4.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="a" class="1"/>
|
||||
<ClassDef glyph="acute" class="3"/>
|
||||
@ -17,7 +17,7 @@
|
||||
</GDEF>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
4
Lib/fontTools/feaLib/testdata/GPOS_5.ttx
vendored
4
Lib/fontTools/feaLib/testdata/GPOS_5.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="acute" class="3"/>
|
||||
<ClassDef glyph="c_t" class="2"/>
|
||||
@ -17,7 +17,7 @@
|
||||
</GDEF>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
4
Lib/fontTools/feaLib/testdata/GPOS_6.ttx
vendored
4
Lib/fontTools/feaLib/testdata/GPOS_6.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="acute" class="3"/>
|
||||
<ClassDef glyph="caron" class="3"/>
|
||||
@ -15,7 +15,7 @@
|
||||
</GDEF>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/GPOS_8.ttx
vendored
2
Lib/fontTools/feaLib/testdata/GPOS_8.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/GSUB_2.ttx
vendored
2
Lib/fontTools/feaLib/testdata/GSUB_2.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/GSUB_3.ttx
vendored
2
Lib/fontTools/feaLib/testdata/GSUB_3.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/GSUB_6.ttx
vendored
2
Lib/fontTools/feaLib/testdata/GSUB_6.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/GSUB_8.ttx
vendored
2
Lib/fontTools/feaLib/testdata/GSUB_8.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="a" class="1"/>
|
||||
<ClassDef glyph="b" class="2"/>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<LigCaretList>
|
||||
<Coverage>
|
||||
<Glyph value="c_t"/>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<LigCaretList>
|
||||
<Coverage>
|
||||
<Glyph value="c_h"/>
|
||||
|
4
Lib/fontTools/feaLib/testdata/bug453.ttx
vendored
4
Lib/fontTools/feaLib/testdata/bug453.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="acute" class="3"/>
|
||||
<ClassDef glyph="e" class="1"/>
|
||||
@ -10,7 +10,7 @@
|
||||
</GDEF>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/bug463.ttx
vendored
2
Lib/fontTools/feaLib/testdata/bug463.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/bug501.ttx
vendored
2
Lib/fontTools/feaLib/testdata/bug501.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/bug502.ttx
vendored
2
Lib/fontTools/feaLib/testdata/bug502.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/bug504.ttx
vendored
2
Lib/fontTools/feaLib/testdata/bug504.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/bug505.ttx
vendored
2
Lib/fontTools/feaLib/testdata/bug505.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/bug506.ttx
vendored
2
Lib/fontTools/feaLib/testdata/bug506.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/bug509.ttx
vendored
2
Lib/fontTools/feaLib/testdata/bug509.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/bug512.ttx
vendored
2
Lib/fontTools/feaLib/testdata/bug512.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/bug568.ttx
vendored
2
Lib/fontTools/feaLib/testdata/bug568.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=2 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/enum.ttx
vendored
2
Lib/fontTools/feaLib/testdata/enum.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/ignore_pos.ttx
vendored
2
Lib/fontTools/feaLib/testdata/ignore_pos.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/lookup.ttx
vendored
2
Lib/fontTools/feaLib/testdata/lookup.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/lookupflag.ttx
vendored
2
Lib/fontTools/feaLib/testdata/lookupflag.ttx
vendored
@ -33,7 +33,7 @@
|
||||
</GDEF>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/markClass.ttx
vendored
2
Lib/fontTools/feaLib/testdata/markClass.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="acute" class="3"/>
|
||||
<ClassDef glyph="breve" class="3"/>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=2 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/size.ttx
vendored
2
Lib/fontTools/feaLib/testdata/size.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/size2.ttx
vendored
2
Lib/fontTools/feaLib/testdata/size2.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec4h1.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec4h1.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=3 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec4h2.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec4h2.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=4 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec5d1.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec5d1.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec5d2.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec5d2.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec5fi1.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec5fi1.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec5fi2.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec5fi2.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec5fi3.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec5fi3.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec5fi4.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec5fi4.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec5h1.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec5h1.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec6b_ii.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec6b_ii.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
4
Lib/fontTools/feaLib/testdata/spec6d2.ttx
vendored
4
Lib/fontTools/feaLib/testdata/spec6d2.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="a" class="1"/>
|
||||
<ClassDef glyph="acute" class="3"/>
|
||||
@ -17,7 +17,7 @@
|
||||
</GDEF>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
4
Lib/fontTools/feaLib/testdata/spec6e.ttx
vendored
4
Lib/fontTools/feaLib/testdata/spec6e.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="kasratan" class="3"/>
|
||||
<ClassDef glyph="lam_meem_jeem" class="2"/>
|
||||
@ -11,7 +11,7 @@
|
||||
</GDEF>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
4
Lib/fontTools/feaLib/testdata/spec6f.ttx
vendored
4
Lib/fontTools/feaLib/testdata/spec6f.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="damma" class="3"/>
|
||||
<ClassDef glyph="hamza" class="3"/>
|
||||
@ -10,7 +10,7 @@
|
||||
</GDEF>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
4
Lib/fontTools/feaLib/testdata/spec6h_ii.ttx
vendored
4
Lib/fontTools/feaLib/testdata/spec6h_ii.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="acute" class="3"/>
|
||||
<ClassDef glyph="c" class="1"/>
|
||||
@ -12,7 +12,7 @@
|
||||
</GDEF>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec8a.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec8a.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=3 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec8b.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec8b.ttx
vendored
@ -14,7 +14,7 @@
|
||||
</name>
|
||||
|
||||
<GPOS>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec8c.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec8c.ttx
vendored
@ -17,7 +17,7 @@
|
||||
</name>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec9a.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec9a.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont>
|
||||
|
||||
<BASE>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<HorizAxis>
|
||||
<BaseTagList>
|
||||
<!-- BaseTagCount=2 -->
|
||||
|
2
Lib/fontTools/feaLib/testdata/spec9b.ttx
vendored
2
Lib/fontTools/feaLib/testdata/spec9b.ttx
vendored
@ -2,7 +2,7 @@
|
||||
<ttFont sfntVersion="true" ttLibVersion="3.0">
|
||||
|
||||
<GDEF>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<GlyphClassDef>
|
||||
<ClassDef glyph="acute" class="3"/>
|
||||
<ClassDef glyph="c_s" class="2"/>
|
||||
|
@ -1,82 +1,82 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools import ttLib
|
||||
from fontTools.merge import *
|
||||
import unittest
|
||||
|
||||
|
||||
class MergeIntegrationTest(unittest.TestCase):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
class CmapMergeUnitTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.merger = Merger()
|
||||
self.table1 = ttLib.newTable('cmap')
|
||||
self.table2 = ttLib.newTable('cmap')
|
||||
self.mergedTable = ttLib.newTable('cmap')
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
|
||||
def makeSubtable(self, format, platformID, platEncID, cmap):
|
||||
module = ttLib.getTableModule('cmap')
|
||||
subtable = module.cmap_classes[format](format)
|
||||
(subtable.platformID,
|
||||
subtable.platEncID,
|
||||
subtable.language,
|
||||
subtable.cmap) = (platformID, platEncID, 0, cmap)
|
||||
return subtable
|
||||
|
||||
# 4-3-1 table merged with 12-3-10 table with no dupes with codepoints outside BMP
|
||||
def test_cmap_merge_no_dupes(self):
|
||||
table1 = self.table1
|
||||
table2 = self.table2
|
||||
mergedTable = self.mergedTable
|
||||
|
||||
cmap1 = {0x2603: 'SNOWMAN'}
|
||||
table1.tables = [self.makeSubtable(4,3,1, cmap1)]
|
||||
|
||||
cmap2 = {0x26C4: 'SNOWMAN WITHOUT SNOW'}
|
||||
cmap2Extended = {0x1F93C: 'WRESTLERS'}
|
||||
cmap2Extended.update(cmap2)
|
||||
table2.tables = [self.makeSubtable(4,3,1, cmap2), self.makeSubtable(12,3,10, cmap2Extended)]
|
||||
|
||||
self.merger.alternateGlyphsPerFont = [{},{}]
|
||||
mergedTable.merge(self.merger, [table1, table2])
|
||||
|
||||
expectedCmap = cmap2.copy()
|
||||
expectedCmap.update(cmap1)
|
||||
expectedCmapExtended = cmap2Extended.copy()
|
||||
expectedCmapExtended.update(cmap1)
|
||||
self.assertEqual(mergedTable.numSubTables, 2)
|
||||
self.assertEqual([(table.format, table.platformID, table.platEncID, table.language) for table in mergedTable.tables],
|
||||
[(4,3,1,0),(12,3,10,0)])
|
||||
self.assertEqual(mergedTable.tables[0].cmap, expectedCmap)
|
||||
self.assertEqual(mergedTable.tables[1].cmap, expectedCmapExtended)
|
||||
|
||||
# Tests Issue #322
|
||||
def test_cmap_merge_three_dupes(self):
|
||||
table1 = self.table1
|
||||
table2 = self.table2
|
||||
mergedTable = self.mergedTable
|
||||
|
||||
cmap1 = {0x20: 'space#0', 0xA0: 'space#0'}
|
||||
table1.tables = [self.makeSubtable(4,3,1,cmap1)]
|
||||
cmap2 = {0x20: 'space#1', 0xA0: 'uni00A0#1'}
|
||||
table2.tables = [self.makeSubtable(4,3,1,cmap2)]
|
||||
|
||||
self.merger.duplicateGlyphsPerFont = [{},{}]
|
||||
mergedTable.merge(self.merger, [table1, table2])
|
||||
|
||||
expectedCmap = cmap1.copy()
|
||||
self.assertEqual(mergedTable.numSubTables, 1)
|
||||
table = mergedTable.tables[0]
|
||||
self.assertEqual((table.format, table.platformID, table.platEncID, table.language), (4,3,1,0))
|
||||
self.assertEqual(table.cmap, expectedCmap)
|
||||
self.assertEqual(self.merger.duplicateGlyphsPerFont, [{}, {'space#0': 'space#1'}])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools import ttLib
|
||||
from fontTools.merge import *
|
||||
import unittest
|
||||
|
||||
|
||||
class MergeIntegrationTest(unittest.TestCase):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
class CmapMergeUnitTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.merger = Merger()
|
||||
self.table1 = ttLib.newTable('cmap')
|
||||
self.table2 = ttLib.newTable('cmap')
|
||||
self.mergedTable = ttLib.newTable('cmap')
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
|
||||
def makeSubtable(self, format, platformID, platEncID, cmap):
|
||||
module = ttLib.getTableModule('cmap')
|
||||
subtable = module.cmap_classes[format](format)
|
||||
(subtable.platformID,
|
||||
subtable.platEncID,
|
||||
subtable.language,
|
||||
subtable.cmap) = (platformID, platEncID, 0, cmap)
|
||||
return subtable
|
||||
|
||||
# 4-3-1 table merged with 12-3-10 table with no dupes with codepoints outside BMP
|
||||
def test_cmap_merge_no_dupes(self):
|
||||
table1 = self.table1
|
||||
table2 = self.table2
|
||||
mergedTable = self.mergedTable
|
||||
|
||||
cmap1 = {0x2603: 'SNOWMAN'}
|
||||
table1.tables = [self.makeSubtable(4,3,1, cmap1)]
|
||||
|
||||
cmap2 = {0x26C4: 'SNOWMAN WITHOUT SNOW'}
|
||||
cmap2Extended = {0x1F93C: 'WRESTLERS'}
|
||||
cmap2Extended.update(cmap2)
|
||||
table2.tables = [self.makeSubtable(4,3,1, cmap2), self.makeSubtable(12,3,10, cmap2Extended)]
|
||||
|
||||
self.merger.alternateGlyphsPerFont = [{},{}]
|
||||
mergedTable.merge(self.merger, [table1, table2])
|
||||
|
||||
expectedCmap = cmap2.copy()
|
||||
expectedCmap.update(cmap1)
|
||||
expectedCmapExtended = cmap2Extended.copy()
|
||||
expectedCmapExtended.update(cmap1)
|
||||
self.assertEqual(mergedTable.numSubTables, 2)
|
||||
self.assertEqual([(table.format, table.platformID, table.platEncID, table.language) for table in mergedTable.tables],
|
||||
[(4,3,1,0),(12,3,10,0)])
|
||||
self.assertEqual(mergedTable.tables[0].cmap, expectedCmap)
|
||||
self.assertEqual(mergedTable.tables[1].cmap, expectedCmapExtended)
|
||||
|
||||
# Tests Issue #322
|
||||
def test_cmap_merge_three_dupes(self):
|
||||
table1 = self.table1
|
||||
table2 = self.table2
|
||||
mergedTable = self.mergedTable
|
||||
|
||||
cmap1 = {0x20: 'space#0', 0xA0: 'space#0'}
|
||||
table1.tables = [self.makeSubtable(4,3,1,cmap1)]
|
||||
cmap2 = {0x20: 'space#1', 0xA0: 'uni00A0#1'}
|
||||
table2.tables = [self.makeSubtable(4,3,1,cmap2)]
|
||||
|
||||
self.merger.duplicateGlyphsPerFont = [{},{}]
|
||||
mergedTable.merge(self.merger, [table1, table2])
|
||||
|
||||
expectedCmap = cmap1.copy()
|
||||
self.assertEqual(mergedTable.numSubTables, 1)
|
||||
table = mergedTable.tables[0]
|
||||
self.assertEqual((table.format, table.platformID, table.platEncID, table.language), (4,3,1,0))
|
||||
self.assertEqual(table.cmap, expectedCmap)
|
||||
self.assertEqual(self.merger.duplicateGlyphsPerFont, [{}, {'space#0': 'space#1'}])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
@ -1582,13 +1582,13 @@ def prune_post_subset(self, options):
|
||||
table.MarkGlyphSetsDef and
|
||||
not table.MarkGlyphSetsDef.Coverage):
|
||||
table.MarkGlyphSetsDef = None
|
||||
if table.Version == 0x00010002/0x10000:
|
||||
table.Version = 1.0
|
||||
if table.Version == 0x00010002:
|
||||
table.Version = 0x00010000
|
||||
return bool(table.LigCaretList or
|
||||
table.MarkAttachClassDef or
|
||||
table.GlyphClassDef or
|
||||
table.AttachList or
|
||||
(table.Version >= 0x00010002/0x10000 and table.MarkGlyphSetsDef))
|
||||
(table.Version >= 0x00010002 and table.MarkGlyphSetsDef))
|
||||
|
||||
@_add_method(ttLib.getTableClass('kern'))
|
||||
def prune_pre_subset(self, font, options):
|
||||
@ -2368,8 +2368,10 @@ def prune_pre_subset(self, font, options):
|
||||
nameIDs = set(options.name_IDs)
|
||||
fvar = font.get('fvar')
|
||||
if fvar:
|
||||
nameIDs.update([inst.nameID for inst in fvar.instances])
|
||||
nameIDs.update([axis.nameID for axis in fvar.axes])
|
||||
nameIDs.update([axis.axisNameID for axis in fvar.axes])
|
||||
nameIDs.update([inst.subfamilyNameID for inst in fvar.instances])
|
||||
nameIDs.update([inst.postscriptNameID for inst in fvar.instances
|
||||
if inst.postscriptNameID != 0xFFFF])
|
||||
if '*' not in options.name_IDs:
|
||||
self.names = [n for n in self.names if n.nameID in nameIDs]
|
||||
if not options.name_legacy:
|
||||
|
14
Lib/fontTools/subset/testdata/TestGVAR.ttx
vendored
14
Lib/fontTools/subset/testdata/TestGVAR.ttx
vendored
@ -374,7 +374,7 @@
|
||||
</gasp>
|
||||
|
||||
<GSUB>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
</GSUB>
|
||||
|
||||
<avar>
|
||||
@ -394,31 +394,31 @@
|
||||
<MinValue>100.0</MinValue>
|
||||
<DefaultValue>400.0</DefaultValue>
|
||||
<MaxValue>900.0</MaxValue>
|
||||
<NameID>257</NameID>
|
||||
<AxisNameID>257</AxisNameID>
|
||||
</Axis>
|
||||
|
||||
<!-- Thin -->
|
||||
<NamedInstance nameID="258">
|
||||
<NamedInstance subfamilyNameID="258">
|
||||
<coord axis="wght" value="100.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Light -->
|
||||
<NamedInstance nameID="259">
|
||||
<NamedInstance subfamilyNameID="259">
|
||||
<coord axis="wght" value="300.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Regular -->
|
||||
<NamedInstance nameID="260">
|
||||
<NamedInstance subfamilyNameID="260">
|
||||
<coord axis="wght" value="400.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Bold -->
|
||||
<NamedInstance nameID="261">
|
||||
<NamedInstance subfamilyNameID="261">
|
||||
<coord axis="wght" value="700.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Black -->
|
||||
<NamedInstance nameID="262">
|
||||
<NamedInstance subfamilyNameID="262">
|
||||
<coord axis="wght" value="900.0"/>
|
||||
</NamedInstance>
|
||||
</fvar>
|
||||
|
@ -5330,7 +5330,7 @@
|
||||
</CFF>
|
||||
|
||||
<MATH>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<MathConstants>
|
||||
<ScriptPercentScaleDown value="75"/>
|
||||
<ScriptScriptPercentScaleDown value="60"/>
|
||||
|
@ -25,31 +25,31 @@
|
||||
<MinValue>100.0</MinValue>
|
||||
<DefaultValue>400.0</DefaultValue>
|
||||
<MaxValue>900.0</MaxValue>
|
||||
<NameID>257</NameID>
|
||||
<AxisNameID>257</AxisNameID>
|
||||
</Axis>
|
||||
|
||||
<!-- Thin -->
|
||||
<NamedInstance nameID="258">
|
||||
<NamedInstance subfamilyNameID="258">
|
||||
<coord axis="wght" value="100.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Light -->
|
||||
<NamedInstance nameID="259">
|
||||
<NamedInstance subfamilyNameID="259">
|
||||
<coord axis="wght" value="300.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Regular -->
|
||||
<NamedInstance nameID="260">
|
||||
<NamedInstance subfamilyNameID="260">
|
||||
<coord axis="wght" value="400.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Bold -->
|
||||
<NamedInstance nameID="261">
|
||||
<NamedInstance subfamilyNameID="261">
|
||||
<coord axis="wght" value="700.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Black -->
|
||||
<NamedInstance nameID="262">
|
||||
<NamedInstance subfamilyNameID="262">
|
||||
<coord axis="wght" value="900.0"/>
|
||||
</NamedInstance>
|
||||
</fvar>
|
||||
|
@ -24,31 +24,31 @@
|
||||
<MinValue>100.0</MinValue>
|
||||
<DefaultValue>400.0</DefaultValue>
|
||||
<MaxValue>900.0</MaxValue>
|
||||
<NameID>257</NameID>
|
||||
<AxisNameID>257</AxisNameID>
|
||||
</Axis>
|
||||
|
||||
<!-- Thin -->
|
||||
<NamedInstance nameID="258">
|
||||
<NamedInstance subfamilyNameID="258">
|
||||
<coord axis="wght" value="100.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Light -->
|
||||
<NamedInstance nameID="259">
|
||||
<NamedInstance subfamilyNameID="259">
|
||||
<coord axis="wght" value="300.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Regular -->
|
||||
<NamedInstance nameID="260">
|
||||
<NamedInstance subfamilyNameID="260">
|
||||
<coord axis="wght" value="400.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Bold -->
|
||||
<NamedInstance nameID="261">
|
||||
<NamedInstance subfamilyNameID="261">
|
||||
<coord axis="wght" value="700.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Black -->
|
||||
<NamedInstance nameID="262">
|
||||
<NamedInstance subfamilyNameID="262">
|
||||
<coord axis="wght" value="900.0"/>
|
||||
</NamedInstance>
|
||||
</fvar>
|
||||
|
@ -24,6 +24,8 @@
|
||||
</GlyphOrder>
|
||||
|
||||
<CFF>
|
||||
<major value="1"/>
|
||||
<minor value="0"/>
|
||||
<CFFFont name="XITSMath">
|
||||
<version value="1.108"/>
|
||||
<Notice value="Copyright (c) 2001-2011 by the STI Pub Companies, consisting of the American Chemical Society, the American Institute of Physics, the American Mathematical Society, the American Physical Society, Elsevier, Inc., and The Institute of Electrical and Electronic Engineers, Inc. Portions copyright (c) 1998-2003 by MicroPress, Inc. Portions copyright (c) 1990 by Elsevier, Inc. Portions copyright (c) 2009-2012 by Khaled Hosny. All rights reserved. "/>
|
||||
@ -252,7 +254,7 @@
|
||||
</CFF>
|
||||
|
||||
<MATH>
|
||||
<Version value="1.0"/>
|
||||
<Version value="0x00010000"/>
|
||||
<MathConstants>
|
||||
<ScriptPercentScaleDown value="75"/>
|
||||
<ScriptScriptPercentScaleDown value="60"/>
|
||||
|
@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="OTTO" ttLibVersion="2.5">
|
||||
<ttFont sfntVersion="OTTO" ttLibVersion="3.0">
|
||||
|
||||
<CFF>
|
||||
<major value="1"/>
|
||||
<minor value="0"/>
|
||||
<CFFFont name="TestCID-Regular">
|
||||
<ROS Registry="Adobe" Order="Identity" Supplement="0"/>
|
||||
<FullName value="Test CID Regular"/>
|
||||
@ -25,15 +27,7 @@
|
||||
<FDArray>
|
||||
<FontDict index="0">
|
||||
<FontName value="TestCID-Regular-One"/>
|
||||
<isFixedPitch value="0"/>
|
||||
<ItalicAngle value="0"/>
|
||||
<UnderlineThickness value="50"/>
|
||||
<PaintType value="0"/>
|
||||
<CharstringType value="2"/>
|
||||
<FontMatrix value="0.001 0 0 0.001 0 0"/>
|
||||
<FontBBox value="0 0 0 0"/>
|
||||
<StrokeWidth value="0"/>
|
||||
<Encoding name="StandardEncoding"/>
|
||||
<Private>
|
||||
<BlueValues value="-250 -250 1100 1100"/>
|
||||
<BlueScale value="0.039625"/>
|
||||
|
@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="OTTO" ttLibVersion="2.5">
|
||||
<ttFont sfntVersion="OTTO" ttLibVersion="3.0">
|
||||
|
||||
<CFF>
|
||||
<major value="1"/>
|
||||
<minor value="0"/>
|
||||
<CFFFont name="TestOTF-Regular">
|
||||
<version value="1.0"/>
|
||||
<FamilyName value="Test OTF"/>
|
||||
|
384
Lib/fontTools/ttLib/tables/C_F_F_test.py
Normal file
384
Lib/fontTools/ttLib/tables/C_F_F_test.py
Normal file
@ -0,0 +1,384 @@
|
||||
"""cffLib_test.py -- unit test for Adobe CFF fonts."""
|
||||
|
||||
from __future__ import print_function, division, absolute_import, unicode_literals
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.testTools import parseXML
|
||||
from fontTools.misc.textTools import deHexStr, hexStr
|
||||
from fontTools.misc.xmlWriter import XMLWriter
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
cffXML = """
|
||||
<major value="1"/>
|
||||
<minor value="0"/>
|
||||
<CFFFont name="CFF2TestFont1Master-0">
|
||||
<version value="1.0"/>
|
||||
<FullName value="CFF2 Test Font1 Master 0"/>
|
||||
<FamilyName value="CFF2 Test Font1 Master"/>
|
||||
<isFixedPitch value="0"/>
|
||||
<ItalicAngle value="0"/>
|
||||
<UnderlineThickness value="50"/>
|
||||
<PaintType value="0"/>
|
||||
<CharstringType value="2"/>
|
||||
<FontMatrix value="0.001 0 0 0.001 0 0"/>
|
||||
<FontBBox value="0 -128 651 762"/>
|
||||
<StrokeWidth value="0"/>
|
||||
<!-- charset is dumped separately as the 'GlyphOrder' element -->
|
||||
<Encoding name="StandardEncoding"/>
|
||||
<Private>
|
||||
<BlueValues value="-20 0 466 484 531 546 652 667 677 697 738 758"/>
|
||||
<OtherBlues value="-255 -245"/>
|
||||
<FamilyBlues value="-20 0 473 491 525 540 644 659 669 689 729 749"/>
|
||||
<FamilyOtherBlues value="-249 -239"/>
|
||||
<BlueScale value="0.0375"/>
|
||||
<BlueShift value="7"/>
|
||||
<BlueFuzz value="0"/>
|
||||
<StdHW value="26"/>
|
||||
<StdVW value="28"/>
|
||||
<StemSnapH value="20 26"/>
|
||||
<StemSnapV value="28 32"/>
|
||||
<ForceBold value="0"/>
|
||||
<LanguageGroup value="0"/>
|
||||
<ExpansionFactor value="0.06"/>
|
||||
<initialRandomSeed value="0"/>
|
||||
<defaultWidthX value="490"/>
|
||||
<nominalWidthX value="597"/>
|
||||
</Private>
|
||||
<CharStrings>
|
||||
<CharString name=".notdef">
|
||||
-97 0 50 600 50 hstem
|
||||
0 50 400 50 vstem
|
||||
0 0 rmoveto
|
||||
500 0 rlineto
|
||||
0 700 rlineto
|
||||
-500 0 rlineto
|
||||
0 -700 rlineto
|
||||
250 395 rmoveto
|
||||
-170 255 rlineto
|
||||
340 0 rlineto
|
||||
-170 -255 rlineto
|
||||
30 -45 rmoveto
|
||||
170 255 rlineto
|
||||
0 -510 rlineto
|
||||
-170 255 rlineto
|
||||
-200 -300 rmoveto
|
||||
170 255 rlineto
|
||||
170 -255 rlineto
|
||||
-340 0 rlineto
|
||||
-30 555 rmoveto
|
||||
170 -255 rlineto
|
||||
-170 -255 rlineto
|
||||
0 510 rlineto
|
||||
endchar
|
||||
</CharString>
|
||||
<CharString name="A">
|
||||
56 523 26 rmoveto
|
||||
-120 -6 rlineto
|
||||
0 -20 rlineto
|
||||
248 0 rlineto
|
||||
0 20 rlineto
|
||||
-114 6 rlineto
|
||||
-14 0 rlineto
|
||||
-424 0 rmoveto
|
||||
-87 -6 rlineto
|
||||
0 -20 rlineto
|
||||
198 0 rlineto
|
||||
0 20 rlineto
|
||||
-97 6 rlineto
|
||||
-14 0 rlineto
|
||||
369 221 rmoveto
|
||||
-8 20 rlineto
|
||||
-278 0 rlineto
|
||||
-9 -20 rlineto
|
||||
295 0 rlineto
|
||||
-161 430 rmoveto
|
||||
-222 -677 rlineto
|
||||
27 0 rlineto
|
||||
211 660 rlineto
|
||||
-17 -10 rlineto
|
||||
216 -650 rlineto
|
||||
34 0 rlineto
|
||||
-229 677 rlineto
|
||||
-20 0 rlineto
|
||||
endchar
|
||||
</CharString>
|
||||
<CharString name="B">
|
||||
-3 167 310 rmoveto
|
||||
0 -104 0 -104 -2 -102 rrcurveto
|
||||
34 0 rlineto
|
||||
-2 102 0 104 0 144 rrcurveto
|
||||
0 7 rlineto
|
||||
0 114 0 104 2 102 rrcurveto
|
||||
-34 0 rlineto
|
||||
2 -102 0 -104 0 -104 rrcurveto
|
||||
0 -57 rlineto
|
||||
8 340 rmoveto
|
||||
7 0 rlineto
|
||||
0 27 rlineto
|
||||
-124 0 rlineto
|
||||
0 -20 rlineto
|
||||
117 -7 rlineto
|
||||
0 -623 rmoveto
|
||||
-117 -7 rlineto
|
||||
0 -20 rlineto
|
||||
124 0 rlineto
|
||||
0 27 rlineto
|
||||
-7 0 rlineto
|
||||
7 316 rmoveto
|
||||
101 0 rlineto
|
||||
162 0 69 -60 0 -102 rrcurveto
|
||||
0 -102 -75 -57 -125 0 rrcurveto
|
||||
-132 0 rlineto
|
||||
0 -22 rlineto
|
||||
131 0 rlineto
|
||||
156 0 75 77 0 102 rrcurveto
|
||||
0 100 -68 76 -162 2 rrcurveto
|
||||
-10 -8 rlineto
|
||||
141 11 64 75 0 84 rrcurveto
|
||||
0 95 -66 63 -146 0 rrcurveto
|
||||
-115 0 rlineto
|
||||
0 -22 rlineto
|
||||
104 0 rlineto
|
||||
145 0 50 -57 0 -76 rrcurveto
|
||||
0 -95 -75 -64 -136 0 rrcurveto
|
||||
-88 0 rlineto
|
||||
0 -20 rlineto
|
||||
endchar
|
||||
</CharString>
|
||||
<CharString name="C">
|
||||
47 386 7 rmoveto
|
||||
-167 0 -123 128 0 203 rrcurveto
|
||||
0 199 116 133 180 0 rrcurveto
|
||||
73 0 40 -17 56 -37 rrcurveto
|
||||
-21 29 rlineto
|
||||
18 -145 rlineto
|
||||
24 0 rlineto
|
||||
-4 139 rlineto
|
||||
-60 35 -49 18 -80 0 rrcurveto
|
||||
-190 0 -135 -144 0 -210 rrcurveto
|
||||
0 -209 129 -144 195 0 rrcurveto
|
||||
72 0 57 12 67 41 rrcurveto
|
||||
4 139 rlineto
|
||||
-24 0 rlineto
|
||||
-18 -139 rlineto
|
||||
17 20 rlineto
|
||||
-55 -37 -55 -14 -67 0 rrcurveto
|
||||
endchar
|
||||
</CharString>
|
||||
<CharString name="dollar">
|
||||
245 5 rmoveto
|
||||
-65 0 -39 15 -46 50 rrcurveto
|
||||
36 -48 rlineto
|
||||
-28 100 rlineto
|
||||
-6 15 -10 5 -11 0 rrcurveto
|
||||
-14 0 -8 -7 -1 -14 rrcurveto
|
||||
24 -85 61 -51 107 0 rrcurveto
|
||||
91 0 90 54 0 112 rrcurveto
|
||||
0 70 -26 66 -134 57 rrcurveto
|
||||
-19 8 rlineto
|
||||
-93 39 -42 49 0 68 rrcurveto
|
||||
0 91 60 48 88 0 rrcurveto
|
||||
56 0 35 -14 44 -50 rrcurveto
|
||||
-38 47 rlineto
|
||||
28 -100 rlineto
|
||||
6 -15 10 -5 11 0 rrcurveto
|
||||
14 0 8 7 1 14 rrcurveto
|
||||
-24 88 -67 48 -84 0 rrcurveto
|
||||
-92 0 -82 -51 0 -108 rrcurveto
|
||||
0 -80 45 -53 92 -42 rrcurveto
|
||||
37 -17 rlineto
|
||||
114 -52 26 -46 0 -65 rrcurveto
|
||||
0 -93 -65 -55 -90 0 rrcurveto
|
||||
18 318 rmoveto
|
||||
0 439 rlineto
|
||||
-22 0 rlineto
|
||||
0 -425 rlineto
|
||||
22 -14 rlineto
|
||||
-20 -438 rmoveto
|
||||
22 0 rlineto
|
||||
0 438 rlineto
|
||||
-22 14 rlineto
|
||||
0 -452 rlineto
|
||||
endchar
|
||||
</CharString>
|
||||
<CharString name="dollar.black">
|
||||
3 245 5 rmoveto
|
||||
-65 0 -39 15 -46 50 rrcurveto
|
||||
36 -48 rlineto
|
||||
-28 100 rlineto
|
||||
-6 15 -10 5 -11 0 rrcurveto
|
||||
-14 0 -8 -7 -1 -14 rrcurveto
|
||||
24 -85 61 -51 107 0 rrcurveto
|
||||
91 0 90 54 0 112 rrcurveto
|
||||
0 70 -26 66 -134 57 rrcurveto
|
||||
-19 8 rlineto
|
||||
-93 39 -42 49 0 68 rrcurveto
|
||||
0 91 60 48 88 0 rrcurveto
|
||||
56 0 35 -14 44 -50 rrcurveto
|
||||
-38 47 rlineto
|
||||
28 -100 rlineto
|
||||
6 -15 10 -5 11 0 rrcurveto
|
||||
14 0 8 7 1 14 rrcurveto
|
||||
-24 88 -67 48 -84 0 rrcurveto
|
||||
-92 0 -82 -51 0 -108 rrcurveto
|
||||
0 -80 45 -53 92 -42 rrcurveto
|
||||
37 -17 rlineto
|
||||
114 -52 26 -46 0 -65 rrcurveto
|
||||
0 -93 -65 -55 -90 0 rrcurveto
|
||||
17 651 rmoveto
|
||||
1 106 rlineto
|
||||
-22 0 rlineto
|
||||
1 -107 rlineto
|
||||
20 1 rlineto
|
||||
-15 -784 rmoveto
|
||||
22 0 rlineto
|
||||
-3 121 rlineto
|
||||
-20 2 rlineto
|
||||
1 -123 rlineto
|
||||
endchar
|
||||
</CharString>
|
||||
<CharString name="one">
|
||||
91 618 rmoveto
|
||||
0 -20 rlineto
|
||||
155 35 rlineto
|
||||
0 -421 rlineto
|
||||
0 -70 -1 -71 -2 -72 rrcurveto
|
||||
34 0 rlineto
|
||||
-2 72 -1 71 0 70 rrcurveto
|
||||
0 297 rlineto
|
||||
4 146 rlineto
|
||||
-14 12 rlineto
|
||||
-173 -49 rlineto
|
||||
176 -593 rmoveto
|
||||
-14 0 rlineto
|
||||
-170 -6 rlineto
|
||||
0 -20 rlineto
|
||||
344 0 rlineto
|
||||
0 20 rlineto
|
||||
-160 6 rlineto
|
||||
endchar
|
||||
</CharString>
|
||||
<CharString name="three">
|
||||
endchar
|
||||
</CharString>
|
||||
<CharString name="two">
|
||||
endchar
|
||||
</CharString>
|
||||
</CharStrings>
|
||||
</CFFFont>
|
||||
|
||||
<GlobalSubrs>
|
||||
<!-- The 'index' attribute is only for humans; it is ignored when parsed. -->
|
||||
</GlobalSubrs>
|
||||
"""
|
||||
|
||||
cffHexData = """
|
||||
01 00 04 02 00 01 01 01 16 43 46 46 32 54 65 73 74 46 6F 6E 74 31 4D 61 73 74
|
||||
65 72 2D 30 00 01 01 01 1C F8 1C 00 F8 1D 02 F8 1E 03 8B FB 14 F9 1F F9 8E 05 F7
|
||||
19 0F CA FA E4 12 F7 26 11 00 04 01 01 0D 10 28 3E 64 6F 6C 6C 61 72 2E 62 6C 61
|
||||
63 6B 31 2E 30 43 46 46 32 20 54 65 73 74 20 46 6F 6E 74 31 20 4D 61 73 74 65 72
|
||||
20 30 43 46 46 32 20 54 65 73 74 20 46 6F 6E 74 31 20 4D 61 73 74 65 72 00 00 01
|
||||
00 05 00 00 12 02 00 22 02 01 87 00 00 09 02 00 01 00 6B 01 1A 01 64 01 65 01 66
|
||||
01 D0 02 90 02 FA 03 A8 2A 8B BD F8 EC BD 01 8B BD F8 24 BD 03 8B 8B 15 F8 88 8B
|
||||
05 8B F9 50 05 FC 88 8B 05 8B FD 50 05 F7 8E F8 1F 15 FB 3E F7 93 05 F7 E8 8B 05
|
||||
FB 3E FB 93 05 A9 5E 15 F7 3E F7 93 05 8B FC 92 05 FB 3E F7 93 05 FB 5C FB C0 15
|
||||
F7 3E F7 93 05 F7 3E FB 93 05 FB E8 8B 05 6D F8 BF 15 F7 3E FB 93 05 FB 3E FB 93
|
||||
05 8B F8 92 05 0E F7 89 90 15 4A 8B 64 9A 5D BD 08 AF 5B 05 6F EF 05 85 9A 81 90
|
||||
80 8B 08 7D 8B 83 84 8A 7D 08 A3 36 C8 58 F6 8B 08 E6 8B E5 C1 8B F7 04 08 8B D1
|
||||
71 CD FB 1A C4 08 78 93 05 2E B2 61 BC 8B CF 08 8B E6 C7 BB E3 8B 08 C3 8B AE 7D
|
||||
B7 59 08 65 BA 05 A7 27 05 91 7C 95 86 96 8B 08 99 8B 93 92 8C 99 08 73 E3 48 BB
|
||||
37 8B 08 2F 8B 39 58 8B FB 00 08 8B 3B B8 56 E7 61 08 B0 7A 05 F7 06 57 A5 5D 8B
|
||||
4A 08 8B 2E 4A 54 31 8B 08 9D F7 D2 15 8B F8 4B 05 75 8B 05 8B FC 3D 05 A1 7D 05
|
||||
77 FC 4A 15 A1 8B 05 8B F8 4A 05 75 99 05 8B FC 58 05 0E E6 F8 FE 15 8B 77 05 F7
|
||||
2F AE 05 8B FC 39 05 8B 45 8A 44 89 43 08 AD 8B 05 89 D3 8A D2 8B D1 08 8B F7 BD
|
||||
05 8F F7 26 05 7D 97 05 FB 41 5A 05 F7 44 FC E5 15 7D 8B 05 FB 3E 85 05 8B 77 05
|
||||
F7 EC 8B 05 8B 9F 05 FB 34 91 05 0E 0E 0E C3 F8 9F A5 15 FB 0C 85 05 8B 77 05 F7
|
||||
8C 8B 05 8B 9F 05 FB 06 91 05 7D 8B 05 FC 3C 8B 15 34 85 05 8B 77 05 F7 5A 8B 05
|
||||
8B 9F 05 2A 91 05 7D 8B 05 F8 05 F7 71 15 83 9F 05 FB AA 8B 05 82 77 05 F7 BB 8B
|
||||
05 FB 35 F8 42 15 FB 72 FD 39 05 A6 8B 05 F7 67 F9 28 05 7A 81 05 F7 6C FD 1E 05
|
||||
AD 8B 05 FB 79 F9 39 05 77 8B 05 0E 88 F7 3B F7 CA 15 8B 23 8B 23 89 25 08 AD 8B
|
||||
05 89 F1 8B F3 8B F7 24 08 8B 92 05 8B F7 06 8B F3 8D F1 08 69 8B 05 8D 25 8B 23
|
||||
8B 23 08 8B 52 05 93 F7 E8 15 92 8B 05 8B A6 05 FB 10 8B 05 8B 77 05 F7 09 84 05
|
||||
8B FD 03 15 FB 09 84 05 8B 77 05 F7 10 8B 05 8B A6 05 84 8B 05 92 F7 D0 15 F0 8B
|
||||
05 F7 36 8B D0 4F 8B 25 08 8B 25 40 52 FB 11 8B 08 FB 18 8B 05 8B 75 05 F7 17 8B
|
||||
05 F7 30 8B D6 D8 8B F1 08 8B EF 47 D7 FB 36 8D 08 81 83 05 F7 21 96 CB D6 8B DF
|
||||
08 8B EA 49 CA FB 26 8B 08 FB 07 8B 05 8B 75 05 F3 8B 05 F7 25 8B BD 52 8B 3F 08
|
||||
8B 2C 40 4B FB 1C 8B 08 33 8B 05 8B 77 05 0E BA F8 16 92 15 FB 3B 8B FB 0F F7 14
|
||||
8B F7 5F 08 8B F7 5B F7 08 F7 19 F7 48 8B 08 D4 8B B3 7A C3 66 08 76 A8 05 9D FB
|
||||
25 05 A3 8B 05 87 F7 1F 05 4F AE 5A 9D 3B 8B 08 FB 52 8B FB 1B FB 24 8B FB 66 08
|
||||
8B FB 65 F7 15 FB 24 F7 57 8B 08 D3 8B C4 97 CE B4 08 8F F7 1F 05 73 8B 05 79 FB
|
||||
1F 05 9C 9F 05 54 66 54 7D 48 8B 08 0E 8E F7 89 90 15 4A 8B 64 9A 5D BD 08 AF 5B
|
||||
05 6F EF 05 85 9A 81 90 80 8B 08 7D 8B 83 84 8A 7D 08 A3 36 C8 58 F6 8B 08 E6 8B
|
||||
E5 C1 8B F7 04 08 8B D1 71 CD FB 1A C4 08 78 93 05 2E B2 61 BC 8B CF 08 8B E6 C7
|
||||
BB E3 8B 08 C3 8B AE 7D B7 59 08 65 BA 05 A7 27 05 91 7C 95 86 96 8B 08 99 8B 93
|
||||
92 8C 99 08 73 E3 48 BB 37 8B 08 2F 8B 39 58 8B FB 00 08 8B 3B B8 56 E7 61 08 B0
|
||||
7A 05 F7 06 57 A5 5D 8B 4A 08 8B 2E 4A 54 31 8B 08 9C F9 1F 15 8C F5 05 75 8B 05
|
||||
8C 20 05 9F 8C 05 7C FD A4 15 A1 8B 05 88 F7 0D 05 77 8D 05 8C FB 0F 05 0E 77 9F
|
||||
F8 66 9D BA 9A F5 9A 95 9F B4 9F 06 FB 93 95 07 77 9F F8 6D 9D AD 9A F3 9A 95 9F
|
||||
B3 9F 08 FB 8D 95 09 1E A0 37 5F 0C 09 8B 0C 0B A5 0A A7 0B 9F 91 0C 0C A7 8F 0C
|
||||
0D F8 7E 14 F8 E9 15"""
|
||||
|
||||
glyphOrder = ['.notdef', 'dollar', 'one', 'two', 'three', 'A', 'B', 'C', 'dollar.black']
|
||||
|
||||
def hexencode(s):
|
||||
h = hexStr(s).upper()
|
||||
return ' '.join([h[i:i+2] for i in range(0, len(h), 2)])
|
||||
|
||||
def parseTableXML(otTable, xmlText, font):
|
||||
xmlList = parseXML(xmlText)
|
||||
for entry in xmlList:
|
||||
if len(entry) != 3: # skip comments and newlines.
|
||||
continue
|
||||
name, attrs, content = entry
|
||||
otTable.fromXML(name, attrs, content, font)
|
||||
|
||||
def MakeFont():
|
||||
font = TTFont()
|
||||
return font
|
||||
|
||||
class CFFTableTest(unittest.TestCase):
|
||||
def test_toXML(self):
|
||||
global cffXML
|
||||
# make font with post table.
|
||||
font = MakeFont()
|
||||
|
||||
cffData = deHexStr(cffHexData)
|
||||
CFF2Table = font['CFF '] = newTable('CFF ')
|
||||
CFF2Table.decompile(cffData, font)
|
||||
writer = XMLWriter(BytesIO())
|
||||
font['CFF '].toXML(writer, font)
|
||||
xml = writer.file.getvalue().decode("utf-8")
|
||||
# normalize spacing and new-lines, so we can edit the XML without being very careful.
|
||||
xml = re.sub(r" +", " ", xml)
|
||||
xml = re.sub(r"[\r\n][\r\n]+", "\r", xml)
|
||||
# cffXML does not have the initial xml definition line.
|
||||
xml = re.sub(r"\<\?xml[^\r\n]+", "", xml)
|
||||
cffXML = re.sub(r"\t", " ", cffXML)
|
||||
cffXML = re.sub(r" +", " ", cffXML)
|
||||
cffXML = re.sub(r"[\r\n][\r\n]+", "\r", cffXML)
|
||||
self.assertEqual(xml, cffXML)
|
||||
|
||||
def test_fromXML(self):
|
||||
global cffHexData
|
||||
from fontTools.misc import xmlReader
|
||||
font = MakeFont()
|
||||
# charset data is stored in the ttx GlyphOrder table, not
|
||||
# in the CFF XML dump. Need to to provide this externally to the
|
||||
# ttx dump of the CFF table.
|
||||
font.glyphOrder = glyphOrder
|
||||
cffTable = font['CFF '] = newTable('CFF ')
|
||||
cffXML2 = cffXML
|
||||
parseTableXML(cffTable, cffXML, font)
|
||||
cffData = cffTable.compile(font)
|
||||
cffHexDatafromTable = hexencode(cffData)
|
||||
cffHexDatafromTable = re.sub(r"\s+", " ", cffHexDatafromTable)
|
||||
cffHexDatafromTable = cffHexDatafromTable.strip()
|
||||
cffHexData = re.sub(r"\s+", " ", cffHexData)
|
||||
cffHexData = cffHexData.strip()
|
||||
self.assertEqual(cffHexDatafromTable, cffHexData)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
7
Lib/fontTools/ttLib/tables/H_V_A_R_.py
Normal file
7
Lib/fontTools/ttLib/tables/H_V_A_R_.py
Normal file
@ -0,0 +1,7 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from .otBase import BaseTTXConverter
|
||||
|
||||
|
||||
class table_H_V_A_R_(BaseTTXConverter):
|
||||
pass
|
7
Lib/fontTools/ttLib/tables/V_V_A_R_.py
Normal file
7
Lib/fontTools/ttLib/tables/V_V_A_R_.py
Normal file
@ -0,0 +1,7 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from .otBase import BaseTTXConverter
|
||||
|
||||
|
||||
class table_V_V_A_R_(BaseTTXConverter):
|
||||
pass
|
@ -24,6 +24,7 @@ def _moduleFinderHint():
|
||||
from . import G_P_K_G_
|
||||
from . import G_P_O_S_
|
||||
from . import G_S_U_B_
|
||||
from . import H_V_A_R_
|
||||
from . import J_S_T_F_
|
||||
from . import L_T_S_H_
|
||||
from . import M_A_T_H_
|
||||
@ -44,6 +45,7 @@ def _moduleFinderHint():
|
||||
from . import T_S_I__5
|
||||
from . import V_D_M_X_
|
||||
from . import V_O_R_G_
|
||||
from . import V_V_A_R_
|
||||
from . import _a_v_a_r
|
||||
from . import _c_m_a_p
|
||||
from . import _c_v_t
|
||||
|
@ -29,12 +29,12 @@ FVAR_AXIS_FORMAT = """
|
||||
defaultValue: 16.16F
|
||||
maxValue: 16.16F
|
||||
flags: H
|
||||
nameID: H
|
||||
axisNameID: H
|
||||
"""
|
||||
|
||||
FVAR_INSTANCE_FORMAT = """
|
||||
> # big endian
|
||||
nameID: H
|
||||
subfamilyNameID: H
|
||||
flags: H
|
||||
"""
|
||||
|
||||
@ -47,6 +47,9 @@ class table__f_v_a_r(DefaultTable.DefaultTable):
|
||||
self.instances = []
|
||||
|
||||
def compile(self, ttFont):
|
||||
instanceSize = sstruct.calcsize(FVAR_INSTANCE_FORMAT) + (len(self.axes) * 4)
|
||||
if any(instance.postscriptNameID != 0xFFFF for instance in self.instances):
|
||||
instanceSize += 2
|
||||
header = {
|
||||
"version": 0x00010000,
|
||||
"offsetToData": sstruct.calcsize(FVAR_HEADER_FORMAT),
|
||||
@ -54,12 +57,12 @@ class table__f_v_a_r(DefaultTable.DefaultTable):
|
||||
"axisCount": len(self.axes),
|
||||
"axisSize": sstruct.calcsize(FVAR_AXIS_FORMAT),
|
||||
"instanceCount": len(self.instances),
|
||||
"instanceSize": sstruct.calcsize(FVAR_INSTANCE_FORMAT) + len(self.axes) * 4
|
||||
"instanceSize": instanceSize,
|
||||
}
|
||||
result = [sstruct.pack(FVAR_HEADER_FORMAT, header)]
|
||||
result.extend([axis.compile() for axis in self.axes])
|
||||
axisTags = [axis.axisTag for axis in self.axes]
|
||||
result.extend([instance.compile(axisTags) for instance in self.instances])
|
||||
result.extend([instance.compile(axisTags)[:instanceSize] for instance in self.instances])
|
||||
return bytesjoin(result)
|
||||
|
||||
def decompile(self, data, ttFont):
|
||||
@ -102,7 +105,7 @@ class table__f_v_a_r(DefaultTable.DefaultTable):
|
||||
class Axis(object):
|
||||
def __init__(self):
|
||||
self.axisTag = None
|
||||
self.nameID = 0
|
||||
self.axisNameID = 0
|
||||
self.flags = 0 # not exposed in XML because spec defines no values
|
||||
self.minValue = -1.0
|
||||
self.defaultValue = 0.0
|
||||
@ -115,7 +118,7 @@ class Axis(object):
|
||||
sstruct.unpack2(FVAR_AXIS_FORMAT, data, self)
|
||||
|
||||
def toXML(self, writer, ttFont):
|
||||
name = ttFont["name"].getDebugName(self.nameID)
|
||||
name = ttFont["name"].getDebugName(self.axisNameID)
|
||||
if name is not None:
|
||||
writer.newline()
|
||||
writer.comment(name)
|
||||
@ -126,7 +129,7 @@ class Axis(object):
|
||||
("MinValue", str(self.minValue)),
|
||||
("DefaultValue", str(self.defaultValue)),
|
||||
("MaxValue", str(self.maxValue)),
|
||||
("NameID", str(self.nameID))]:
|
||||
("AxisNameID", str(self.axisNameID))]:
|
||||
writer.begintag(tag)
|
||||
writer.write(value)
|
||||
writer.endtag(tag)
|
||||
@ -139,13 +142,14 @@ class Axis(object):
|
||||
for tag, _, value in filter(lambda t: type(t) is tuple, content):
|
||||
value = ''.join(value)
|
||||
if tag == "AxisTag":
|
||||
self.axisTag = value
|
||||
elif tag in ["MinValue", "DefaultValue", "MaxValue", "NameID"]:
|
||||
self.axisTag = Tag(value)
|
||||
elif tag in ["MinValue", "DefaultValue", "MaxValue", "AxisNameID"]:
|
||||
setattr(self, tag[0].lower() + tag[1:], safeEval(value))
|
||||
|
||||
class NamedInstance(object):
|
||||
def __init__(self):
|
||||
self.nameID = 0
|
||||
self.subfamilyNameID = 0
|
||||
self.postscriptNameID = 0xFFFF
|
||||
self.flags = 0 # not exposed in XML because spec defines no values
|
||||
self.coordinates = {}
|
||||
|
||||
@ -154,6 +158,7 @@ class NamedInstance(object):
|
||||
for axis in axisTags:
|
||||
fixedCoord = floatToFixed(self.coordinates[axis], 16)
|
||||
result.append(struct.pack(">l", fixedCoord))
|
||||
result.append(struct.pack(">H", self.postscriptNameID))
|
||||
return bytesjoin(result)
|
||||
|
||||
def decompile(self, data, axisTags):
|
||||
@ -163,14 +168,22 @@ class NamedInstance(object):
|
||||
value = struct.unpack(">l", data[pos : pos + 4])[0]
|
||||
self.coordinates[axis] = fixedToFloat(value, 16)
|
||||
pos += 4
|
||||
if pos + 2 <= len(data):
|
||||
self.postscriptNameID = struct.unpack(">H", data[pos : pos + 2])[0]
|
||||
else:
|
||||
self.postscriptNameID = 0xFFFF
|
||||
|
||||
def toXML(self, writer, ttFont):
|
||||
name = ttFont["name"].getDebugName(self.nameID)
|
||||
name = ttFont["name"].getDebugName(self.subfamilyNameID)
|
||||
if name is not None:
|
||||
writer.newline()
|
||||
writer.comment(name)
|
||||
writer.newline()
|
||||
writer.begintag("NamedInstance", nameID=self.nameID)
|
||||
if self.postscriptNameID == 0xFFFF:
|
||||
writer.begintag("NamedInstance", subfamilyNameID=self.subfamilyNameID)
|
||||
else:
|
||||
writer.begintag("NamedInstance", subfamilyNameID=self.subfamilyNameID,
|
||||
postscriptNameID=self.postscriptNameID, )
|
||||
writer.newline()
|
||||
for axis in ttFont["fvar"].axes:
|
||||
writer.simpletag("coord", axis=axis.axisTag,
|
||||
@ -181,7 +194,12 @@ class NamedInstance(object):
|
||||
|
||||
def fromXML(self, name, attrs, content, ttFont):
|
||||
assert(name == "NamedInstance")
|
||||
self.nameID = safeEval(attrs["nameID"])
|
||||
self.subfamilyNameID = safeEval(attrs["subfamilyNameID"])
|
||||
if "postscriptNameID" in attrs:
|
||||
self.postscriptNameID = safeEval(attrs["postscriptNameID"])
|
||||
else:
|
||||
self.postscriptNameID = 0xFFFF
|
||||
|
||||
for tag, elementAttrs, _ in filter(lambda t: type(t) is tuple, content):
|
||||
if tag == "coord":
|
||||
self.coordinates[elementAttrs["axis"]] = safeEval(elementAttrs["value"])
|
||||
|
@ -20,7 +20,7 @@ FVAR_DATA = deHexStr(
|
||||
FVAR_AXIS_DATA = deHexStr(
|
||||
"6F 70 73 7a ff ff 80 00 00 01 4c cd 00 01 80 00 00 00 01 59")
|
||||
|
||||
FVAR_INSTANCE_DATA = deHexStr("01 59 00 00 00 00 b3 33 00 00 80 00")
|
||||
FVAR_INSTANCE_DATA = deHexStr("01 59 00 00 00 00 b3 33 00 00 80 00 ff ff")
|
||||
|
||||
|
||||
def xml_lines(writer):
|
||||
@ -51,11 +51,11 @@ def MakeFont():
|
||||
axis.axisTag = tag
|
||||
axis.defaultValue = defaultValue
|
||||
axis.minValue, axis.maxValue = minValue, maxValue
|
||||
axis.nameID = AddName(font, name).nameID
|
||||
axis.axisNameID = AddName(font, name).nameID
|
||||
fvarTable.axes.append(axis)
|
||||
for name, weight, width in instances:
|
||||
inst = NamedInstance()
|
||||
inst.nameID = AddName(font, name).nameID
|
||||
inst.subfamilyNameID = AddName(font, name).nameID
|
||||
inst.coordinates = {"wght": weight, "wdth": width}
|
||||
fvarTable.instances.append(inst)
|
||||
return font
|
||||
@ -71,7 +71,7 @@ class FontVariationTableTest(unittest.TestCase):
|
||||
fvar = table__f_v_a_r()
|
||||
fvar.decompile(FVAR_DATA, ttFont={"fvar": fvar})
|
||||
self.assertEqual(["wght", "wdth"], [a.axisTag for a in fvar.axes])
|
||||
self.assertEqual([259, 260], [i.nameID for i in fvar.instances])
|
||||
self.assertEqual([259, 260], [i.subfamilyNameID for i in fvar.instances])
|
||||
|
||||
def test_toXML(self):
|
||||
font = MakeFont()
|
||||
@ -94,17 +94,17 @@ class FontVariationTableTest(unittest.TestCase):
|
||||
'<Axis>'
|
||||
' <AxisTag>slnt</AxisTag>'
|
||||
'</Axis>'
|
||||
'<NamedInstance nameID="765"/>'
|
||||
'<NamedInstance nameID="234"/>'):
|
||||
'<NamedInstance subfamilyNameID="765"/>'
|
||||
'<NamedInstance subfamilyNameID="234"/>'):
|
||||
fvar.fromXML(name, attrs, content, ttFont=None)
|
||||
self.assertEqual(["opsz", "slnt"], [a.axisTag for a in fvar.axes])
|
||||
self.assertEqual([765, 234], [i.nameID for i in fvar.instances])
|
||||
self.assertEqual([765, 234], [i.subfamilyNameID for i in fvar.instances])
|
||||
|
||||
|
||||
class AxisTest(unittest.TestCase):
|
||||
def test_compile(self):
|
||||
axis = Axis()
|
||||
axis.axisTag, axis.nameID = ('opsz', 345)
|
||||
axis.axisTag, axis.axisNameID = ('opsz', 345)
|
||||
axis.minValue, axis.defaultValue, axis.maxValue = (-0.5, 1.3, 1.5)
|
||||
self.assertEqual(FVAR_AXIS_DATA, axis.compile())
|
||||
|
||||
@ -112,7 +112,7 @@ class AxisTest(unittest.TestCase):
|
||||
axis = Axis()
|
||||
axis.decompile(FVAR_AXIS_DATA)
|
||||
self.assertEqual("opsz", axis.axisTag)
|
||||
self.assertEqual(345, axis.nameID)
|
||||
self.assertEqual(345, axis.axisNameID)
|
||||
self.assertEqual(-0.5, axis.minValue)
|
||||
self.assertEqual(1.3, axis.defaultValue)
|
||||
self.assertEqual(1.5, axis.maxValue)
|
||||
@ -122,7 +122,7 @@ class AxisTest(unittest.TestCase):
|
||||
axis = Axis()
|
||||
axis.decompile(FVAR_AXIS_DATA)
|
||||
AddName(font, "Optical Size").nameID = 256
|
||||
axis.nameID = 256
|
||||
axis.axisNameID = 256
|
||||
writer = XMLWriter(BytesIO())
|
||||
axis.toXML(writer, font)
|
||||
self.assertEqual([
|
||||
@ -133,7 +133,7 @@ class AxisTest(unittest.TestCase):
|
||||
'<MinValue>-0.5</MinValue>',
|
||||
'<DefaultValue>1.3</DefaultValue>',
|
||||
'<MaxValue>1.5</MaxValue>',
|
||||
'<NameID>256</NameID>',
|
||||
'<AxisNameID>256</AxisNameID>',
|
||||
'</Axis>'
|
||||
], xml_lines(writer))
|
||||
|
||||
@ -145,40 +145,40 @@ class AxisTest(unittest.TestCase):
|
||||
' <MinValue>100</MinValue>'
|
||||
' <DefaultValue>400</DefaultValue>'
|
||||
' <MaxValue>900</MaxValue>'
|
||||
' <NameID>256</NameID>'
|
||||
' <AxisNameID>256</AxisNameID>'
|
||||
'</Axis>'):
|
||||
axis.fromXML(name, attrs, content, ttFont=None)
|
||||
self.assertEqual("wght", axis.axisTag)
|
||||
self.assertEqual(100, axis.minValue)
|
||||
self.assertEqual(400, axis.defaultValue)
|
||||
self.assertEqual(900, axis.maxValue)
|
||||
self.assertEqual(256, axis.nameID)
|
||||
self.assertEqual(256, axis.axisNameID)
|
||||
|
||||
|
||||
class NamedInstanceTest(unittest.TestCase):
|
||||
def test_compile(self):
|
||||
inst = NamedInstance()
|
||||
inst.nameID = 345
|
||||
inst.subfamilyNameID = 345
|
||||
inst.coordinates = {"wght": 0.7, "wdth": 0.5}
|
||||
self.assertEqual(FVAR_INSTANCE_DATA, inst.compile(["wght", "wdth"]))
|
||||
|
||||
def test_decompile(self):
|
||||
inst = NamedInstance()
|
||||
inst.decompile(FVAR_INSTANCE_DATA, ["wght", "wdth"])
|
||||
self.assertEqual(345, inst.nameID)
|
||||
self.assertEqual(345, inst.subfamilyNameID)
|
||||
self.assertEqual({"wght": 0.7, "wdth": 0.5}, inst.coordinates)
|
||||
|
||||
def test_toXML(self):
|
||||
font = MakeFont()
|
||||
inst = NamedInstance()
|
||||
inst.nameID = AddName(font, "Light Condensed").nameID
|
||||
inst.subfamilyNameID = AddName(font, "Light Condensed").nameID
|
||||
inst.coordinates = {"wght": 0.7, "wdth": 0.5}
|
||||
writer = XMLWriter(BytesIO())
|
||||
inst.toXML(writer, font)
|
||||
self.assertEqual([
|
||||
'',
|
||||
'<!-- Light Condensed -->',
|
||||
'<NamedInstance nameID="%s">' % inst.nameID,
|
||||
'<NamedInstance subfamilyNameID="%s">' % inst.subfamilyNameID,
|
||||
'<coord axis="wght" value="0.7"/>',
|
||||
'<coord axis="wdth" value="0.5"/>',
|
||||
'</NamedInstance>'
|
||||
@ -187,12 +187,12 @@ class NamedInstanceTest(unittest.TestCase):
|
||||
def test_fromXML(self):
|
||||
inst = NamedInstance()
|
||||
for name, attrs, content in parseXML(
|
||||
'<NamedInstance nameID="345">'
|
||||
'<NamedInstance subfamilyNameID="345">'
|
||||
' <coord axis="wght" value="0.7"/>'
|
||||
' <coord axis="wdth" value="0.5"/>'
|
||||
'</NamedInstance>'):
|
||||
inst.fromXML(name, attrs, content, ttFont=MakeFont())
|
||||
self.assertEqual(345, inst.nameID)
|
||||
self.assertEqual(345, inst.subfamilyNameID)
|
||||
self.assertEqual({"wght": 0.7, "wdth": 0.5}, inst.coordinates)
|
||||
|
||||
|
||||
|
@ -427,6 +427,8 @@ class GlyphVariation(object):
|
||||
def compile(self, axisTags, sharedCoordIndices, sharedPoints):
|
||||
tupleData = []
|
||||
|
||||
assert all(tag in axisTags for tag in self.axes.keys()), ("Unknown axis tag found.", self.axes.keys(), axisTags)
|
||||
|
||||
coord = self.compileCoord(axisTags)
|
||||
if coord in sharedCoordIndices:
|
||||
flags = sharedCoordIndices[coord]
|
||||
|
@ -164,6 +164,13 @@ class OTTableReader(object):
|
||||
self.pos = newpos
|
||||
return value
|
||||
|
||||
def readInt8(self):
|
||||
pos = self.pos
|
||||
newpos = pos + 1
|
||||
value, = struct.unpack(">b", self.data[pos:newpos])
|
||||
self.pos = newpos
|
||||
return value
|
||||
|
||||
def readShort(self):
|
||||
pos = self.pos
|
||||
newpos = pos + 2
|
||||
@ -429,12 +436,17 @@ class OTTableWriter(object):
|
||||
self.items.append(struct.pack(">H", value))
|
||||
|
||||
def writeShort(self, value):
|
||||
assert -32768 <= value < 32768, value
|
||||
self.items.append(struct.pack(">h", value))
|
||||
|
||||
def writeUInt8(self, value):
|
||||
assert 0 <= value < 256
|
||||
assert 0 <= value < 256, value
|
||||
self.items.append(struct.pack(">B", value))
|
||||
|
||||
def writeInt8(self, value):
|
||||
assert -128 <= value < 128, value
|
||||
self.items.append(struct.pack(">b", value))
|
||||
|
||||
def writeUInt24(self, value):
|
||||
assert 0 <= value < 0x1000000, value
|
||||
b = struct.pack(">L", value)
|
||||
@ -454,8 +466,8 @@ class OTTableWriter(object):
|
||||
def writeSubTable(self, subWriter):
|
||||
self.items.append(subWriter)
|
||||
|
||||
def writeCountReference(self, table, name):
|
||||
ref = CountReference(table, name)
|
||||
def writeCountReference(self, table, name, size=2):
|
||||
ref = CountReference(table, name, size=size)
|
||||
self.items.append(ref)
|
||||
return ref
|
||||
|
||||
@ -502,9 +514,10 @@ class OTTableWriter(object):
|
||||
|
||||
class CountReference(object):
|
||||
"""A reference to a Count value, not a count of references."""
|
||||
def __init__(self, table, name):
|
||||
def __init__(self, table, name, size=None):
|
||||
self.table = table
|
||||
self.name = name
|
||||
self.size = size
|
||||
def setValue(self, value):
|
||||
table = self.table
|
||||
name = self.name
|
||||
@ -513,9 +526,10 @@ class CountReference(object):
|
||||
else:
|
||||
assert table[name] == value, (name, table[name], value)
|
||||
def getCountData(self):
|
||||
assert self.size in (2, 4)
|
||||
v = self.table[self.name]
|
||||
if v is None: v = 0
|
||||
return packUShort(v)
|
||||
return packUShort(v) if self.size == 2 else packULong(v)
|
||||
|
||||
|
||||
def packUShort(value):
|
||||
@ -645,12 +659,12 @@ class BaseTable(object):
|
||||
# table. We will later store it here.
|
||||
# We add a reference: by the time the data is assembled
|
||||
# the Count value will be filled in.
|
||||
ref = writer.writeCountReference(table, conv.name)
|
||||
ref = writer.writeCountReference(table, conv.name, conv.staticSize)
|
||||
table[conv.name] = None
|
||||
if conv.isPropagated:
|
||||
writer[conv.name] = ref
|
||||
elif conv.isLookupType:
|
||||
ref = writer.writeCountReference(table, conv.name)
|
||||
ref = writer.writeCountReference(table, conv.name, conv.staticSize)
|
||||
table[conv.name] = None
|
||||
writer['LookupType'] = ref
|
||||
else:
|
||||
|
@ -3,6 +3,7 @@ from fontTools.misc.py23 import *
|
||||
from fontTools.misc.textTools import safeEval
|
||||
from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi
|
||||
from .otBase import ValueRecordFactory
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
|
||||
@ -21,8 +22,8 @@ def buildConverters(tableSpec, tableNamespace):
|
||||
assert tp == "uint16"
|
||||
converterClass = ValueFormat
|
||||
elif name.endswith("Count") or name.endswith("LookupType"):
|
||||
assert tp == "uint16"
|
||||
converterClass = ComputedUShort
|
||||
assert tp in ("uint16", "uint32")
|
||||
converterClass = ComputedUShort if tp == 'uint16' else ComputedULong
|
||||
elif name == "SubTable":
|
||||
converterClass = SubTable
|
||||
elif name == "ExtSubTable":
|
||||
@ -30,13 +31,16 @@ def buildConverters(tableSpec, tableNamespace):
|
||||
elif name == "FeatureParams":
|
||||
converterClass = FeatureParams
|
||||
else:
|
||||
if not tp in converterMapping:
|
||||
if not tp in converterMapping and '(' not in tp:
|
||||
tableName = tp
|
||||
converterClass = Struct
|
||||
else:
|
||||
converterClass = converterMapping[tp]
|
||||
converterClass = eval(tp, tableNamespace, converterMapping)
|
||||
tableClass = tableNamespace.get(tableName)
|
||||
conv = converterClass(name, repeat, aux, tableClass)
|
||||
if tableClass is not None:
|
||||
conv = converterClass(name, repeat, aux, tableClass=tableClass)
|
||||
else:
|
||||
conv = converterClass(name, repeat, aux)
|
||||
if name in ["SubTable", "ExtSubTable"]:
|
||||
conv.lookupTypes = tableNamespace['lookupTypes']
|
||||
# also create reverse mapping
|
||||
@ -82,14 +86,14 @@ class BaseConverter(object):
|
||||
"""Base class for converter objects. Apart from the constructor, this
|
||||
is an abstract class."""
|
||||
|
||||
def __init__(self, name, repeat, aux, tableClass):
|
||||
def __init__(self, name, repeat, aux, tableClass=None):
|
||||
self.name = name
|
||||
self.repeat = repeat
|
||||
self.aux = aux
|
||||
self.tableClass = tableClass
|
||||
self.isCount = name.endswith("Count")
|
||||
self.isLookupType = name.endswith("LookupType")
|
||||
self.isPropagated = name in ["ClassCount", "Class2Count", "FeatureTag", "SettingsCount", "AxisCount"]
|
||||
self.isPropagated = name in ["ClassCount", "Class2Count", "FeatureTag", "SettingsCount", "VarRegionCount", "MappingCount", "RegionAxisCount"]
|
||||
|
||||
def readArray(self, reader, font, tableDict, count):
|
||||
"""Read an array of values from the reader."""
|
||||
@ -178,6 +182,13 @@ class UShort(IntValue):
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeUShort(value)
|
||||
|
||||
class Int8(IntValue):
|
||||
staticSize = 1
|
||||
def read(self, reader, font, tableDict):
|
||||
return reader.readInt8()
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeInt8(value)
|
||||
|
||||
class UInt8(IntValue):
|
||||
staticSize = 1
|
||||
def read(self, reader, font, tableDict):
|
||||
@ -192,11 +203,16 @@ class UInt24(IntValue):
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeUInt24(value)
|
||||
|
||||
class ComputedUShort(UShort):
|
||||
class ComputedInt(IntValue):
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
xmlWriter.comment("%s=%s" % (name, value))
|
||||
xmlWriter.newline()
|
||||
|
||||
class ComputedUShort(ComputedInt, UShort):
|
||||
pass
|
||||
class ComputedULong(ComputedInt, ULong):
|
||||
pass
|
||||
|
||||
class Tag(SimpleValue):
|
||||
staticSize = 4
|
||||
def read(self, reader, font, tableDict):
|
||||
@ -239,33 +255,47 @@ class Fixed(FloatValue):
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeLong(fl2fi(value, 16))
|
||||
|
||||
class F2Dot14(FloatValue):
|
||||
staticSize = 2
|
||||
def read(self, reader, font, tableDict):
|
||||
return fi2fl(reader.readShort(), 14)
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
writer.writeShort(fl2fi(value, 14))
|
||||
|
||||
class Version(BaseConverter):
|
||||
staticSize = 4
|
||||
def read(self, reader, font, tableDict):
|
||||
value = reader.readLong()
|
||||
assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
|
||||
return fi2fl(value, 16)
|
||||
return value
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
if value < 0x10000:
|
||||
value = fl2fi(value, 16)
|
||||
value = int(round(value))
|
||||
newValue = self.fromFloat(value)
|
||||
log.warning("Table version value is a float: %g; fix code to use hex instead: %08x", value, newValue)
|
||||
value = newValue
|
||||
assert (value >> 16) == 1, "Unsupported version 0x%08x" % value
|
||||
writer.writeLong(value)
|
||||
def xmlRead(self, attrs, content, font):
|
||||
value = attrs["value"]
|
||||
value = float(int(value, 0)) if value.startswith("0") else float(value)
|
||||
if value >= 0x10000:
|
||||
value = fi2fl(value, 16)
|
||||
value = int(value, 0) if value.startswith("0") else float(value)
|
||||
if value < 0x10000:
|
||||
newValue = self.fromFloat(value)
|
||||
log.warning("Table version value is a float: %g; fix XML to use hex instead: %08x", value, newValue)
|
||||
value = newValue
|
||||
return value
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
if value >= 0x10000:
|
||||
value = fi2fl(value, 16)
|
||||
if value % 1 != 0:
|
||||
# Write as hex
|
||||
value = "0x%08x" % fl2fi(value, 16)
|
||||
if value < 0x10000:
|
||||
newValue = self.fromFloat(value)
|
||||
log.warning("Table version value is a float: %g; fix code to use hex instead: %08x", value, newValue)
|
||||
value = newValue
|
||||
value = "0x%08x" % value
|
||||
xmlWriter.simpletag(name, attrs + [("value", value)])
|
||||
xmlWriter.newline()
|
||||
|
||||
@staticmethod
|
||||
def fromFloat(v):
|
||||
return fl2fi(v, 16)
|
||||
|
||||
|
||||
class Struct(BaseConverter):
|
||||
|
||||
@ -389,7 +419,7 @@ class FeatureParams(Table):
|
||||
|
||||
class ValueFormat(IntValue):
|
||||
staticSize = 2
|
||||
def __init__(self, name, repeat, aux, tableClass):
|
||||
def __init__(self, name, repeat, aux, tableClass=None):
|
||||
BaseConverter.__init__(self, name, repeat, aux, tableClass)
|
||||
self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1")
|
||||
def read(self, reader, font, tableDict):
|
||||
@ -474,10 +504,97 @@ class DeltaValue(BaseConverter):
|
||||
return safeEval(attrs["value"])
|
||||
|
||||
|
||||
class VarIdxMapValue(BaseConverter):
|
||||
|
||||
def read(self, reader, font, tableDict):
|
||||
fmt = tableDict['EntryFormat']
|
||||
nItems = tableDict['MappingCount']
|
||||
|
||||
innerBits = 1 + (fmt & 0x000F)
|
||||
innerMask = (1<<innerBits) - 1
|
||||
outerMask = 0xFFFFFFFF - innerMask
|
||||
outerShift = 16 - innerBits
|
||||
|
||||
entrySize = 1 + ((fmt & 0x0030) >> 4)
|
||||
read = {
|
||||
1: reader.readUInt8,
|
||||
2: reader.readUShort,
|
||||
3: reader.readUInt24,
|
||||
4: reader.readULong,
|
||||
}[entrySize]
|
||||
|
||||
mapping = []
|
||||
for i in range(nItems):
|
||||
raw = read()
|
||||
idx = ((raw & outerMask) << outerShift) | (raw & innerMask)
|
||||
mapping.append(idx)
|
||||
|
||||
return mapping
|
||||
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
fmt = tableDict['EntryFormat']
|
||||
mapping = value
|
||||
writer['MappingCount'].setValue(len(mapping))
|
||||
|
||||
innerBits = 1 + (fmt & 0x000F)
|
||||
innerMask = (1<<innerBits) - 1
|
||||
outerShift = 16 - innerBits
|
||||
|
||||
entrySize = 1 + ((fmt & 0x0030) >> 4)
|
||||
write = {
|
||||
1: writer.writeUInt8,
|
||||
2: writer.writeUShort,
|
||||
3: writer.writeUInt24,
|
||||
4: writer.writeULong,
|
||||
}[entrySize]
|
||||
|
||||
for idx in mapping:
|
||||
raw = ((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask)
|
||||
write(raw)
|
||||
|
||||
|
||||
class VarDataValue(BaseConverter):
|
||||
|
||||
def read(self, reader, font, tableDict):
|
||||
values = []
|
||||
|
||||
regionCount = tableDict["VarRegionCount"]
|
||||
shortCount = tableDict["NumShorts"]
|
||||
|
||||
for i in range(min(regionCount, shortCount)):
|
||||
values.append(reader.readShort())
|
||||
for i in range(min(regionCount, shortCount), regionCount):
|
||||
values.append(reader.readInt8())
|
||||
for i in range(regionCount, shortCount):
|
||||
reader.readInt8()
|
||||
|
||||
return values
|
||||
|
||||
def write(self, writer, font, tableDict, value, repeatIndex=None):
|
||||
regionCount = tableDict["VarRegionCount"]
|
||||
shortCount = tableDict["NumShorts"]
|
||||
|
||||
for i in range(min(regionCount, shortCount)):
|
||||
writer.writeShort(value[i])
|
||||
for i in range(min(regionCount, shortCount), regionCount):
|
||||
writer.writeInt8(value[i])
|
||||
for i in range(regionCount, shortCount):
|
||||
writer.writeInt8(0)
|
||||
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
xmlWriter.simpletag(name, attrs + [("value", value)])
|
||||
xmlWriter.newline()
|
||||
|
||||
def xmlRead(self, attrs, content, font):
|
||||
return safeEval(attrs["value"])
|
||||
|
||||
|
||||
converterMapping = {
|
||||
# type class
|
||||
"int8": Int8,
|
||||
"int16": Short,
|
||||
"uint8": UInt8,
|
||||
"uint8": UInt8,
|
||||
"uint16": UShort,
|
||||
"uint24": UInt24,
|
||||
"uint32": ULong,
|
||||
@ -486,9 +603,15 @@ converterMapping = {
|
||||
"GlyphID": GlyphID,
|
||||
"DeciPoints": DeciPoints,
|
||||
"Fixed": Fixed,
|
||||
"F2Dot14": F2Dot14,
|
||||
"struct": Struct,
|
||||
"Offset": Table,
|
||||
"LOffset": LTable,
|
||||
"ValueRecord": ValueRecord,
|
||||
"DeltaValue": DeltaValue,
|
||||
"VarIdxMapValue": VarIdxMapValue,
|
||||
"VarDataValue": VarDataValue,
|
||||
# "Template" types
|
||||
"OffsetTo": lambda C: partial(Table, tableClass=C),
|
||||
"LOffsetTo": lambda C: partial(LTable, tableClass=C),
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ otData = [
|
||||
('uint16', 'StartSize', None, None, 'Smallest size to correct-in ppem'),
|
||||
('uint16', 'EndSize', None, None, 'Largest size to correct-in ppem'),
|
||||
('uint16', 'DeltaFormat', None, None, 'Format of DeltaValue array data: 1, 2, or 3'),
|
||||
('DeltaValue', 'DeltaValue', '', 0, 'Array of compressed data'),
|
||||
('DeltaValue', 'DeltaValue', '', 'DeltaFormat in (1,2,3)', 'Array of compressed data'),
|
||||
]),
|
||||
|
||||
|
||||
@ -143,10 +143,11 @@ otData = [
|
||||
#
|
||||
|
||||
('GPOS', [
|
||||
('Version', 'Version', None, None, 'Version of the GPOS table-initially = 0x00010000'),
|
||||
('Version', 'Version', None, None, 'Version of the GPOS table- 0x00010000 or 0x00010001'),
|
||||
('Offset', 'ScriptList', None, None, 'Offset to ScriptList table-from beginning of GPOS table'),
|
||||
('Offset', 'FeatureList', None, None, 'Offset to FeatureList table-from beginning of GPOS table'),
|
||||
('Offset', 'LookupList', None, None, 'Offset to LookupList table-from beginning of GPOS table'),
|
||||
('LOffset', 'FeatureVariations', None, 'Version >= 0x00010001', 'Offset to FeatureVariations table-from beginning of GPOS table'),
|
||||
]),
|
||||
|
||||
('SinglePosFormat1', [
|
||||
@ -443,10 +444,11 @@ otData = [
|
||||
#
|
||||
|
||||
('GSUB', [
|
||||
('Version', 'Version', None, None, 'Version of the GSUB table-initially set to 0x00010000'),
|
||||
('Version', 'Version', None, None, 'Version of the GSUB table- 0x00010000 or 0x00010001'),
|
||||
('Offset', 'ScriptList', None, None, 'Offset to ScriptList table-from beginning of GSUB table'),
|
||||
('Offset', 'FeatureList', None, None, 'Offset to FeatureList table-from beginning of GSUB table'),
|
||||
('Offset', 'LookupList', None, None, 'Offset to LookupList table-from beginning of GSUB table'),
|
||||
('LOffset', 'FeatureVariations', None, 'Version >= 0x00010001', 'Offset to FeatureVariations table-from beginning of GSUB table'),
|
||||
]),
|
||||
|
||||
('SingleSubstFormat1', [
|
||||
@ -639,12 +641,13 @@ otData = [
|
||||
#
|
||||
|
||||
('GDEF', [
|
||||
('Version', 'Version', None, None, 'Version of the GDEF table-initially 0x00010000'),
|
||||
('Version', 'Version', None, None, 'Version of the GDEF table- 0x00010000, 0x00010002, or 0x00010003'),
|
||||
('Offset', 'GlyphClassDef', None, None, 'Offset to class definition table for glyph type-from beginning of GDEF header (may be NULL)'),
|
||||
('Offset', 'AttachList', None, None, 'Offset to list of glyphs with attachment points-from beginning of GDEF header (may be NULL)'),
|
||||
('Offset', 'LigCaretList', None, None, 'Offset to list of positioning points for ligature carets-from beginning of GDEF header (may be NULL)'),
|
||||
('Offset', 'MarkAttachClassDef', None, None, 'Offset to class definition table for mark attachment type-from beginning of GDEF header (may be NULL)'),
|
||||
('Offset', 'MarkGlyphSetsDef', None, 'int(round(Version*0x10000)) >= 0x00010002', 'Offset to the table of mark set definitions-from beginning of GDEF header (may be NULL)'),
|
||||
('Offset', 'MarkGlyphSetsDef', None, 'Version >= 0x00010002', 'Offset to the table of mark set definitions-from beginning of GDEF header (may be NULL)'),
|
||||
('LOffset', 'VarStore', None, 'Version >= 0x00010003', 'Offset to variation store (may be NULL)'),
|
||||
]),
|
||||
|
||||
('AttachList', [
|
||||
@ -836,6 +839,107 @@ otData = [
|
||||
('Offset', 'Lookup', 'LookupCount', 0, 'Array of offsets to GPOS-type lookup tables-from beginning of JstfMax table-in design order'),
|
||||
]),
|
||||
|
||||
|
||||
#
|
||||
# Variation fonts
|
||||
#
|
||||
|
||||
# GSUB/GPOS FeatureVariations
|
||||
|
||||
('FeatureVariations', [
|
||||
('Version', 'Version', None, None, 'Version of the table-initially set to 0x00010000'),
|
||||
('uint32', 'FeatureVariationCount', None, None, 'Number of records in the FeatureVariationRecord array'),
|
||||
('struct', 'FeatureVariationRecord', 'FeatureVariationCount', 0, 'Array of FeatureVariationRecord'),
|
||||
]),
|
||||
|
||||
('FeatureVariationRecord', [
|
||||
('LOffset', 'ConditionSet', None, None, 'Offset to a ConditionSet table, from beginning of the FeatureVariations table.'),
|
||||
('LOffset', 'FeatureTableSubstitution', None, None, 'Offset to a FeatureTableSubstitution table, from beginning of the FeatureVariations table'),
|
||||
]),
|
||||
|
||||
('ConditionSet', [
|
||||
('uint16', 'ConditionCount', None, None, 'Number of condition tables in the ConditionTable array'),
|
||||
('LOffset', 'ConditionTable', 'ConditionCount', 0, 'Array of condition tables.'),
|
||||
]),
|
||||
|
||||
('ConditionTableFormat1', [
|
||||
('uint16', 'Format', None, None, 'Format, = 1'),
|
||||
('uint16', 'AxisIndex', None, None, 'Index for the variation axis within the fvar table, base 0.'),
|
||||
('F2Dot14', 'FilterRangeMinValue', None, None, 'Minimum normalized axis value of the font variation instances that satisfy this condition.'),
|
||||
('F2Dot14', 'FilterRangeMaxValue', None, None, 'Maximum value that satisfies this condition.'),
|
||||
]),
|
||||
|
||||
('FeatureTableSubstitution', [
|
||||
('Version', 'Version', None, None, 'Version of the table-initially set to 0x00010000'),
|
||||
('uint16', 'SubstitutionCount', None, None, 'Number of records in the FeatureVariationRecords array'),
|
||||
('FeatureTableSubstitutionRecord', 'SubstitutionRecord', 'SubstitutionCount', 0, 'Array of FeatureTableSubstitutionRecord'),
|
||||
]),
|
||||
|
||||
('FeatureTableSubstitutionRecord', [
|
||||
('uint16', 'FeatureIndex', None, None, 'The feature table index to match.'),
|
||||
('LOffset', 'Feature', None, None, 'Offset to an alternate feature table, from start of the FeatureTableSubstitution table.'),
|
||||
]),
|
||||
|
||||
# VariationStore
|
||||
|
||||
('VarRegionAxis', [
|
||||
('F2Dot14', 'StartCoord', None, None, ''),
|
||||
('F2Dot14', 'PeakCoord', None, None, ''),
|
||||
('F2Dot14', 'EndCoord', None, None, ''),
|
||||
]),
|
||||
|
||||
('VarRegion', [
|
||||
('struct', 'VarRegionAxis', 'RegionAxisCount', 0, ''),
|
||||
]),
|
||||
|
||||
('VarRegionList', [
|
||||
('uint16', 'RegionAxisCount', None, None, ''),
|
||||
('uint16', 'RegionCount', None, None, ''),
|
||||
('VarRegion', 'Region', 'RegionCount', 0, ''),
|
||||
]),
|
||||
|
||||
('VarData', [
|
||||
('uint16', 'ItemCount', None, None, ''),
|
||||
('uint16', 'NumShorts', None, None, ''), # Automatically computed
|
||||
('uint16', 'VarRegionCount', None, None, ''),
|
||||
('uint16', 'VarRegionIndex', 'VarRegionCount', 0, ''),
|
||||
('VarDataValue', 'Item', 'ItemCount', 0, ''),
|
||||
]),
|
||||
|
||||
('VarStore', [
|
||||
('uint16', 'Format', None, None, 'Set to 1.'),
|
||||
('LOffset', 'VarRegionList', None, None, ''),
|
||||
('uint16', 'VarDataCount', None, None, ''),
|
||||
('LOffset', 'VarData', 'VarDataCount', 0, ''),
|
||||
]),
|
||||
|
||||
# Variation helpers
|
||||
|
||||
('VarIdxMap', [
|
||||
('uint16', 'EntryFormat', None, None, ''), # Automatically computed
|
||||
('uint16', 'MappingCount', None, None, ''), # Automatically computed
|
||||
('VarIdxMapValue', 'mapping', '', 0, 'Array of compressed data'),
|
||||
]),
|
||||
|
||||
# Glyph advance variations
|
||||
|
||||
('HVAR', [
|
||||
('Version', 'Version', None, None, 'Version of the HVAR table-initially = 0x00010000'),
|
||||
('LOffset', 'VarStore', None, None, ''),
|
||||
('LOffsetTo(VarIdxMap)', 'AdvWidthMap', None, None, ''),
|
||||
('LOffsetTo(VarIdxMap)', 'LsbMap', None, None, ''),
|
||||
('LOffsetTo(VarIdxMap)', 'RsbMap', None, None, ''),
|
||||
]),
|
||||
('VVAR', [
|
||||
('Version', 'Version', None, None, 'Version of the VVAR table-initially = 0x00010000'),
|
||||
('LOffset', 'VarStore', None, None, ''),
|
||||
('LOffsetTo(VarIdxMap)', 'AdvHeightMap', None, None, ''),
|
||||
('LOffsetTo(VarIdxMap)', 'TsbMap', None, None, ''),
|
||||
('LOffsetTo(VarIdxMap)', 'BsbMap', None, None, ''),
|
||||
('LOffsetTo(VarIdxMap)', 'VOrgMap', None, None, 'Vertical origin mapping.'),
|
||||
]),
|
||||
|
||||
|
||||
#
|
||||
# math
|
||||
#
|
||||
|
@ -6,6 +6,7 @@ converter objects from otConverters.py.
|
||||
"""
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.textTools import safeEval
|
||||
from .otBase import BaseTable, FormatSwitchingBaseTable
|
||||
import operator
|
||||
import logging
|
||||
@ -133,6 +134,91 @@ class Coverage(FormatSwitchingBaseTable):
|
||||
glyphs.append(attrs["value"])
|
||||
|
||||
|
||||
class VarIdxMap(BaseTable):
|
||||
|
||||
def postRead(self, rawTable, font):
|
||||
assert (rawTable['EntryFormat'] & 0xFFC0) == 0
|
||||
self.mapping = rawTable['mapping']
|
||||
|
||||
def preWrite(self, font):
|
||||
mapping = getattr(self, "mapping", None)
|
||||
if mapping is None:
|
||||
mapping = self.mapping = []
|
||||
rawTable = { 'mapping': mapping }
|
||||
rawTable['MappingCount'] = len(mapping)
|
||||
|
||||
# TODO Remove this abstraction/optimization and move it varLib.builder?
|
||||
|
||||
ored = 0
|
||||
for idx in mapping:
|
||||
ored |= idx
|
||||
|
||||
inner = ored & 0xFFFF
|
||||
innerBits = 0
|
||||
while inner:
|
||||
innerBits += 1
|
||||
inner >>= 1
|
||||
innerBits = max(innerBits, 1)
|
||||
assert innerBits <= 16
|
||||
|
||||
ored = (ored >> (16-innerBits)) | (ored & ((1<<innerBits)-1))
|
||||
if ored <= 0x000000FF:
|
||||
entrySize = 1
|
||||
elif ored <= 0x0000FFFF:
|
||||
entrySize = 2
|
||||
elif ored <= 0x00FFFFFF:
|
||||
entrySize = 3
|
||||
else:
|
||||
entrySize = 4
|
||||
|
||||
entryFormat = ((entrySize - 1) << 4) | (innerBits - 1)
|
||||
|
||||
rawTable['EntryFormat'] = entryFormat
|
||||
return rawTable
|
||||
|
||||
def toXML2(self, xmlWriter, font):
|
||||
for i, value in enumerate(getattr(self, "mapping", [])):
|
||||
attrs = (
|
||||
('index', i),
|
||||
('outer', value >> 16),
|
||||
('inner', value & 0xFFFF),
|
||||
)
|
||||
xmlWriter.simpletag("Map", attrs)
|
||||
xmlWriter.newline()
|
||||
|
||||
def fromXML(self, name, attrs, content, font):
|
||||
mapping = getattr(self, "mapping", None)
|
||||
if mapping is None:
|
||||
mapping = []
|
||||
self.mapping = mapping
|
||||
outer = safeEval(attrs['outer'])
|
||||
inner = safeEval(attrs['inner'])
|
||||
assert inner <= 0xFFFF
|
||||
mapping.append((outer << 16) | inner)
|
||||
|
||||
|
||||
class VarData(BaseTable):
|
||||
|
||||
def preWrite(self, font):
|
||||
rawTable = self.__dict__.copy()
|
||||
|
||||
# TODO Remove this abstraction/optimization and move it varLib.builder?
|
||||
|
||||
numShorts = 0
|
||||
count = len(self.VarRegionIndex)
|
||||
for item in self.Item:
|
||||
assert len(item) == count, ("Item length mismatch", len(item), count)
|
||||
for i in range(count - 1, numShorts - 1, -1):
|
||||
if not (-128 <= item[i] <= 127):
|
||||
numShorts = i + 1
|
||||
break
|
||||
if numShorts == count:
|
||||
break
|
||||
|
||||
rawTable['NumShorts'] = numShorts
|
||||
return rawTable
|
||||
|
||||
|
||||
class SingleSubst(FormatSwitchingBaseTable):
|
||||
|
||||
def postRead(self, rawTable, font):
|
||||
|
@ -3,7 +3,7 @@ Module for dealing with 'gvar'-style font variations, also known as run-time
|
||||
interpolation.
|
||||
|
||||
The ideas here are very similar to MutatorMath. There is even code to read
|
||||
MutatorMath .designspace files.
|
||||
MutatorMath .designspace files in the varLib.designspace module.
|
||||
|
||||
For now, if you run this file on a designspace file, it tries to find
|
||||
ttf-interpolatable files for the masters and build a GX variation font from
|
||||
@ -20,284 +20,18 @@ API *will* change in near future.
|
||||
"""
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.ttLib.tables._n_a_m_e import NameRecord
|
||||
from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis, NamedInstance
|
||||
from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
||||
from fontTools.ttLib.tables._g_v_a_r import table__g_v_a_r, GlyphVariation
|
||||
from fontTools.ttLib.tables._g_v_a_r import GlyphVariation
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.ttLib.tables import otBase as otBase
|
||||
from fontTools.varLib import designspace, models, builder
|
||||
from fontTools.varLib.merger import merge_tables, Merger
|
||||
import warnings
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
import os.path
|
||||
|
||||
|
||||
#
|
||||
# Variation space, aka design space, model
|
||||
#
|
||||
|
||||
def supportScalar(location, support):
|
||||
"""Returns the scalar multiplier at location, for a master
|
||||
with support.
|
||||
>>> supportScalar({}, {})
|
||||
1.0
|
||||
>>> supportScalar({'wght':.2}, {})
|
||||
1.0
|
||||
>>> supportScalar({'wght':.2}, {'wght':(0,2,3)})
|
||||
0.1
|
||||
>>> supportScalar({'wght':2.5}, {'wght':(0,2,4)})
|
||||
0.75
|
||||
"""
|
||||
scalar = 1.
|
||||
for axis,(lower,peak,upper) in support.items():
|
||||
if axis not in location:
|
||||
scalar = 0.
|
||||
break
|
||||
v = location[axis]
|
||||
if v == peak:
|
||||
continue
|
||||
if v <= lower or upper <= v:
|
||||
scalar = 0.
|
||||
break;
|
||||
if v < peak:
|
||||
scalar *= (v - lower) / (peak - lower)
|
||||
else: # v > peak
|
||||
scalar *= (v - upper) / (peak - upper)
|
||||
return scalar
|
||||
|
||||
|
||||
class VariationModel(object):
|
||||
|
||||
"""
|
||||
Locations must be in normalized space. Ie. base master
|
||||
is at origin (0).
|
||||
>>> from pprint import pprint
|
||||
>>> locations = [ \
|
||||
{'wght':100}, \
|
||||
{'wght':-100}, \
|
||||
{'wght':-180}, \
|
||||
{'wdth':+.3}, \
|
||||
{'wght':+120,'wdth':.3}, \
|
||||
{'wght':+120,'wdth':.2}, \
|
||||
{}, \
|
||||
{'wght':+180,'wdth':.3}, \
|
||||
{'wght':+180}, \
|
||||
]
|
||||
>>> model = VariationModel(locations, axisOrder=['wght'])
|
||||
>>> pprint(model.locations)
|
||||
[{},
|
||||
{'wght': -100},
|
||||
{'wght': -180},
|
||||
{'wght': 100},
|
||||
{'wght': 180},
|
||||
{'wdth': 0.3},
|
||||
{'wdth': 0.3, 'wght': 180},
|
||||
{'wdth': 0.3, 'wght': 120},
|
||||
{'wdth': 0.2, 'wght': 120}]
|
||||
>>> pprint(model.deltaWeights)
|
||||
[{},
|
||||
{0: 1.0},
|
||||
{0: 1.0},
|
||||
{0: 1.0},
|
||||
{0: 1.0},
|
||||
{0: 1.0},
|
||||
{0: 1.0, 4: 1.0, 5: 1.0},
|
||||
{0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.25},
|
||||
{0: 1.0,
|
||||
3: 0.75,
|
||||
4: 0.25,
|
||||
5: 0.6666666666666667,
|
||||
6: 0.16666666666666669,
|
||||
7: 0.6666666666666667}]
|
||||
"""
|
||||
|
||||
def __init__(self, locations, axisOrder=[]):
|
||||
locations = [{k:v for k,v in loc.items() if v != 0.} for loc in locations]
|
||||
keyFunc = self.getMasterLocationsSortKeyFunc(locations, axisOrder=axisOrder)
|
||||
axisPoints = keyFunc.axisPoints
|
||||
self.locations = sorted(locations, key=keyFunc)
|
||||
# TODO Assert that locations are unique.
|
||||
self.mapping = [self.locations.index(l) for l in locations] # Mapping from user's master order to our master order
|
||||
self.reverseMapping = [locations.index(l) for l in self.locations] # Reverse of above
|
||||
|
||||
self._computeMasterSupports(axisPoints)
|
||||
|
||||
@staticmethod
|
||||
def getMasterLocationsSortKeyFunc(locations, axisOrder=[]):
|
||||
assert {} in locations, "Base master not found."
|
||||
axisPoints = {}
|
||||
for loc in locations:
|
||||
if len(loc) != 1:
|
||||
continue
|
||||
axis = next(iter(loc))
|
||||
value = loc[axis]
|
||||
if axis not in axisPoints:
|
||||
axisPoints[axis] = {0}
|
||||
assert value not in axisPoints[axis]
|
||||
axisPoints[axis].add(value)
|
||||
|
||||
def getKey(axisPoints, axisOrder):
|
||||
def sign(v):
|
||||
return -1 if v < 0 else +1 if v > 0 else 0
|
||||
def key(loc):
|
||||
rank = len(loc)
|
||||
onPointAxes = [axis for axis,value in loc.items() if value in axisPoints[axis]]
|
||||
orderedAxes = [axis for axis in axisOrder if axis in loc]
|
||||
orderedAxes.extend([axis for axis in sorted(loc.keys()) if axis not in axisOrder])
|
||||
return (
|
||||
rank, # First, order by increasing rank
|
||||
-len(onPointAxes), # Next, by decreasing number of onPoint axes
|
||||
tuple(axisOrder.index(axis) if axis in axisOrder else 0x10000 for axis in orderedAxes), # Next, by known axes
|
||||
tuple(orderedAxes), # Next, by all axes
|
||||
tuple(sign(loc[axis]) for axis in orderedAxes), # Next, by signs of axis values
|
||||
tuple(abs(loc[axis]) for axis in orderedAxes), # Next, by absolute value of axis values
|
||||
)
|
||||
return key
|
||||
|
||||
ret = getKey(axisPoints, axisOrder)
|
||||
ret.axisPoints = axisPoints
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def lowerBound(value, lst):
|
||||
if any(v < value for v in lst):
|
||||
return max(v for v in lst if v < value)
|
||||
else:
|
||||
return value
|
||||
@staticmethod
|
||||
def upperBound(value, lst):
|
||||
if any(v > value for v in lst):
|
||||
return min(v for v in lst if v > value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def _computeMasterSupports(self, axisPoints):
|
||||
supports = []
|
||||
deltaWeights = []
|
||||
locations = self.locations
|
||||
for i,loc in enumerate(locations):
|
||||
box = {}
|
||||
|
||||
# Account for axisPoints first
|
||||
for axis,values in axisPoints.items():
|
||||
if not axis in loc:
|
||||
continue
|
||||
locV = loc[axis]
|
||||
box[axis] = (self.lowerBound(locV, values), locV, self.upperBound(locV, values))
|
||||
|
||||
locAxes = set(loc.keys())
|
||||
# Walk over previous masters now
|
||||
for j,m in enumerate(locations[:i]):
|
||||
# Master with extra axes do not participte
|
||||
if not set(m.keys()).issubset(locAxes):
|
||||
continue
|
||||
# If it's NOT in the current box, it does not participate
|
||||
relevant = True
|
||||
for axis, (lower,_,upper) in box.items():
|
||||
if axis in m and not (lower < m[axis] < upper):
|
||||
relevant = False
|
||||
break
|
||||
if not relevant:
|
||||
continue
|
||||
# Split the box for new master
|
||||
for axis,val in m.items():
|
||||
assert axis in box
|
||||
lower,locV,upper = box[axis]
|
||||
if val < locV:
|
||||
lower = val
|
||||
elif locV < val:
|
||||
upper = val
|
||||
box[axis] = (lower,locV,upper)
|
||||
supports.append(box)
|
||||
|
||||
deltaWeight = {}
|
||||
# Walk over previous masters now, populate deltaWeight
|
||||
for j,m in enumerate(locations[:i]):
|
||||
scalar = supportScalar(loc, supports[j])
|
||||
if scalar:
|
||||
deltaWeight[j] = scalar
|
||||
deltaWeights.append(deltaWeight)
|
||||
|
||||
self.supports = supports
|
||||
self.deltaWeights = deltaWeights
|
||||
|
||||
def getDeltas(self, masterValues):
|
||||
count = len(self.locations)
|
||||
assert len(masterValues) == len(self.deltaWeights)
|
||||
mapping = self.reverseMapping
|
||||
out = []
|
||||
for i,weights in enumerate(self.deltaWeights):
|
||||
delta = masterValues[mapping[i]]
|
||||
for j,weight in weights.items():
|
||||
delta -= out[j] * weight
|
||||
out.append(delta)
|
||||
return out
|
||||
|
||||
def interpolateFromDeltas(self, loc, deltas):
|
||||
v = None
|
||||
supports = self.supports
|
||||
assert len(deltas) == len(supports)
|
||||
for i,(delta,support) in enumerate(zip(deltas, supports)):
|
||||
scalar = supportScalar(loc, support)
|
||||
if not scalar: continue
|
||||
contribution = delta * scalar
|
||||
if i == 0:
|
||||
v = contribution
|
||||
else:
|
||||
v += contribution
|
||||
return v
|
||||
|
||||
def interpolateFromMasters(self, loc, masterValues):
|
||||
deltas = self.getDeltas(masterValues)
|
||||
return self.interpolateFromDeltas(loc, deltas)
|
||||
|
||||
#
|
||||
# .designspace routines
|
||||
#
|
||||
|
||||
def _xmlParseLocation(et):
|
||||
loc = {}
|
||||
for dim in et.find('location'):
|
||||
assert dim.tag == 'dimension'
|
||||
name = dim.attrib['name']
|
||||
value = float(dim.attrib['xvalue'])
|
||||
assert name not in loc
|
||||
loc[name] = value
|
||||
return loc
|
||||
|
||||
def _designspace_load(et):
|
||||
base_idx = None
|
||||
masters = []
|
||||
ds = et.getroot()
|
||||
for master in ds.find('sources'):
|
||||
name = master.attrib['name']
|
||||
filename = master.attrib['filename']
|
||||
isBase = master.find('info')
|
||||
if isBase is not None:
|
||||
assert base_idx is None
|
||||
base_idx = len(masters)
|
||||
loc = _xmlParseLocation(master)
|
||||
masters.append((filename, loc, name))
|
||||
|
||||
instances = []
|
||||
for instance in ds.find('instances'):
|
||||
name = master.attrib['name']
|
||||
family = instance.attrib['familyname']
|
||||
style = instance.attrib['stylename']
|
||||
filename = instance.attrib['filename']
|
||||
loc = _xmlParseLocation(instance)
|
||||
instances.append((filename, loc, name, family, style))
|
||||
|
||||
return masters, instances, base_idx
|
||||
|
||||
def designspace_load(filename):
|
||||
return _designspace_load(ET.parse(filename))
|
||||
|
||||
def designspace_loads(string):
|
||||
return _designspace_load(ET.fromstring(string))
|
||||
|
||||
|
||||
#
|
||||
# Creation routines
|
||||
#
|
||||
@ -305,32 +39,55 @@ def designspace_loads(string):
|
||||
# TODO: Move to name table proper; also, is mac_roman ok for ASCII names?
|
||||
def _AddName(font, name):
|
||||
"""(font, "Bold") --> NameRecord"""
|
||||
name = tounicode(name)
|
||||
|
||||
nameTable = font.get("name")
|
||||
namerec = NameRecord()
|
||||
namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256])
|
||||
namerec.string = name.encode("mac_roman")
|
||||
namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0)
|
||||
namerec.string = name
|
||||
namerec.platformID, namerec.platEncID, namerec.langID = (3, 1, 0x409)
|
||||
nameTable.names.append(namerec)
|
||||
return namerec
|
||||
|
||||
# Move to fvar table proper?
|
||||
def _add_fvar(font, axes, instances):
|
||||
assert "fvar" not in font
|
||||
font['fvar'] = fvar = table__f_v_a_r()
|
||||
# TODO how to provide axis order?
|
||||
def _add_fvar(font, axes, instances, axis_map):
|
||||
"""
|
||||
Add 'fvar' table to font.
|
||||
|
||||
for tag in sorted(axes.keys()):
|
||||
axes is a dictionary mapping axis-id to axis (min,default,max)
|
||||
coordinate values.
|
||||
|
||||
instances is list of dictionary objects with 'location', 'stylename',
|
||||
and possibly 'postscriptfontname' entries.
|
||||
|
||||
axisMap is dictionary mapping axis-id to (axis-tag, axis-name).
|
||||
"""
|
||||
|
||||
assert "fvar" not in font
|
||||
font['fvar'] = fvar = newTable('fvar')
|
||||
|
||||
for iden in sorted(axes.keys(), key=lambda k: axis_map[k][0]):
|
||||
axis = Axis()
|
||||
axis.axisTag = tag
|
||||
name, axis.minValue, axis.defaultValue, axis.maxValue = axes[tag]
|
||||
axis.nameID = _AddName(font, name).nameID
|
||||
axis.axisTag = Tag(axis_map[iden][0])
|
||||
axis.minValue, axis.defaultValue, axis.maxValue = axes[iden]
|
||||
axis.axisNameID = _AddName(font, axis_map[iden][1]).nameID
|
||||
fvar.axes.append(axis)
|
||||
|
||||
for name, coordinates in instances:
|
||||
for instance in instances:
|
||||
coordinates = instance['location']
|
||||
name = instance['stylename']
|
||||
psname = instance.get('postscriptfontname')
|
||||
|
||||
inst = NamedInstance()
|
||||
inst.nameID = _AddName(font, name).nameID
|
||||
inst.coordinates = coordinates
|
||||
inst.subfamilyNameID = _AddName(font, name).nameID
|
||||
if psname:
|
||||
inst.postscriptNameID = _AddName(font, psname).nameID
|
||||
inst.coordinates = {axis_map[k][0]:v for k,v in coordinates.items()}
|
||||
fvar.instances.append(inst)
|
||||
|
||||
return fvar
|
||||
|
||||
# TODO Move to glyf or gvar table proper
|
||||
def _GetCoordinates(font, glyphName):
|
||||
"""font, glyphName --> glyph coordinates as expected by "gvar" table
|
||||
@ -403,42 +160,16 @@ def _SetCoordinates(font, glyphName, coord):
|
||||
# XXX Remove the round when https://github.com/behdad/fonttools/issues/593 is fixed
|
||||
font["hmtx"].metrics[glyphName] = int(round(horizontalAdvanceWidth)), int(round(leftSideBearing))
|
||||
|
||||
def _add_gvar(font, axes, master_ttfs, master_locs, base_idx):
|
||||
|
||||
# Make copies for modification
|
||||
master_ttfs = master_ttfs[:]
|
||||
master_locs = [l.copy() for l in master_locs]
|
||||
|
||||
axis_tags = axes.keys()
|
||||
|
||||
# Normalize master locations. TODO Move to a separate function.
|
||||
for tag,(name,lower,default,upper) in axes.items():
|
||||
for l in master_locs:
|
||||
v = l[tag]
|
||||
if v == default:
|
||||
v = 0
|
||||
elif v < default:
|
||||
v = (v - default) / (default - lower)
|
||||
else:
|
||||
v = (v - default) / (upper - default)
|
||||
l[tag] = v
|
||||
# Locations are normalized now
|
||||
|
||||
print("Normalized master positions:")
|
||||
print(master_locs)
|
||||
def _add_gvar(font, model, master_ttfs):
|
||||
|
||||
print("Generating gvar")
|
||||
assert "gvar" not in font
|
||||
gvar = font["gvar"] = table__g_v_a_r()
|
||||
gvar = font["gvar"] = newTable('gvar')
|
||||
gvar.version = 1
|
||||
gvar.reserved = 0
|
||||
gvar.variations = {}
|
||||
|
||||
# Assume single-model for now.
|
||||
model = VariationModel(master_locs)
|
||||
model_base_idx = model.mapping[base_idx]
|
||||
assert 0 == model_base_idx
|
||||
|
||||
for glyph in font.getGlyphOrder():
|
||||
|
||||
allData = [_GetCoordinates(m, glyph) for m in master_ttfs]
|
||||
@ -450,42 +181,186 @@ def _add_gvar(font, axes, master_ttfs, master_locs, base_idx):
|
||||
continue
|
||||
del allControls
|
||||
|
||||
# Update gvar
|
||||
gvar.variations[glyph] = []
|
||||
|
||||
deltas = model.getDeltas(allCoords)
|
||||
supports = model.supports
|
||||
assert len(deltas) == len(supports)
|
||||
for i,(delta,support) in enumerate(zip(deltas, supports)):
|
||||
if i == model_base_idx:
|
||||
continue
|
||||
for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])):
|
||||
var = GlyphVariation(support, delta)
|
||||
gvar.variations[glyph].append(var)
|
||||
|
||||
def main(args=None):
|
||||
def _add_HVAR(font, model, master_ttfs, axisTags):
|
||||
|
||||
import sys
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
print("Generating HVAR")
|
||||
|
||||
(designspace_filename,) = args
|
||||
finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf')
|
||||
axisMap = None # dict mapping axis id to (axis tag, axis name)
|
||||
outfile = os.path.splitext(designspace_filename)[0] + '-GX.ttf'
|
||||
hAdvanceDeltas = {}
|
||||
metricses = [m["hmtx"].metrics for m in master_ttfs]
|
||||
for glyph in font.getGlyphOrder():
|
||||
hAdvances = [metrics[glyph][0] for metrics in metricses]
|
||||
# TODO move round somewhere else?
|
||||
hAdvanceDeltas[glyph] = tuple(round(d) for d in model.getDeltas(hAdvances)[1:])
|
||||
|
||||
masters, instances, base_idx = designspace_load(designspace_filename)
|
||||
# We only support the direct mapping right now.
|
||||
|
||||
supports = model.supports[1:]
|
||||
varTupleList = builder.buildVarRegionList(supports, axisTags)
|
||||
varTupleIndexes = list(range(len(supports)))
|
||||
n = len(supports)
|
||||
items = []
|
||||
zeroes = [0]*n
|
||||
for glyphName in font.getGlyphOrder():
|
||||
items.append(hAdvanceDeltas.get(glyphName, zeroes))
|
||||
while items and items[-1] is zeroes:
|
||||
del items[-1]
|
||||
|
||||
advanceMapping = None
|
||||
# Add indirect mapping to save on duplicates
|
||||
uniq = set(items)
|
||||
# TODO Improve heuristic
|
||||
if (len(items) - len(uniq)) * len(varTupleIndexes) > len(items):
|
||||
newItems = sorted(uniq)
|
||||
mapper = {v:i for i,v in enumerate(newItems)}
|
||||
mapping = [mapper[item] for item in items]
|
||||
while len(mapping) > 1 and mapping[-1] == mapping[-2]:
|
||||
del mapping[-1]
|
||||
advanceMapping = builder.buildVarIdxMap(mapping)
|
||||
items = newItems
|
||||
del mapper, mapping, newItems
|
||||
del uniq
|
||||
|
||||
varData = builder.buildVarData(varTupleIndexes, items)
|
||||
varStore = builder.buildVarStore(varTupleList, [varData])
|
||||
|
||||
assert "HVAR" not in font
|
||||
HVAR = font["HVAR"] = newTable('HVAR')
|
||||
hvar = HVAR.table = ot.HVAR()
|
||||
hvar.Version = 0x00010000
|
||||
hvar.VarStore = varStore
|
||||
hvar.AdvWidthMap = advanceMapping
|
||||
hvar.LsbMap = hvar.RsbMap = None
|
||||
|
||||
|
||||
def _all_equal(lst):
|
||||
it = iter(lst)
|
||||
v0 = next(it)
|
||||
for v in it:
|
||||
if v0 != v:
|
||||
return False
|
||||
return True
|
||||
|
||||
def buildVarDevTable(store_builder, master_values):
|
||||
if _all_equal(master_values):
|
||||
return None
|
||||
varIdx = store_builder.storeMasters(master_values)
|
||||
return builder.buildVarDevTable(varIdx)
|
||||
|
||||
class VariationMerger(Merger):
|
||||
|
||||
def __init__(self, model, axisTags):
|
||||
self.model = model
|
||||
self.store_builder = builder.OnlineVarStoreBuilder(axisTags)
|
||||
self.store_builder.setModel(model)
|
||||
|
||||
@VariationMerger.merger(ot.Anchor)
|
||||
def merge(merger, self, lst):
|
||||
assert self.Format == 1
|
||||
XDeviceTable = buildVarDevTable(merger.store_builder, [a.XCoordinate for a in lst])
|
||||
YDeviceTable = buildVarDevTable(merger.store_builder, [a.YCoordinate for a in lst])
|
||||
if XDeviceTable or YDeviceTable:
|
||||
self.Format = 3
|
||||
self.XDeviceTable = XDeviceTable
|
||||
self.YDeviceTable = YDeviceTable
|
||||
|
||||
@VariationMerger.merger(ot.PairPos)
|
||||
def merge(merger, self, lst):
|
||||
# We need to interecept the merger processing of the object tree here so
|
||||
# that we can change the PairPos value formats after the ValueRecords are
|
||||
# changed.
|
||||
merger.mergeObjects(self, lst)
|
||||
# Now examine the list of value records, and update to the union of format values.
|
||||
vf1 = self.ValueFormat1
|
||||
vf2 = self.ValueFormat2
|
||||
if self.Format == 1:
|
||||
for pairSet in self.PairSet:
|
||||
for pairValueRecord in pairSet.PairValueRecord:
|
||||
pv1 = pairValueRecord.Value1
|
||||
if pv1 is not None:
|
||||
vf1 |= pv1.getFormat()
|
||||
pv2 = pairValueRecord.Value2
|
||||
if pv2 is not None:
|
||||
vf2 |= pv2.getFormat()
|
||||
elif self.Format == 2:
|
||||
for class1Record in self.Class1Record:
|
||||
for class2Record in class1Record.Class2Record:
|
||||
pv1 = class2Record.Value1
|
||||
if pv1 is not None:
|
||||
vf1 |= pv1.getFormat()
|
||||
pv2 = class2Record.Value2
|
||||
if pv2 is not None:
|
||||
vf2 |= pv2.getFormat()
|
||||
self.ValueFormat1 = vf1
|
||||
self.ValueFormat2 = vf2
|
||||
|
||||
@VariationMerger.merger(otBase.ValueRecord)
|
||||
def merge(merger, self, lst):
|
||||
for name, tableName in [('XAdvance','XAdvDevice'),
|
||||
('YAdvance','YAdvDevice'),
|
||||
('XPlacement','XPlaDevice'),
|
||||
('YPlacement','YPlaDevice')]:
|
||||
|
||||
if hasattr(self, name):
|
||||
deviceTable = buildVarDevTable(merger.store_builder,
|
||||
[getattr(a, name) for a in lst])
|
||||
if deviceTable:
|
||||
setattr(self, tableName, deviceTable)
|
||||
|
||||
|
||||
def _merge_OTL(font, model, master_fonts, axisTags, base_idx):
|
||||
|
||||
print("Merging OpenType Layout tables")
|
||||
merger = VariationMerger(model, axisTags)
|
||||
|
||||
merge_tables(font, merger, master_fonts, axisTags, base_idx, ['GPOS'])
|
||||
store = merger.store_builder.finish()
|
||||
try:
|
||||
GDEF = font['GDEF'].table
|
||||
assert GDEF.Version <= 0x00010002
|
||||
except KeyError:
|
||||
font['GDEF']= newTable('GDEF')
|
||||
GDEFTable = font["GDEF"] = newTable('GDEF')
|
||||
GDEF = GDEFTable.table = ot.GDEF()
|
||||
GDEF.Version = 0x00010003
|
||||
GDEF.VarStore = store
|
||||
|
||||
|
||||
def build(designspace_filename, master_finder=lambda s:s, axisMap=None):
|
||||
"""
|
||||
Build variation font from a designspace file.
|
||||
|
||||
If master_finder is set, it should be a callable that takes master
|
||||
filename as found in designspace file and map it to master font
|
||||
binary as to be opened (eg. .ttf or .otf).
|
||||
|
||||
If axisMap is set, it should be dictionary mapping axis-id to
|
||||
(axis-tag, axis-name).
|
||||
"""
|
||||
|
||||
masters, instances = designspace.load(designspace_filename)
|
||||
base_idx = None
|
||||
for i,m in enumerate(masters):
|
||||
if 'info' in m and m['info']['copy']:
|
||||
assert base_idx is None
|
||||
base_idx = i
|
||||
assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document."
|
||||
|
||||
from pprint import pprint
|
||||
print("Masters:")
|
||||
pprint(masters)
|
||||
print("Instances:")
|
||||
pprint(instances)
|
||||
print("Index of base master:", base_idx)
|
||||
|
||||
print("Building GX")
|
||||
print("Loading TTF masters")
|
||||
basedir = os.path.dirname(designspace_filename)
|
||||
master_ttfs = [finder(os.path.join(basedir, m[0])) for m in masters]
|
||||
master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters]
|
||||
master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]
|
||||
|
||||
standard_axis_map = {
|
||||
@ -503,56 +378,72 @@ def main(args=None):
|
||||
|
||||
# TODO: For weight & width, use OS/2 values and setup 'avar' mapping.
|
||||
|
||||
# Set up master locations
|
||||
master_locs = []
|
||||
instance_locs = []
|
||||
out = []
|
||||
for loc in [m[1] for m in masters+instances]:
|
||||
# Apply modifications for default axes; and apply tags
|
||||
l = {}
|
||||
for axis,value in loc.items():
|
||||
tag,name = axis_map[axis]
|
||||
l[tag] = value
|
||||
out.append(l)
|
||||
master_locs = out[:len(masters)]
|
||||
instance_locs = out[len(masters):]
|
||||
master_locs = [o['location'] for o in masters]
|
||||
|
||||
axis_tags = set(master_locs[0].keys())
|
||||
assert all(axis_tags == set(m.keys()) for m in master_locs)
|
||||
print("Axis tags:", axis_tags)
|
||||
print("Master positions:")
|
||||
pprint(master_locs)
|
||||
|
||||
# Set up axes
|
||||
axes = {}
|
||||
axis_names = {}
|
||||
for tag,name in axis_map.values():
|
||||
if tag not in axis_tags: continue
|
||||
axis_names[tag] = name
|
||||
for tag in axis_tags:
|
||||
default = master_locs[base_idx][tag]
|
||||
lower = min(m[tag] for m in master_locs)
|
||||
upper = max(m[tag] for m in master_locs)
|
||||
name = axis_names[tag]
|
||||
axes[tag] = (name, lower, default, upper)
|
||||
axes[tag] = (lower, default, upper)
|
||||
print("Axes:")
|
||||
pprint(axes)
|
||||
|
||||
# Set up named instances
|
||||
instance_list = []
|
||||
for loc,instance in zip(instance_locs,instances):
|
||||
style = instance[4]
|
||||
instance_list.append((style, loc))
|
||||
# TODO append masters as named-instances as well; needs .designspace change.
|
||||
print("Master locations:")
|
||||
pprint(master_locs)
|
||||
|
||||
# We can use the base font straight, but it's faster to load it again since
|
||||
# then we won't be recompiling the existing ('glyf', 'hmtx', ...) tables.
|
||||
#gx = master_fonts[base_idx]
|
||||
gx = TTFont(master_ttfs[base_idx])
|
||||
|
||||
_add_fvar(gx, axes, instance_list)
|
||||
# TODO append masters as named-instances as well; needs .designspace change.
|
||||
fvar = _add_fvar(gx, axes, instances, axis_map)
|
||||
|
||||
print("Setting up glyph variations")
|
||||
_add_gvar(gx, axes, master_fonts, master_locs, base_idx)
|
||||
|
||||
print("Saving GX font", outfile)
|
||||
# Normalize master locations
|
||||
master_locs = [models.normalizeLocation(m, axes) for m in master_locs]
|
||||
|
||||
print("Normalized master locations:")
|
||||
pprint(master_locs)
|
||||
|
||||
# TODO Clean this up.
|
||||
del instances
|
||||
del axes
|
||||
master_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in master_locs]
|
||||
#instance_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in instance_locs]
|
||||
axisTags = [axis.axisTag for axis in fvar.axes]
|
||||
|
||||
# Assume single-model for now.
|
||||
model = models.VariationModel(master_locs)
|
||||
assert 0 == model.mapping[base_idx]
|
||||
|
||||
print("Building variations tables")
|
||||
if 'glyf' in gx:
|
||||
_add_gvar(gx, model, master_fonts)
|
||||
_add_HVAR(gx, model, master_fonts, axisTags)
|
||||
_merge_OTL(gx, model, master_fonts, axisTags, base_idx)
|
||||
|
||||
return gx, model, master_ttfs
|
||||
|
||||
|
||||
def main(args=None):
|
||||
|
||||
if args is None:
|
||||
import sys
|
||||
args = sys.argv[1:]
|
||||
|
||||
(designspace_filename,) = args
|
||||
finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf')
|
||||
outfile = os.path.splitext(designspace_filename)[0] + '-GX.ttf'
|
||||
|
||||
gx, model, master_ttfs = build(designspace_filename, finder)
|
||||
|
||||
print("Saving variation font", outfile)
|
||||
gx.save(outfile)
|
||||
|
||||
|
||||
|
148
Lib/fontTools/varLib/builder.py
Normal file
148
Lib/fontTools/varLib/builder.py
Normal file
@ -0,0 +1,148 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools import ttLib
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
|
||||
# VariationStore
|
||||
|
||||
def buildVarRegionAxis(axisSupport):
|
||||
self = ot.VarRegionAxis()
|
||||
self.StartCoord, self.PeakCoord, self.EndCoord = axisSupport
|
||||
return self
|
||||
|
||||
def buildVarRegion(support, axisTags):
|
||||
assert all(tag in axisTags for tag in support.keys()), ("Unknown axis tag found.", support, axisTags)
|
||||
self = ot.VarRegion()
|
||||
self.VarRegionAxis = []
|
||||
for tag in axisTags:
|
||||
self.VarRegionAxis.append(buildVarRegionAxis(support.get(tag, (0,0,0))))
|
||||
self.VarRegionAxisCount = len(self.VarRegionAxis)
|
||||
return self
|
||||
|
||||
def buildVarRegionList(supports, axisTags):
|
||||
self = ot.VarRegionList()
|
||||
self.AxisCount = len(axisTags)
|
||||
self.Region = []
|
||||
for support in supports:
|
||||
self.Region.append(buildVarRegion(support, axisTags))
|
||||
self.RegionCount = len(self.Region)
|
||||
return self
|
||||
|
||||
|
||||
def _reorderItem(lst, narrows):
|
||||
out = []
|
||||
count = len(lst)
|
||||
for i in range(count):
|
||||
if i not in narrows:
|
||||
out.append(lst[i])
|
||||
for i in range(count):
|
||||
if i in narrows:
|
||||
out.append(lst[i])
|
||||
return out
|
||||
|
||||
def optimizeVarData(self):
|
||||
# Reorder columns such that all SHORT columns come before UINT8
|
||||
count = self.VarRegionCount
|
||||
items = self.Item
|
||||
narrows = set(range(count))
|
||||
for item in items:
|
||||
for i in narrows:
|
||||
if not (-128 <= item[i] <= 127):
|
||||
narrows.remove(i)
|
||||
break
|
||||
if not narrows:
|
||||
break
|
||||
|
||||
self.VarRegionIndex = _reorderItem(self.VarRegionIndex, narrows)
|
||||
for i in range(self.ItemCount):
|
||||
items[i] = _reorderItem(items[i], narrows)
|
||||
|
||||
return self
|
||||
|
||||
def buildVarData(varRegionIndices, items, optimize=True):
|
||||
self = ot.VarData()
|
||||
self.VarRegionIndex = list(varRegionIndices)
|
||||
regionCount = self.VarRegionCount = len(self.VarRegionIndex)
|
||||
records = self.Item = []
|
||||
if items:
|
||||
for item in items:
|
||||
assert len(item) == regionCount
|
||||
records.append(list(item))
|
||||
self.ItemCount = len(self.Item)
|
||||
if items and optimize:
|
||||
optimizeVarData(self)
|
||||
return self
|
||||
|
||||
|
||||
def buildVarStore(varRegionList, varDataList):
|
||||
self = ot.VarStore()
|
||||
self.Format = 1
|
||||
self.VarRegionList = varRegionList
|
||||
self.VarData = list(varDataList)
|
||||
self.VarDataCount = len(self.VarData)
|
||||
return self
|
||||
|
||||
|
||||
def _getLocationKey(loc):
|
||||
return tuple(sorted(loc.items(), key=lambda kv: kv[0]))
|
||||
|
||||
class OnlineVarStoreBuilder(object):
|
||||
|
||||
def __init__(self, axisTags):
|
||||
self._axisTags = axisTags
|
||||
self._regionMap = {}
|
||||
self._regionList = buildVarRegionList([], axisTags)
|
||||
self._store = buildVarStore(self._regionList, [])
|
||||
|
||||
def setModel(self, model):
|
||||
self._model = model
|
||||
|
||||
regionMap = self._regionMap
|
||||
regionList = self._regionList
|
||||
|
||||
regions = model.supports[1:]
|
||||
regionIndices = []
|
||||
for region in regions:
|
||||
key = _getLocationKey(region)
|
||||
idx = regionMap.get(key)
|
||||
if idx is None:
|
||||
varRegion = buildVarRegion(region, self._axisTags)
|
||||
idx = regionMap[key] = len(regionList.Region)
|
||||
regionList.Region.append(varRegion)
|
||||
regionIndices.append(idx)
|
||||
|
||||
data = self._data = buildVarData(regionIndices, [], optimize=False)
|
||||
self._outer = len(self._store.VarData)
|
||||
self._store.VarData.append(data)
|
||||
|
||||
def finish(self, optimize=True):
|
||||
self._regionList.RegionCount = len(self._regionList.Region)
|
||||
self._store.VarDataCount = len(self._store.VarData)
|
||||
for data in self._store.VarData:
|
||||
data.ItemCount = len(data.Item)
|
||||
if optimize:
|
||||
optimizeVarData(data)
|
||||
return self._store
|
||||
|
||||
def storeMasters(self, master_values):
|
||||
deltas = [int(round(d)) for d in self._model.getDeltas(master_values)[1:]]
|
||||
inner = len(self._data.Item)
|
||||
self._data.Item.append(deltas)
|
||||
# TODO Check for full data array?
|
||||
return (self._outer << 16) + inner
|
||||
|
||||
|
||||
|
||||
# Variation helpers
|
||||
|
||||
def buildVarIdxMap(varIdxes):
|
||||
# TODO Change VarIdxMap mapping to hold separate outer,inner indices
|
||||
self = ot.VarIdxMap()
|
||||
self.mapping = list(varIdxes)
|
||||
return self
|
||||
|
||||
def buildVarDevTable(varIdx):
|
||||
self = ot.Device()
|
||||
self.DeltaFormat = 0x8000
|
||||
self.StartSize = varIdx >> 16
|
||||
self.EndSize = varIdx & 0xFFFF
|
||||
return self
|
53
Lib/fontTools/varLib/designspace.py
Normal file
53
Lib/fontTools/varLib/designspace.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Rudimentary support for loading MutatorMath .designspace files."""
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
__all__ = ['load', 'loads']
|
||||
|
||||
def _xmlParseLocation(et):
|
||||
loc = {}
|
||||
for dim in et.find('location'):
|
||||
assert dim.tag == 'dimension'
|
||||
name = dim.attrib['name']
|
||||
value = float(dim.attrib['xvalue'])
|
||||
assert name not in loc
|
||||
loc[name] = value
|
||||
return loc
|
||||
|
||||
def _loadItem(et):
|
||||
item = dict(et.attrib)
|
||||
for elt in et:
|
||||
if elt.tag == 'location':
|
||||
value = _xmlParseLocation(et)
|
||||
else:
|
||||
value = {}
|
||||
if 'copy' in elt.attrib:
|
||||
value['copy'] = bool(int(elt.attrib['copy']))
|
||||
# TODO load more?!
|
||||
item[elt.tag] = value
|
||||
return item
|
||||
|
||||
def _load(et):
|
||||
masters = []
|
||||
ds = et.getroot()
|
||||
for et in ds.find('sources'):
|
||||
masters.append(_loadItem(et))
|
||||
|
||||
instances = []
|
||||
for et in ds.find('instances'):
|
||||
instances.append(_loadItem(et))
|
||||
|
||||
return masters, instances
|
||||
|
||||
def load(filename):
|
||||
"""Load designspace from a file name or object. Returns two items:
|
||||
list of masters (aka sources) and list of instances."""
|
||||
return _load(ET.parse(filename))
|
||||
|
||||
def loads(string):
|
||||
"""Load designspace from a string."""
|
||||
return _load(ET.fromstring(string))
|
@ -1,6 +1,6 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from fontTools.varLib import designspace_load
|
||||
from fontTools.varLib import designspace
|
||||
import os
|
||||
import unittest
|
||||
|
||||
@ -8,16 +8,22 @@ import unittest
|
||||
class DesignspaceTest(unittest.TestCase):
|
||||
def test_load(self):
|
||||
self.assertEqual(
|
||||
designspace_load(_getpath("VarLibTest.designspace")),
|
||||
(
|
||||
[
|
||||
('VarLibTest-Light.ufo', {'weight': 0.0}, 'master_1'),
|
||||
('VarLibTest-Bold.ufo', {'weight': 1.0}, 'master_2')
|
||||
],
|
||||
[('instance/VarLibTest-Medium.ufo', {'weight': 0.5},
|
||||
'master_2', 'VarLibTest', 'Medium')],
|
||||
0
|
||||
)
|
||||
designspace.load(_getpath("VarLibTest.designspace")),
|
||||
([{'filename': 'VarLibTest-Light.ufo',
|
||||
'groups': {'copy': True},
|
||||
'info': {'copy': True},
|
||||
'lib': {'copy': True},
|
||||
'location': {'weight': 0.0},
|
||||
'name': 'master_1'},
|
||||
{'filename': 'VarLibTest-Bold.ufo',
|
||||
'location': {'weight': 1.0},
|
||||
'name': 'master_2'}],
|
||||
[{'filename': 'instance/VarLibTest-Medium.ufo',
|
||||
'location': {'weight': 0.5},
|
||||
'familyname': 'VarLibTest',
|
||||
'stylename': 'Medium',
|
||||
'info': {},
|
||||
'kerning': {}}])
|
||||
)
|
||||
|
||||
|
||||
|
112
Lib/fontTools/varLib/interpolate-layout.py
Normal file
112
Lib/fontTools/varLib/interpolate-layout.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""
|
||||
Interpolate OpenType Layout tables (GDEF / GPOS / GSUB).
|
||||
"""
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
||||
from fontTools.varLib import designspace, models, builder
|
||||
from fontTools.varLib.merger import merge_tables, Merger
|
||||
import os.path
|
||||
|
||||
class InstancerMerger(Merger):
|
||||
|
||||
def __init__(self, model, location):
|
||||
self.model = model
|
||||
self.location = location
|
||||
|
||||
@InstancerMerger.merger(ot.Anchor)
|
||||
def merge(merger, self, lst):
|
||||
XCoords = [a.XCoordinate for a in lst]
|
||||
YCoords = [a.YCoordinate for a in lst]
|
||||
model = merger.model
|
||||
location = merger.location
|
||||
self.XCoordinate = round(model.interpolateFromMasters(location, XCoords))
|
||||
self.YCoordinate = round(model.interpolateFromMasters(location, YCoords))
|
||||
|
||||
|
||||
def main(args=None):
|
||||
|
||||
import sys
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
designspace_filename = args[0]
|
||||
locargs = args[1:]
|
||||
outfile = os.path.splitext(designspace_filename)[0] + '-instance.ttf'
|
||||
|
||||
finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf')
|
||||
|
||||
loc = {}
|
||||
for arg in locargs:
|
||||
tag,val = arg.split('=')
|
||||
loc[tag] = float(val)
|
||||
|
||||
masters, instances = designspace.load(designspace_filename)
|
||||
base_idx = None
|
||||
for i,m in enumerate(masters):
|
||||
if 'info' in m and m['info']['copy']:
|
||||
assert base_idx is None
|
||||
base_idx = i
|
||||
assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document."
|
||||
|
||||
from pprint import pprint
|
||||
print("Index of base master:", base_idx)
|
||||
|
||||
print("Building GX")
|
||||
print("Loading TTF masters")
|
||||
basedir = os.path.dirname(designspace_filename)
|
||||
master_ttfs = [finder(os.path.join(basedir, m['filename'])) for m in masters]
|
||||
master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]
|
||||
|
||||
#font = master_fonts[base_idx]
|
||||
font = TTFont(master_ttfs[base_idx])
|
||||
|
||||
master_locs = [o['location'] for o in masters]
|
||||
|
||||
axis_tags = set(master_locs[0].keys())
|
||||
assert all(axis_tags == set(m.keys()) for m in master_locs)
|
||||
|
||||
# Set up axes
|
||||
axes = {}
|
||||
for tag in axis_tags:
|
||||
default = master_locs[base_idx][tag]
|
||||
lower = min(m[tag] for m in master_locs)
|
||||
upper = max(m[tag] for m in master_locs)
|
||||
axes[tag] = (lower, default, upper)
|
||||
print("Axes:")
|
||||
pprint(axes)
|
||||
|
||||
print("Location:", loc)
|
||||
print("Master locations:")
|
||||
pprint(master_locs)
|
||||
|
||||
# Normalize locations
|
||||
loc = models.normalizeLocation(loc, axes)
|
||||
master_locs = [models.normalizeLocation(m, axes) for m in master_locs]
|
||||
|
||||
print("Normalized location:", loc)
|
||||
print("Normalized master locations:")
|
||||
pprint(master_locs)
|
||||
|
||||
# Assume single-model for now.
|
||||
model = models.VariationModel(master_locs)
|
||||
assert 0 == model.mapping[base_idx]
|
||||
|
||||
merger = InstancerMerger(model, loc)
|
||||
|
||||
print("Building variations tables")
|
||||
merge_tables(font, merger, master_fonts, axes, base_idx, ['GPOS'])
|
||||
|
||||
print("Saving font", outfile)
|
||||
font.save(outfile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
main()
|
||||
#sys.exit(0)
|
||||
import doctest, sys
|
||||
sys.exit(doctest.testmod().failed)
|
61
Lib/fontTools/varLib/merger.py
Normal file
61
Lib/fontTools/varLib/merger.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""
|
||||
Merge OpenType Layout tables (GDEF / GPOS / GSUB).
|
||||
"""
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
||||
|
||||
class Merger(object):
|
||||
|
||||
mergers = None
|
||||
|
||||
@classmethod
|
||||
def merger(celf, *clazzes):
|
||||
assert celf != Merger, 'Subclass Merger instead.'
|
||||
if celf.mergers is None:
|
||||
celf.mergers = {}
|
||||
def wrapper(method):
|
||||
assert method.__name__ == 'merge'
|
||||
done = []
|
||||
for clazz in clazzes:
|
||||
if clazz in done: continue # Support multiple names of a clazz
|
||||
done.append(clazz)
|
||||
assert clazz not in celf.mergers, \
|
||||
"Oops, class '%s' has merge function defined already." % (clazz.__name__)
|
||||
celf.mergers[clazz] = method
|
||||
return None
|
||||
return wrapper
|
||||
|
||||
def mergeObjects(self, out, lst):
|
||||
keys = vars(out).keys()
|
||||
assert all(vars(table).keys() == keys for table in lst)
|
||||
for key in keys:
|
||||
value = getattr(out, key)
|
||||
values = [getattr(table, key) for table in lst]
|
||||
self.mergeThings(value, values)
|
||||
|
||||
def mergeLists(self, out, lst):
|
||||
count = len(out)
|
||||
assert all(count == len(v) for v in lst), (count, [len(v) for v in lst])
|
||||
for value,values in zip(out, zip(*lst)):
|
||||
self.mergeThings(value, values)
|
||||
|
||||
def mergeThings(self, out, lst):
|
||||
clazz = type(out)
|
||||
assert all(type(item) == clazz for item in lst), lst
|
||||
mergerFunc = self.mergers.get(type(out), None)
|
||||
if mergerFunc is not None:
|
||||
mergerFunc(self, out, lst)
|
||||
elif hasattr(out, '__dict__'):
|
||||
self.mergeObjects(out, lst)
|
||||
elif isinstance(out, list):
|
||||
self.mergeLists(out, lst)
|
||||
else:
|
||||
assert all(out == v for v in lst), lst
|
||||
|
||||
def merge_tables(font, merger, master_ttfs, axes, base_idx, tables):
|
||||
|
||||
for tag in tables:
|
||||
print('Merging', tag)
|
||||
merger.mergeThings(font[tag], [m[tag] for m in master_ttfs])
|
234
Lib/fontTools/varLib/models.py
Normal file
234
Lib/fontTools/varLib/models.py
Normal file
@ -0,0 +1,234 @@
|
||||
"""Variation fonts interpolation models."""
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
|
||||
__all__ = ['normalizeLocation', 'supportScalar', 'VariationModel']
|
||||
|
||||
def normalizeLocation(location, axes):
|
||||
"""Normalizes location based on axis min/default/max values from axes."""
|
||||
out = {}
|
||||
for tag,(lower,default,upper) in axes.items():
|
||||
v = location.get(tag, default)
|
||||
if v == default:
|
||||
v = 0
|
||||
elif v < default:
|
||||
v = (max(v, lower) - default) / (default - lower)
|
||||
else:
|
||||
v = (min(v, upper) - default) / (upper - default)
|
||||
out[tag] = v
|
||||
return out
|
||||
|
||||
def supportScalar(location, support):
|
||||
"""Returns the scalar multiplier at location, for a master
|
||||
with support.
|
||||
>>> supportScalar({}, {})
|
||||
1.0
|
||||
>>> supportScalar({'wght':.2}, {})
|
||||
1.0
|
||||
>>> supportScalar({'wght':.2}, {'wght':(0,2,3)})
|
||||
0.1
|
||||
>>> supportScalar({'wght':2.5}, {'wght':(0,2,4)})
|
||||
0.75
|
||||
"""
|
||||
scalar = 1.
|
||||
for axis,(lower,peak,upper) in support.items():
|
||||
if axis not in location:
|
||||
scalar = 0.
|
||||
break
|
||||
v = location[axis]
|
||||
if v == peak:
|
||||
continue
|
||||
if v <= lower or upper <= v:
|
||||
scalar = 0.
|
||||
break;
|
||||
if v < peak:
|
||||
scalar *= (v - lower) / (peak - lower)
|
||||
else: # v > peak
|
||||
scalar *= (v - upper) / (peak - upper)
|
||||
return scalar
|
||||
|
||||
|
||||
class VariationModel(object):
|
||||
|
||||
"""
|
||||
Locations must be in normalized space. Ie. base master
|
||||
is at origin (0).
|
||||
>>> from pprint import pprint
|
||||
>>> locations = [ \
|
||||
{'wght':100}, \
|
||||
{'wght':-100}, \
|
||||
{'wght':-180}, \
|
||||
{'wdth':+.3}, \
|
||||
{'wght':+120,'wdth':.3}, \
|
||||
{'wght':+120,'wdth':.2}, \
|
||||
{}, \
|
||||
{'wght':+180,'wdth':.3}, \
|
||||
{'wght':+180}, \
|
||||
]
|
||||
>>> model = VariationModel(locations, axisOrder=['wght'])
|
||||
>>> pprint(model.locations)
|
||||
[{},
|
||||
{'wght': -100},
|
||||
{'wght': -180},
|
||||
{'wght': 100},
|
||||
{'wght': 180},
|
||||
{'wdth': 0.3},
|
||||
{'wdth': 0.3, 'wght': 180},
|
||||
{'wdth': 0.3, 'wght': 120},
|
||||
{'wdth': 0.2, 'wght': 120}]
|
||||
>>> pprint(model.deltaWeights)
|
||||
[{},
|
||||
{0: 1.0},
|
||||
{0: 1.0},
|
||||
{0: 1.0},
|
||||
{0: 1.0},
|
||||
{0: 1.0},
|
||||
{0: 1.0, 4: 1.0, 5: 1.0},
|
||||
{0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.25},
|
||||
{0: 1.0,
|
||||
3: 0.75,
|
||||
4: 0.25,
|
||||
5: 0.6666666666666667,
|
||||
6: 0.16666666666666669,
|
||||
7: 0.6666666666666667}]
|
||||
"""
|
||||
|
||||
def __init__(self, locations, axisOrder=[]):
|
||||
locations = [{k:v for k,v in loc.items() if v != 0.} for loc in locations]
|
||||
keyFunc = self.getMasterLocationsSortKeyFunc(locations, axisOrder=axisOrder)
|
||||
axisPoints = keyFunc.axisPoints
|
||||
self.locations = sorted(locations, key=keyFunc)
|
||||
# TODO Assert that locations are unique.
|
||||
self.mapping = [self.locations.index(l) for l in locations] # Mapping from user's master order to our master order
|
||||
self.reverseMapping = [locations.index(l) for l in self.locations] # Reverse of above
|
||||
|
||||
self._computeMasterSupports(axisPoints)
|
||||
|
||||
@staticmethod
|
||||
def getMasterLocationsSortKeyFunc(locations, axisOrder=[]):
|
||||
assert {} in locations, "Base master not found."
|
||||
axisPoints = {}
|
||||
for loc in locations:
|
||||
if len(loc) != 1:
|
||||
continue
|
||||
axis = next(iter(loc))
|
||||
value = loc[axis]
|
||||
if axis not in axisPoints:
|
||||
axisPoints[axis] = {0}
|
||||
assert value not in axisPoints[axis]
|
||||
axisPoints[axis].add(value)
|
||||
|
||||
def getKey(axisPoints, axisOrder):
|
||||
def sign(v):
|
||||
return -1 if v < 0 else +1 if v > 0 else 0
|
||||
def key(loc):
|
||||
rank = len(loc)
|
||||
onPointAxes = [axis for axis,value in loc.items() if value in axisPoints[axis]]
|
||||
orderedAxes = [axis for axis in axisOrder if axis in loc]
|
||||
orderedAxes.extend([axis for axis in sorted(loc.keys()) if axis not in axisOrder])
|
||||
return (
|
||||
rank, # First, order by increasing rank
|
||||
-len(onPointAxes), # Next, by decreasing number of onPoint axes
|
||||
tuple(axisOrder.index(axis) if axis in axisOrder else 0x10000 for axis in orderedAxes), # Next, by known axes
|
||||
tuple(orderedAxes), # Next, by all axes
|
||||
tuple(sign(loc[axis]) for axis in orderedAxes), # Next, by signs of axis values
|
||||
tuple(abs(loc[axis]) for axis in orderedAxes), # Next, by absolute value of axis values
|
||||
)
|
||||
return key
|
||||
|
||||
ret = getKey(axisPoints, axisOrder)
|
||||
ret.axisPoints = axisPoints
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def lowerBound(value, lst):
|
||||
if any(v < value for v in lst):
|
||||
return max(v for v in lst if v < value)
|
||||
else:
|
||||
return value
|
||||
@staticmethod
|
||||
def upperBound(value, lst):
|
||||
if any(v > value for v in lst):
|
||||
return min(v for v in lst if v > value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def _computeMasterSupports(self, axisPoints):
|
||||
supports = []
|
||||
deltaWeights = []
|
||||
locations = self.locations
|
||||
for i,loc in enumerate(locations):
|
||||
box = {}
|
||||
|
||||
# Account for axisPoints first
|
||||
for axis,values in axisPoints.items():
|
||||
if not axis in loc:
|
||||
continue
|
||||
locV = loc[axis]
|
||||
box[axis] = (self.lowerBound(locV, values), locV, self.upperBound(locV, values))
|
||||
|
||||
locAxes = set(loc.keys())
|
||||
# Walk over previous masters now
|
||||
for j,m in enumerate(locations[:i]):
|
||||
# Master with extra axes do not participte
|
||||
if not set(m.keys()).issubset(locAxes):
|
||||
continue
|
||||
# If it's NOT in the current box, it does not participate
|
||||
relevant = True
|
||||
for axis, (lower,_,upper) in box.items():
|
||||
if axis in m and not (lower < m[axis] < upper):
|
||||
relevant = False
|
||||
break
|
||||
if not relevant:
|
||||
continue
|
||||
# Split the box for new master
|
||||
for axis,val in m.items():
|
||||
assert axis in box
|
||||
lower,locV,upper = box[axis]
|
||||
if val < locV:
|
||||
lower = val
|
||||
elif locV < val:
|
||||
upper = val
|
||||
box[axis] = (lower,locV,upper)
|
||||
supports.append(box)
|
||||
|
||||
deltaWeight = {}
|
||||
# Walk over previous masters now, populate deltaWeight
|
||||
for j,m in enumerate(locations[:i]):
|
||||
scalar = supportScalar(loc, supports[j])
|
||||
if scalar:
|
||||
deltaWeight[j] = scalar
|
||||
deltaWeights.append(deltaWeight)
|
||||
|
||||
self.supports = supports
|
||||
self.deltaWeights = deltaWeights
|
||||
|
||||
def getDeltas(self, masterValues):
|
||||
count = len(self.locations)
|
||||
assert len(masterValues) == len(self.deltaWeights)
|
||||
mapping = self.reverseMapping
|
||||
out = []
|
||||
for i,weights in enumerate(self.deltaWeights):
|
||||
delta = masterValues[mapping[i]]
|
||||
for j,weight in weights.items():
|
||||
delta -= out[j] * weight
|
||||
out.append(delta)
|
||||
return out
|
||||
|
||||
def interpolateFromDeltas(self, loc, deltas):
|
||||
v = None
|
||||
supports = self.supports
|
||||
assert len(deltas) == len(supports)
|
||||
for i,(delta,support) in enumerate(zip(deltas, supports)):
|
||||
scalar = supportScalar(loc, support)
|
||||
if not scalar: continue
|
||||
contribution = delta * scalar
|
||||
if i == 0:
|
||||
v = contribution
|
||||
else:
|
||||
v += contribution
|
||||
return v
|
||||
|
||||
def interpolateFromMasters(self, loc, masterValues):
|
||||
deltas = self.getDeltas(masterValues)
|
||||
return self.interpolateFromDeltas(loc, deltas)
|
@ -7,13 +7,14 @@ from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
||||
from fontTools.varLib import VariationModel, supportScalar, _GetCoordinates, _SetCoordinates
|
||||
from fontTools.varLib import _GetCoordinates, _SetCoordinates
|
||||
from fontTools.varLib.models import VariationModel, supportScalar, normalizeLocation
|
||||
import os.path
|
||||
|
||||
def main(args=None):
|
||||
|
||||
import sys
|
||||
if args is None:
|
||||
import sys
|
||||
args = sys.argv[1:]
|
||||
|
||||
varfilename = args[0]
|
||||
@ -22,29 +23,18 @@ def main(args=None):
|
||||
|
||||
loc = {}
|
||||
for arg in locargs:
|
||||
tag,valstr = arg.split('=')
|
||||
while len(tag) < 4:
|
||||
tag += ' '
|
||||
tag,val = arg.split('=')
|
||||
assert len(tag) <= 4
|
||||
loc[tag] = float(valstr)
|
||||
loc[tag.ljust(4)] = float(val)
|
||||
print("Location:", loc)
|
||||
|
||||
print("Loading GX font")
|
||||
varfont = TTFont(varfilename)
|
||||
|
||||
fvar = varfont['fvar']
|
||||
for axis in fvar.axes:
|
||||
lower, default, upper = axis.minValue, axis.defaultValue, axis.maxValue
|
||||
v = loc.get(axis.axisTag, default)
|
||||
if v < lower: v = lower
|
||||
if v > upper: v = upper
|
||||
if v == default:
|
||||
v = 0
|
||||
elif v < default:
|
||||
v = (v - default) / (default - lower)
|
||||
else:
|
||||
v = (v - default) / (upper - default)
|
||||
loc[axis.axisTag] = v
|
||||
axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes}
|
||||
# TODO Round to F2Dot14?
|
||||
loc = normalizeLocation(loc, axes)
|
||||
# Location is normalized now
|
||||
print("Normalized location:", loc)
|
||||
|
||||
|
10
README.md
10
README.md
@ -76,11 +76,11 @@ These additional options include:
|
||||
The following tables are currently supported:
|
||||
<!-- begin table list -->
|
||||
BASE, CBDT, CBLC, CFF, COLR, CPAL, DSIG, EBDT, EBLC, FFTM, GDEF,
|
||||
GMAP, GPKG, GPOS, GSUB, JSTF, LTSH, MATH, META, OS/2, SING, SVG,
|
||||
TSI0, TSI1, TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV,
|
||||
VDMX, VORG, avar, cmap, cvt, feat, fpgm, fvar, gasp, glyf, gvar,
|
||||
hdmx, head, hhea, hmtx, kern, loca, ltag, maxp, meta, name, post,
|
||||
prep, sbix, trak, vhea and vmtx
|
||||
GMAP, GPKG, GPOS, GSUB, HVAR, JSTF, LTSH, MATH, META, OS/2, SING,
|
||||
SVG, TSI0, TSI1, TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS,
|
||||
TSIV, VDMX, VORG, VVAR, avar, cmap, cvt, feat, fpgm, fvar, gasp,
|
||||
glyf, gvar, hdmx, head, hhea, hmtx, kern, loca, ltag, maxp, meta,
|
||||
name, post, prep, sbix, trak, vhea and vmtx
|
||||
<!-- end table list -->
|
||||
Other tables are dumped as hexadecimal data.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user