1# Copyright 2013 Google, Inc. All Rights Reserved. 2# 3# Google Author(s): Behdad Esfahbod, Roozbeh Pournader 4 5from fontTools import ttLib, cffLib 6from fontTools.ttLib.tables.DefaultTable import DefaultTable 7from fontTools.merge.base import add_method, mergeObjects 8from fontTools.merge.cmap import computeMegaCmap 9from fontTools.merge.util import * 10import logging 11 12 13log = logging.getLogger("fontTools.merge") 14 15 16ttLib.getTableClass('maxp').mergeMap = { 17 '*': max, 18 'tableTag': equal, 19 'tableVersion': equal, 20 'numGlyphs': sum, 21 'maxStorage': first, 22 'maxFunctionDefs': first, 23 'maxInstructionDefs': first, 24 # TODO When we correctly merge hinting data, update these values: 25 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions 26} 27 28headFlagsMergeBitMap = { 29 'size': 16, 30 '*': bitwise_or, 31 1: bitwise_and, # Baseline at y = 0 32 2: bitwise_and, # lsb at x = 0 33 3: bitwise_and, # Force ppem to integer values. FIXME? 34 5: bitwise_and, # Font is vertical 35 6: lambda bit: 0, # Always set to zero 36 11: bitwise_and, # Font data is 'lossless' 37 13: bitwise_and, # Optimized for ClearType 38 14: bitwise_and, # Last resort font. FIXME? equal or first may be better 39 15: lambda bit: 0, # Always set to zero 40} 41 42ttLib.getTableClass('head').mergeMap = { 43 'tableTag': equal, 44 'tableVersion': max, 45 'fontRevision': max, 46 'checkSumAdjustment': lambda lst: 0, # We need *something* here 47 'magicNumber': equal, 48 'flags': mergeBits(headFlagsMergeBitMap), 49 'unitsPerEm': equal, 50 'created': current_time, 51 'modified': current_time, 52 'xMin': min, 53 'yMin': min, 54 'xMax': max, 55 'yMax': max, 56 'macStyle': first, 57 'lowestRecPPEM': max, 58 'fontDirectionHint': lambda lst: 2, 59 'indexToLocFormat': first, 60 'glyphDataFormat': equal, 61} 62 63ttLib.getTableClass('hhea').mergeMap = { 64 '*': equal, 65 'tableTag': equal, 66 'tableVersion': max, 67 'ascent': max, 68 'descent': min, 69 'lineGap': max, 70 'advanceWidthMax': max, 71 'minLeftSideBearing': min, 72 'minRightSideBearing': min, 73 'xMaxExtent': max, 74 'caretSlopeRise': first, 75 'caretSlopeRun': first, 76 'caretOffset': first, 77 'numberOfHMetrics': recalculate, 78} 79 80ttLib.getTableClass('vhea').mergeMap = { 81 '*': equal, 82 'tableTag': equal, 83 'tableVersion': max, 84 'ascent': max, 85 'descent': min, 86 'lineGap': max, 87 'advanceHeightMax': max, 88 'minTopSideBearing': min, 89 'minBottomSideBearing': min, 90 'yMaxExtent': max, 91 'caretSlopeRise': first, 92 'caretSlopeRun': first, 93 'caretOffset': first, 94 'numberOfVMetrics': recalculate, 95} 96 97os2FsTypeMergeBitMap = { 98 'size': 16, 99 '*': lambda bit: 0, 100 1: bitwise_or, # no embedding permitted 101 2: bitwise_and, # allow previewing and printing documents 102 3: bitwise_and, # allow editing documents 103 8: bitwise_or, # no subsetting permitted 104 9: bitwise_or, # no embedding of outlines permitted 105} 106 107def mergeOs2FsType(lst): 108 lst = list(lst) 109 if all(item == 0 for item in lst): 110 return 0 111 112 # Compute least restrictive logic for each fsType value 113 for i in range(len(lst)): 114 # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set 115 if lst[i] & 0x000C: 116 lst[i] &= ~0x0002 117 # set bit 2 (allow previewing) if bit 3 is set (allow editing) 118 elif lst[i] & 0x0008: 119 lst[i] |= 0x0004 120 # set bits 2 and 3 if everything is allowed 121 elif lst[i] == 0: 122 lst[i] = 0x000C 123 124 fsType = mergeBits(os2FsTypeMergeBitMap)(lst) 125 # unset bits 2 and 3 if bit 1 is set (some font is "no embedding") 126 if fsType & 0x0002: 127 fsType &= ~0x000C 128 return fsType 129 130 131ttLib.getTableClass('OS/2').mergeMap = { 132 '*': first, 133 'tableTag': equal, 134 'version': max, 135 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this 136 'fsType': mergeOs2FsType, # Will be overwritten 137 'panose': first, # FIXME: should really be the first Latin font 138 'ulUnicodeRange1': bitwise_or, 139 'ulUnicodeRange2': bitwise_or, 140 'ulUnicodeRange3': bitwise_or, 141 'ulUnicodeRange4': bitwise_or, 142 'fsFirstCharIndex': min, 143 'fsLastCharIndex': max, 144 'sTypoAscender': max, 145 'sTypoDescender': min, 146 'sTypoLineGap': max, 147 'usWinAscent': max, 148 'usWinDescent': max, 149 # Version 1 150 'ulCodePageRange1': onlyExisting(bitwise_or), 151 'ulCodePageRange2': onlyExisting(bitwise_or), 152 # Version 2, 3, 4 153 'sxHeight': onlyExisting(max), 154 'sCapHeight': onlyExisting(max), 155 'usDefaultChar': onlyExisting(first), 156 'usBreakChar': onlyExisting(first), 157 'usMaxContext': onlyExisting(max), 158 # version 5 159 'usLowerOpticalPointSize': onlyExisting(min), 160 'usUpperOpticalPointSize': onlyExisting(max), 161} 162 163@add_method(ttLib.getTableClass('OS/2')) 164def merge(self, m, tables): 165 DefaultTable.merge(self, m, tables) 166 if self.version < 2: 167 # bits 8 and 9 are reserved and should be set to zero 168 self.fsType &= ~0x0300 169 if self.version >= 3: 170 # Only one of bits 1, 2, and 3 may be set. We already take 171 # care of bit 1 implications in mergeOs2FsType. So unset 172 # bit 2 if bit 3 is already set. 173 if self.fsType & 0x0008: 174 self.fsType &= ~0x0004 175 return self 176 177ttLib.getTableClass('post').mergeMap = { 178 '*': first, 179 'tableTag': equal, 180 'formatType': max, 181 'isFixedPitch': min, 182 'minMemType42': max, 183 'maxMemType42': lambda lst: 0, 184 'minMemType1': max, 185 'maxMemType1': lambda lst: 0, 186 'mapping': onlyExisting(sumDicts), 187 'extraNames': lambda lst: [], 188} 189 190ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = { 191 'tableTag': equal, 192 'metrics': sumDicts, 193} 194 195ttLib.getTableClass('name').mergeMap = { 196 'tableTag': equal, 197 'names': first, # FIXME? Does mixing name records make sense? 198} 199 200ttLib.getTableClass('loca').mergeMap = { 201 '*': recalculate, 202 'tableTag': equal, 203} 204 205ttLib.getTableClass('glyf').mergeMap = { 206 'tableTag': equal, 207 'glyphs': sumDicts, 208 'glyphOrder': sumLists, 209} 210 211@add_method(ttLib.getTableClass('glyf')) 212def merge(self, m, tables): 213 for i,table in enumerate(tables): 214 for g in table.glyphs.values(): 215 if i: 216 # Drop hints for all but first font, since 217 # we don't map functions / CVT values. 218 g.removeHinting() 219 # Expand composite glyphs to load their 220 # composite glyph names. 221 if g.isComposite(): 222 g.expand(table) 223 return DefaultTable.merge(self, m, tables) 224 225ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst) 226ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst) 227ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst) 228ttLib.getTableClass('gasp').mergeMap = lambda self, lst: first(lst) # FIXME? Appears irreconcilable 229 230@add_method(ttLib.getTableClass('CFF ')) 231def merge(self, m, tables): 232 233 if any(hasattr(table, "FDSelect") for table in tables): 234 raise NotImplementedError( 235 "Merging CID-keyed CFF tables is not supported yet" 236 ) 237 238 for table in tables: 239 table.cff.desubroutinize() 240 241 newcff = tables[0] 242 newfont = newcff.cff[0] 243 private = newfont.Private 244 storedNamesStrings = [] 245 glyphOrderStrings = [] 246 glyphOrder = set(newfont.getGlyphOrder()) 247 248 for name in newfont.strings.strings: 249 if name not in glyphOrder: 250 storedNamesStrings.append(name) 251 else: 252 glyphOrderStrings.append(name) 253 254 chrset = list(newfont.charset) 255 newcs = newfont.CharStrings 256 log.debug("FONT 0 CharStrings: %d.", len(newcs)) 257 258 for i, table in enumerate(tables[1:], start=1): 259 font = table.cff[0] 260 font.Private = private 261 fontGlyphOrder = set(font.getGlyphOrder()) 262 for name in font.strings.strings: 263 if name in fontGlyphOrder: 264 glyphOrderStrings.append(name) 265 cs = font.CharStrings 266 gs = table.cff.GlobalSubrs 267 log.debug("Font %d CharStrings: %d.", i, len(cs)) 268 chrset.extend(font.charset) 269 if newcs.charStringsAreIndexed: 270 for i, name in enumerate(cs.charStrings, start=len(newcs)): 271 newcs.charStrings[name] = i 272 newcs.charStringsIndex.items.append(None) 273 for name in cs.charStrings: 274 newcs[name] = cs[name] 275 276 newfont.charset = chrset 277 newfont.numGlyphs = len(chrset) 278 newfont.strings.strings = glyphOrderStrings + storedNamesStrings 279 280 return newcff 281 282@add_method(ttLib.getTableClass('cmap')) 283def merge(self, m, tables): 284 285 # TODO Handle format=14. 286 if not hasattr(m, 'cmap'): 287 computeMegaCmap(m, tables) 288 cmap = m.cmap 289 290 cmapBmpOnly = {uni: gid for uni,gid in cmap.items() if uni <= 0xFFFF} 291 self.tables = [] 292 module = ttLib.getTableModule('cmap') 293 if len(cmapBmpOnly) != len(cmap): 294 # format-12 required. 295 cmapTable = module.cmap_classes[12](12) 296 cmapTable.platformID = 3 297 cmapTable.platEncID = 10 298 cmapTable.language = 0 299 cmapTable.cmap = cmap 300 self.tables.append(cmapTable) 301 # always create format-4 302 cmapTable = module.cmap_classes[4](4) 303 cmapTable.platformID = 3 304 cmapTable.platEncID = 1 305 cmapTable.language = 0 306 cmapTable.cmap = cmapBmpOnly 307 # ordered by platform then encoding 308 self.tables.insert(0, cmapTable) 309 self.tableVersion = 0 310 self.numSubTables = len(self.tables) 311 return self 312