alhena 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +29 -2
- data/lib/alhena/font.rb +37 -1
- data/lib/alhena/rasterizer.rb +16 -0
- data/lib/alhena/version.rb +1 -1
- data/lib/alhena.rb +1 -0
- data/sig/alhena.rbs +10 -0
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4c0b230268d5ea31a81de61c2921d5e4935867b6404f5cce8b1845f72e684fd
|
|
4
|
+
data.tar.gz: 50efd3bdb4e99ae0cab4af5de6ef774475bce172f393acdae2331a46d50025f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90ffa83e7e39302c55686750ef08230df82f825c05b7a7f00802825475b57876abbd37e7fc8d96296c8d51bc9380fa1ac5bdd11a3aef5a799a5ae80dbab32a57
|
|
7
|
+
data.tar.gz: c9956c2c6c282c20dbd7b304ebb548e04029a57dafe6a87b2888d868477ce85ee971e0ae1ffc1c2f1e9b1eabc62170102c42f2bf5ad5d389f1c0611f4a26abb6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 — 2026-09-15
|
|
4
|
+
|
|
5
|
+
- Added text advance and line metric measurement without rasterization.
|
|
6
|
+
- Added low-resolution outline rendering for minimap-style ink distribution.
|
|
7
|
+
- Added an asserted 10,000-row downsampling performance budget.
|
|
8
|
+
|
|
3
9
|
## 0.1.0 — 2026-09-11
|
|
4
10
|
|
|
5
11
|
- Initial release.
|
data/README.md
CHANGED
|
@@ -16,18 +16,20 @@
|
|
|
16
16
|
<a href="#quick-start">Quick Start</a> ·
|
|
17
17
|
<a href="#usage">Usage</a> ·
|
|
18
18
|
<a href="#supported-formats">Supported Formats</a> ·
|
|
19
|
-
<a href="#
|
|
19
|
+
<a href="#performance">Performance</a>
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
-
Alhena reads TrueType, OpenType, and TTC fonts, extracts their outlines, and rasterizes antialiased grayscale, LCD, and color glyph bitmaps. It
|
|
24
|
+
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.
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
28
28
|
- TrueType, OpenType CFF1/CFF2, and TTC font parsing
|
|
29
29
|
- Analytic grayscale and LCD rasterization with subpixel positioning
|
|
30
30
|
- Variable font axes, outlines, and metrics
|
|
31
|
+
- Fast text advance measurement without rasterization
|
|
32
|
+
- Low-resolution ink distribution rendering for minimaps
|
|
31
33
|
- COLR/CPAL, sbix, and CBDT/CBLC color glyphs
|
|
32
34
|
- Entry- and byte-bounded LRU glyph cache
|
|
33
35
|
- Lazy, bounds-checked table parsing
|
|
@@ -53,6 +55,10 @@ Or install it directly:
|
|
|
53
55
|
gem install alhena
|
|
54
56
|
```
|
|
55
57
|
|
|
58
|
+
### Requirements
|
|
59
|
+
|
|
60
|
+
- Ruby 3.1 or later
|
|
61
|
+
|
|
56
62
|
## Quick Start
|
|
57
63
|
|
|
58
64
|
```ruby
|
|
@@ -72,6 +78,18 @@ puts bitmap.to_ascii
|
|
|
72
78
|
advance = font.advance(glyph, size: 24)
|
|
73
79
|
```
|
|
74
80
|
|
|
81
|
+
Measure a UTF-8 string without rasterizing its glyphs:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
metrics = font.measure("Inline hint", size: 14)
|
|
85
|
+
width = font.advance_width("Inline hint".codepoints, size: 14)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`Metrics` contains the scaled `width`, `ascent`, `descent`, and `line_gap`;
|
|
89
|
+
`descent` retains the font's signed value (normally negative).
|
|
90
|
+
OpenType shaping is outside Alhena's scope, so non-empty `features:` values raise
|
|
91
|
+
`Alhena::UnsupportedFont`.
|
|
92
|
+
|
|
75
93
|
## Usage
|
|
76
94
|
|
|
77
95
|
### Fonts and metrics
|
|
@@ -93,6 +111,14 @@ bitmap = Alhena::Rasterizer.new(width: 100, height: 100).fill(path)
|
|
|
93
111
|
|
|
94
112
|
`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.
|
|
95
113
|
|
|
114
|
+
For minimaps, combine already-positioned outlines into a low-resolution coverage
|
|
115
|
+
bitmap without rasterizing each glyph separately:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
mini = Alhena::Rasterizer.new(width: 1, height: 1)
|
|
119
|
+
.fill_downsampled(outlines, scale: 0.05, width: 120, height: 2)
|
|
120
|
+
```
|
|
121
|
+
|
|
96
122
|
`Font#rasterize` and `Rasterizer#fill` accept `gamma:`, `darkening:`, and `lcd: :rgb` or `:bgr`. Use grayscale when the display subpixel order is unknown.
|
|
97
123
|
|
|
98
124
|
### Glyph cache
|
|
@@ -149,6 +175,7 @@ Measurements below are medians of five batches on Ruby 4.0.0 with YJIT on arm64-
|
|
|
149
175
|
| A at 14px, cache hit | 0.48 µs | 5 µs |
|
|
150
176
|
| ASCII 95 glyph prewarm | 4.79 ms | 60 ms |
|
|
151
177
|
| 鬱 at 48px, uncached | 0.47 ms | 3 ms |
|
|
178
|
+
| 10,000 downsampled rows | 100.37 ms | 200 ms |
|
|
152
179
|
|
|
153
180
|
Cache glyphs in interactive applications so each glyph is normally rasterized once per size and position. These measurements are local evidence, not universal guarantees.
|
|
154
181
|
|
data/lib/alhena/font.rb
CHANGED
|
@@ -126,6 +126,31 @@ module Alhena
|
|
|
126
126
|
cached_metric(glyph, vertical: vertical)[0] * scale_factor(size)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
def advance_width(codepoints, size:, features: [])
|
|
130
|
+
validate_features(features)
|
|
131
|
+
raise ArgumentError, "codepoints must be an Array" unless codepoints.is_a?(Array)
|
|
132
|
+
|
|
133
|
+
factor = scale_factor(size)
|
|
134
|
+
units = codepoints.each_with_index.sum do |codepoint, index|
|
|
135
|
+
next 0 if variation_selector?(codepoint)
|
|
136
|
+
|
|
137
|
+
selector = codepoints[index + 1]
|
|
138
|
+
glyph = glyph_id(codepoint, variation_selector: variation_selector?(selector) ? selector : nil)
|
|
139
|
+
glyph = glyph_id(codepoint) if glyph.zero? && variation_selector?(selector)
|
|
140
|
+
cached_metric(glyph, vertical: false)[0]
|
|
141
|
+
end
|
|
142
|
+
units * factor
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def measure(text, size:, features: [])
|
|
146
|
+
valid_text = text.is_a?(String) && text.encoding == Encoding::UTF_8 && text.valid_encoding?
|
|
147
|
+
raise ArgumentError, "text must be a valid UTF-8 String" unless valid_text
|
|
148
|
+
|
|
149
|
+
factor = scale_factor(size)
|
|
150
|
+
Metrics.new(width: advance_width(text.codepoints, size: size, features: features),
|
|
151
|
+
ascent: ascent * factor, descent: descent * factor, line_gap: line_gap * factor).freeze
|
|
152
|
+
end
|
|
153
|
+
|
|
129
154
|
def bearing(glyph, size: units_per_em, vertical: false)
|
|
130
155
|
cached_metric(glyph, vertical: vertical)[1] * scale_factor(size)
|
|
131
156
|
end
|
|
@@ -166,10 +191,21 @@ module Alhena
|
|
|
166
191
|
private
|
|
167
192
|
|
|
168
193
|
def scale_factor(size)
|
|
169
|
-
|
|
194
|
+
valid = size.is_a?(Numeric) && size.real? && size.finite? && size > 0
|
|
195
|
+
raise ArgumentError, "size must be positive and finite" unless valid
|
|
196
|
+
|
|
170
197
|
size.to_f / units_per_em
|
|
171
198
|
end
|
|
172
199
|
|
|
200
|
+
def validate_features(features)
|
|
201
|
+
raise ArgumentError, "features must be an Array" unless features.is_a?(Array)
|
|
202
|
+
raise UnsupportedFont, "OpenType layout features are not supported" unless features.empty?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def variation_selector?(codepoint)
|
|
206
|
+
codepoint.is_a?(Integer) && (codepoint.between?(0xfe00, 0xfe0f) || codepoint.between?(0xe0100, 0xe01ef))
|
|
207
|
+
end
|
|
208
|
+
|
|
173
209
|
def validate_glyph(glyph)
|
|
174
210
|
raise ArgumentError, "glyph ID out of range" unless glyph.is_a?(Integer) && glyph >= 0 && glyph < glyph_count
|
|
175
211
|
end
|
data/lib/alhena/rasterizer.rb
CHANGED
|
@@ -31,6 +31,22 @@ module Alhena
|
|
|
31
31
|
@area = nil
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def fill_downsampled(outlines, scale:, width:, height:)
|
|
35
|
+
valid_scale = scale.is_a?(Numeric) && scale.real? && scale.finite? && scale > 0
|
|
36
|
+
raise ArgumentError, "scale must be positive and finite" unless valid_scale
|
|
37
|
+
raise ArgumentError, "outlines must be enumerable" unless outlines.respond_to?(:each)
|
|
38
|
+
|
|
39
|
+
target = self.class.new(width: width, height: height, tolerance: @tolerance)
|
|
40
|
+
combined = Outline.new
|
|
41
|
+
outlines.each do |outline|
|
|
42
|
+
raise ArgumentError, "outlines must contain Outline values" unless outline.is_a?(Outline)
|
|
43
|
+
|
|
44
|
+
combined.append(outline)
|
|
45
|
+
end
|
|
46
|
+
combined.coordinates.map! { |coordinate| coordinate * scale } unless scale == 1
|
|
47
|
+
target.fill(combined)
|
|
48
|
+
end
|
|
49
|
+
|
|
34
50
|
private
|
|
35
51
|
|
|
36
52
|
def lcd_bitmap(outline, left:, top:, gamma:, darkening:, order:)
|
data/lib/alhena/version.rb
CHANGED
data/lib/alhena.rb
CHANGED
data/sig/alhena.rbs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
module Alhena
|
|
2
|
+
class Metrics < Struct[untyped]
|
|
3
|
+
attr_accessor width: Float
|
|
4
|
+
attr_accessor ascent: Float
|
|
5
|
+
attr_accessor descent: Float
|
|
6
|
+
attr_accessor line_gap: Float
|
|
7
|
+
def initialize: (?width: Float, ?ascent: Float, ?descent: Float, ?line_gap: Float) -> void
|
|
8
|
+
end
|
|
2
9
|
VERSION: String
|
|
3
10
|
type matrix = Array[Numeric]
|
|
4
11
|
class Error < StandardError
|
|
@@ -70,6 +77,8 @@ module Alhena
|
|
|
70
77
|
def post: () -> Hash[Symbol, Numeric | bool]
|
|
71
78
|
def glyph_id: (String | Integer character, ?variation_selector: String | Integer | nil) -> Integer
|
|
72
79
|
def advance: (Integer glyph, ?size: Numeric, ?vertical: bool) -> Float
|
|
80
|
+
def advance_width: (Array[Integer] codepoints, size: Numeric, ?features: Array[String | Symbol]) -> Float
|
|
81
|
+
def measure: (String text, size: Numeric, ?features: Array[String | Symbol]) -> Metrics
|
|
73
82
|
def bearing: (Integer glyph, ?size: Numeric, ?vertical: bool) -> Float
|
|
74
83
|
def outline: (Integer glyph) -> Outline
|
|
75
84
|
def axes: () -> Hash[String, Hash[Symbol, Numeric | bool | String | nil]]
|
|
@@ -104,6 +113,7 @@ module Alhena
|
|
|
104
113
|
MAX_PIXELS: Integer
|
|
105
114
|
def initialize: (width: Integer, height: Integer, ?tolerance: Numeric) -> void
|
|
106
115
|
def fill: (Outline, ?transform: matrix?, ?left: Integer, ?top: Integer, ?gamma: Numeric, ?darkening: Numeric, ?lcd: :rgb | :bgr | nil) -> Bitmap
|
|
116
|
+
def fill_downsampled: (Enumerable[Outline] outlines, scale: Numeric, width: Integer, height: Integer) -> Bitmap
|
|
107
117
|
end
|
|
108
118
|
class Cache
|
|
109
119
|
attr_reader capacity: Integer
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: alhena
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -9,8 +9,8 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: Read sfnt and TTC fonts, extract outlines, and produce
|
|
13
|
-
or
|
|
12
|
+
description: Read sfnt and TTC fonts, measure text, extract outlines, and produce
|
|
13
|
+
antialiased or downsampled glyph bitmaps without native dependencies.
|
|
14
14
|
email:
|
|
15
15
|
- t.yudai92@gmail.com
|
|
16
16
|
executables: []
|
|
@@ -58,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
59
|
version: '0'
|
|
60
60
|
requirements: []
|
|
61
|
-
rubygems_version:
|
|
61
|
+
rubygems_version: 3.6.9
|
|
62
62
|
specification_version: 4
|
|
63
|
-
summary: Pure Ruby
|
|
63
|
+
summary: Pure Ruby font rasterization and measurement
|
|
64
64
|
test_files: []
|