Skip to content

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Alhena

Pure Ruby TrueType/OpenType/TTC font rasterization with antialiased glyph bitmaps

Gem Version Downloads CI Ruby Version License

Features · Installation · Quick Start · Usage · Supported Formats · Performance


Alhena reads TrueType, OpenType, and TTC fonts, extracts their outlines, and rasterizes antialiased grayscale, LCD, and color glyph bitmaps. It has no runtime gem dependencies or native extensions.

Alhena rasterized glyphs

Features

  • TrueType, OpenType CFF1/CFF2, and TTC font parsing
  • TrueType and supported CFF1 subset construction and glyph-to-Unicode lookup for PDF embedding
  • Analytic grayscale and LCD rasterization with subpixel positioning
  • Variable font axes, outlines, and metrics
  • Fast text advance measurement without rasterization
  • Low-resolution ink distribution rendering for minimaps
  • COLR/CPAL, sbix, and CBDT/CBLC color glyphs
  • Entry- and byte-bounded LRU glyph cache
  • Lazy, bounds-checked table parsing
  • RBS type signatures

Installation

Add Alhena to your Gemfile:

gem "alhena"

Then run:

bundle install

Or install it directly:

gem install alhena

Requirements

  • Ruby 3.1 or later

Quick Start

require "alhena"

font = Alhena::Font.open("/path/to/font.ttf")
glyph = font.glyph_id("A")
bitmap = font.rasterize(glyph, size: 24)

puts font.family
puts bitmap.to_ascii

bitmap.coverage is an immutable binary String. Grayscale bitmaps contain one coverage byte per pixel; LCD bitmaps contain three. width, height, left, and top describe the bitmap and its bearing. Advances are separate:

advance = font.advance(glyph, size: 24)

Measure a UTF-8 string without rasterizing its glyphs:

metrics = font.measure("Inline hint", size: 14)
width = font.advance_width("Inline hint".codepoints, size: 14)

Metrics contains the scaled width, ascent, descent, and line_gap; descent retains the font's signed value (normally negative). OpenType shaping is outside Alhena's scope, so non-empty features: values raise Alhena::UnsupportedFont.

Usage

Fonts and metrics

Font.open(path, index: 0) reads a font file. Font.new(bytes, index: 0) accepts font bytes directly. Tables are parsed on demand.

Metadata methods include family, names, units_per_em, ascent, descent, line_gap, glyph_count, os2, and post. advance and bearing accept vertical: true. glyph_id accepts a character or Unicode scalar and an optional variation_selector:; unmapped characters return glyph 0.

Outlines and rasterization

outline = font.outline(glyph) # font coordinates, Y up
outline.each { |operation, *coordinates| p [operation, coordinates] }

path = Alhena::Outline.new
path.move_to(0, 0).quad_to(50, 100, 100, 0).close
bitmap = Alhena::Rasterizer.new(width: 100, height: 100).fill(path)

Outline supports lines, quadratic and cubic curves, transforms, bounds, appending, and cubic-to-quadratic conversion. Rasterizer#fill uses pixel coordinates with Y down and implicitly closes open subpaths.

For minimaps, combine already-positioned outlines into a low-resolution coverage bitmap without rasterizing each glyph separately:

mini = Alhena::Rasterizer.new(width: 1, height: 1)
  .fill_downsampled(outlines, scale: 0.05, width: 120, height: 2)

Font#rasterize and Rasterizer#fill accept gamma:, darkening:, and lcd: :rgb or :bgr. Use grayscale when the display subpixel order is unknown.

Glyph cache

