340 lines
10 KiB
Python
340 lines
10 KiB
Python
|
# 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
|