Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
A
Axiom
/Marketplace/christiangeorgelucas/font-tools
Sign in

christiangeorgelucas/font-tools

v0.1.0Python

The font-tools package provides deterministic and stateless structural parsing, low-level table inspection, and container-format conversion for SFNT-based font files such as TTF, OTF, WOFF, and WOFF2. It wraps the fontTools library to enable detailed inspection of font tables and conversion between different font formats, making it ideal for developers working with font file structures and web font optimization.

Published by Christian George Lucas· MIT

font-parsingfont-conversionsfntttfotfwoffwoff2table-inspectionvariable-fonts

Use cases

  • List all tables in a font's SFNT table directory
  • Convert TTF fonts to WOFF2 for web deployment
  • Read and analyze font metrics from the OS/2 table
  • Extract glyph names from a font's post table
  • Pin variable font design axes to create static instances

Nodes (9)

ConvertFontFormatunary
ConvertFontFormatRequest·ConvertFontFormatResponse

The ConvertFontFormat node facilitates the conversion of font files between various container formats, specifically SFNT (TTF/OTF), WOFF, and WOFF2, while maintaining the integrity of the font's table content. This is particularly useful for generating web-compatible font formats from desktop versions or for extracting raw SFNT data from compressed formats.

View source
import io

from gen.messages_pb2 import ConvertFontFormatRequest, ConvertFontFormatResponse
from gen.axiom_context import AxiomContext

from nodes._common import load_font, make_error, detect_format

# target_format value -> fontTools TTFont.flavor value. "ttf"/"otf"/"sfnt"
# are all aliases for the plain (uncompressed) SFNT container -- the
# source's own outline format (TrueType glyf-based vs. CFF-based OpenType)
# is preserved either way; this package doesn't convert outline formats,
# only the outer container.
_TARGET_FLAVORS = {
    "ttf": None,
    "otf": None,
    "sfnt": None,
    "woff": "woff",
    "woff2": "woff2",
}


def convert_font_format(ax: AxiomContext, input: ConvertFontFormatRequest) -> ConvertFontFormatResponse:
    """Convert a font between SFNT (TTF/OTF), WOFF, and WOFF2 container
    formats, preserving every table's content -- only the outer container
    and compression change. Useful for producing a web-deployable WOFF2
    from a desktop TTF, or unwrapping a WOFF/WOFF2 back to raw SFNT for
    tools (like this package's other nodes) that expect it.
    """
    loaded, font_error = load_font(input.font)
    if font_error is not None:
        return ConvertFontFormatResponse(error=font_error)

    target = input.target_format.strip().lower()
    if target not in _TARGET_FLAVORS:
        return ConvertFontFormatResponse(
            error=make_error(
                "INVALID_ARGUMENT",
                f"unsupported target_format '{input.target_format}' -- must be one of "
                f"{sorted(set(_TARGET_FLAVORS) - {'sfnt', 'otf'})}",
            )
        )

    source_format = detect_format(loaded.raw_data)

    try:
        loaded.ttfont.flavor = _TARGET_FLAVORS[target]
        out = io.BytesIO()
        loaded.ttfont.save(out)
        converted = out.getvalue()
    except Exception as exc:
        return ConvertFontFormatResponse(
            error=make_error("CONVERSION_FAILED", f"could not convert to '{target}': {exc}")
        )

    return ConvertFontFormatResponse(
        converted_font_data=converted,
        source_format=source_format,
        target_format=target,
        original_size_bytes=len(loaded.raw_data),
        converted_size_bytes=len(converted),
    )
GetGlyphNamesunary
GetGlyphNamesRequest·GetGlyphNamesResponse

The GetGlyphNames node retrieves the complete order of glyph names from a font's 'post' table, including synthesized placeholders for CFF fonts with no per-glyph names. It also reports the format of the post table present in the font.

View source
from gen.messages_pb2 import GetGlyphNamesRequest, GetGlyphNamesResponse
from gen.axiom_context import AxiomContext

