"""Combine the upright and ten-degree oblique into one weight/italic variable font.""" import hashlib import json from pathlib import Path from fontTools.designspaceLib import AxisDescriptor, DesignSpaceDocument, InstanceDescriptor, SourceDescriptor from fontTools.otlLib.builder import buildStatTable from fontTools.pens.recordingPen import DecomposingRecordingPen from fontTools.pens.ttGlyphPen import TTGlyphPen from fontTools.ttLib import TTFont from fontTools.varLib import build from fontTools.varLib.instancer import instantiateVariableFont from masters import MASTERS, STATIC_STYLES from oblique import name_oblique ROOT = Path(__file__).resolve().parent.parent VERSION = 0.517 def main(): output = ROOT / 'build/combined' output.mkdir(parents=True, exist_ok=True) designspace = DesignSpaceDocument() for name, tag, minimum, default, maximum in (('Weight', 'wght', 200, 400, 800), ('Italic', 'ital', 0, 0, 1)): axis = AxisDescriptor() axis.name, axis.tag = name, tag axis.minimum, axis.default, axis.maximum = minimum, default, maximum designspace.addAxis(axis) signatures = None order = None sources = {} for italic, directory in ((0, 'variable'), (1, 'oblique')): for style, weight in MASTERS: path = ROOT / 'build' / directory / f'{style}.ttf' sources[str(path.relative_to(ROOT))] = hashlib.sha256(path.read_bytes()).hexdigest() with TTFont(path, recalcTimestamp=False) as font: if order is None: order = font.getGlyphOrder() if font.getGlyphOrder() != order: raise ValueError(f'{path}: incompatible glyph order') glyphs = font.getGlyphSet() replacements = {} # gvar can vary component offsets, but not their affine transforms. for name in order: if not font['glyf'][name].isComposite(): continue recording = DecomposingRecordingPen(glyphs) glyphs[name].draw(recording) pen = TTGlyphPen(None) recording.replay(pen) glyph = pen.glyph() if glyph.numberOfContours: glyph.flags[0] |= 0x40 # Preserve overlapping accent joins after decomposition. replacements[name] = glyph for name, glyph in replacements.items(): font['glyf'][name] = glyph glyph.recalcBounds(font['glyf']) font['hmtx'][name] = (font['hmtx'][name][0], getattr(glyph, 'xMin', 0)) # Like varLib, which otherwise skips the glyph with only a warning, compare every flag. current = {name: (tuple(g.endPtsOfContours), tuple(g.flags)) for name in order if (g := font['glyf'][name]).numberOfContours} if signatures is None: signatures = current elif signatures != current: different = [name for name in order if signatures.get(name) != current.get(name)] raise ValueError(f'{path}: incompatible point structures: {different}') master = output / f'{style}-{italic}.ttf' font['hhea'].caretSlopeRise = 1000 font.save(master) source = SourceDescriptor() source.name, source.path = f'{style}-{italic}', str(master) source.familyName = 'iocgn Mono' source.styleName = ('Italic' if style == 'Regular' else f'{style} Italic') if italic else style source.location = {'Weight': weight, 'Italic': italic} source.copyInfo = source.copyLib = source.copyFeatures = weight == 400 and italic == 0 designspace.addSource(source) weights = (*MASTERS[:2], *STATIC_STYLES, *MASTERS[2:]) for italic in (0, 1): for style, weight in weights: instance = InstanceDescriptor() instance.familyName = 'iocgn Mono' instance.styleName = ('Italic' if style == 'Regular' else f'{style} Italic') if italic else style instance.postScriptFontName = f'iocgnMono-{instance.styleName.replace(" ", "")}' instance.location = {'Weight': weight, 'Italic': italic} designspace.addInstance(instance) designspace.write(output / 'iocgn.designspace') variable, _, _ = build(designspace, optimize=False) variable['name'].removeNames(platformID=1) # Approximate IUP can make accented letters' base outlines diverge. for name, variations in variable['gvar'].variations.items(): coordinates, controls = variable['glyf']._getCoordinatesAndControls(name, variable['hmtx'].metrics) for variation in variations: variation.optimize(coordinates, controls.endPts, tolerance=0) buildStatTable(variable, [ {'tag': 'wght', 'name': 'Weight', 'values': [ {'name': style, 'value': weight, **({'flags': 2, 'linkedValue': 700} if weight == 400 else {})} for style, weight in weights]}, {'tag': 'ital', 'name': 'Italic', 'values': [ {'name': 'Roman', 'value': 0, 'flags': 2, 'linkedValue': 1}, {'name': 'Italic', 'value': 1}, ]}, ]) variable['head'].fontRevision = version = VERSION variable['OS/2'].recalcUnicodeRanges(variable) variable['OS/2'].recalcCodePageRanges(variable) values = {3: f'iocgnMono-Variable-{version:.3f}', 4: 'iocgn Mono', 5: f'Version {version:.3f}; variable weight and italic', 6: 'iocgnMono-Variable', 25: 'iocgnMono'} for record in variable['name'].names: if record.nameID in values: variable['name'].setName(values[record.nameID], record.nameID, record.platformID, record.platEncID, record.langID) for name_id, value in values.items(): variable['name'].setName(value, name_id, 3, 1, 0x409) variable.recalcTimestamp = False variable['head'].created = variable['head'].modified = 2082844800 destination = ROOT / 'build/TTF/iocgnMono-Variable.ttf' destination.parent.mkdir(parents=True, exist_ok=True) variable.save(destination) static_paths = [] # Reload compiled tables: varLib leaves optional GDEF fields unset. with TTFont(destination, recalcTimestamp=False) as compiled: for style, weight in (('Regular', 400), *STATIC_STYLES, ('Bold', 700)): for italic in (0, 1): font = instantiateVariableFont(compiled, {'wght': weight, 'ital': italic}, updateFontNames=True) label = style if italic: name_oblique(font, style, weight) label = 'Oblique' if style == 'Regular' else f'{style}Oblique' path = destination.with_name(f'iocgnMono-{label}.ttf') font.save(path) font.close() static_paths.append(path) print(f'Built {path.name}: static wght {weight}, ital {italic}.') report = {'axes': {'wght': [200, 400, 800], 'ital': [0, 0, 1]}, 'sources': sources, 'builder_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), 'static_fonts': {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in static_paths}, 'font': {'path': str(destination.relative_to(ROOT)), 'sha256': hashlib.sha256(destination.read_bytes()).hexdigest()}} (output / 'manifest.json').write_text(json.dumps(report, indent=2) + '\n') print(f'Built {destination.name}: wght 200–800 and ital 0 upright / 1 ten-degree oblique.') if __name__ == '__main__': main()