cache = Alhena::Cache.new(capacity: 4096, max_bytes: 16 * 1024 * 1024)
cache.prewarm(font, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", size: 24)
bitmap = cache.rasterize(font, glyph, size: 24, subpixel_x: 0.25)

The cache quantizes horizontal positions to quarter pixels and evicts least-recently-used entries. Cache and rasterizer instances are intended for one owner; protect shared instances or use one per thread.

Variable fonts

font.axes
bold = font.variation(wght: 700, wdth: 90)

You can also pass axes to Font.open(path, axes: {wght: 700}). Unspecified axes use their defaults, and out-of-range values are clamped.

Color glyphs

color = font.color_bitmap(glyph, size: 48, palette: 0)
rgba = color&.rgba

ColorBitmap#rgba stores straight sRGB RGBA8 pixels, and to_bitmap extracts alpha coverage. Font#rasterize automatically returns alpha coverage for supported color glyphs. embedded_bitmap exposes original sbix/CBDT image data and strike metrics.

Supported Formats

  • sfnt TrueType, OpenType CFF1/CFF2, and TTC collections
  • Unicode cmap formats 0, 4, 6, 12, 13, and 14
  • Simple and composite glyf outlines with short or long loca
  • Type 2 charstrings, CID CFF, variation stores, and CFF2 blends
  • fvar, gvar, avar v1, HVAR, and VVAR variable font data
  • COLR/CPAL v0, sbix PNG, and CBDT/CBLC PNG, grayscale, BGRA, and composite bitmaps

Limits

Alhena does not provide TrueType hinting, shaping, GSUB/GPOS, kerning, system font discovery, COLR v1 paint graphs, avar v2, JPEG/TIFF decoding, or MVAR global metric variation. Use embedded_bitmap to retrieve unsupported sbix image formats for external decoding.

Alhena::Subset.build(font, glyph_ids) creates a compact TrueType font containing the requested glyphs and any composite-glyph dependencies, or a compact static name-keyed CFF1 font for the supported CFF subset described below. CFF2, CID-keyed CFF, predefined CFF charsets, custom CFF encodings, and variable CFF1 fonts are rejected with Alhena::UnsupportedFont; they are never silently returned unchanged. CFF subsets retain selected outlines, Unicode mappings, horizontal metrics, and the original subroutine indexes, but omit layout, shaping, color, and vertical-metric tables whose glyph references are not rewritten.

For PDF consumers, Alhena::Subset.build_cid(font, glyph_ids) converts a static, name-keyed CFF1 font to a compact CID-keyed CFF1 program. Array order assigns CIDs (the first glyph must be glyph 0), and repeated glyph IDs are allowed. It intentionally rejects CFF2, variable, and already CID-keyed source fonts.

Unknown formats raise Alhena::UnsupportedFont; malformed bounds and structures raise Alhena::InvalidFont. Bitmap allocations are limited to 16,777,216 samples.

Performance

Measurements below are medians of five batches on Ruby 4.0.0 with YJIT on arm64-darwin24. Run ruby --yjit bench/bench.rb to reproduce them.

Operation Measured Budget
Open Noto Sans (569,208 bytes) 64.73 µs 30,000 µs
A at 14px, uncached 40.11 µs 500 µs
A at 14px, cache hit 0.48 µs 5 µs
ASCII 95 glyph prewarm 4.79 ms 60 ms
鬱 at 48px, uncached 0.47 ms 3 ms
10,000 downsampled rows 100.37 ms 250 ms

Cache glyphs in interactive applications so each glyph is normally rasterized once per size and position. These measurements are local evidence, not universal guarantees.

Development

bundle install
bundle exec rake test
bundle exec rake test:oracle
bundle exec rake test:fuzz
bundle exec rake bench:assert

Oracle tests compare Alhena with FreeType, ttfunk, and committed PNG references. FreeType is development-only; install libfreetype6 on Linux or freetype with Homebrew on macOS, or set FREETYPE_LIBRARY. Optional oracle dependencies are skipped when unavailable.

Render the bundled examples with:

bundle exec ruby examples/render.rb test/fonts/NotoSans-Regular.ttf "Hello, Ruby!" 48 text.png
bundle exec ruby examples/color.rb test/fonts/NotoColorEmoji.ttf "😀" 64 color.png

See the changelog for release history. Bug reports and pull requests are welcome on GitHub.

License

Alhena is available under the MIT License. Test font licenses and upstream sources are recorded in test/fonts/README.md.

About

Pure Ruby TrueType/OpenType/TTC font rasterization with antialiased glyph bitmaps

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages