Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

font-matrix-merger

License: MIT Python 3.8+ FontTools Version

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.

About

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.subset closure 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 → ttx compile) instead of fontTools.merge, which raises NotImplementedError on CID-keyed CFF.

Project layout

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)

Features

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

Merge matrix (5×5)

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”

General rules (all combinations)

  1. 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).
  2. Naming: Family becomes <main font name> mod; copyrights are written into all copyright fields (^n^n-separated); everything else follows the main font.
  3. 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.
  4. 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.
  5. 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.

Quick Start

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.py

The 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}.

Three-font input/output example (main + 2 base levels)

========================================================
  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 y to continue.

Library API

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

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.

Known Limitations

  1. 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.
  2. 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.
  3. 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.
  4. VORG: values are correct when instancing at the default axis position; non-default positions need recomputation.
  5. Multi-level OT features: a feature already merged at an earlier level is re-detected at later levels (idempotent, but lookups may become redundant).
  6. Packaging: the repository root is the package itself, so pip install from a clone is not wired up yet — clone-and-run for now; a PyPI-ready layout is planned.
  7. 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.

License

Released under the MIT License. Sample fonts used for testing remain © their respective authors.

About

Merge TrueType, OTF and CFF/CFF2 fonts with GSUB/GPOS feature merging, variable-font axis unions, multi-step chained merges and CID-keyed CFF support.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages