Spaces:
Running
Running
# 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), | |
} | |
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, | |
} | |
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 | |
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 | |
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 | |