Python library and CLI for splitting large font files into smaller unicode-range webfont subsets.
Font Splitter uses FontTools directly, supports CSS unicode-range templates from font stylesheets, and can fall back to Unicode Blocks for the remaining codepoints in the target font.
The CLI and Python API share the same planner and subsetting implementation, so command-line runs, disk output, in-memory output, and dry-run planning all follow the same rules.
- Python 3.11+
- Git, when installing directly from GitHub
- FontTools with WOFF/WOFF2 support, installed automatically by this package
Install a released tag:
python -m pip install "font-splitter @ git+https://github.com/VdustR/font-splitter.git@v0.2.1"For CLI-only usage, pipx keeps the command isolated:
pipx install "font-splitter @ git+https://github.com/VdustR/font-splitter.git@v0.2.1"Split a font with the default Unicode Blocks fallback:
font-splitter NotoSansTC.ttf --output distUse a local font CSS file as the preferred range template, then fall back to Unicode Blocks for the remaining codepoints:
font-splitter NotoSansTC.ttf \
--source font-css:noto-sans-tc.css \
--font-css-family "Noto Sans TC" \
--font-css-weight 400 \
--output distPlan the split without writing files:
font-splitter NotoSansTC.ttf --dryDisable fallback when only the CSS template ranges should be emitted:
font-splitter NotoSansTC.ttf \
--source font-css:noto-sans-tc.css \
--no-fallback \
--output distUse no-split mode:
font-splitter NotoSansTC.ttf --chunk - --output distCommon options:
-o, --output <dir> output directory, default: output
-f, --flavor <woff|woff2> output flavor, default: woff2
-c, --chunk <size> v1-compatible alias for --max-codepoints
--max-codepoints <size> max codepoints per planned bucket; '-' disables splitting
--source <source> ordered range source: font-css:<path> or unicode-blocks
--no-fallback disable the default Unicode Blocks fallback
-n, --family <family> output font family override
--style <style> output font style, default: normal
-i, --italic output italic font style
-w, --weight <weight> output font weight, default: 400
--stretch <stretch> output font stretch override
-d, --dry plan only, do not write assets
-q, --quiet disable stdout
--quite deprecated alias for --quiet
CLI output includes matched source count, planned bucket count, total codepoints, fallback codepoints, empty buckets, overlap drops, max bucket codepoints, estimated files, and generated asset count. If FontTools emits warnings during subsetting, the summary also includes the warning count.
Choose the API by where you want the output to go:
| API | Behavior |
|---|---|
split_font() |
Writes generated font files and CSS to disk. Returns written paths without retaining generated bytes. |
split_font_to_memory() |
Returns generated font files and CSS as bytes without writing to disk. |
plan_font() |
Plans buckets and output file names without subsetting or writing files. |
Write files to disk:
from font_splitter import FontCssSource, split_font
result = split_font(
"NotoSansTC.ttf",
output_dir="dist",
sources=[
FontCssSource.from_file(
"noto-sans-tc.css",
family="Noto Sans TC",
weight=400,
)
],
max_codepoints=1024,
flavor="woff2",
)
print(result.assets_written)
print(result.stats.planned_bucket_count)Return generated assets in memory:
from font_splitter import split_font_to_memory
result = split_font_to_memory(
font_bytes,
max_codepoints=1024,
flavor="woff2",
)
for name, data in result.assets.items():
print(name, len(data))split_font_to_memory() keeps every generated asset in memory. For large CJK fonts or server workloads, use plan_font() first to inspect the planned output, or use split_font() to write assets directly to disk.
Plan without generating font subsets:
from font_splitter import plan_font
plan = plan_font("NotoSansTC.ttf", max_codepoints=1024)
print(plan.stats.estimated_files)
print(plan.stats.fallback_codepoint_count)font accepts a path string, os.PathLike, bytes, or a binary file-like object.
Sources are ordered. Earlier sources win overlaps, and later sources only receive remaining codepoints that exist in the target font cmap.
Supported sources:
FontCssSource: parses generic@font-facerules withunicode-rangedescriptors from a local CSS file.UnicodeBlockSource: groups remaining codepoints by Unicode Blocks using FontTools' bundled Unicode Character Database data.
Both source classes are exported from font_splitter as part of the public API.
By default, Font Splitter uses UnicodeBlockSource as the fallback. Passing CSS sources lets you preserve an existing webfont range strategy first, while still covering extra codepoints that exist in your local font. Font CSS sources are parsed as local files; Font Splitter does not download CSS URLs, follow @import, or fetch font files referenced by src.
max_codepoints is a planning bound, not a byte-size guarantee. Final file sizes still depend on glyph outlines, layout tables, compression, and font internals.
Use the published Docker Hub image:
docker run --rm -v "$PWD:/fonts" vdustr/font-splitter:v0.2.1 input.ttf --output outputlatest follows the latest GitHub release. Version tags are also published without the v prefix, such as vdustr/font-splitter:0.2.1.
Docker tags before v0.2.0 contain the legacy Node.js implementation. Use v0.2.0 or newer for the Python package.
Build locally:
docker build -t font-splitter .v1 was distributed as an npm package and used Node.js to shell out to FontTools commands. v2 removes the Node.js runtime path and uses FontTools directly from Python.
Compatibility notes:
--chunk -still means no split.--quiteis accepted as a deprecated alias, but--quietis the documented spelling.woffandwoff2remain the supported webfont output flavors.- Font CSS input is now a generic
@font-face unicode-rangesource, not a Google Fonts-specific parser.
Font Splitter is distributed under the MIT license. This license applies only to the Font Splitter source code and documentation.
Input fonts and generated subset fonts remain governed by the source font's own license. Before using this tool, verify that the source font license permits the operations you need, including subsetting, format conversion, web serving, embedding, and redistribution.
Set up a local environment:
python3 -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'Run tests:
.venv/bin/python -m pytest -vBuild distributions:
.venv/bin/python -m buildReleases are managed with Release Please on main.
- Use Conventional Commit titles for merged changes.
fix:produces a patch release,feat:produces a minor release, and!orBREAKING CHANGE:marks a breaking change. While the package is still0.x, breaking changes bump the minor version. - PR titles are checked against the Conventional Commits format. This keeps squash-merge commit titles compatible with Release Please.
- Release Please opens or updates a release PR that updates
CHANGELOG.md,pyproject.toml,.release-please-manifest.json, and versioned README install examples. - Merging the release PR creates the GitHub Release tag. The release workflow then builds the wheel and source distribution, uploads them as GitHub Release assets, and publishes
vdustr/font-splitterto Docker Hub whenDOCKERHUB_TOKENis configured. - Docker images are published as
vX.Y.Z,X.Y.Z, andlatesttags. The separate Docker workflow is manual-only and is used to backfill or repair an existing release image. - If Release Please PR checks must run automatically, configure
RELEASE_PLEASE_TOKEN; otherwise the workflow falls back to GitHub's default token. - PyPI publishing is intentionally not configured yet. The documented install path is the GitHub tag URL.
Release Please was bootstrapped from the last npm release, v0.1.5. The first Python release was v0.2.0.
Unit tests use generated in-memory fonts to avoid unclear redistribution rights. Generated fixtures cover multiple Unicode blocks, WOFF output, and WOFF2 output.
Do not commit third-party font files unless all of these are documented with the fixture:
- source URL
- license
- checksum
- reason the generated fixtures are insufficient
Large or CJK-heavy integration fixtures should be downloaded through a checksum-verified helper instead of committed directly.
The optional downloaded-font integration test is configured through environment variables:
FONT_SPLITTER_FIXTURE_URL=https://example.com/font.ttf \
FONT_SPLITTER_FIXTURE_SHA256=<sha256> \
.venv/bin/python -m pytest tests/test_downloaded_fixture.py -v