# Copyright 2013 Google, Inc. All Rights Reserved. # # 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 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, "_reverseGlyphOrder": recalculate, "axisTags": equal, } @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.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: # The following will be wrong if the width is added # by a subroutine. Ouch! 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