aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/merge/tables.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/merge/tables.py')
-rw-r--r--Lib/fontTools/merge/tables.py558
1 files changed, 293 insertions, 265 deletions
diff --git a/Lib/fontTools/merge/tables.py b/Lib/fontTools/merge/tables.py
index ac6d59b5..57ed64d3 100644
--- a/Lib/fontTools/merge/tables.py
+++ b/Lib/fontTools/merge/tables.py
@@ -3,6 +3,7 @@
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
from fontTools import ttLib, cffLib
+from fontTools.misc.psCharStrings import T2WidthExtractor
from fontTools.ttLib.tables.DefaultTable import DefaultTable
from fontTools.merge.base import add_method, mergeObjects
from fontTools.merge.cmap import computeMegaCmap
@@ -13,299 +14,326 @@ import logging
log = logging.getLogger("fontTools.merge")
-ttLib.getTableClass('maxp').mergeMap = {
- '*': max,
- 'tableTag': equal,
- 'tableVersion': equal,
- 'numGlyphs': sum,
- 'maxStorage': first,
- 'maxFunctionDefs': first,
- 'maxInstructionDefs': first,
- # TODO When we correctly merge hinting data, update these values:
- # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
+ttLib.getTableClass("maxp").mergeMap = {
+ "*": max,
+ "tableTag": equal,
+ "tableVersion": equal,
+ "numGlyphs": sum,
+ "maxStorage": first,
+ "maxFunctionDefs": first,
+ "maxInstructionDefs": first,
+ # TODO When we correctly merge hinting data, update these values:
+ # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
}
headFlagsMergeBitMap = {
- 'size': 16,
- '*': bitwise_or,
- 1: bitwise_and, # Baseline at y = 0
- 2: bitwise_and, # lsb at x = 0
- 3: bitwise_and, # Force ppem to integer values. FIXME?
- 5: bitwise_and, # Font is vertical
- 6: lambda bit: 0, # Always set to zero
- 11: bitwise_and, # Font data is 'lossless'
- 13: bitwise_and, # Optimized for ClearType
- 14: bitwise_and, # Last resort font. FIXME? equal or first may be better
- 15: lambda bit: 0, # Always set to zero
+ "size": 16,
+ "*": bitwise_or,
+ 1: bitwise_and, # Baseline at y = 0
+ 2: bitwise_and, # lsb at x = 0
+ 3: bitwise_and, # Force ppem to integer values. FIXME?
+ 5: bitwise_and, # Font is vertical
+ 6: lambda bit: 0, # Always set to zero
+ 11: bitwise_and, # Font data is 'lossless'
+ 13: bitwise_and, # Optimized for ClearType
+ 14: bitwise_and, # Last resort font. FIXME? equal or first may be better
+ 15: lambda bit: 0, # Always set to zero
}
-ttLib.getTableClass('head').mergeMap = {
- 'tableTag': equal,
- 'tableVersion': max,
- 'fontRevision': max,
- 'checkSumAdjustment': lambda lst: 0, # We need *something* here
- 'magicNumber': equal,
- 'flags': mergeBits(headFlagsMergeBitMap),
- 'unitsPerEm': equal,
- 'created': current_time,
- 'modified': current_time,
- 'xMin': min,
- 'yMin': min,
- 'xMax': max,
- 'yMax': max,
- 'macStyle': first,
- 'lowestRecPPEM': max,
- 'fontDirectionHint': lambda lst: 2,
- 'indexToLocFormat': first,
- 'glyphDataFormat': equal,
+ttLib.getTableClass("head").mergeMap = {
+ "tableTag": equal,
+ "tableVersion": max,
+ "fontRevision": max,
+ "checkSumAdjustment": lambda lst: 0, # We need *something* here
+ "magicNumber": equal,
+ "flags": mergeBits(headFlagsMergeBitMap),
+ "unitsPerEm": equal,
+ "created": current_time,
+ "modified": current_time,
+ "xMin": min,
+ "yMin": min,
+ "xMax": max,
+ "yMax": max,
+ "macStyle": first,
+ "lowestRecPPEM": max,
+ "fontDirectionHint": lambda lst: 2,
+ "indexToLocFormat": first,
+ "glyphDataFormat": equal,
}
-ttLib.getTableClass('hhea').mergeMap = {
- '*': equal,
- 'tableTag': equal,
- 'tableVersion': max,
- 'ascent': max,
- 'descent': min,
- 'lineGap': max,
- 'advanceWidthMax': max,
- 'minLeftSideBearing': min,
- 'minRightSideBearing': min,
- 'xMaxExtent': max,
- 'caretSlopeRise': first,
- 'caretSlopeRun': first,
- 'caretOffset': first,
- 'numberOfHMetrics': recalculate,
+ttLib.getTableClass("hhea").mergeMap = {
+ "*": equal,
+ "tableTag": equal,
+ "tableVersion": max,
+ "ascent": max,
+ "descent": min,
+ "lineGap": max,
+ "advanceWidthMax": max,
+ "minLeftSideBearing": min,
+ "minRightSideBearing": min,
+ "xMaxExtent": max,
+ "caretSlopeRise": first,
+ "caretSlopeRun": first,
+ "caretOffset": first,
+ "numberOfHMetrics": recalculate,
}
-ttLib.getTableClass('vhea').mergeMap = {
- '*': equal,
- 'tableTag': equal,
- 'tableVersion': max,
- 'ascent': max,
- 'descent': min,
- 'lineGap': max,
- 'advanceHeightMax': max,
- 'minTopSideBearing': min,
- 'minBottomSideBearing': min,
- 'yMaxExtent': max,
- 'caretSlopeRise': first,
- 'caretSlopeRun': first,
- 'caretOffset': first,
- 'numberOfVMetrics': recalculate,
+ttLib.getTableClass("vhea").mergeMap = {
+ "*": equal,
+ "tableTag": equal,
+ "tableVersion": max,
+ "ascent": max,
+ "descent": min,
+ "lineGap": max,
+ "advanceHeightMax": max,
+ "minTopSideBearing": min,
+ "minBottomSideBearing": min,
+ "yMaxExtent": max,
+ "caretSlopeRise": first,
+ "caretSlopeRun": first,
+ "caretOffset": first,
+ "numberOfVMetrics": recalculate,
}
os2FsTypeMergeBitMap = {
- 'size': 16,
- '*': lambda bit: 0,
- 1: bitwise_or, # no embedding permitted
- 2: bitwise_and, # allow previewing and printing documents
- 3: bitwise_and, # allow editing documents
- 8: bitwise_or, # no subsetting permitted
- 9: bitwise_or, # no embedding of outlines permitted
+ "size": 16,
+ "*": lambda bit: 0,
+ 1: bitwise_or, # no embedding permitted
+ 2: bitwise_and, # allow previewing and printing documents
+ 3: bitwise_and, # allow editing documents
+ 8: bitwise_or, # no subsetting permitted
+ 9: bitwise_or, # no embedding of outlines permitted
}
+
def mergeOs2FsType(lst):
- lst = list(lst)
- if all(item == 0 for item in lst):
- return 0
-
- # Compute least restrictive logic for each fsType value
- for i in range(len(lst)):
- # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
- if lst[i] & 0x000C:
- lst[i] &= ~0x0002
- # set bit 2 (allow previewing) if bit 3 is set (allow editing)
- elif lst[i] & 0x0008:
- lst[i] |= 0x0004
- # set bits 2 and 3 if everything is allowed
- elif lst[i] == 0:
- lst[i] = 0x000C
-
- fsType = mergeBits(os2FsTypeMergeBitMap)(lst)
- # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
- if fsType & 0x0002:
- fsType &= ~0x000C
- return fsType
-
-
-ttLib.getTableClass('OS/2').mergeMap = {
- '*': first,
- 'tableTag': equal,
- 'version': max,
- 'xAvgCharWidth': first, # Will be recalculated at the end on the merged font
- 'fsType': mergeOs2FsType, # Will be overwritten
- 'panose': first, # FIXME: should really be the first Latin font
- 'ulUnicodeRange1': bitwise_or,
- 'ulUnicodeRange2': bitwise_or,
- 'ulUnicodeRange3': bitwise_or,
- 'ulUnicodeRange4': bitwise_or,
- 'fsFirstCharIndex': min,
- 'fsLastCharIndex': max,
- 'sTypoAscender': max,
- 'sTypoDescender': min,
- 'sTypoLineGap': max,
- 'usWinAscent': max,
- 'usWinDescent': max,
- # Version 1
- 'ulCodePageRange1': onlyExisting(bitwise_or),
- 'ulCodePageRange2': onlyExisting(bitwise_or),
- # Version 2, 3, 4
- 'sxHeight': onlyExisting(max),
- 'sCapHeight': onlyExisting(max),
- 'usDefaultChar': onlyExisting(first),
- 'usBreakChar': onlyExisting(first),
- 'usMaxContext': onlyExisting(max),
- # version 5
- 'usLowerOpticalPointSize': onlyExisting(min),
- 'usUpperOpticalPointSize': onlyExisting(max),
+ lst = list(lst)
+ if all(item == 0 for item in lst):
+ return 0
+
+ # Compute least restrictive logic for each fsType value
+ for i in range(len(lst)):
+ # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
+ if lst[i] & 0x000C:
+ lst[i] &= ~0x0002
+ # set bit 2 (allow previewing) if bit 3 is set (allow editing)
+ elif lst[i] & 0x0008:
+ lst[i] |= 0x0004
+ # set bits 2 and 3 if everything is allowed
+ elif lst[i] == 0:
+ lst[i] = 0x000C
+
+ fsType = mergeBits(os2FsTypeMergeBitMap)(lst)
+ # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
+ if fsType & 0x0002:
+ fsType &= ~0x000C
+ return fsType
+
+
+ttLib.getTableClass("OS/2").mergeMap = {
+ "*": first,
+ "tableTag": equal,
+ "version": max,
+ "xAvgCharWidth": first, # Will be recalculated at the end on the merged font
+ "fsType": mergeOs2FsType, # Will be overwritten
+ "panose": first, # FIXME: should really be the first Latin font
+ "ulUnicodeRange1": bitwise_or,
+ "ulUnicodeRange2": bitwise_or,
+ "ulUnicodeRange3": bitwise_or,
+ "ulUnicodeRange4": bitwise_or,
+ "fsFirstCharIndex": min,
+ "fsLastCharIndex": max,
+ "sTypoAscender": max,
+ "sTypoDescender": min,
+ "sTypoLineGap": max,
+ "usWinAscent": max,
+ "usWinDescent": max,
+ # Version 1
+ "ulCodePageRange1": onlyExisting(bitwise_or),
+ "ulCodePageRange2": onlyExisting(bitwise_or),
+ # Version 2, 3, 4
+ "sxHeight": onlyExisting(max),
+ "sCapHeight": onlyExisting(max),
+ "usDefaultChar": onlyExisting(first),
+ "usBreakChar": onlyExisting(first),
+ "usMaxContext": onlyExisting(max),
+ # version 5
+ "usLowerOpticalPointSize": onlyExisting(min),
+ "usUpperOpticalPointSize": onlyExisting(max),
}
-@add_method(ttLib.getTableClass('OS/2'))
+
+@add_method(ttLib.getTableClass("OS/2"))
def merge(self, m, tables):
- DefaultTable.merge(self, m, tables)
- if self.version < 2:
- # bits 8 and 9 are reserved and should be set to zero
- self.fsType &= ~0x0300
- if self.version >= 3:
- # Only one of bits 1, 2, and 3 may be set. We already take
- # care of bit 1 implications in mergeOs2FsType. So unset
- # bit 2 if bit 3 is already set.
- if self.fsType & 0x0008:
- self.fsType &= ~0x0004
- return self
-
-ttLib.getTableClass('post').mergeMap = {
- '*': first,
- 'tableTag': equal,
- 'formatType': max,
- 'isFixedPitch': min,
- 'minMemType42': max,
- 'maxMemType42': lambda lst: 0,
- 'minMemType1': max,
- 'maxMemType1': lambda lst: 0,
- 'mapping': onlyExisting(sumDicts),
- 'extraNames': lambda lst: [],
+ DefaultTable.merge(self, m, tables)
+ if self.version < 2:
+ # bits 8 and 9 are reserved and should be set to zero
+ self.fsType &= ~0x0300
+ if self.version >= 3:
+ # Only one of bits 1, 2, and 3 may be set. We already take
+ # care of bit 1 implications in mergeOs2FsType. So unset
+ # bit 2 if bit 3 is already set.
+ if self.fsType & 0x0008:
+ self.fsType &= ~0x0004
+ return self
+
+
+ttLib.getTableClass("post").mergeMap = {
+ "*": first,
+ "tableTag": equal,
+ "formatType": max,
+ "isFixedPitch": min,
+ "minMemType42": max,
+ "maxMemType42": lambda lst: 0,
+ "minMemType1": max,
+ "maxMemType1": lambda lst: 0,
+ "mapping": onlyExisting(sumDicts),
+ "extraNames": lambda lst: [],
}
-ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
- 'tableTag': equal,
- 'metrics': sumDicts,
+ttLib.getTableClass("vmtx").mergeMap = ttLib.getTableClass("hmtx").mergeMap = {
+ "tableTag": equal,
+ "metrics": sumDicts,
}
-ttLib.getTableClass('name').mergeMap = {
- 'tableTag': equal,
- 'names': first, # FIXME? Does mixing name records make sense?
+ttLib.getTableClass("name").mergeMap = {
+ "tableTag": equal,
+ "names": first, # FIXME? Does mixing name records make sense?
}
-ttLib.getTableClass('loca').mergeMap = {
- '*': recalculate,
- 'tableTag': equal,
+ttLib.getTableClass("loca").mergeMap = {
+ "*": recalculate,
+ "tableTag": equal,
}
-ttLib.getTableClass('glyf').mergeMap = {
- 'tableTag': equal,
- 'glyphs': sumDicts,
- 'glyphOrder': sumLists,
+ttLib.getTableClass("glyf").mergeMap = {
+ "tableTag": equal,
+ "glyphs": sumDicts,
+ "glyphOrder": sumLists,
+ "_reverseGlyphOrder": recalculate,
+ "axisTags": equal,
}
-@add_method(ttLib.getTableClass('glyf'))
+
+@add_method(ttLib.getTableClass("glyf"))
def merge(self, m, tables):
- for i,table in enumerate(tables):
- for g in table.glyphs.values():
- if i:
- # Drop hints for all but first font, since
- # we don't map functions / CVT values.
- g.removeHinting()
- # Expand composite glyphs to load their
- # composite glyph names.
- if g.isComposite():
- g.expand(table)
- return DefaultTable.merge(self, m, tables)
-
-ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst)
-ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst)
-ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst)
-ttLib.getTableClass('gasp').mergeMap = lambda self, lst: first(lst) # FIXME? Appears irreconcilable
-
-@add_method(ttLib.getTableClass('CFF '))
+ for i, table in enumerate(tables):
+ for g in table.glyphs.values():
+ if i:
+ # Drop hints for all but first font, since
+ # we don't map functions / CVT values.
+ g.removeHinting()
+ # Expand composite glyphs to load their
+ # composite glyph names.
+ if g.isComposite() or g.isVarComposite():
+ g.expand(table)
+ return DefaultTable.merge(self, m, tables)
+
+
+ttLib.getTableClass("prep").mergeMap = lambda self, lst: first(lst)
+ttLib.getTableClass("fpgm").mergeMap = lambda self, lst: first(lst)
+ttLib.getTableClass("cvt ").mergeMap = lambda self, lst: first(lst)
+ttLib.getTableClass("gasp").mergeMap = lambda self, lst: first(
+ lst
+) # FIXME? Appears irreconcilable
+
+
+@add_method(ttLib.getTableClass("CFF "))
def merge(self, m, tables):
-
- if any(hasattr(table, "FDSelect") for table in tables):
- raise NotImplementedError(
- "Merging CID-keyed CFF tables is not supported yet"
- )
-
- for table in tables:
- table.cff.desubroutinize()
-
- newcff = tables[0]
- newfont = newcff.cff[0]
- private = newfont.Private
- storedNamesStrings = []
- glyphOrderStrings = []
- glyphOrder = set(newfont.getGlyphOrder())
-
- for name in newfont.strings.strings:
- if name not in glyphOrder:
- storedNamesStrings.append(name)
- else:
- glyphOrderStrings.append(name)
-
- chrset = list(newfont.charset)
- newcs = newfont.CharStrings
- log.debug("FONT 0 CharStrings: %d.", len(newcs))
-
- for i, table in enumerate(tables[1:], start=1):
- font = table.cff[0]
- font.Private = private
- fontGlyphOrder = set(font.getGlyphOrder())
- for name in font.strings.strings:
- if name in fontGlyphOrder:
- glyphOrderStrings.append(name)
- cs = font.CharStrings
- gs = table.cff.GlobalSubrs
- log.debug("Font %d CharStrings: %d.", i, len(cs))
- chrset.extend(font.charset)
- if newcs.charStringsAreIndexed:
- for i, name in enumerate(cs.charStrings, start=len(newcs)):
- newcs.charStrings[name] = i
- newcs.charStringsIndex.items.append(None)
- for name in cs.charStrings:
- newcs[name] = cs[name]
-
- newfont.charset = chrset
- newfont.numGlyphs = len(chrset)
- newfont.strings.strings = glyphOrderStrings + storedNamesStrings
-
- return newcff
-
-@add_method(ttLib.getTableClass('cmap'))
+ if any(hasattr(table.cff[0], "FDSelect") for table in tables):
+ raise NotImplementedError("Merging CID-keyed CFF tables is not supported yet")
+
+ for table in tables:
+ table.cff.desubroutinize()
+
+ newcff = tables[0]
+ newfont = newcff.cff[0]
+ private = newfont.Private
+ newDefaultWidthX, newNominalWidthX = private.defaultWidthX, private.nominalWidthX
+ storedNamesStrings = []
+ glyphOrderStrings = []
+ glyphOrder = set(newfont.getGlyphOrder())
+
+ for name in newfont.strings.strings:
+ if name not in glyphOrder:
+ storedNamesStrings.append(name)
+ else:
+ glyphOrderStrings.append(name)
+
+ chrset = list(newfont.charset)
+ newcs = newfont.CharStrings
+ log.debug("FONT 0 CharStrings: %d.", len(newcs))
+
+ for i, table in enumerate(tables[1:], start=1):
+ font = table.cff[0]
+ defaultWidthX, nominalWidthX = (
+ font.Private.defaultWidthX,
+ font.Private.nominalWidthX,
+ )
+ widthsDiffer = (
+ defaultWidthX != newDefaultWidthX or nominalWidthX != newNominalWidthX
+ )
+ font.Private = private
+ fontGlyphOrder = set(font.getGlyphOrder())
+ for name in font.strings.strings:
+ if name in fontGlyphOrder:
+ glyphOrderStrings.append(name)
+ cs = font.CharStrings
+ gs = table.cff.GlobalSubrs
+ log.debug("Font %d CharStrings: %d.", i, len(cs))
+ chrset.extend(font.charset)
+ if newcs.charStringsAreIndexed:
+ for i, name in enumerate(cs.charStrings, start=len(newcs)):
+ newcs.charStrings[name] = i
+ newcs.charStringsIndex.items.append(None)
+ for name in cs.charStrings:
+ if widthsDiffer:
+ c = cs[name]
+ defaultWidthXToken = object()
+ extractor = T2WidthExtractor([], [], nominalWidthX, defaultWidthXToken)
+ extractor.execute(c)
+ width = extractor.width
+ if width is not defaultWidthXToken:
+ c.program.pop(0)
+ else:
+ width = defaultWidthX
+ if width != newDefaultWidthX:
+ c.program.insert(0, width - newNominalWidthX)
+ newcs[name] = cs[name]
+
+ newfont.charset = chrset
+ newfont.numGlyphs = len(chrset)
+ newfont.strings.strings = glyphOrderStrings + storedNamesStrings
+
+ return newcff
+
+
+@add_method(ttLib.getTableClass("cmap"))
def merge(self, m, tables):
-
- # TODO Handle format=14.
- if not hasattr(m, 'cmap'):
- computeMegaCmap(m, tables)
- cmap = m.cmap
-
- cmapBmpOnly = {uni: gid for uni,gid in cmap.items() if uni <= 0xFFFF}
- self.tables = []
- module = ttLib.getTableModule('cmap')
- if len(cmapBmpOnly) != len(cmap):
- # format-12 required.
- cmapTable = module.cmap_classes[12](12)
- cmapTable.platformID = 3
- cmapTable.platEncID = 10
- cmapTable.language = 0
- cmapTable.cmap = cmap
- self.tables.append(cmapTable)
- # always create format-4
- cmapTable = module.cmap_classes[4](4)
- cmapTable.platformID = 3
- cmapTable.platEncID = 1
- cmapTable.language = 0
- cmapTable.cmap = cmapBmpOnly
- # ordered by platform then encoding
- self.tables.insert(0, cmapTable)
- self.tableVersion = 0
- self.numSubTables = len(self.tables)
- return self
+ # TODO Handle format=14.
+ if not hasattr(m, "cmap"):
+ computeMegaCmap(m, tables)
+ cmap = m.cmap
+
+ cmapBmpOnly = {uni: gid for uni, gid in cmap.items() if uni <= 0xFFFF}
+ self.tables = []
+ module = ttLib.getTableModule("cmap")
+ if len(cmapBmpOnly) != len(cmap):
+ # format-12 required.
+ cmapTable = module.cmap_classes[12](12)
+ cmapTable.platformID = 3
+ cmapTable.platEncID = 10
+ cmapTable.language = 0
+ cmapTable.cmap = cmap
+ self.tables.append(cmapTable)
+ # always create format-4
+ cmapTable = module.cmap_classes[4](4)
+ cmapTable.platformID = 3
+ cmapTable.platEncID = 1
+ cmapTable.language = 0
+ cmapTable.cmap = cmapBmpOnly
+ # ordered by platform then encoding
+ self.tables.insert(0, cmapTable)
+ self.tableVersion = 0
+ self.numSubTables = len(self.tables)
+ return self