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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35e4670f6299d744074cb8bc0c64eba91b2f0691d07f1034b9a5ae7423c9dffa
4
- data.tar.gz: 0df90b79072d3f967af91362b80b4cd7ba3c78dcf72a8e3e61d7227c1933ec91
3
+ metadata.gz: f4c0b230268d5ea31a81de61c2921d5e4935867b6404f5cce8b1845f72e684fd
4
+ data.tar.gz: 50efd3bdb4e99ae0cab4af5de6ef774475bce172f393acdae2331a46d50025f2
5
5
  SHA512:
6
- metadata.gz: 4fd434b589dc0feb1c2e71ab0553b3b8059e11613ffa6cbeae33550456c10a2547e811b61fd94a6310e5f53248459967ff2ecc634fc9eda389e2876d62554bd3
7
- data.tar.gz: 2b80e1d2e093842035364cecc9ad4a5be2389dcdfbd4589ebc776c99f95be3149451fd9977c2455bba5c5da824d4601cc0d1059ff68357487ba7a28eb66bb991
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="#development">Development</a>
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 requires Ruby 3.1+ and has no runtime gem dependencies or native extensions.
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
- raise ArgumentError, "size must be positive and finite" unless size.is_a?(Numeric) && size.finite? && size > 0
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
@@ -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:)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alhena
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/alhena.rb CHANGED
@@ -7,6 +7,7 @@ module Alhena
7
7
  class Error < StandardError; end
8
8
  class InvalidFont < Error; end
9
9
  class UnsupportedFont < Error; end
10
+ Metrics = Struct.new(:width, :ascent, :descent, :line_gap, keyword_init: true)
10
11
  end
11
12
 
12
13
  require_relative "alhena/bitmap"
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.1.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 antialiased grayscale
13
- or LCD glyph bitmaps without native dependencies.
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: 4.0.19
61
+ rubygems_version: 3.6.9
62
62
  specification_version: 4
63
- summary: Pure Ruby TrueType and CFF font rasterization
63
+ summary: Pure Ruby font rasterization and measurement
64
64
  test_files: []