from nodes._common import load_font, make_error


def get_glyph_names(ax: AxiomContext, input: GetGlyphNamesRequest) -> GetGlyphNamesResponse:
    """List the font's complete glyph order: every glyph index's name, from
    its 'post' table (or synthesized "glyphNNNNN" placeholders for a CFF
    font with post format 3.0, which declares no per-glyph names).
    post_format reports which format was actually present.
    """
    loaded, font_error = load_font(input.font)
    if font_error is not None:
        return GetGlyphNamesResponse(error=font_error)

    try:
        names = list(loaded.ttfont.getGlyphOrder())
        post_format = "unknown"
        if "post" in loaded.ttfont:
            post_format = f"{float(loaded.ttfont['post'].formatType):.1f}"
    except Exception as exc:
        return GetGlyphNamesResponse(
            error=make_error("INVALID_FONT", f"could not read glyph order: {exc}")
        )

    return GetGlyphNamesResponse(glyph_names=names, post_format=post_format)
GetHeadTableunary
GetHeadTableRequest·GetHeadTableResponse

The GetHeadTable node reads the 'head' table of a font, extracting key metadata such as units-per-em, font revision, timestamps, glyph bounding box dimensions, macStyle flags for bold and italic, and the loca-table format used by the font.

View source
from gen.messages_pb2 import GetHeadTableRequest, GetHeadTableResponse, HeadInfo
from gen.axiom_context import AxiomContext

from nodes._common import load_font, make_error, mac_datetime_to_rfc3339


def get_head_table(ax: AxiomContext, input: GetHeadTableRequest) -> GetHeadTableResponse:
    """Read the font's 'head' table: units-per-em (the design-grid scale),
    font revision, creation/modification timestamps, the font's overall
    glyph bounding box, decoded macStyle bold/italic flags, and which
    loca-table format (short/long offsets) the font uses internally.
    """
    loaded, font_error = load_font(input.font)
    if font_error is not None:
        return GetHeadTableResponse(error=font_error)

    try:
        head = loaded.ttfont["head"]
        mac_style = int(head.macStyle)
        info = HeadInfo(
            units_per_em=int(head.unitsPerEm),
            font_revision=f"{float(head.fontRevision):.4f}".rstrip("0").rstrip("."),
            created=mac_datetime_to_rfc3339(head.created),
            modified=mac_datetime_to_rfc3339(head.modified),
            x_min=int(head.xMin),
            y_min=int(head.yMin),
            x_max=int(head.xMax),
            y_max=int(head.yMax),
            style_bold=bool(mac_style & 0x1),
            style_italic=bool(mac_style & 0x2),
            index_to_loc_format=int(head.indexToLocFormat),
        )
    except Exception as exc:
        return GetHeadTableResponse(
            error=make_error("INVALID_FONT", f"could not read 'head' table: {exc}")
        )

    return GetHeadTableResponse(head=info)
GetHheaTableunary
GetHheaTableRequest·GetHheaTableResponse

The GetHheaTable node retrieves the 'hhea' table from a font, which contains horizontal layout metrics such as ascender, descender, and line gap. This information is used by some renderers for default line layout when OS/2 typography metrics are not preferred.

View source
from gen.messages_pb2 import GetHheaTableRequest, GetHheaTableResponse, HheaInfo
from gen.axiom_context import AxiomContext

from nodes._common import load_font, make_error


