English | 简体中文
font-matrix-merger is a powerful Python library for merging multiple fonts into one, covering every combination of static / variable × OTF / TTF for both the main font and the base fonts — with format conversion, CID-keyed CFF handling, OpenType feature merging, variable-font axis unions, scaling and baseline offsets, and multi-level (n-step) chained merges.
The library is built on top of FontTools (MIT) — used for all glyph/table-level manipulation, CFF/CFF2 conversion, varLib instancing and subsetting — and follows the conventions of AFDKO (Adobe Font Development Kit for OpenType, Apache-2.0), which is bundled in the project's PyInstaller build pipeline and referenced for its CID-keyed UFO / CIDMap remapping approach.
Key highlights:
- 16-way merge matrix:
merge_two()auto-dispatches to the right strategy for any main/base combination. - Multi-level chaining: a main font plus 1..n base fonts, merged level by level; format/export questions are asked only once (answer memorization).
- CID-aware: dual-CID fonts are merged via CID offset + materialized CharString copy; CID↔name-keyed normalization is handled automatically.
- Variable fonts: a variable main font stays variable in the output, with axis union across main and base fonts (fvar/avar/STAT synchronized, varStore regions extended).
- OpenType merging: base GSUB/GPOS/GDEF tables are pruned with
fontTools.subsetclosure to the surviving glyphs, then appended with lookup-index remapping and deep glyph-name remapping; conflicts resolve in favor of the main font. - Pure FontTools pipeline: glyph injection goes through the official TTX XML roundtrip (
saveXML→ inject →ttxcompile) instead offontTools.merge, which raisesNotImplementedErroron CID-keyed CFF.
FontMerger/
├── FontMerging.py # Interactive CLI entry point
├── __init__.py # Public API exports
├── core/ # 16-way dispatch, TTX glyph injection, conflict & naming
├── format/ # static extraction, CFF↔glyf, CID, axis union, transform, subset, webfont
├── tables/ # per-table merge registry + GSUB/GPOS/GDEF merge
├── tests/ # 16-matrix generator + unit tests
└── build/ # PyInstaller packaging (bundles FontTools + AFDKO)
| Capability | Description |
|---|---|
| 16-way merge matrix | All static/variable × OTF/TTF × main/base combinations, auto-dispatched |
| Multi-level chaining | Main + 1..n base fonts; each level can have its own scale & baseline offset |
| Format conversion | CFF↔glyf (cubic↔quadratic), CFF2→CFF (_convertCFF2ToCFF), CFF→CFF2 |
| CID merging | Dual-CID: CID offset + direct CharString copy; CID↔name-keyed conversion (incl. cmap format 14 UVS) |
| Variable fonts | Main VF preserved in output (CFF2/HVAR/STAT/fvar kept & extended) |
| Axis union | Union of main/base axis spaces; varStore (incl. GDEF/HVAR/MVAR) constant-axis region extension |
| OpenType feature merging | Base GSUB/GPOS/GDEF pruned via Subsetter closure; per-tag conflict → main wins |
| Scale + baseline offset | Pen-pipeline outline rebuild (T2CharStringPen/TTGlyphPen + TransformPen) with synced metrics |
| Overlap removal | removeOverlaps boolean union after variable→static instancing |
| Subsetting | create_glyph_subset — keep only glyphs for a given character set |
| WOFF/WOFF2 unwrap | Web fonts accepted as input directly |
| Naming | Output family <main font> mod, copyrights merged with ^n^n separators |
Main and base fonts are each classified as static/variable × OTF/TTF — 16 semantic combinations. Rows = main font type, columns = base font type.
| Static OTF base | Static TTF base | Variable OTF base | Variable TTF base | |
|---|---|---|---|---|
| Static OTF main | General: multi-level merging, per-font independent scale/baseline offset | Asks the user whether to export TTF or OTF | Drops base glyphs whose codepoint or name already exists in main; the rest + only the OpenType features referencing surviving glyphs are merged into main (base auto-instanced to the default instance) | Converts the main OTF outlines to quadratic curves; then same as “Variable OTF main × Static OTF base” |
| Static TTF main | Asks the user whether to export TTF or OTF | General | Converts the main TTF outlines to cubic curves; then same as “Variable OTF main × Static OTF base” | Same as “Variable OTF main × Static OTF base” |
| Variable OTF main | Asks whether to export variable or static: ① Static → interpolate a font at the main’s axis values, then handle as static; ② Variable → drop base glyphs conflicting with main, merge main glyphs into the variable OTF’s default master, insert main’s OpenType features into each master | Same as left (static path) | Master handling: drop base glyphs conflicting in each master; merge base masters into main — same axis values → add glyphs/features to the main master, different axis values → create a new master. Axis handling: axis min/max = union; base-only axes are added (missing masters filled with default values) | Asks the user whether to export TTF or OTF; then same as “Variable OTF main × Variable OTF base” |
| Variable TTF main | Asks whether to export variable or static (same as “Variable OTF main”; on the variable path main glyphs are converted to quadratic first) | Same as left (static path) | Asks TTF or OTF; then same as “Variable OTF main × Variable OTF base” | Same as “Variable OTF main × Variable OTF base” |
- Multi-level chaining: the result of main + first (n−1) base levels becomes the “main” of level n; already-asked questions are not asked again (memorized).
- Naming: Family becomes
<main font name> mod; copyrights are written into all copyright fields (^n^n-separated); everything else follows the main font. - OpenType features: the main font’s features are fully preserved; non-conflicting base language/feature tables can be kept wholesale (items referring to deleted glyphs are removed); on conflict, the main font wins.
- Glyph retention: all main-font glyphs are kept; base glyphs are merged only when both codepoint and name do not conflict with the main font.
- Input formats: WOFF/WOFF2 are unwrapped automatically; TTC/OTC must be unpacked first; other formats are rejected.
Implementation status: the “② Variable path” of a variable main font is supported (main VF preserved + axis union + base glyphs merged at the default instance). Per-axis master-level interpolation composition (base glyphs varying with the base’s axes) is not yet implemented.
Requirements: Python ≥ 3.8, FontTools ≥ 4.49.
git clone https://github.com/ChangGGcn/font-matrix-merger.git
cd font-matrix-merger
pip install "fonttools>=4.49"
# Interactive CLI
python FontMerging.pyThe CLI is interactive: enter the main font path (plus scale % and baseline offset), then any number of base fonts (press Y to finish), and compatibility warnings can be confirmed with y. The output is written to <main font>_mod.{otf|ttf}.
========================================================
FontMerger 字体合并工具
支持: 静态/可变 x OTF/TTF x 多级打底 x WOFF/WOFF2
========================================================
请输入主字体(拖入文件或输入路径):
主字体路径: test/OpenType/LibreCaslonText-Regular.otf
缩放倍率(%) [100]: 100
基线偏移 [0]: 0
第 1 级打底字体(Y 结束):
路径: test/otf_variable_fonts/SourceSerif4Variable-Roman.otf
缩放倍率(%) [100]: 100
基线偏移 [0]: 0
第 2 级打底字体(Y 结束):
路径: test/TrueType/LXGWWenKaiTC-Regular.ttf
缩放倍率(%) [100]: 100
基线偏移 [0]: 0
第 3 级打底字体(Y 结束):
路径: Y
加载字体...
test/OpenType/LibreCaslonText-Regular.otf
静态OTF, 537 字形
test/otf_variable_fonts/SourceSerif4Variable-Roman.otf
可变OTF, 1464 字形
test/TrueType/LXGWWenKaiTC-Regular.ttf
静态TTF, 25764 字形
兼容性检查...
[警告] 打底字体为可变字体, 将先实例化为静态再合并
[警告] 混合轮廓格式: 主字体为CFF, 打底为glyf. 将自动转换打底字体
[警告] 主字体为可变字体, 将先实例化为静态再合并
开始合并...
--- 第 1 级 ---
主: 静态OTF | 打底: 可变OTF
[CFF2→CFF] 转换完成 (1464 glyphs)
[重叠合并] 完成 (1464 glyphs)
[CID归一] 打底字体 CID→name (输出统一为 name-keyed)
[CID→name] 转换完成 (1464 glyphs)
[冲突] 409 个字形
[新增] 1054 个字形
合并后字形: 1591
[OT合并] GDEF: 来自打底 (主无, 过滤后 200 个字类)
[OT合并] GSUB: 追加 10 个打底 feature (主冲突跳过)
[OT合并] GPOS: 追加 2 个打底 feature (主冲突跳过)
--- 第 2 级 ---
主: 静态OTF | 打底: 静态TTF
[询问] 导出为 TTF 还是 OTF?
1. OTF
2. TTF
> 1
[转换] 轮廓格式不同,自动转换打底字体...
[冲突] 915 个字形
[新增] 24848 个字形
[分块] 5 块, 每块 ≤5000 字形
[OT合并] GSUB: 追加 2 个打底 feature (主冲突跳过)
[OT合并] GPOS: 追加 3 个打底 feature (主冲突跳过)
处理名称...
输出路径 [test/OpenType/LibreCaslonText-Regular_mod.otf]:
完成! test/OpenType/LibreCaslonText-Regular_mod.otf
静态OTF, 26439 字形
Notes:
- Export-format / output questions are asked only once; later levels reuse the memorized answer.
- Each base level can have its own scale % and baseline offset.
- Compatibility warnings are informational; confirm with
yto continue.
from FontMerger import FontMerger, apply_naming, get_family, get_copyrights
from FontMerger.format.static_extract import variable_to_static
from fontTools.ttLib import TTFont
merger = FontMerger()
# Pre-answer the interactive questions (non-interactive use)
merger.mem = {"sOTF_sTTF": "OTF", "vOTF_sOTF": "可变"}
# Level-by-level merge
r = merger.merge_two(main_font, base_font)
r = apply_naming(r, get_family(main_font), get_copyrights(main_font, base_font))
r.save("merged.otf")
# Or the one-line convenience helper
from FontMerger import merge_fonts
result = merge_fonts("main.otf", ["base1.otf", "base2.ttf"])tests/generate_matrix.py generates the full 16-combination matrix; tests/test_merger.py contains the unit tests. The test suite needs a local font collection (paths are resolved under tests/../test/) — the fonts themselves are not redistributed with the repository. OFL Google Fonts typefaces used in the samples (Libre Caslon Text, LXGW WenKai TC, Source Serif/Source Han, Zed Text) can be downloaded freely; commercially licensed test fonts are mapped in the local, non-committed file tests/local_fonts.py (template: tests/local_fonts.example.py) and any missing font simply skips the corresponding case.
- JP (CID CFF2) as the main font (matrix cells C1/D1): the merge itself succeeds (~18,600–18,868 glyphs), but the save stage can leave incomplete cmap format-4 references for base Latin glyphs; the matrix generator falls back to a static-instanced path. A true variable output requires solving deeper CFF2 CID namespace issues.
- Master-level interpolation composition (base glyphs varying along the base font’s axes) is not implemented: base glyphs are merged at the base’s default instance and are constant across the main VF’s axes.
- OpenType features: appended base features are merged only if all referenced glyphs exist in the main font (otherwise skipped); GDEF conflicts follow the main font.
- VORG: values are correct when instancing at the default axis position; non-default positions need recomputation.
- Multi-level OT features: a feature already merged at an earlier level is re-detected at later levels (idempotent, but lookups may become redundant).
- Packaging: the repository root is the package itself, so
pip installfrom a clone is not wired up yet — clone-and-run for now; a PyPI-ready layout is planned. - Locally licensed fonts are used only for local testing and are intentionally excluded from this repository (paths live in the non-committed
tests/local_fonts.py); all fonts named in the documentation samples are OFL-licensed.
Released under the MIT License. Sample fonts used for testing remain © their respective authors.