truetype
truetype
A pure 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 pure Crystal implementation for working with these font formats without external dependencies (except optional Brotli for WOFF2).
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 tables for ligatures, kerning, etc.
- Text Shaping: Basic shaping with kerning and layout
- 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 support, also add:
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
# 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")
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
# 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:
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:
Installation:
-
Install HarfBuzz on your system:
# macOS brew install harfbuzz # Ubuntu/Debian apt install libharfbuzz-dev # Fedora dnf install harfbuzz-devel # Arch pacman -S harfbuzz -
Compile with the
-Dharfbuzzflag: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.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
instance = font.instance(0) # First named instance
# Get interpolated metrics
puts instance.ascender
puts instance.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}px 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.features.each { |f| puts f.tag }
end
# Glyph outlines
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 | Complete |
| Kerning | kern table, GPOS kern feature | Complete |
| Math | MATH table | Complete |
| Subsetting | TrueType, CFF | Complete |
| Text Shaping | Basic (kerning, width), HarfBuzz (optional) | Complete |
| Text Layout | Width, height, line breaking | Complete |
| Validation | Table validation, warnings | Complete |
See ROADMAP.md for detailed feature status.
Maintainers
Contributing
Issues and pull requests are welcome! Feel free to check the issue tracker if you want to contribute.
- Fork it (https://github.com/watzon/truetype/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
MIT © Chris Watson
truetype
- 0
- 0
- 0
- 0
- 1
- about 7 hours ago
- January 19, 2026
MIT License
Mon, 19 Jan 2026 23:30:25 GMT