Flutter bindings for the fontations Rust font stack, generated with flutter_rust_bridge.
Dart can read OpenType fonts directly — names, metrics, glyphs, outlines and layout tables — and write fonts from contours you draw. No platform text stack is involved, and variable fonts work at any location.
English | 简体中文
import 'package:flutter/services.dart' show rootBundle;
import 'package:fontations/fontations.dart';
await initFontations(); // once, before anything else
final bytes = await rootBundle
.load('assets/fonts/Inter.ttf')
.then((data) => data.buffer.asUint8List());
final font = Font.fromBytes(bytes: bytes);
print(font.info().names.family); // Inter
print(font.info().metrics.unitsPerEm); // 2048
final glyphId = font.charToGlyph(codePoint: 'A'.codeUnitAt(0))!;
final outline = await font.outline(
glyphId: glyphId,
size: 48,
variations: const [VariationSetting(tag: 'wght', value: 700)],
);
final path = outline.toPath(); // draw it with a CustomPainterNote
- Pass
indextoFont.fromBytesto pick a face out of a.ttcfile.Font.countFontstells you how many there are. size: nullgives font units instead of pixels. Axis values are in user space (wght: 700), just like CSSfont-variation-settings.outlineandoutlinesare async; otherFontreads are synchronous. Every font-writing function is async.- A
Fontkeeps the whole file in Rust. Reuse the handle instead of parsing the same bytes again. woff/woff2are compressed containers; decompress them first.
Font.outline draws through the skrifa 0.50 scaler and returns path elements, bounds, advance width and a scaler field describing the hint engine that ran. Font.outlines handles a list of glyphs with one scaler. Use hinting: HintingSpec(...) to choose an engine and raster target; the older hinted: true still works.
Outlines stay linearly scaled: hinting never silently changes bounds or advanceWidth. The plugin returns paths, not pixels — rasterization is Flutter's canvas. GlyphOutline.toPath() flips Y for you.
tableData(tag) returns the raw bytes of any table, offsets relative to the start of that table. hasTable and tableLength answer the obvious questions. For GSUB and GPOS, layout() gives a structured view of scripts, languages and features.
final gsub = font.tableData(tag: 'GSUB'); // null when absent
print(font.layout(table: 'GSUB').single.features.map((f) => f.tag));extendedTables() structures SVG documents, gasp ranges and meta entries, and reports whether BASE, the AAT tables, cvar, VVAR, MVAR, DSIG and hdmx are present.
colorPalettes()/colorGlyph()flattenCOLRglyphs into layers.ColorGlyphInfo.paint(canvas, size:, unitsPerEm:)draws them; gradients are already resolved against the palette.bitmapStrikes()/bitmapGlyph()readsbix,CBDTandEBDTstrikes, decoded to RGBA (or raw PNG forsbix).verticalMetrics(),verticalAdvance()andverticalOrigin()readvhea,vmtxandVORG.attributes(),glyphName()andnameRecords()cover style info, glyph names and localized names.
hasMath(),mathConstants({ppem}),mathGlyphInfo()andmathVariants(glyphId, axis, {ppem})expose theMATHdata: constants, per glyph corrections and kerns, ready-made delimiter sizes and assembly parts.valueanddeltaPxare separate because a device table adjusts by whole pixels. The plugin returns data, it does not lay out formulas.hasVarc()andvarcComponents()return the parts of aVARCcomposite, with transforms, axis values and whether its condition holds.outline()already flattens the composite into its final outline.mapVariant(codePoint:, selector:)andvariantMappings()readcmapformat 14.avarInfo()andnormalizedCoords()show howavarandavar2remap an axis. Every API that takesvariationsapplies these steps internally, so these are mainly for inspection.
layout() describes the layout tables; shape() applies them. It maps text through cmap, runs the selected GSUB and GPOS rules, and returns positioned glyphs.
final result = font.shape(
text: 'ffi',
size: 48,
variations: const [],
features: ['liga', 'kern'],
);
for (final glyph in result.glyphs) {
// glyph.glyphId, glyph.xAdvance, glyph.xOffset, glyph.yOffset, glyph.cluster
}script and language are four-character OpenType tags; omitted, the font's default script and language are used. features: null enables the usual defaults (kern, liga, ccmp, mark, mkmk, calt, …), a list names exactly the features to apply, and an empty list turns everything off — useful for comparing shaped and unshaped output. cluster is the index of the source character, so a ligature covers several and a decomposition shares one.
Covered: all GSUB substitutions (single, multiple, alternate, ligature, contextual, chained contextual, reverse chain) and GPOS positioning (single, pair, cursive, mark-to-base / ligature / mark, contextual). Both the original and the 2026 v2 subtable encodings are read. Feature variations are applied at the current variation location: condition sets (axis ranges, variable values, AND/OR/NOT) select a record that replaces the lookup lists of named features, and version 1.1 lookup variations are applied too.
This is a level 1 shaper, not HarfBuzz. There is no Unicode script detection, normalization or cluster splitting, and no vertical or right-to-left runs. Latin, Cyrillic and Greek scripts are supported; use the platform text stack for Arabic and Indic.
buildFont compiles contours in font units (Y up) into a .ttf, which you can register with FontLoader and use directly in a Text widget:
final bytes = await buildFont(
request: FontBuildRequest(
familyName: 'MyHandwriting',
unitsPerEm: 1000,
ascender: 800,
descender: -200,
glyphs: [
GlyphSpec(codePoint: '一'.runes.first, contours: myContours, advanceWidth: 1000),
],
),
);
await (FontLoader('MyHandwriting1')
..addFont(Future.value(ByteData.sublistView(bytes))))
.load();Consecutive off-curve points follow TrueType semantics (implied midpoints). The example glyph editor page (live demo) demonstrates the full draw-and-export flow.
What else FontBuildRequest can carry:
| Field | Result |
|---|---|
axes, instances, GlyphSpec.masters |
A variable font with fvar, gvar and STAT. |
kernPairs, ligatures |
GPOS kerning and GSUB ligatures. |
GlyphSpec.alternates |
Discrete GSUB substitutions such as ss01 / cv01, turned on with FontFeature. |
GlyphSpec.components |
Composite glyphs that reuse other glyphs from the same request. |
palette, GlyphSpec.colorLayers |
COLR v0 color glyphs with a CPAL palette. |
verticalMetrics, GlyphSpec.instructions, rawTables, variationSelectors |
vhea/vmtx, TrueType hinting, verbatim tables, cmap format 14. |
subfamilyName, designer, license, version |
name and OS/2 metadata. |
Variable fonts need point compatible masters: same contour count, same point count, same order. The example resamples every stroke to a fixed number of points to guarantee that; incompatible masters are rejected instead of producing a broken font. All axis values are in user space. VariationAxisSpec.mappings writes avar, and when every master declares advanceWidth the font also gets HVAR plus MVAR for global metrics (hasc, hdsc, hlgp, hcla).
modifyFont rewrites only what the request names — metadata, kerning, ligatures and substitutions — and keeps outlines, cmap and glyph ids untouched, so existing shaping data stays valid.
The add* functions install individual tables. They work on any font, including third-party ones, and leave every table they do not mention unchanged:
| Function | Writes |
|---|---|
addBitmapStrikes |
sbix bitmap strikes. |
addSubstitutions |
GSUB multiple / alternate / contextual / reverse-chain lookups, original or 2026 v2 form. |
addColorPaints |
COLR v1 paint graphs and CPAL. |
addFeatureVariations |
Version 1.1 GSUB / GPOS feature variations: condition sets and feature table substitutions. |
addMathTable |
MATH constants, glyph corrections and stretchy constructions. |
addVarcGlyphs |
VARC composites, conditions, transforms and a multi item variation store. |
addSvgDocuments, addVerticalOrigins |
SVG documents and VORG vertical origins. |
addRawTables |
Arbitrary table bytes, e.g. hand-built VVAR, CVAR or CBDT. |
For example, this turns ligatures off once the first axis reaches the upper half of its normalized range (0.5 to 1.0), by substituting an empty lookup list:
final patched = await addFeatureVariations(
bytes: original,
request: FeatureVariationPatchRequest(
table: 'GSUB',
conditions: [
FeatureConditionSpec(
kind: 'axisRange', axisIndex: 0,
minValue: 0.5, maxValue: 1.0,
defaultValue: 0, varIndex: 0xFFFFFFFF,
children: Uint32List(0),
),
],
records: [
FeatureVariationRecordSpec(
conditionIndices: Uint32List.fromList([0]),
substitutions: [
FeatureSubstitutionSpec(featureTag: 'liga', lookupIndices: Uint32List(0)),
],
),
],
),
);Conditions are a flat list; a compound condition may only refer to earlier entries, which keeps the set acyclic. Everything written by these functions can be read back through the corresponding Font methods.
subsetFont keeps only the characters or glyphs you ask for and rewrites cmap, layout, hinting and variation tables — the recommended approach for shipping a large CJK font. withVariations() returns a handle pinned to one variation location, so later reads need not repeat the settings; it is a view, not a smaller font file.
final subset = await subsetFont(
bytes: original,
request: FontSubsetRequest(
codePoints: Uint32List.fromList('ABC'.runes.toList()),
glyphIds: Uint32List(0),
retainGlyphIds: false,
noHinting: false,
passthroughUnrecognized: false,
glyphNames: false,
keepEverything: false,
layoutFeatures: const [],
dropTables: const [],
),
);
final bold = font.withVariations(
variations: const [VariationSetting(tag: 'wght', value: 700)],
);| Dart | Description |
|---|---|
initFontations() |
Loads the native library. Call once. |
Font.fromBytes(bytes, {index}), Font.countFonts(bytes), readFontInfo(bytes, {index}) |
Parse a font (or one face of a .ttc), count faces, read metadata without keeping a handle. |
Font.info(), .names(), .metrics(), .axes(), .namedInstances(), .tables() |
Names, metrics, axes, instances and table tags. |
Font.charToGlyph(), .charmapInfo(), .charMappings(), .glyphToChar() |
cmap lookups in both directions. |
Font.advanceWidth(), .glyphMetrics() |
Advance width, side bearing and bounds. |
Font.outline(), .outlines(), .outlineFormat(), GlyphOutline.toPath() |
Scaled outlines and the dart:ui path. |
Font.tableData(), .tableLength(), .hasTable(), .layout() |
Raw table bytes and structured GSUB / GPOS. |
Font.attributes(), .glyphName(), .nameRecords() |
Style attributes, glyph names, localized names. |
Font.verticalMetrics(), .verticalAdvance(), .verticalOrigin() |
Vertical text data. |
Font.colorPalettes(), .hasColorGlyph(), .colorGlyph(), ColorGlyphInfo.paint() |
CPAL and COLR glyphs. |
Font.bitmapStrikes(), .bitmapGlyph() |
Embedded bitmap strikes. |
Font.shape() |
Applies GSUB / GPOS, returns positioned glyphs. |
Font.hasMath(), .mathConstants(), .mathGlyphInfo(), .mathVariants() |
MATH constants and stretchy glyphs. |
Font.hasVarc(), .varcComponents() |
VARC components. |
Font.mapVariant(), .variantMappings(), .avarInfo(), .normalizedCoords() |
cmap format 14 and axis remapping. |
Font.withVariations(), .pinnedVariations(), .extendedTables() |
Pinned handle and the less common tables. |
fontationsVersion() |
Versions of the linked fontations crates. |
buildFont(request), modifyFont(bytes, request), subsetFont(bytes, request) |
Write, patch and subset fonts. |
addBitmapStrikes, addSubstitutions, addColorPaints, addFeatureVariations, addMathTable, addVarcGlyphs, addSvgDocuments, addVerticalOrigins, addRawTables |
Install individual tables in an existing font. |
fontations/
├── lib/ # public Dart API (fontations.dart)
│ └── src/ # glyph_path, color_glyph, library loader
│ └── rust/ # generated bindings — do not edit
├── rust/
│ ├── src/
│ │ ├── api/ # single source of the Dart API
│ │ │ ├── fontations.rs # read
│ │ │ ├── build.rs # write
│ │ │ └── upstream.rs # patches
│ │ ├── *.rs # implementations (shaping, math, varc, ...)
│ │ └── frb_generated.rs # generated — do not edit
│ └── fuzz/ # fuzz targets, seeds and tests
├── cargokit/ # Rust build for Gradle, CMake and CocoaPods
├── android/, linux/, windows/ # platform runners
├── ios/, macos/ # SwiftPM packages with prebuilt XCFrameworks
├── scripts/ # XCFramework, wasm, test and COOP/COEP helpers
├── test/ # Dart unit and integration tests
└── example/ # demo app
After changing rust/src/api/:
flutter_rust_bridge_codegen generate # cargo install flutter_rust_bridge_codegen --version 2.13.0
./scripts/build_xcframework.shThe codegen version must match Cargo.toml; CI checks that the committed binaries match rust/.
| Platform | Build |
|---|---|
| Android | Gradle + NDK, needs cargo-ndk. |
| Linux / Windows | CMake. |
| iOS / macOS | SwiftPM prebuilt XCFrameworks, or CocoaPods from source. |
Flutter picks SwiftPM or CocoaPods automatically; no app-side configuration is needed.
./scripts/test.sh is the single entry point; CI calls the same script.
./scripts/test.sh # all suites except on-device
./scripts/test.sh --unit # pure Dart only
./scripts/test.sh --integration # Dart <-> Rust boundary
./scripts/test.sh --example # example app widget tests
./scripts/test.sh --rust # cargo test --all-targets
./scripts/test.sh --fuzz # deterministic fuzz target tests
./scripts/test.sh --device macos # on-device integration_test
./scripts/test.sh --coverage # coverage/lcov.{unit,integration,example}.info
./scripts/test.sh --no-build # reuse an existing host libraryFlags can be combined. The web path is compile-checked, not browser-tested.
.github/workflows/ci.yml— eight jobs: Rust, fuzz, Dart, web, artifacts, macOS, Windows and Linux..github/workflows/deploy_web.yml— publishesexample/to GitHub Pages (main, PR previews at/pr-<number>/)..github/workflows/build_apps.yml— builds release APK / Windows / Linux / Web bundles ofexample/.
./scripts/build_web.sh # writes example/web/pkg
cd example && flutter build web --no-web-resources-cdnCopy pkg/ into your web root. The wasm module is threaded, so the page must be cross-origin isolated (Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp); example/web/coi_serviceworker.js adds these headers when the server cannot.
- Writing:
CFF/CFF2output (upstream has no charstring encoder),CBDT/EBDT/EBLC/CBLCbitmaps,gvardeltas on composite glyphs, typed requests forVVAR/CVAR(useaddRawTablesfor hand-built bytes), and structured v2GPOSwriting. - Reading: complex-script shaping (use the platform text stack), formula layout from
MATHdata, and structuredBASE/ AAT parsing — those tables are only reported as present or absent. - Upstream crates:
incremental-font-transferand theiftfeatures ofread-fonts/write-fonts. - The plugin returns paths plus scaler settings; bitmap rasterization is left to the caller.
See LICENSE. The bundled example font Inter is licensed under the SIL Open Font License 1.1. The underlying fontations Rust crate is licensed under its own terms — see the upstream repository for details.
If fontations helps you read and write fonts in Flutter, please consider supporting it.
It only takes a few seconds and helps other Flutter developers discover the library.
| Buy Me a Coffee | Donate with PayPal |
|---|---|
![]() |
![]() |

