truetype

truetype

A Crystal library for parsing and manipulating TrueType/OpenType fonts.

Table of Contents

Background

TrueType and OpenType fonts are complex binary formats that require careful parsing to extract glyph data, metrics, and other font information. This library provides a Crystal implementation for reading and manipulating these formats, with optional integrations for Brotli (WOFF2) and HarfBuzz (advanced shaping).

Key capabilities include:

  • Format Support: TTF, OTF, WOFF, WOFF2, TTC/OTC font collections
  • Variable Fonts: Full variation axis support (weight, width, slant, etc.)
  • Color Fonts: COLR/CPAL, SVG, sbix, CBDT/CBLC
  • OpenType Layout: GSUB/GPOS parsing with best-effort default lookup application
  • Text Shaping: Built-in shaping for common cases, optional HarfBuzz for full script support
  • Subsetting: Create minimal fonts with only needed glyphs
  • Math Fonts: MATH table for mathematical typesetting

Install

Add the dependency to your shard.yml:

dependencies:
  truetype:
    github: watzon/truetype

For WOFF2 input/output, also add Brotli (this shard declares it as an optional dependency):

dependencies:
  brotli:
    github: naqvis/brotli.cr

Then run:

shards install

Usage

Quick Start

require "truetype"

# Open any font format with auto-detection
font = TrueType::Font.open("path/to/font.ttf")  # or .otf, .woff, .woff2, .ttc/.otc

# Access font information
puts font.name              # "DejaVu Sans"
puts font.postscript_name   # "DejaVuSans"
puts font.units_per_em      # 2048

# Shape text (with kerning)
glyphs = font.shape("Hello!")
glyphs.each do |g|
  puts "Glyph #{g.id}: advance=#{g.x_advance}"
end

# Create a subset for embedding
subset = font.subset("Hello World!")
File.write("subset.ttf", subset)

Opening Fonts

The Font.open method provides automatic format detection:

# Open from file path
font = TrueType::Font.open("font.ttf")     # TrueType
font = TrueType::Font.open("font.otf")     # OpenType (CFF)
font = TrueType::Font.open("font.woff")    # WOFF
font = TrueType::Font.open("font.woff2")   # WOFF2

# Open from bytes
data = File.read("font.ttf").to_slice
font = TrueType::Font.open(data)

# Open font collections (returns all fonts)
fonts = TrueType::Font.open_collection("collection.ttc") # or .otc
fonts.each { |f| puts f.name }

# Check if data is a valid font before opening
TrueType::Font.font?("font.ttf")  # => true
TrueType::Font.font?(data)        # Check bytes directly

# Detect font format without parsing
TrueType::Font.detect_format(data)  # => :ttf, :otf, :woff, :woff2, :collection, :type1, :unknown

# Access font properties
puts font.name           # Family name
puts font.style          # Style (Regular, Bold, etc.)
puts font.version        # Version string
puts font.copyright      # Copyright notice

# Check font type
font.truetype?   # TrueType outlines (glyf)
font.cff?        # CFF outlines
font.variable?   # Variable font
font.color?      # Color font
font.monospaced? # Monospaced
font.bold?       # Bold weight
font.italic?     # Italic style

Text Shaping

Shape text into positioned glyphs.

The default shaper handles common ligature/kerning cases and applies a subset of GSUB/GPOS behavior. For full shaping parity across complex scripts, use HarfBuzz (next section).

font = TrueType::Font.open("font.ttf")

# Basic shaping (includes kerning)
glyphs = font.shape("Hello World!")

glyphs.each do |glyph|
  puts "Glyph ID: #{glyph.id}"
  puts "Cluster: #{glyph.cluster}"
  puts "Advance: #{glyph.x_advance}"
end

# Shaping options
options = TrueType::ShapingOptions.new(
  kerning: true,
  ligatures: true,
  contextual_alternates: true
)
glyphs = font.shape("fi fl", options)

# Get absolute positions for rendering
rendered = font.render("Hello!")
rendered.each do |g|
  puts "Glyph #{g.id} at x=#{g.x_offset}"
end

# Calculate text width
width = font.text_width("Hello World!")

Advanced Text Shaping (HarfBuzz)

For complex scripts (Arabic, Hebrew, Devanagari, Thai, etc.) or advanced OpenType features, you can optionally enable HarfBuzz integration.

HarfBuzz-backed APIs are only compiled when you pass the -Dharfbuzz flag.