def get_hhea_table(ax: AxiomContext, input: GetHheaTableRequest) -> GetHheaTableResponse:
    """Read the font's 'hhea' table: the horizontal-layout metrics
    (ascender, descender, line gap, advance-width max, side bearings,
    caret slope, hMetrics count) some renderers use for default line
    layout when OS/2 typo metrics aren't preferred.
    """
    loaded, font_error = load_font(input.font)
    if font_error is not None:
        return GetHheaTableResponse(error=font_error)

    try:
        hhea = loaded.ttfont["hhea"]
        info = HheaInfo(
            ascender=int(hhea.ascender),
            descender=int(hhea.descender),
            line_gap=int(hhea.lineGap),
            advance_width_max=int(hhea.advanceWidthMax),
            min_left_side_bearing=int(hhea.minLeftSideBearing),
            min_right_side_bearing=int(hhea.minRightSideBearing),
            x_max_extent=int(hhea.xMaxExtent),
            caret_slope_rise=int(hhea.caretSlopeRise),
            caret_slope_run=int(hhea.caretSlopeRun),
            caret_offset=int(hhea.caretOffset),
            number_of_h_metrics=int(hhea.numberOfHMetrics),
        )
    except Exception as exc:
        return GetHheaTableResponse(
            error=make_error("INVALID_FONT", f"could not read 'hhea' table: {exc}")
        )

    return GetHheaTableResponse(hhea=info)
GetKerningPairsunary
GetKerningPairsRequest·GetKerningPairsResponse

The GetKerningPairs node retrieves legacy kerning adjustments from a font's old-style 'kern' table, specifically from format-0 subtables. If the font does not contain a 'kern' table, it returns an empty list instead of an error.

View source
from gen.messages_pb2 import GetKerningPairsRequest, GetKerningPairsResponse, KerningPair
from gen.axiom_context import AxiomContext

from nodes._common import load_font, make_error


def get_kerning_pairs(ax: AxiomContext, input: GetKerningPairsRequest) -> GetKerningPairsResponse:
    """Read legacy pair-kerning adjustments from the font's old-style
    'kern' table (format-0 subtables). Most current fonts use GPOS
    kerning instead and have no 'kern' table -- that's reported as an
    empty list, not an error.
    """
    loaded, font_error = load_font(input.font)
    if font_error is not None:
        return GetKerningPairsResponse(error=font_error)

    if "kern" not in loaded.ttfont:
        return GetKerningPairsResponse(pairs=[])

    try:
        kern = loaded.ttfont["kern"]
        pairs = []
        for subtable in getattr(kern, "kernTables", []):
            kern_table = getattr(subtable, "kernTable", None)
            if not kern_table:
                continue
            for (left, right), value in kern_table.items():
                pairs.append(
                    KerningPair(left_glyph=str(left), right_glyph=str(right), value=int(value))
                )
    except Exception as exc:
        return GetKerningPairsResponse(
            error=make_error("INVALID_FONT", f"could not read 'kern' table: {exc}")
        )

    return GetKerningPairsResponse(pairs=pairs)
GetOS2Metricsunary
GetOS2MetricsRequest·GetOS2MetricsResponse

The GetOS2Metrics node reads the 'OS/2' table of a font, extracting various metrics such as weight and width classifications, embedding permissions, typographic metrics, and PANOSE classification. It also handles cases where fields introduced in later versions of the OS/2 table are not present by returning zero for those fields.

View source
from gen.messages_pb2 import GetOS2MetricsRequest, GetOS2MetricsResponse, OS2Info
from gen.axiom_context import AxiomContext

from nodes._common import load_font, make_error

_ITALIC = 0x0001
_BOLD = 0x0020
_REGULAR = 0x0040

_PANOSE_FIELDS = (
    "bFamilyType",
    "bSerifStyle",
    "bWeight",
    "bProportion",
    "bContrast",
    "bStrokeVariation",
    "bArmStyle",
    "bLetterForm",
    "bMidline",
    "bXHeight",
)


