aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/ttLib/tables/otTables.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/ttLib/tables/otTables.py')
-rw-r--r--Lib/fontTools/ttLib/tables/otTables.py438
1 files changed, 147 insertions, 291 deletions
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index fbd9db7b..85befb3b 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -5,12 +5,12 @@ OpenType subtables.
Most are constructed upon import from data in otData.py, all are populated with
converter objects from otConverters.py.
"""
-import copy
from enum import IntEnum
import itertools
-from collections import defaultdict, namedtuple
+from collections import namedtuple
+from fontTools.misc.py23 import bytesjoin
from fontTools.misc.roundTools import otRound
-from fontTools.misc.textTools import bytesjoin, pad, safeEval
+from fontTools.misc.textTools import pad, safeEval
from .otBase import (
BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference,
getFormatSwitchingBaseTableClass,
@@ -425,7 +425,8 @@ class InsertionMorphAction(AATAction):
return []
reader = actionReader.getSubReader(
actionReader.pos + index * 2)
- return font.getGlyphNameMany(reader.readUShortArray(count))
+ return [font.getGlyphName(glyphID)
+ for glyphID in reader.readUShortArray(count)]
def toXML(self, xmlWriter, font, attrs, name):
xmlWriter.begintag(name, **attrs)
@@ -520,10 +521,12 @@ class Coverage(FormatSwitchingBaseTable):
def postRead(self, rawTable, font):
if self.Format == 1:
+ # TODO only allow glyphs that are valid?
self.glyphs = rawTable["GlyphArray"]
elif self.Format == 2:
glyphs = self.glyphs = []
ranges = rawTable["RangeRecord"]
+ glyphOrder = font.getGlyphOrder()
# Some SIL fonts have coverage entries that don't have sorted
# StartCoverageIndex. If it is so, fixup and warn. We undo
# this when writing font out.
@@ -533,11 +536,25 @@ class Coverage(FormatSwitchingBaseTable):
ranges = sorted_ranges
del sorted_ranges
for r in ranges:
+ assert r.StartCoverageIndex == len(glyphs), \
+ (r.StartCoverageIndex, len(glyphs))
start = r.Start
end = r.End
- startID = font.getGlyphID(start)
- endID = font.getGlyphID(end) + 1
- glyphs.extend(font.getGlyphNameMany(range(startID, endID)))
+ try:
+ startID = font.getGlyphID(start, requireReal=True)
+ except KeyError:
+ log.warning("Coverage table has start glyph ID out of range: %s.", start)
+ continue
+ try:
+ endID = font.getGlyphID(end, requireReal=True) + 1
+ except KeyError:
+ # Apparently some tools use 65535 to "match all" the range
+ if end != 'glyph65535':
+ log.warning("Coverage table has end glyph ID out of range: %s.", end)
+ # NOTE: We clobber out-of-range things here. There are legit uses for those,
+ # but none that we have seen in the wild.
+ endID = len(glyphOrder)
+ glyphs.extend(glyphOrder[glyphID] for glyphID in range(startID, endID))
else:
self.glyphs = []
log.warning("Unknown Coverage format: %s", self.Format)
@@ -549,9 +566,10 @@ class Coverage(FormatSwitchingBaseTable):
glyphs = self.glyphs = []
format = 1
rawTable = {"GlyphArray": glyphs}
+ getGlyphID = font.getGlyphID
if glyphs:
# find out whether Format 2 is more compact or not
- glyphIDs = font.getGlyphIDMany(glyphs)
+ glyphIDs = [getGlyphID(glyphName) for glyphName in glyphs ]
brokenOrder = sorted(glyphIDs) != glyphIDs
last = glyphIDs[0]
@@ -600,18 +618,32 @@ class Coverage(FormatSwitchingBaseTable):
glyphs.append(attrs["value"])
-class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
+class VarIdxMap(BaseTable):
def populateDefaults(self, propagator=None):
if not hasattr(self, 'mapping'):
- self.mapping = []
+ self.mapping = {}
def postRead(self, rawTable, font):
assert (rawTable['EntryFormat'] & 0xFFC0) == 0
- self.mapping = rawTable['mapping']
+ glyphOrder = font.getGlyphOrder()
+ mapList = rawTable['mapping']
+ mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList)))
+ self.mapping = dict(zip(glyphOrder, mapList))
+
+ def preWrite(self, font):
+ mapping = getattr(self, "mapping", None)
+ if mapping is None:
+ mapping = self.mapping = {}
+
+ glyphOrder = font.getGlyphOrder()
+ mapping = [mapping[g] for g in glyphOrder]
+ while len(mapping) > 1 and mapping[-2] == mapping[-1]:
+ del mapping[-1]
+
+ rawTable = { 'mapping': mapping }
+ rawTable['MappingCount'] = len(mapping)
- @staticmethod
- def getEntryFormat(mapping):
ored = 0
for idx in mapping:
ored |= idx
@@ -634,65 +666,9 @@ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
else:
entrySize = 4
- return ((entrySize - 1) << 4) | (innerBits - 1)
-
- def preWrite(self, font):
- mapping = getattr(self, "mapping", None)
- if mapping is None:
- mapping = self.mapping = []
- self.Format = 1 if len(mapping) > 0xFFFF else 0
- rawTable = self.__dict__.copy()
- rawTable['MappingCount'] = len(mapping)
- rawTable['EntryFormat'] = self.getEntryFormat(mapping)
- 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:
- self.mapping = mapping = []
- index = safeEval(attrs['index'])
- outer = safeEval(attrs['outer'])
- inner = safeEval(attrs['inner'])
- assert inner <= 0xFFFF
- mapping.insert(index, (outer << 16) | inner)
-
-
-class VarIdxMap(BaseTable):
-
- def populateDefaults(self, propagator=None):
- if not hasattr(self, 'mapping'):
- self.mapping = {}
-
- def postRead(self, rawTable, font):
- assert (rawTable['EntryFormat'] & 0xFFC0) == 0
- glyphOrder = font.getGlyphOrder()
- mapList = rawTable['mapping']
- mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList)))
- self.mapping = dict(zip(glyphOrder, mapList))
-
- def preWrite(self, font):
- mapping = getattr(self, "mapping", None)
- if mapping is None:
- mapping = self.mapping = {}
-
- glyphOrder = font.getGlyphOrder()
- mapping = [mapping[g] for g in glyphOrder]
- while len(mapping) > 1 and mapping[-2] == mapping[-1]:
- del mapping[-1]
+ entryFormat = ((entrySize - 1) << 4) | (innerBits - 1)
- rawTable = {'mapping': mapping}
- rawTable['MappingCount'] = len(mapping)
- rawTable['EntryFormat'] = DeltaSetIndexMap.getEntryFormat(mapping)
+ rawTable['EntryFormat'] = entryFormat
return rawTable
def toXML2(self, xmlWriter, font):
@@ -750,9 +726,9 @@ class SingleSubst(FormatSwitchingBaseTable):
input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
if self.Format == 1:
delta = rawTable["DeltaGlyphID"]
- inputGIDS = font.getGlyphIDMany(input)
+ inputGIDS = [ font.getGlyphID(name) for name in input ]
outGIDS = [ (glyphID + delta) % 65536 for glyphID in inputGIDS ]
- outNames = font.getGlyphNameMany(outGIDS)
+ outNames = [ font.getGlyphName(glyphID) for glyphID in outGIDS ]
for inp, out in zip(input, outNames):
mapping[inp] = out
elif self.Format == 2:
@@ -906,30 +882,51 @@ class ClassDef(FormatSwitchingBaseTable):
def postRead(self, rawTable, font):
classDefs = {}
+ glyphOrder = font.getGlyphOrder()
if self.Format == 1:
start = rawTable["StartGlyph"]
classList = rawTable["ClassValueArray"]
- startID = font.getGlyphID(start)
+ try:
+ startID = font.getGlyphID(start, requireReal=True)
+ except KeyError:
+ log.warning("ClassDef table has start glyph ID out of range: %s.", start)
+ startID = len(glyphOrder)
endID = startID + len(classList)
- glyphNames = font.getGlyphNameMany(range(startID, endID))
- for glyphName, cls in zip(glyphNames, classList):
+ if endID > len(glyphOrder):
+ log.warning("ClassDef table has entries for out of range glyph IDs: %s,%s.",
+ start, len(classList))
+ # NOTE: We clobber out-of-range things here. There are legit uses for those,
+ # but none that we have seen in the wild.
+ endID = len(glyphOrder)
+
+ for glyphID, cls in zip(range(startID, endID), classList):
if cls:
- classDefs[glyphName] = cls
+ classDefs[glyphOrder[glyphID]] = cls
elif self.Format == 2:
records = rawTable["ClassRangeRecord"]
for rec in records:
- cls = rec.Class
- if not cls:
- continue
start = rec.Start
end = rec.End
- startID = font.getGlyphID(start)
- endID = font.getGlyphID(end) + 1
- glyphNames = font.getGlyphNameMany(range(startID, endID))
- for glyphName in glyphNames:
- classDefs[glyphName] = cls
+ cls = rec.Class
+ try:
+ startID = font.getGlyphID(start, requireReal=True)
+ except KeyError:
+ log.warning("ClassDef table has start glyph ID out of range: %s.", start)
+ continue
+ try:
+ endID = font.getGlyphID(end, requireReal=True) + 1
+ except KeyError:
+ # Apparently some tools use 65535 to "match all" the range
+ if end != 'glyph65535':
+ log.warning("ClassDef table has end glyph ID out of range: %s.", end)
+ # NOTE: We clobber out-of-range things here. There are legit uses for those,
+ # but none that we have seen in the wild.
+ endID = len(glyphOrder)
+ for glyphID in range(startID, endID):
+ if cls:
+ classDefs[glyphOrder[glyphID]] = cls
else:
log.warning("Unknown ClassDef format: %s", self.Format)
self.classDefs = classDefs
@@ -1182,6 +1179,7 @@ class COLR(BaseTable):
if conv.name != "LayerRecordCount":
subReader.advance(conv.staticSize)
continue
+ conv = self.getConverterByName("LayerRecordCount")
reader[conv.name] = conv.read(subReader, font, tableDict={})
break
else:
@@ -1247,176 +1245,51 @@ class BaseGlyphRecordArray(BaseTable):
return self.__dict__.copy()
-class BaseGlyphList(BaseTable):
+class BaseGlyphV1List(BaseTable):
def preWrite(self, font):
- self.BaseGlyphPaintRecord = sorted(
- self.BaseGlyphPaintRecord,
+ self.BaseGlyphV1Record = sorted(
+ self.BaseGlyphV1Record,
key=lambda rec: font.getGlyphID(rec.BaseGlyph)
)
return self.__dict__.copy()
-class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
- def as_tuple(self):
- return tuple(getattr(self, conv.name) for conv in self.getConverters())
+class VariableValue(namedtuple("VariableValue", ["value", "varIdx"])):
+ __slots__ = ()
- def __repr__(self):
- return f"{self.__class__.__name__}{self.as_tuple()}"
+ _value_mapper = None
+ def __new__(cls, value, varIdx=0):
+ return super().__new__(
+ cls,
+ cls._value_mapper(value) if cls._value_mapper else value,
+ varIdx
+ )
-class ClipList(getFormatSwitchingBaseTableClass("uint8")):
+ @classmethod
+ def _make(cls, iterable):
+ if cls._value_mapper:
+ it = iter(iterable)
+ try:
+ value = next(it)
+ except StopIteration:
+ pass
+ else:
+ value = cls._value_mapper(value)
+ iterable = itertools.chain((value,), it)
+ return super()._make(iterable)
- def populateDefaults(self, propagator=None):
- if not hasattr(self, "clips"):
- self.clips = {}
- def postRead(self, rawTable, font):
- clips = {}
- glyphOrder = font.getGlyphOrder()
- for i, rec in enumerate(rawTable["ClipRecord"]):
- if rec.StartGlyphID > rec.EndGlyphID:
- log.warning(
- "invalid ClipRecord[%i].StartGlyphID (%i) > "
- "EndGlyphID (%i); skipped",
- i,
- rec.StartGlyphID,
- rec.EndGlyphID,
- )
- continue
- redefinedGlyphs = []
- missingGlyphs = []
- for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1):
- try:
- glyph = glyphOrder[glyphID]
- except IndexError:
- missingGlyphs.append(glyphID)
- continue
- if glyph not in clips:
- clips[glyph] = copy.copy(rec.ClipBox)
- else:
- redefinedGlyphs.append(glyphID)
- if redefinedGlyphs:
- log.warning(
- "ClipRecord[%i] overlaps previous records; "
- "ignoring redefined clip boxes for the "
- "following glyph ID range: [%i-%i]",
- i,
- min(redefinedGlyphs),
- max(redefinedGlyphs),
- )
- if missingGlyphs:
- log.warning(
- "ClipRecord[%i] range references missing "
- "glyph IDs: [%i-%i]",
- i,
- min(missingGlyphs),
- max(missingGlyphs),
- )
- self.clips = clips
-
- def groups(self):
- glyphsByClip = defaultdict(list)
- uniqueClips = {}
- for glyphName, clipBox in self.clips.items():
- key = clipBox.as_tuple()
- glyphsByClip[key].append(glyphName)
- if key not in uniqueClips:
- uniqueClips[key] = clipBox
- return {
- frozenset(glyphs): uniqueClips[key]
- for key, glyphs in glyphsByClip.items()
- }
-
- def preWrite(self, font):
- if not hasattr(self, "clips"):
- self.clips = {}
- clipBoxRanges = {}
- glyphMap = font.getReverseGlyphMap()
- for glyphs, clipBox in self.groups().items():
- glyphIDs = sorted(
- glyphMap[glyphName] for glyphName in glyphs
- if glyphName in glyphMap
- )
- if not glyphIDs:
- continue
- last = glyphIDs[0]
- ranges = [[last]]
- for glyphID in glyphIDs[1:]:
- if glyphID != last + 1:
- ranges[-1].append(last)
- ranges.append([glyphID])
- last = glyphID
- ranges[-1].append(last)
- for start, end in ranges:
- assert (start, end) not in clipBoxRanges
- clipBoxRanges[(start, end)] = clipBox
-
- clipRecords = []
- for (start, end), clipBox in sorted(clipBoxRanges.items()):
- record = ClipRecord()
- record.StartGlyphID = start
- record.EndGlyphID = end
- record.ClipBox = clipBox
- clipRecords.append(record)
- rawTable = {
- "ClipCount": len(clipRecords),
- "ClipRecord": clipRecords,
- }
- return rawTable
+class VariableFloat(VariableValue):
+ __slots__ = ()
+ _value_mapper = float
- def toXML(self, xmlWriter, font, attrs=None, name=None):
- tableName = name if name else self.__class__.__name__
- if attrs is None:
- attrs = []
- if hasattr(self, "Format"):
- attrs.append(("Format", self.Format))
- xmlWriter.begintag(tableName, attrs)
- xmlWriter.newline()
- # sort clips alphabetically to ensure deterministic XML dump
- for glyphs, clipBox in sorted(
- self.groups().items(), key=lambda item: min(item[0])
- ):
- xmlWriter.begintag("Clip")
- xmlWriter.newline()
- for glyphName in sorted(glyphs):
- xmlWriter.simpletag("Glyph", value=glyphName)
- xmlWriter.newline()
- xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)])
- xmlWriter.newline()
- clipBox.toXML2(xmlWriter, font)
- xmlWriter.endtag("ClipBox")
- xmlWriter.newline()
- xmlWriter.endtag("Clip")
- xmlWriter.newline()
- xmlWriter.endtag(tableName)
- xmlWriter.newline()
- def fromXML(self, name, attrs, content, font):
- clips = getattr(self, "clips", None)
- if clips is None:
- self.clips = clips = {}
- assert name == "Clip"
- glyphs = []
- clipBox = None
- for elem in content:
- if not isinstance(elem, tuple):
- continue
- name, attrs, content = elem
- if name == "Glyph":
- glyphs.append(attrs["value"])
- elif name == "ClipBox":
- clipBox = ClipBox()
- clipBox.Format = safeEval(attrs["Format"])
- for elem in content:
- if not isinstance(elem, tuple):
- continue
- name, attrs, content = elem
- clipBox.fromXML(name, attrs, content, font)
- if clipBox:
- for glyphName in glyphs:
- clips[glyphName] = clipBox
+class VariableInt(VariableValue):
+ __slots__ = ()
+ _value_mapper = otRound
class ExtendMode(IntEnum):
@@ -1440,22 +1313,21 @@ class CompositeMode(IntEnum):
SRC_ATOP = 9
DEST_ATOP = 10
XOR = 11
- PLUS = 12
- SCREEN = 13
- OVERLAY = 14
- DARKEN = 15
- LIGHTEN = 16
- COLOR_DODGE = 17
- COLOR_BURN = 18
- HARD_LIGHT = 19
- SOFT_LIGHT = 20
- DIFFERENCE = 21
- EXCLUSION = 22
- MULTIPLY = 23
- HSL_HUE = 24
- HSL_SATURATION = 25
- HSL_COLOR = 26
- HSL_LUMINOSITY = 27
+ SCREEN = 12
+ OVERLAY = 13
+ DARKEN = 14
+ LIGHTEN = 15
+ COLOR_DODGE = 16
+ COLOR_BURN = 17
+ HARD_LIGHT = 18
+ SOFT_LIGHT = 19
+ DIFFERENCE = 20
+ EXCLUSION = 21
+ MULTIPLY = 22
+ HSL_HUE = 23
+ HSL_SATURATION = 24
+ HSL_COLOR = 25
+ HSL_LUMINOSITY = 26
class PaintFormat(IntEnum):
@@ -1474,23 +1346,11 @@ class PaintFormat(IntEnum):
PaintVarTransform = 13
PaintTranslate = 14
PaintVarTranslate = 15
- PaintScale = 16
- PaintVarScale = 17
- PaintScaleAroundCenter = 18
- PaintVarScaleAroundCenter = 19
- PaintScaleUniform = 20
- PaintVarScaleUniform = 21
- PaintScaleUniformAroundCenter = 22
- PaintVarScaleUniformAroundCenter = 23
- PaintRotate = 24
- PaintVarRotate = 25
- PaintRotateAroundCenter = 26
- PaintVarRotateAroundCenter = 27
- PaintSkew = 28
- PaintVarSkew = 29
- PaintSkewAroundCenter = 30
- PaintVarSkewAroundCenter = 31
- PaintComposite = 32
+ PaintRotate = 16
+ PaintVarRotate = 17
+ PaintSkew = 18
+ PaintVarSkew = 19
+ PaintComposite = 20
class Paint(getFormatSwitchingBaseTableClass("uint8")):
@@ -1515,20 +1375,16 @@ class Paint(getFormatSwitchingBaseTableClass("uint8")):
def getChildren(self, colr):
if self.Format == PaintFormat.PaintColrLayers:
- # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists
- layers = []
- if colr.LayerList is not None:
- layers = colr.LayerList.Paint
- return layers[
+ return colr.LayerV1List.Paint[
self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers
]
if self.Format == PaintFormat.PaintColrGlyph:
- for record in colr.BaseGlyphList.BaseGlyphPaintRecord:
+ for record in colr.BaseGlyphV1List.BaseGlyphV1Record:
if record.BaseGlyph == self.Glyph:
return [record.Paint]
else:
- raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList")
+ raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphV1List")
children = []
for conv in self.getConverters():
@@ -1634,22 +1490,20 @@ def fixLookupOverFlows(ttf, overflowRecord):
return ok
lookup = lookups[lookupIndex]
- for lookupIndex in range(lookupIndex, len(lookups)):
- lookup = lookups[lookupIndex]
- if lookup.LookupType != extType:
- lookup.LookupType = extType
- for si in range(len(lookup.SubTable)):
- subTable = lookup.SubTable[si]
- extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
- extSubTable = extSubTableClass()
- extSubTable.Format = 1
- extSubTable.ExtSubTable = subTable
- lookup.SubTable[si] = extSubTable
+ lookup.LookupType = extType
+ for si in range(len(lookup.SubTable)):
+ subTable = lookup.SubTable[si]
+ extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
+ extSubTable = extSubTableClass()
+ extSubTable.Format = 1
+ extSubTable.ExtSubTable = subTable
+ lookup.SubTable[si] = extSubTable
ok = 1
return ok
def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
ok = 1
+ newSubTable.Format = oldSubTable.Format
oldMapping = sorted(oldSubTable.mapping.items())
oldLen = len(oldMapping)
@@ -1675,6 +1529,7 @@ def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
ok = 1
+ newSubTable.Format = oldSubTable.Format
if hasattr(oldSubTable, 'sortCoverageLast'):
newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast
@@ -1704,6 +1559,7 @@ def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord):
ok = 1
+ newSubTable.Format = oldSubTable.Format
oldLigs = sorted(oldSubTable.ligatures.items())
oldLen = len(oldLigs)