[merge] Remove most 'from .. import *'
This commit is contained in:
parent
deaf30d17c
commit
7c542684be
@ -3,11 +3,11 @@
|
|||||||
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
|
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
|
||||||
|
|
||||||
from fontTools import ttLib
|
from fontTools import ttLib
|
||||||
from fontTools.merge.base import *
|
import fontTools.merge.base
|
||||||
from fontTools.merge.cmap import *
|
from fontTools.merge.cmap import computeMegaGlyphOrder, computeMegaCmap, renameCFFCharStrings
|
||||||
from fontTools.merge.layout import *
|
from fontTools.merge.layout import layoutPreMerge, layoutPostMerge
|
||||||
from fontTools.merge.options import *
|
from fontTools.merge.options import Options
|
||||||
from fontTools.merge.util import *
|
import fontTools.merge.tables
|
||||||
from fontTools.misc.loggingTools import Timer
|
from fontTools.misc.loggingTools import Timer
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
import sys
|
import sys
|
||||||
|
@ -2,305 +2,75 @@
|
|||||||
#
|
#
|
||||||
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
|
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
|
||||||
|
|
||||||
from fontTools import ttLib, cffLib
|
|
||||||
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
||||||
from fontTools.merge.cmap import *
|
import logging
|
||||||
from fontTools.merge.util import *
|
|
||||||
|
|
||||||
|
|
||||||
ttLib.getTableClass('maxp').mergeMap = {
|
log = logging.getLogger("fontTools.merge")
|
||||||
'*': 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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('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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = {
|
def add_method(*clazzes, **kwargs):
|
||||||
'*': first,
|
"""Returns a decorator function that adds a new method to one or
|
||||||
'tableTag': equal,
|
more classes."""
|
||||||
'version': max,
|
allowDefault = kwargs.get('allowDefaultTable', False)
|
||||||
'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
|
def wrapper(method):
|
||||||
'fsType': mergeOs2FsType, # Will be overwritten
|
done = []
|
||||||
'panose': first, # FIXME: should really be the first Latin font
|
for clazz in clazzes:
|
||||||
'ulUnicodeRange1': bitwise_or,
|
if clazz in done: continue # Support multiple names of a clazz
|
||||||
'ulUnicodeRange2': bitwise_or,
|
done.append(clazz)
|
||||||
'ulUnicodeRange3': bitwise_or,
|
assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.'
|
||||||
'ulUnicodeRange4': bitwise_or,
|
assert method.__name__ not in clazz.__dict__, \
|
||||||
'fsFirstCharIndex': min,
|
"Oops, class '%s' has method '%s'." % (clazz.__name__, method.__name__)
|
||||||
'fsLastCharIndex': max,
|
setattr(clazz, method.__name__, method)
|
||||||
'sTypoAscender': max,
|
return None
|
||||||
'sTypoDescender': min,
|
return wrapper
|
||||||
'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'))
|
def mergeObjects(lst):
|
||||||
|
lst = [item for item in lst if item is not NotImplemented]
|
||||||
|
if not lst:
|
||||||
|
return NotImplemented
|
||||||
|
lst = [item for item in lst if item is not None]
|
||||||
|
if not lst:
|
||||||
|
return None
|
||||||
|
|
||||||
|
clazz = lst[0].__class__
|
||||||
|
assert all(type(item) == clazz for item in lst), lst
|
||||||
|
|
||||||
|
logic = clazz.mergeMap
|
||||||
|
returnTable = clazz()
|
||||||
|
returnDict = {}
|
||||||
|
|
||||||
|
allKeys = set.union(set(), *(vars(table).keys() for table in lst))
|
||||||
|
for key in allKeys:
|
||||||
|
try:
|
||||||
|
mergeLogic = logic[key]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
mergeLogic = logic['*']
|
||||||
|
except KeyError:
|
||||||
|
raise Exception("Don't know how to merge key %s of class %s" %
|
||||||
|
(key, clazz.__name__))
|
||||||
|
if mergeLogic is NotImplemented:
|
||||||
|
continue
|
||||||
|
value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
|
||||||
|
if value is not NotImplemented:
|
||||||
|
returnDict[key] = value
|
||||||
|
|
||||||
|
returnTable.__dict__ = returnDict
|
||||||
|
|
||||||
|
return returnTable
|
||||||
|
|
||||||
|
@add_method(DefaultTable, allowDefaultTable=True)
|
||||||
def merge(self, m, tables):
|
def merge(self, m, tables):
|
||||||
DefaultTable.merge(self, m, tables)
|
if not hasattr(self, 'mergeMap'):
|
||||||
if self.version < 2:
|
log.info("Don't know how to merge '%s'.", self.tableTag)
|
||||||
# bits 8 and 9 are reserved and should be set to zero
|
return NotImplemented
|
||||||
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 = {
|
logic = self.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 = {
|
if isinstance(logic, dict):
|
||||||
'tableTag': equal,
|
return m.mergeObjects(self, self.mergeMap, tables)
|
||||||
'metrics': sumDicts,
|
|
||||||
}
|
|
||||||
|
|
||||||
ttLib.getTableClass('name').mergeMap = {
|
|
||||||
'tableTag': equal,
|
|
||||||
'names': first, # FIXME? Does mixing name records make sense?
|
|
||||||
}
|
|
||||||
|
|
||||||
ttLib.getTableClass('loca').mergeMap = {
|
|
||||||
'*': recalculate,
|
|
||||||
'tableTag': equal,
|
|
||||||
}
|
|
||||||
|
|
||||||
ttLib.getTableClass('glyf').mergeMap = {
|
|
||||||
'tableTag': equal,
|
|
||||||
'glyphs': sumDicts,
|
|
||||||
'glyphOrder': sumLists,
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 '))
|
|
||||||
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:
|
else:
|
||||||
glyphOrderStrings.append(name)
|
return logic(tables)
|
||||||
|
|
||||||
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'))
|
|
||||||
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
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
|
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
|
||||||
|
|
||||||
from fontTools.merge.unicode import *
|
from fontTools.merge.unicode import is_Default_Ignorable
|
||||||
from fontTools.pens.recordingPen import DecomposingRecordingPen
|
from fontTools.pens.recordingPen import DecomposingRecordingPen
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ def _glyphsAreSame(glyphSet1, glyphSet2, glyph1, glyph2,
|
|||||||
# Unicode BMP-only and Unicode Full Repertoire semantics.
|
# Unicode BMP-only and Unicode Full Repertoire semantics.
|
||||||
# Cf. OpenType spec for "Platform specific encodings":
|
# Cf. OpenType spec for "Platform specific encodings":
|
||||||
# https://docs.microsoft.com/en-us/typography/opentype/spec/name
|
# https://docs.microsoft.com/en-us/typography/opentype/spec/name
|
||||||
class CmapUnicodePlatEncodings:
|
class _CmapUnicodePlatEncodings:
|
||||||
BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)}
|
BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)}
|
||||||
FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)}
|
FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)}
|
||||||
|
|
||||||
@ -68,9 +68,9 @@ def computeMegaCmap(merger, cmapTables):
|
|||||||
format12 = None
|
format12 = None
|
||||||
for subtable in table.tables:
|
for subtable in table.tables:
|
||||||
properties = (subtable.format, subtable.platformID, subtable.platEncID)
|
properties = (subtable.format, subtable.platformID, subtable.platEncID)
|
||||||
if properties in CmapUnicodePlatEncodings.BMP:
|
if properties in _CmapUnicodePlatEncodings.BMP:
|
||||||
format4 = subtable
|
format4 = subtable
|
||||||
elif properties in CmapUnicodePlatEncodings.FullRepertoire:
|
elif properties in _CmapUnicodePlatEncodings.FullRepertoire:
|
||||||
format12 = subtable
|
format12 = subtable
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(
|
||||||
@ -114,3 +114,16 @@ def computeMegaCmap(merger, cmapTables):
|
|||||||
# gid, because of another Unicode character.
|
# gid, because of another Unicode character.
|
||||||
# TODO: Try harder to do something about these.
|
# TODO: Try harder to do something about these.
|
||||||
log.warning("Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid)
|
log.warning("Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid)
|
||||||
|
|
||||||
|
|
||||||
|
def renameCFFCharStrings(merger, glyphOrder, cffTable):
|
||||||
|
"""Rename topDictIndex charStrings based on glyphOrder."""
|
||||||
|
td = cffTable.cff.topDictIndex[0]
|
||||||
|
|
||||||
|
charStrings = {}
|
||||||
|
for i, v in enumerate(td.CharStrings.charStrings.values()):
|
||||||
|
glyphName = glyphOrder[i]
|
||||||
|
charStrings[glyphName] = v
|
||||||
|
td.CharStrings.charStrings = charStrings
|
||||||
|
|
||||||
|
td.charset = list(glyphOrder)
|
||||||
|
@ -5,9 +5,13 @@
|
|||||||
from fontTools import ttLib
|
from fontTools import ttLib
|
||||||
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
||||||
from fontTools.ttLib.tables import otTables
|
from fontTools.ttLib.tables import otTables
|
||||||
|
from fontTools.merge.base import add_method, mergeObjects
|
||||||
from fontTools.merge.util import *
|
from fontTools.merge.util import *
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger("fontTools.merge")
|
||||||
|
|
||||||
|
|
||||||
def mergeLookupLists(lst):
|
def mergeLookupLists(lst):
|
||||||
# TODO Do smarter merge.
|
# TODO Do smarter merge.
|
||||||
|
311
Lib/fontTools/merge/tables.py
Normal file
311
Lib/fontTools/merge/tables.py
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
# Copyright 2013 Google, Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
|
||||||
|
|
||||||
|
from fontTools import ttLib, cffLib
|
||||||
|
from fontTools.ttLib.tables.DefaultTable import DefaultTable
|
||||||
|
from fontTools.merge.base import add_method, mergeObjects
|
||||||
|
from fontTools.merge.cmap import computeMegaCmap
|
||||||
|
from fontTools.merge.util import *
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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('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
|
||||||
|
}
|
||||||
|
|
||||||
|
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': avg_int, # Apparently fontTools doesn't recalc this
|
||||||
|
'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'))
|
||||||
|
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: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
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('loca').mergeMap = {
|
||||||
|
'*': recalculate,
|
||||||
|
'tableTag': equal,
|
||||||
|
}
|
||||||
|
|
||||||
|
ttLib.getTableClass('glyf').mergeMap = {
|
||||||
|
'tableTag': equal,
|
||||||
|
'glyphs': sumDicts,
|
||||||
|
'glyphOrder': sumLists,
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 '))
|
||||||
|
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'))
|
||||||
|
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
|
@ -11,21 +11,6 @@ import logging
|
|||||||
|
|
||||||
log = logging.getLogger("fontTools.merge")
|
log = logging.getLogger("fontTools.merge")
|
||||||
|
|
||||||
def add_method(*clazzes, **kwargs):
|
|
||||||
"""Returns a decorator function that adds a new method to one or
|
|
||||||
more classes."""
|
|
||||||
allowDefault = kwargs.get('allowDefaultTable', False)
|
|
||||||
def wrapper(method):
|
|
||||||
done = []
|
|
||||||
for clazz in clazzes:
|
|
||||||
if clazz in done: continue # Support multiple names of a clazz
|
|
||||||
done.append(clazz)
|
|
||||||
assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.'
|
|
||||||
assert method.__name__ not in clazz.__dict__, \
|
|
||||||
"Oops, class '%s' has method '%s'." % (clazz.__name__, method.__name__)
|
|
||||||
setattr(clazz, method.__name__, method)
|
|
||||||
return None
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
# General utility functions for merging values from different fonts
|
# General utility functions for merging values from different fonts
|
||||||
|
|
||||||
@ -79,41 +64,6 @@ def sumDicts(lst):
|
|||||||
d.update(item)
|
d.update(item)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def mergeObjects(lst):
|
|
||||||
lst = [item for item in lst if item is not NotImplemented]
|
|
||||||
if not lst:
|
|
||||||
return NotImplemented
|
|
||||||
lst = [item for item in lst if item is not None]
|
|
||||||
if not lst:
|
|
||||||
return None
|
|
||||||
|
|
||||||
clazz = lst[0].__class__
|
|
||||||
assert all(type(item) == clazz for item in lst), lst
|
|
||||||
|
|
||||||
logic = clazz.mergeMap
|
|
||||||
returnTable = clazz()
|
|
||||||
returnDict = {}
|
|
||||||
|
|
||||||
allKeys = set.union(set(), *(vars(table).keys() for table in lst))
|
|
||||||
for key in allKeys:
|
|
||||||
try:
|
|
||||||
mergeLogic = logic[key]
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
mergeLogic = logic['*']
|
|
||||||
except KeyError:
|
|
||||||
raise Exception("Don't know how to merge key %s of class %s" %
|
|
||||||
(key, clazz.__name__))
|
|
||||||
if mergeLogic is NotImplemented:
|
|
||||||
continue
|
|
||||||
value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
|
|
||||||
if value is not NotImplemented:
|
|
||||||
returnDict[key] = value
|
|
||||||
|
|
||||||
returnTable.__dict__ = returnDict
|
|
||||||
|
|
||||||
return returnTable
|
|
||||||
|
|
||||||
def mergeBits(bitmap):
|
def mergeBits(bitmap):
|
||||||
|
|
||||||
def wrapper(lst):
|
def wrapper(lst):
|
||||||
@ -135,20 +85,6 @@ def mergeBits(bitmap):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@add_method(DefaultTable, allowDefaultTable=True)
|
|
||||||
def merge(self, m, tables):
|
|
||||||
if not hasattr(self, 'mergeMap'):
|
|
||||||
log.info("Don't know how to merge '%s'.", self.tableTag)
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
logic = self.mergeMap
|
|
||||||
|
|
||||||
if isinstance(logic, dict):
|
|
||||||
return m.mergeObjects(self, self.mergeMap, tables)
|
|
||||||
else:
|
|
||||||
return logic(tables)
|
|
||||||
|
|
||||||
|
|
||||||
class AttendanceRecordingIdentityDict(object):
|
class AttendanceRecordingIdentityDict(object):
|
||||||
"""A dictionary-like object that records indices of items actually accessed
|
"""A dictionary-like object that records indices of items actually accessed
|
||||||
from a list."""
|
from a list."""
|
||||||
@ -193,16 +129,3 @@ class NonhashableDict(object):
|
|||||||
|
|
||||||
def __delitem__(self, k):
|
def __delitem__(self, k):
|
||||||
del self.d[id(k)]
|
del self.d[id(k)]
|
||||||
|
|
||||||
|
|
||||||
def renameCFFCharStrings(merger, glyphOrder, cffTable):
|
|
||||||
"""Rename topDictIndex charStrings based on glyphOrder."""
|
|
||||||
td = cffTable.cff.topDictIndex[0]
|
|
||||||
|
|
||||||
charStrings = {}
|
|
||||||
for i, v in enumerate(td.CharStrings.charStrings.values()):
|
|
||||||
glyphName = glyphOrder[i]
|
|
||||||
charStrings[glyphName] = v
|
|
||||||
td.CharStrings.charStrings = charStrings
|
|
||||||
|
|
||||||
td.charset = list(glyphOrder)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user