import os
os.environ.setdefault('SOURCE_DATE_EPOCH', '0')
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.pens.t2CharStringPen import T2CharStringPen
from fontTools.cffLib import (
FDArrayIndex,
FDSelect,
FontDict,
PrivateDict,
)
UPM = 1000
LETTERS = [chr(ord('A') + i) for i in range(10)] ADVANCES = {'.notdef': 500, 'space': 250}
ADVANCES.update({ch: 300 + 50 * i for i, ch in enumerate(LETTERS)})
GLYPH_ORDER = ['.notdef', 'space'] + LETTERS
CIDS = [0, 700, 901, 811, 821, 831, 841, 851, 861, 871, 881, 891]
OUT_DIR = os.path.join(os.path.dirname(__file__), '..', 'tests', 'assets', 'fonts', 'mock')
def draw_rect(pen, width):
pen.moveTo((0, 0))
pen.lineTo((width, 0))
pen.lineTo((width, 700))
pen.lineTo((0, 700))
pen.closePath()
def cmap():
m = {0x20: 'space'}
m.update({ord(ch): ch for ch in LETTERS})
return m
def common_setup(fb, glyph_order, scale=1):
fb.setupGlyphOrder(glyph_order)
fb.setupCharacterMap({cp: n for cp, n in cmap().items()
if n in glyph_order or n in ('space',)})
fb.setupHorizontalMetrics({n: (ADVANCES[base_name(n)] * scale, 0) for n in glyph_order})
fb.setupHorizontalHeader(ascent=800 * scale, descent=-200 * scale)
fb.setupNameTable({'familyName': 'MockFont', 'styleName': 'Regular'})
fb.setupOS2(sTypoAscender=800 * scale, sTypoDescender=-200 * scale,
usWinAscent=800 * scale, usWinDescent=200 * scale)
fb.setupPost()
def base_name(glyph_name):
return glyph_name
def build_ttf(path):
fb = FontBuilder(UPM, isTTF=True)
glyphs = {}
for name in GLYPH_ORDER:
pen = TTGlyphPen(None)
if name not in ('.notdef', 'space'):
draw_rect(pen, ADVANCES[name])
glyphs[name] = pen.glyph()
common_setup(fb, GLYPH_ORDER)
fb.setupGlyf(glyphs)
fb.font.save(path)
def build_cff_named(path):
fb = FontBuilder(UPM, isTTF=False)
charstrings = {}
for name in GLYPH_ORDER:
pen = T2CharStringPen(ADVANCES[name], None)
if name not in ('.notdef', 'space'):
draw_rect(pen, ADVANCES[name])
charstrings[name] = pen.getCharString()
common_setup(fb, GLYPH_ORDER)
fb.setupCFF('MockFont-Regular', {'FullName': 'MockFont Regular'}, charstrings, {})
fb.font.save(path)
def build_cff_cid(path, upm=UPM, font_matrix=None, arith_width_glyph=None):
from fontTools.misc import psCharStrings
def _op_add(self, index):
b = self.pop()
a = self.pop()
self.push(a + b)
for cls_name in ('SimpleT2Decompiler', 'T2WidthExtractor', 'T2OutlineExtractor'):
cls = getattr(psCharStrings, cls_name, None)
if cls is not None:
cls.op_add = _op_add
scale = upm // UPM
fb = FontBuilder(upm, isTTF=False)
charstrings = {}
for name in GLYPH_ORDER:
adv = ADVANCES[name] * scale
if name == arith_width_glyph:
from fontTools.misc.psCharStrings import T2CharString
assert adv % 2 == 0
h = 700 * scale
charstrings[name] = T2CharString(program=[
adv // 2, adv // 2, 'add', 0, 0, 'rmoveto',
adv, 0, 0, h, -adv, 0, 'rlineto',
'endchar',
])
continue
pen = T2CharStringPen(adv, None)
if name not in ('.notdef', 'space'):
draw_rect(pen, adv)
charstrings[name] = pen.getCharString()
common_setup(fb, GLYPH_ORDER, scale=scale)
fb.setupCFF('MockFont-CID', {'FullName': 'MockFont CID'}, charstrings, {})
font = fb.font
cff = font['CFF '].cff
td = cff[cff.fontNames[0]]
rename = {GLYPH_ORDER[gid]: ('.notdef' if gid == 0 else 'cid%05d' % CIDS[gid])
for gid in range(len(GLYPH_ORDER))}
new_order = [rename[n] for n in GLYPH_ORDER]
cs = td.CharStrings
for old, new in rename.items():
if old == new:
continue
cs.charStrings[new] = cs.charStrings.pop(old)
td.charset = new_order
font.setGlyphOrder(new_order)
if hasattr(font, '_reverseGlyphOrderDict'):
del font._reverseGlyphOrderDict
for table in font['cmap'].tables:
table.cmap = {cp: rename.get(n, n) for cp, n in table.cmap.items()}
hmtx = font['hmtx']
hmtx.metrics = {rename.get(n, n): v for n, v in hmtx.metrics.items()}
td.ROS = ('Adobe', 'Identity', 0)
td.rawDict['ROS'] = td.ROS
td.CIDCount = max(CIDS) + 1
fd = FontDict()
fd.Private = td.Private
fd_array = FDArrayIndex()
fd_array.append(fd)
td.FDArray = fd_array
fd_select = FDSelect()
fd_select.format = 3
fd_select.gidArray = [0] * len(new_order)
td.FDSelect = fd_select
td.rawDict.pop('Private', None)
if hasattr(td, 'Private'):
del td.Private
td.rawDict.pop('Encoding', None)
if hasattr(td, 'Encoding'):
td.Encoding = None
if font_matrix is not None:
td.FontMatrix = font_matrix
td.rawDict['FontMatrix'] = font_matrix
font.save(path)
def main():
os.makedirs(OUT_DIR, exist_ok=True)
build_ttf(os.path.join(OUT_DIR, 'mock_ttf.ttf'))
build_cff_named(os.path.join(OUT_DIR, 'mock_cff_named.otf'))
build_cff_cid(os.path.join(OUT_DIR, 'mock_cff_cid.otf'))
build_cff_cid(os.path.join(OUT_DIR, 'mock_cff_cid_fm.otf'), upm=2 * UPM,
font_matrix=[0.0005, 0, 0, 0.0005, 0, 0], arith_width_glyph='B')
from fontTools.ttLib import TTFont
scales = {'mock_ttf.ttf': 1, 'mock_cff_named.otf': 1,
'mock_cff_cid.otf': 1, 'mock_cff_cid_fm.otf': 2}
for fname, scale in scales.items():
p = os.path.join(OUT_DIR, fname)
f = TTFont(p)
assert f['head'].unitsPerEm == UPM * scale
order = f.getGlyphOrder()
assert len(order) == len(GLYPH_ORDER), (fname, order)
hmtx = f['hmtx']
for gid, name in enumerate(order):
expected = ADVANCES[GLYPH_ORDER[gid]] * scale
assert hmtx[name][0] == expected, (fname, name, hmtx[name][0], expected)
if fname.startswith('mock_cff_cid'):
cff = f['CFF '].cff
td = cff[cff.fontNames[0]]
assert hasattr(td, 'ROS'), 'must be CID-keyed'
got = [0 if n == '.notdef' else int(n[3:]) for n in td.charset]
assert got == CIDS, (got, CIDS)
if fname == 'mock_cff_cid_fm.otf':
assert td.FontMatrix == [0.0005, 0, 0, 0.0005, 0, 0], td.FontMatrix
b = td.CharStrings['cid00811']
assert b'\x0c\x0a' in b.bytecode, 'B charstring must contain add (12 10)'
print(f'{fname}: OK ({os.path.getsize(p)} bytes)')
if __name__ == '__main__':
main()