Installation:

  1. Install HarfBuzz on your system:

    # macOS
    brew install harfbuzz
    
    # Ubuntu/Debian
    apt install libharfbuzz-dev
    
    # Fedora
    dnf install harfbuzz-devel
    
    # Arch
    pacman -S harfbuzz
    
  2. Compile with the -Dharfbuzz flag:

    crystal build -Dharfbuzz your_app.cr
    

Usage:

require "truetype"

font = TrueType::Font.open("font.ttf")

# Check if HarfBuzz is available at runtime
if TrueType.harfbuzz_available?
  # Full HarfBuzz shaping for complex scripts
  glyphs = font.shape_advanced("مرحبا بالعالم")  # Arabic text

  glyphs.each do |g|
    puts "Glyph #{g.id} at (#{g.x_offset}, #{g.y_offset})"
  end

  # With specific features and options
  options = TrueType::HarfBuzz::ShapingOptions.new(
    direction: TrueType::HarfBuzz::Direction::RTL,
    script: "Arab",
    features: [
      TrueType::HarfBuzz::Features.liga,
      TrueType::HarfBuzz::Features.kern
    ]
  )
  glyphs = font.shape_advanced("مرحبا", options)

  # Preset options for common scripts
  glyphs = font.shape_advanced("שלום", TrueType::HarfBuzz::ShapingOptions.hebrew)
  glyphs = font.shape_advanced("Hello", TrueType::HarfBuzz::ShapingOptions.latin)

  # Get positioned glyphs with absolute coordinates
  rendered = font.render_advanced("Hello World!")

  # For repeated shaping, reuse the HarfBuzz font for efficiency
  hb_font = font.harfbuzz_font
  texts = ["مرحبا", "שלום", "Hello"]
  texts.each do |text|
    result = TrueType::HarfBuzz::Shaper.shape_with_font(hb_font, text)
  end
end

# Graceful fallback: uses HarfBuzz if available, otherwise basic shaping
glyphs = font.shape_best_effort("Hello مرحبا")

Available Features:

# Common feature presets
TrueType::HarfBuzz::Features.liga      # Standard ligatures
TrueType::HarfBuzz::Features.kern      # Kerning
TrueType::HarfBuzz::Features.smcp      # Small caps
TrueType::HarfBuzz::Features.onum      # Oldstyle figures
TrueType::HarfBuzz::Features.tnum      # Tabular figures
TrueType::HarfBuzz::Features.frac      # Fractions
TrueType::HarfBuzz::Features.swsh      # Swashes
TrueType::HarfBuzz::Features.salt(2)   # Stylistic alternate #2
TrueType::HarfBuzz::Features.stylistic_set(3)  # Stylistic set ss03

# Parse from CSS-like strings
feature = TrueType::HarfBuzz::Feature.parse!("liga")
feature = TrueType::HarfBuzz::Feature.parse!("-kern")  # Disable
feature = TrueType::HarfBuzz::Feature.parse!("aalt=2")
features = TrueType::HarfBuzz::Feature.parse_list("liga,kern,-calt,smcp")

Variable Fonts

Work with variable font axes:

font = TrueType::Font.open("RobotoFlex.ttf")

# Check if variable
if font.variable?
  # List available axes
  font.variation_axes.each do |axis|
    puts "#{axis.tag}: #{axis.min_value}..#{axis.max_value} (default: #{axis.default_value})"
  end

  # Create an instance with specific axis values
  bold = font.instance(wght: 700)
  condensed = font.instance(wght: 700, wdth: 75)

  # Or use a hash
  instance = font.instance({"wght" => 700.0, "wdth" => 75.0})

  # Use a named instance
  font.named_instances.each_with_index do |inst, i|
    puts "Instance #{i}: #{inst.subfamily_name_id}"
  end
  if named = font.instance(0)  # First named instance
    puts named.ascender
  end

  # Get interpolated metrics
  puts bold.ascender
  puts bold.advance_width('A')
end

Font Subsetting

Create smaller fonts containing only needed glyphs:

font = TrueType::Font.open("DejaVuSans.ttf")

# Basic subset from text
subset = font.subset("Hello World!")
File.write("subset.ttf", subset)

# Subset from character set
chars = Set{'H', 'e', 'l', 'o', ' ', 'W', 'r', 'd', '!'}
subset = font.subset(chars)

# With options
options = TrueType::SubsetOptions.new(
  preserve_hints: true,
  preserve_layout: true,
  preserve_kerning: true
)
subset = font.subset("Hello", options)

