# 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': 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')) 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