def get_os2_metrics(ax: AxiomContext, input: GetOS2MetricsRequest) -> GetOS2MetricsResponse:
    """Read the font's 'OS/2' table: weight/width classification,
    embedding-permission bits (fsType, for web-font licensing checks),
    typographic and Windows ascent/descent metrics, x-height/cap-height,
    PANOSE classification, vendor ID, and decoded fsSelection
    bold/italic/regular flags. Fields introduced in later OS/2 versions
    are zero when the font's table version predates them (version is
    reported so callers can tell).
    """
    loaded, font_error = load_font(input.font)
    if font_error is not None:
        return GetOS2MetricsResponse(error=font_error)

    if "OS/2" not in loaded.ttfont:
        return GetOS2MetricsResponse(
            error=make_error("TABLE_NOT_FOUND", "font has no 'OS/2' table")
        )

    try:
        os2 = loaded.ttfont["OS/2"]
        fs_selection = int(os2.fsSelection)
        panose = [int(getattr(os2.panose, f, 0)) for f in _PANOSE_FIELDS]
        info = OS2Info(
            version=int(os2.version),
            weight_class=int(os2.usWeightClass),
            width_class=int(os2.usWidthClass),
            fs_type=int(os2.fsType),
            s_typo_ascender=int(os2.sTypoAscender),
            s_typo_descender=int(os2.sTypoDescender),
            s_typo_line_gap=int(os2.sTypoLineGap),
            us_win_ascent=int(os2.usWinAscent),
            us_win_descent=int(os2.usWinDescent),
            # sxHeight/sCapHeight only exist on OS/2 version >= 2.
            s_x_height=int(getattr(os2, "sxHeight", 0)),
            s_cap_height=int(getattr(os2, "sCapHeight", 0)),
            panose=panose,
            ach_vend_id=str(os2.achVendID).strip(),
            selection_italic=bool(fs_selection & _ITALIC),
            selection_bold=bool(fs_selection & _BOLD),
            selection_regular=bool(fs_selection & _REGULAR),
        )
    except Exception as exc:
        return GetOS2MetricsResponse(
            error=make_error("INVALID_FONT", f"could not read 'OS/2' table: {exc}")
        )

    return GetOS2MetricsResponse(os2=info)
InstantiateVariableFontunary
InstantiateVariableFontRequest·InstantiateVariableFontResponse

This node pins an OpenType Variable Font's design axes to specific coordinates, creating a standalone static font by removing the variable tables and resolving glyph outlines at the specified design space point.

View source
import io

from fontTools.varLib.instancer import instantiateVariableFont

from gen.messages_pb2 import InstantiateVariableFontRequest, InstantiateVariableFontResponse
from gen.axiom_context import AxiomContext

from nodes._common import load_font, make_error


def instantiate_variable_font(ax: AxiomContext, input: InstantiateVariableFontRequest) -> InstantiateVariableFontResponse:
    """Pin an OpenType Variable Font's design axes (e.g. wght, wdth, opsz)
    to specific coordinates, producing a standalone static font with the
    variable 'fvar'/'gvar'/'avar' tables removed and glyph outlines
    resolved at that exact point in the design space. Any axis left
    unspecified is pinned at the font's own declared default.
    """
    loaded, font_error = load_font(input.font)
    if font_error is not None:
        return InstantiateVariableFontResponse(error=font_error)

    if "fvar" not in loaded.ttfont:
        return InstantiateVariableFontResponse(
            error=make_error("TABLE_NOT_FOUND", "font has no 'fvar' table -- not a variable font")
        )

    fvar = loaded.ttfont["fvar"]
    axis_defaults = {axis.axisTag: axis.defaultValue for axis in fvar.axes}

    axis_limits = dict(axis_defaults)
    for coord in input.coordinates:
        if coord.tag not in axis_defaults:
            return InstantiateVariableFontResponse(
                error=make_error(
                    "INVALID_ARGUMENT",
                    f"'{coord.tag}' is not an axis of this font -- known axes: "
                    f"{sorted(axis_defaults)}",
                )
            )
        axis_limits[coord.tag] = coord.value

    try:
        instantiateVariableFont(loaded.ttfont, axis_limits, inplace=True)
        out = io.BytesIO()
        loaded.ttfont.save(out)
        instance_bytes = out.getvalue()
    except Exception as exc:
        return InstantiateVariableFontResponse(
            error=make_error("INSTANCING_FAILED", f"could not instantiate variable font: {exc}")
        )

    return InstantiateVariableFontResponse(
        instance_font_data=instance_bytes,
        instance_size_bytes=len(instance_bytes),
    )
ListCmapSubtablesunary
ListCmapSubtablesRequest·ListCmapSubtablesResponse

The ListCmapSubtables node enumerates all subtables in a font's 'cmap' table, providing details such as platform ID, encoding ID, subtable format, and the number of codepoint-to-glyph mappings. This allows users to determine which character encodings are supported by the font without needing to decode each mapping.

View source
from gen.messages_pb2 import (
    ListCmapSubtablesRequest,
    ListCmapSubtablesResponse,
    CmapSubtableInfo,
)
from gen.axiom_context import AxiomContext

from nodes._common import load_font, make_error


def list_cmap_subtables(ax: AxiomContext, input: ListCmapSubtablesRequest) -> ListCmapSubtablesResponse:
    """Enumerate every subtable in the font's 'cmap' table: platform ID,
    encoding ID, subtable format, and how many codepoint-to-glyph mappings
    it declares. Answers "which character encodings does this font
    actually support" without decoding every mapping.
    """
    loaded, font_error = load_font(input.font)
    if font_error is not None:
        return ListCmapSubtablesResponse(error=font_error)

    try:
        cmap = loaded.ttfont["cmap"]
        subtables = [
            CmapSubtableInfo(
                platform_id=int(st.platformID),
                encoding_id=int(st.platEncID),
                format=int(st.format),
                num_mappings=len(st.cmap),
            )
            for st in cmap.tables
        ]
    except Exception as exc:
        return ListCmapSubtablesResponse(
            error=make_error("INVALID_FONT", f"could not read 'cmap' table: {exc}")
        )

    return ListCmapSubtablesResponse(subtables=subtables)
ListTablesunary
ListTablesRequest·ListTablesResponse

The ListTables node retrieves and lists all tables present in a font's SFNT table directory, including their tags, byte lengths, and checksums, while also identifying the font's container format (TrueType, OpenType-CFF, WOFF, or WOFF2). This node provides a quick and efficient way to determine the specific tables contained within a font file.

View source
from gen.messages_pb2 import ListTablesRequest, ListTablesResponse, TableEntry
from gen.axiom_context import AxiomContext

from nodes._common import load_font, detect_format, make_error


def list_tables(ax: AxiomContext, input: ListTablesRequest) -> ListTablesResponse:
    """List every table in a font's SFNT table directory (tag, byte length,
    checksum) plus the container's decoded format label (truetype,
    opentype-cff, woff, or woff2). The fastest way to answer "what tables
    does this font actually contain."
    """
    loaded, font_error = load_font(input.font)
    if font_error is not None:
        return ListTablesResponse(error=font_error)

    try:
        entries = []
        for tag, directory_entry in loaded.ttfont.reader.tables.items():
            entries.append(
                TableEntry(
                    tag=str(tag),
                    length=int(directory_entry.length),
                    # WOFF2 directory entries don't record a per-table
                    # checksum (only plain SFNT/WOFF do) -- 0 in that case.
                    checksum=int(getattr(directory_entry, "checkSum", 0)) & 0xFFFFFFFF,
                )
            )
        entries.sort(key=lambda e: e.tag)
    except Exception as exc:
        return ListTablesResponse(
            error=make_error("INVALID_FONT", f"could not read table directory: {exc}")
        )

    return ListTablesResponse(
        tables=entries,
        format=detect_format(loaded.raw_data),
    )

Messages (27)

Download .proto
AxisCoordinate
tag:string
value:double
CmapSubtableInfo
platform_id:int32
encoding_id:int32
format:int32
num_mappings:int32
ConvertFontFormatRequest
font:Font

The input font data that needs to be converted.

target_format:string

The desired output format for the converted font, which can be WOFF or WOFF2.

ConvertFontFormatResponse
converted_font_data:bytes
source_format:string
target_format:string

The desired output format for the converted font, which can be WOFF or WOFF2.

original_size_bytes:int32
converted_size_bytes:int32
error:Error
Error
code:string
message:string
Font
font_data:bytes
face_index:int32
GetGlyphNamesRequest
font:Font

The input field specifying the font from which to retrieve glyph names.

GetGlyphNamesResponse
glyph_names:[]string

The output field containing a list of glyph names in the order defined by the font's 'post' table.

post_format:string

The output field indicating the format type of the 'post' table present in the font.

error:Error
GetHeadTableRequest
font:Font

The font file from which the 'head' table will be read.

GetHeadTableResponse
head:HeadInfo

An object containing the extracted information from the 'head' table, including units-per-em, font revision, timestamps, bounding box dimensions, style flags, and loca-table format.

error:Error

An error message indicating any issues encountered while reading the 'head' table.

GetHheaTableRequest
font:Font

The font from which the 'hhea' table is to be read.

GetHheaTableResponse
hhea:HheaInfo

An object containing the horizontal layout metrics extracted from the 'hhea' table.

error:Error

An error message indicating any issues encountered while reading the 'hhea' table.

GetKerningPairsRequest
font:Font

The input field specifying the font from which to read kerning pairs.

GetKerningPairsResponse
pairs:[]KerningPair

The output field containing a list of kerning pairs, each with left and right glyphs and their corresponding adjustment values.

error:Error

An optional field that contains error information if the font cannot be loaded or if there is an issue reading the 'kern' table.

GetOS2MetricsRequest
font:Font

The font file from which the OS/2 metrics will be extracted.

GetOS2MetricsResponse
os2:OS2Info

An object containing the extracted OS/2 metrics, including weight class, width class, and various typographic metrics.

error:Error

An error message indicating any issues encountered while reading the OS/2 table.

HeadInfo
units_per_em:int32
font_revision:string
created:string
modified:string
x_min:int32
y_min:int32
x_max:int32
y_max:int32
style_bold:bool
style_italic:bool
index_to_loc_format:int32
HheaInfo
ascender:int32
descender:int32
line_gap:int32
advance_width_max:int32
min_left_side_bearing:int32
min_right_side_bearing:int32
x_max_extent:int32
caret_slope_rise:int32
caret_slope_run:int32
caret_offset:int32
number_of_h_metrics:int32
InstantiateVariableFontRequest
font:Font

The input variable font to be instantiated.

coordinates:[]AxisCoordinate

A list of design axis coordinates to pin, specifying the axis tag and its corresponding value.

InstantiateVariableFontResponse
instance_font_data:bytes
instance_size_bytes:int32
error:Error
KerningPair
left_glyph:string
right_glyph:string
value:int32
ListCmapSubtablesRequest
font:Font

The font file from which to extract the cmap subtables.

ListCmapSubtablesResponse
subtables:[]CmapSubtableInfo

A list of subtable information including platform ID, encoding ID, format, and number of mappings.

error:Error
ListTablesRequest
font:Font

The input field specifying the font file to be analyzed.

ListTablesResponse
tables:[]TableEntry

An array of entries representing each table's tag, byte length, and checksum.

format:string

A string indicating the decoded format of the font container.

error:Error
OS2Info
version:int32
weight_class:int32
width_class:int32
fs_type:int32
s_typo_ascender:int32
s_typo_descender:int32
s_typo_line_gap:int32
us_win_ascent:int32
us_win_descent:int32
s_x_height:int32
s_cap_height:int32
panose:[]uint32
ach_vend_id:string
selection_italic:bool
selection_bold:bool
selection_regular:bool
TableEntry
tag:string
length:uint32
checksum:uint32

Use as a tool in any AI agent

christiangeorgelucas/font-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.

claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"

Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.

MCP server setup & other clients →

Use this package

Sign in to import christiangeorgelucas/font-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.

Sign in to useOpen editor

Stats

Error rate (1h)0.0%
Back to marketplace