| # 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() or g.isVarComposite(): |
| 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: |
| 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 |