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