Skip to content

VdustR/font-splitter

Repository files navigation

Font Splitter

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.

Requirements

  • Python 3.11+
  • Git, when installing directly from GitHub
  • FontTools with WOFF/WOFF2 support, installed automatically by this package

Install From GitHub

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"

CLI Quick Start

Split a font with the default Unicode Blocks fallback:

font-splitter NotoSansTC.ttf --output dist

Use 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 dist

Plan the split without writing files:

font-splitter NotoSansTC.ttf --dry

Disable fallback when only the CSS template ranges should be emitted:

font-splitter NotoSansTC.ttf \
  --source font-css:noto-sans-tc.css \
  --no-fallback \
  --output dist

Use no-split mode:

font-splitter NotoSansTC.ttf --chunk - --output dist

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

Python API

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.

Range Sources

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-face rules with unicode-range descriptors 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.

Docker

Use the published Docker Hub image:

docker run --rm -v "$PWD:/fonts" vdustr/font-splitter:v0.2.1 input.ttf --output output

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

Migration From v1

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.
  • --quite is accepted as a deprecated alias, but --quiet is the documented spelling.
  • woff and woff2 remain the supported webfont output flavors.
  • Font CSS input is now a generic @font-face unicode-range source, not a Google Fonts-specific parser.

License And Font Rights

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.

Development

Set up a local environment:

python3 -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'

Run tests:

.venv/bin/python -m pytest -v

Build distributions:

.venv/bin/python -m build

Release Flow

Releases 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 ! or BREAKING CHANGE: marks a breaking change. While the package is still 0.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-splitter to Docker Hub when DOCKERHUB_TOKEN is configured.
  • Docker images are published as vX.Y.Z, X.Y.Z, and latest tags. 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.

Test Fixture Policy

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

About

Python library and CLI for splitting large fonts into unicode-range WOFF/WOFF2 subsets.

Topics

Resources

License

Stars

40 stars

Watchers

1 watching

Forks

Contributors

Languages