alhena 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/examples/color.rb +11 -0
- data/examples/render.rb +21 -0
- data/lib/alhena/binary.rb +49 -0
- data/lib/alhena/bitmap.rb +27 -0
- data/lib/alhena/cache.rb +46 -0
- data/lib/alhena/cff.rb +423 -0
- data/lib/alhena/color.rb +342 -0
- data/lib/alhena/data_compat.rb +25 -0
- data/lib/alhena/font.rb +427 -0
- data/lib/alhena/outline.rb +100 -0
- data/lib/alhena/png.rb +171 -0
- data/lib/alhena/rasterizer.rb +172 -0
- data/lib/alhena/variation.rb +314 -0
- data/lib/alhena/version.rb +5 -0
- data/lib/alhena.rb +21 -0
- data/sig/alhena.rbs +117 -0
- metadata +64 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 35e4670f6299d744074cb8bc0c64eba91b2f0691d07f1034b9a5ae7423c9dffa
|
|
4
|
+
data.tar.gz: 0df90b79072d3f967af91362b80b4cd7ba3c78dcf72a8e3e61d7227c1933ec91
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4fd434b589dc0feb1c2e71ab0553b3b8059e11613ffa6cbeae33550456c10a2547e811b61fd94a6310e5f53248459967ff2ecc634fc9eda389e2876d62554bd3
|
|
7
|
+
data.tar.gz: 2b80e1d2e093842035364cecc9ad4a5be2389dcdfbd4589ebc776c99f95be3149451fd9977c2455bba5c5da824d4601cc0d1059ff68357487ba7a28eb66bb991
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Alhena</h1>
|
|
3
|
+
<p><strong>Pure Ruby TrueType/OpenType/TTC font rasterization with antialiased glyph bitmaps</strong></p>
|
|
4
|
+
<p>
|
|
5
|
+
<a href="https://rubygems.org/gems/alhena"><img src="https://img.shields.io/gem/v/alhena.svg?colorB=319e8c" alt="Gem Version"></a>
|
|
6
|
+
<a href="https://rubygems.org/gems/alhena"><img src="https://img.shields.io/gem/dt/alhena.svg" alt="Downloads"></a>
|
|
7
|
+
<a href="https://github.com/noxdea/alhena/actions/workflows/main.yml"><img src="https://github.com/noxdea/alhena/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
8
|
+
<img src="https://img.shields.io/badge/ruby-%3E%3D%203.1-ruby.svg" alt="Ruby Version">
|
|
9
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="#features">Features</a> ·
|
|
15
|
+
<a href="#installation">Installation</a> ·
|
|
16
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
17
|
+
<a href="#usage">Usage</a> ·
|
|
18
|
+
<a href="#supported-formats">Supported Formats</a> ·
|
|
19
|
+
<a href="#development">Development</a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
---
|
|
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.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- TrueType, OpenType CFF1/CFF2, and TTC font parsing
|
|
29
|
+
- Analytic grayscale and LCD rasterization with subpixel positioning
|
|
30
|
+
- Variable font axes, outlines, and metrics
|
|
31
|
+
- COLR/CPAL, sbix, and CBDT/CBLC color glyphs
|
|
32
|
+
- Entry- and byte-bounded LRU glyph cache
|
|
33
|
+
- Lazy, bounds-checked table parsing
|
|
34
|
+
- RBS type signatures
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Add Alhena to your Gemfile:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
gem "alhena"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then run:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
bundle install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or install it directly:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
gem install alhena
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
require "alhena"
|
|
60
|
+
|
|
61
|
+
font = Alhena::Font.open("/path/to/font.ttf")
|
|
62
|
+
glyph = font.glyph_id("A")
|
|
63
|
+
bitmap = font.rasterize(glyph, size: 24)
|
|
64
|
+
|
|
65
|
+
puts font.family
|
|
66
|
+
puts bitmap.to_ascii
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`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:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
advance = font.advance(glyph, size: 24)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
### Fonts and metrics
|
|
78
|
+
|
|
79
|
+
`Font.open(path, index: 0)` reads a font file. `Font.new(bytes, index: 0)` accepts font bytes directly. Tables are parsed on demand.
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
### Outlines and rasterization
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
outline = font.outline(glyph) # font coordinates, Y up
|
|
87
|
+
outline.each { |operation, *coordinates| p [operation, coordinates] }
|
|
88
|
+
|
|
89
|
+
path = Alhena::Outline.new
|
|
90
|
+
path.move_to(0, 0).quad_to(50, 100, 100, 0).close
|
|
91
|
+
bitmap = Alhena::Rasterizer.new(width: 100, height: 100).fill(path)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`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
|
+
|
|
96
|
+
`Font#rasterize` and `Rasterizer#fill` accept `gamma:`, `darkening:`, and `lcd: :rgb` or `:bgr`. Use grayscale when the display subpixel order is unknown.
|
|
97
|
+
|
|
98
|
+
### Glyph cache
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
cache = Alhena::Cache.new(capacity: 4096, max_bytes: 16 * 1024 * 1024)
|
|
102
|
+
cache.prewarm(font, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", size: 24)
|
|
103
|
+
bitmap = cache.rasterize(font, glyph, size: 24, subpixel_x: 0.25)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
### Variable fonts
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
font.axes
|
|
112
|
+
bold = font.variation(wght: 700, wdth: 90)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You can also pass axes to `Font.open(path, axes: {wght: 700})`. Unspecified axes use their defaults, and out-of-range values are clamped.
|
|
116
|
+
|
|
117
|
+
### Color glyphs
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
color = font.color_bitmap(glyph, size: 48, palette: 0)
|
|
121
|
+
rgba = color&.rgba
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`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.
|
|
125
|
+
|
|
126
|
+
## Supported Formats
|
|
127
|
+
|
|
128
|
+
- sfnt TrueType, OpenType CFF1/CFF2, and TTC collections
|
|
129
|
+
- Unicode `cmap` formats 0, 4, 6, 12, 13, and 14
|
|
130
|
+
- Simple and composite `glyf` outlines with short or long `loca`
|
|
131
|
+
- Type 2 charstrings, CID CFF, variation stores, and CFF2 blends
|
|
132
|
+
- `fvar`, `gvar`, `avar` v1, HVAR, and VVAR variable font data
|
|
133
|
+
- COLR/CPAL v0, sbix PNG, and CBDT/CBLC PNG, grayscale, BGRA, and composite bitmaps
|
|
134
|
+
|
|
135
|
+
### Limits
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
Unknown formats raise `Alhena::UnsupportedFont`; malformed bounds and structures raise `Alhena::InvalidFont`. Bitmap allocations are limited to 16,777,216 samples.
|
|
140
|
+
|
|
141
|
+
## Performance
|
|
142
|
+
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
| Operation | Measured | Budget |
|
|
146
|
+
|---|---:|---:|
|
|
147
|
+
| Open Noto Sans (569,208 bytes) | 64.73 µs | 30,000 µs |
|
|
148
|
+
| A at 14px, uncached | 40.11 µs | 500 µs |
|
|
149
|
+
| A at 14px, cache hit | 0.48 µs | 5 µs |
|
|
150
|
+
| ASCII 95 glyph prewarm | 4.79 ms | 60 ms |
|
|
151
|
+
| 鬱 at 48px, uncached | 0.47 ms | 3 ms |
|
|
152
|
+
|
|
153
|
+
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
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
bundle install
|
|
159
|
+
bundle exec rake test
|
|
160
|
+
bundle exec rake test:oracle
|
|
161
|
+
bundle exec rake test:fuzz
|
|
162
|
+
bundle exec rake bench:assert
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
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.
|
|
166
|
+
|
|
167
|
+
Render the bundled examples with:
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
bundle exec ruby examples/render.rb test/fonts/NotoSans-Regular.ttf "Hello, Ruby!" 48 text.png
|
|
171
|
+
bundle exec ruby examples/color.rb test/fonts/NotoColorEmoji.ttf "😀" 64 color.png
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
See the [changelog](CHANGELOG.md) for release history. Bug reports and pull requests are welcome on [GitHub](https://github.com/noxdea/alhena).
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
Alhena is available under the [MIT License](LICENSE.txt). Test font licenses and upstream sources are recorded in [test/fonts/README.md](test/fonts/README.md).
|
data/examples/color.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "alhena"
|
|
4
|
+
require "chunky_png"
|
|
5
|
+
|
|
6
|
+
font = Alhena::Font.open(ARGV.fetch(0))
|
|
7
|
+
glyph = font.glyph_id(ARGV.fetch(1, "😀"))
|
|
8
|
+
bitmap = font.color_bitmap(glyph, size: Integer(ARGV.fetch(2, "64")))
|
|
9
|
+
abort "The glyph has no supported color image" unless bitmap
|
|
10
|
+
image = ChunkyPNG::Image.new(bitmap.width, bitmap.height, bitmap.rgba.unpack("N*"))
|
|
11
|
+
image.save(ARGV.fetch(3, "color.png"))
|
data/examples/render.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "alhena"
|
|
3
|
+
require "chunky_png"
|
|
4
|
+
|
|
5
|
+
font = Alhena::Font.open(ARGV.fetch(0))
|
|
6
|
+
text, size = ARGV.fetch(1, "Hello, Ruby!"), Integer(ARGV.fetch(2, "48"))
|
|
7
|
+
glyphs = text.codepoints.map { |code| font.glyph_id(code) }
|
|
8
|
+
width = glyphs.sum { |glyph| font.advance(glyph, size: size) }.ceil + 16
|
|
9
|
+
height = ((font.ascent - font.descent) * size.to_f / font.units_per_em).ceil + 16
|
|
10
|
+
baseline = (font.ascent * size.to_f / font.units_per_em).ceil + 8
|
|
11
|
+
image = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::WHITE)
|
|
12
|
+
x = 8.0
|
|
13
|
+
glyphs.each do |glyph|
|
|
14
|
+
bitmap = font.rasterize(glyph, size: size, subpixel_x: x % 1)
|
|
15
|
+
bitmap.coverage.each_byte.with_index do |value, i|
|
|
16
|
+
col, row = x.floor + bitmap.left + i % bitmap.width, baseline - bitmap.top + i / bitmap.width
|
|
17
|
+
image[col, row] = ChunkyPNG::Color.grayscale(255 - value) if col >= 0 && col < width && row >= 0 && row < height
|
|
18
|
+
end
|
|
19
|
+
x += font.advance(glyph, size: size)
|
|
20
|
+
end
|
|
21
|
+
image.save(ARGV.fetch(3, "text.png"))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alhena
|
|
4
|
+
# Bounded big-endian reads shared by sfnt, glyf and CFF.
|
|
5
|
+
class Binary
|
|
6
|
+
attr_reader :data
|
|
7
|
+
attr_accessor :position
|
|
8
|
+
|
|
9
|
+
def initialize(data)
|
|
10
|
+
@data, @position = data, 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def size = @data.bytesize
|
|
14
|
+
|
|
15
|
+
def bytes(offset, length)
|
|
16
|
+
validate_bounds(offset, length)
|
|
17
|
+
@data.byteslice(offset, length)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate_bounds(offset, length)
|
|
21
|
+
raise InvalidFont, "font data out of bounds at #{offset} (#{length} bytes)" unless offset.is_a?(Integer) && length.is_a?(Integer) && offset >= 0 && length >= 0 && offset <= size - length
|
|
22
|
+
end
|
|
23
|
+
alias check validate_bounds
|
|
24
|
+
|
|
25
|
+
def u8(offset = nil) = number(offset, 1, "C")
|
|
26
|
+
def i8(offset = nil) = number(offset, 1, "c")
|
|
27
|
+
def u16(offset = nil) = number(offset, 2, "n")
|
|
28
|
+
def i16(offset = nil) = number(offset, 2, "s>")
|
|
29
|
+
def u32(offset = nil) = number(offset, 4, "N")
|
|
30
|
+
def i32(offset = nil) = number(offset, 4, "l>")
|
|
31
|
+
def fixed(offset = nil) = i32(offset) / 65_536.0
|
|
32
|
+
|
|
33
|
+
def u24(offset = nil)
|
|
34
|
+
offset ||= @position
|
|
35
|
+
validate_bounds(offset, 3)
|
|
36
|
+
@position = offset + 3
|
|
37
|
+
(@data.getbyte(offset) << 16) | (@data.getbyte(offset + 1) << 8) | @data.getbyte(offset + 2)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def number(offset, length, format)
|
|
43
|
+
offset ||= @position
|
|
44
|
+
validate_bounds(offset, length)
|
|
45
|
+
@position = offset + length
|
|
46
|
+
@data.unpack1(format, offset: offset)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alhena
|
|
4
|
+
# Immutable, tightly packed R8 or RGB8 coverage. Y increases downwards.
|
|
5
|
+
class Bitmap
|
|
6
|
+
attr_reader :width, :height, :left, :top, :coverage, :channels
|
|
7
|
+
|
|
8
|
+
def initialize(width:, height:, coverage:, left: 0, top: 0, channels: 1)
|
|
9
|
+
unless [width, height].all? { |n| n.is_a?(Integer) && n >= 0 } && [1, 3].include?(channels) && coverage.bytesize == width * height * channels
|
|
10
|
+
raise ArgumentError, "invalid bitmap dimensions or coverage length"
|
|
11
|
+
end
|
|
12
|
+
@width, @height, @left, @top, @channels = width, height, left, top, channels
|
|
13
|
+
@coverage = coverage.b.freeze
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_ascii(ramp: " .:-=+*#%@")
|
|
18
|
+
raise ArgumentError, "ramp is empty" if ramp.empty?
|
|
19
|
+
(0...height).map do |y|
|
|
20
|
+
(0...width).map do |x|
|
|
21
|
+
value = (0...channels).sum { |c| coverage.getbyte((y * width + x) * channels + c) } / channels
|
|
22
|
+
ramp[(value * (ramp.length - 1) / 255.0).round]
|
|
23
|
+
end.join
|
|
24
|
+
end.join("\n")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/alhena/cache.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alhena
|
|
4
|
+
# Per-instance LRU, bounded by entry count and optionally coverage bytes.
|
|
5
|
+
class Cache
|
|
6
|
+
attr_reader :capacity, :bytesize
|
|
7
|
+
|
|
8
|
+
def initialize(capacity: 4096, max_bytes: nil)
|
|
9
|
+
raise ArgumentError, "capacity must be positive" unless capacity.is_a?(Integer) && capacity > 0
|
|
10
|
+
raise ArgumentError, "max_bytes must be positive" if max_bytes && !(max_bytes.is_a?(Integer) && max_bytes > 0)
|
|
11
|
+
@capacity, @max_bytes, @bytesize, @entries = capacity, max_bytes, 0, {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def size = @entries.size
|
|
15
|
+
|
|
16
|
+
# @return [Bitmap] immutable coverage, reused on an LRU cache hit
|
|
17
|
+
def rasterize(font, glyph, size:, subpixel_x: 0, **options)
|
|
18
|
+
raise ArgumentError, "invalid subpixel position" unless subpixel_x.is_a?(Numeric) && subpixel_x.finite?
|
|
19
|
+
bucket = (subpixel_x * 4).round
|
|
20
|
+
key = [font, glyph, size, bucket, options]
|
|
21
|
+
if (hit = @entries.delete(key))
|
|
22
|
+
return @entries[key] = hit
|
|
23
|
+
end
|
|
24
|
+
bitmap = font.rasterize(glyph, size: size, subpixel_x: bucket / 4.0, **options)
|
|
25
|
+
return bitmap if @max_bytes && bitmap.coverage.bytesize > @max_bytes
|
|
26
|
+
@entries[key] = bitmap
|
|
27
|
+
@bytesize += bitmap.coverage.bytesize
|
|
28
|
+
while @entries.size > @capacity || (@max_bytes && @bytesize > @max_bytes)
|
|
29
|
+
@bytesize -= @entries.shift.last.coverage.bytesize
|
|
30
|
+
end
|
|
31
|
+
bitmap
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def prewarm(font, text, size = nil, **options)
|
|
35
|
+
size ||= options.delete(:size)
|
|
36
|
+
text.each_codepoint { |code| rasterize(font, font.glyph_id(code), size: size, **options) }
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clear
|
|
41
|
+
@entries.clear
|
|
42
|
+
@bytesize = 0
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|