| # 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 |