# Preset options
subset = font.subset("Hello", TrueType::SubsetOptions.pdf)  # Minimal for PDF
subset = font.subset("Hello", TrueType::SubsetOptions.web)  # For web fonts

# Check size reduction
puts "Original: #{font.data.size} bytes"
puts "Subset: #{subset.size} bytes"

Text Layout

Layout text with line breaking:

font = TrueType::Font.open("font.ttf")

# Create layout engine
layout = font.layout_engine

# Measure text
width = layout.measure_width("Hello World!")
height = layout.measure_height(2)  # Height for 2 lines

# Layout with word wrap
options = TrueType::LayoutOptions.new(max_width: 500)
paragraph = font.layout("Hello World! This is a long text that will wrap.", options)

puts "Lines: #{paragraph.line_count}"
puts "Width: #{paragraph.width}"
puts "Height: #{paragraph.height}"

# Iterate over lines with positions
paragraph.each_line_with_position do |line, y|
  puts "Line at y=#{y}: #{line.width} font units wide"
  line.glyphs.each do |glyph|
    # Render glyph...
  end
end

# Layout options
options = TrueType::LayoutOptions.new(
  max_width: 500,
  line_height: 1.2,
  align: TrueType::TextAlign::Left,
  kerning: true,
  ligatures: true,
  word_wrap: true
)

Font Validation

Validate font files:

font = TrueType::Font.open("font.ttf")

# Full validation
result = font.validate

if result.valid?
  puts "Font is valid!"
  if result.warnings?
    puts "Warnings:"
    result.warnings.each { |w| puts "  - #{w}" }
  end
else
  puts "Font is invalid!"
  result.errors.each { |e| puts "  Error: #{e}" }
end

# Quick validity check
font.valid?  # => true/false

Low-Level API

For advanced use cases, access the parser directly. You can also create a Font from an existing parser:

parser = TrueType::Parser.parse("font.ttf")

# Create a Font from an existing parser (useful for low-level work)
font = TrueType::Font.from_parser(parser)

# Access individual tables
parser.head.units_per_em
parser.hhea.ascent
parser.maxp.num_glyphs
parser.cmap.glyph_id('A'.ord.to_u32)

# Check for tables
parser.has_table?("GPOS")
parser.has_table?("GSUB")

# Access table data
if parser.has_kerning?
  parser.kerning('A', 'V')
end

# OpenType layout
if gsub = parser.gsub
  gsub.feature_list.features.each { |tag, _feature| puts tag }
end

# Glyph outlines
glyph_id = parser.glyph_id('A')
outline = parser.glyph_outline(glyph_id)
svg_path = outline.to_svg_path

Supported Features

Category Features Status
Core Tables head, hhea, hmtx, maxp, cmap, name, post, OS/2 Complete
Outlines TrueType (glyf/loca), CFF, CFF2 Complete
Web Fonts WOFF, WOFF2 Complete
Collections TTC/OTC Complete
Variable Fonts fvar, gvar, avar, HVAR, VVAR, MVAR, cvar, STAT Complete
Color Fonts COLR v0/v1, CPAL, SVG, CBDT/CBLC, sbix Complete
OpenType Layout GDEF, GSUB, GPOS parsing + default-shaper hooks Parsed (application partial)
Kerning kern table, GPOS pair-kerning helper Complete
Math MATH table Complete
Subsetting TrueType, CFF Complete
Text Shaping Built-in best-effort shaper, HarfBuzz (optional) Complete with optional HarfBuzz; fallback parity pending
Text Layout Width, height, wrapping, alignment, bidi-aware shaping Complete for current API; shaping-aware wrapping pending
Validation Structural/cross-table checks, warnings Complete for current criteria; exception normalization pending

See ROADMAP.md for detailed feature status.

Current Limitations

  • Non-HarfBuzz shaping is best-effort; full GSUB/GPOS behavior parity is still in progress.
  • Shaping-aware line breaking/layout integration is still in progress.
  • Error surface normalization for malformed layout tables is still in progress.
  • TrueType bytecode interpretation is not implemented (optional future work).

Maintainers

@watzon

Contributing

Issues and pull requests are welcome! Feel free to check the issue tracker if you want to contribute.

  1. Fork it (https://github.com/watzon/truetype/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

License

MIT © Chris Watson

Repository

truetype

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 1
  • 16 days ago
  • January 19, 2026
License

MIT License

Links
Synced at

Tue, 03 Mar 2026 03:36:20 GMT

Languages