glyphic 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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +69 -0
- data/Rakefile +9 -0
- data/lib/glyphic/version.rb +5 -0
- data/lib/glyphic.rb +786 -0
- data/sig/glyphic.rbs +4 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9ed1aac5cdc8652bcd5f5bc782e909e140f0ce5e97fe2225327066a59901a093
|
|
4
|
+
data.tar.gz: 8d113109ea36b66cd55356f487fac8496a24424240cf918ebf3d71c69ab7a9d3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 28995db31a3cebb4e6ddb3958bb1b6d4459598e221e2726de79917d47ec3b6c8b711863b75764b1a25ef5943556ca17f7e510dab9aa0ab20af4e8bad1e8a4a9a
|
|
7
|
+
data.tar.gz: 26e4db1c8d9d0f4231256eeefbd053ad8aa1d715c5845878a656135d2a85e8a1aa129837ed2751545e3a2442d873bef08c697e25700ada05682683707e0b21ad
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ydah
|
|
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,69 @@
|
|
|
1
|
+
# Glyphic
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/glyphic)
|
|
4
|
+
[](https://rubygems.org/gems/glyphic)
|
|
5
|
+
[](https://github.com/rbgfx/glyphic/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.ruby-lang.org/)
|
|
7
|
+
[](LICENSE.txt)
|
|
8
|
+
|
|
9
|
+
> Bitmap and outline font rendering for Ruby graphics.
|
|
10
|
+
|
|
11
|
+
Glyphic loads BDF bitmap fonts and TrueType outlines, measures UTF-8 text, and
|
|
12
|
+
draws it onto Tessel RGBA8 images.
|
|
13
|
+
|
|
14
|
+
**[Features](#features) · [Installation](#installation) · [Quick start](#quick-start) · [Development](#development)**
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- BDF bitmap font loading.
|
|
19
|
+
- TrueType (.ttf) outline loading and rasterization.
|
|
20
|
+
- UTF-8 text measurement, wrapping, and text boxes.
|
|
21
|
+
- Fallback chains for missing glyphs.
|
|
22
|
+
- Direct rendering into Tessel images.
|
|
23
|
+
- A built-in font for examples and tools.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Add Glyphic to your Gemfile:
|
|
28
|
+
|
|
29
|
+
~~~ruby
|
|
30
|
+
gem "glyphic"
|
|
31
|
+
~~~
|
|
32
|
+
|
|
33
|
+
Then run:
|
|
34
|
+
|
|
35
|
+
~~~sh
|
|
36
|
+
bundle install
|
|
37
|
+
~~~
|
|
38
|
+
|
|
39
|
+
Or install the released gem:
|
|
40
|
+
|
|
41
|
+
~~~sh
|
|
42
|
+
gem install glyphic
|
|
43
|
+
~~~
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
~~~ruby
|
|
48
|
+
require "glyphic"
|
|
49
|
+
require "tessel"
|
|
50
|
+
|
|
51
|
+
font = Glyphic.default
|
|
52
|
+
image = Tessel::Image.new(240, 40, fill: "#101827")
|
|
53
|
+
font.draw(image, 4, 4, "Hello, rbgfx", color: "#ffffff")
|
|
54
|
+
image.write("hello.png")
|
|
55
|
+
~~~
|
|
56
|
+
|
|
57
|
+
Load an external font with <code>Glyphic.load(path)</code>. BDF and TrueType
|
|
58
|
+
fonts are supported; CFF/OpenType outlines are currently unsupported.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
~~~sh
|
|
63
|
+
bundle install
|
|
64
|
+
bundle exec rake verify
|
|
65
|
+
~~~
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
[MIT](LICENSE.txt)
|
data/Rakefile
ADDED
data/lib/glyphic.rb
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tessel"
|
|
4
|
+
|
|
5
|
+
require_relative "glyphic/version"
|
|
6
|
+
|
|
7
|
+
module Glyphic
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
class UnsupportedError < ArgumentError; end
|
|
10
|
+
|
|
11
|
+
Glyph = Struct.new(:advance, :bearing_x, :bearing_y, :width, :height, :alpha, :runs, keyword_init: true) do
|
|
12
|
+
def initialize(**attributes)
|
|
13
|
+
super
|
|
14
|
+
self.alpha = String(alpha || "").b.freeze
|
|
15
|
+
self.runs ||= build_runs
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def bytes = alpha
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build_runs
|
|
24
|
+
rows = []
|
|
25
|
+
(0...height).each do |y|
|
|
26
|
+
x = 0
|
|
27
|
+
while x < width
|
|
28
|
+
x += 1 while x < width && alpha.getbyte(y * width + x).zero?
|
|
29
|
+
start = x
|
|
30
|
+
x += 1 while x < width && alpha.getbyte(y * width + x).positive?
|
|
31
|
+
rows << [start, x - 1, y] if x > start
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
rows.freeze
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Font
|
|
39
|
+
attr_reader :line_height, :ascent, :descent
|
|
40
|
+
|
|
41
|
+
def initialize(glyphs, line_height:, ascent:, descent: 0, fallback: nil, skipped_glyphs: 0)
|
|
42
|
+
@glyphs = glyphs
|
|
43
|
+
@line_height = line_height
|
|
44
|
+
@ascent = ascent
|
|
45
|
+
@descent = descent
|
|
46
|
+
@fallback = fallback
|
|
47
|
+
@skipped_glyphs = skipped_glyphs
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
attr_reader :skipped_glyphs
|
|
51
|
+
|
|
52
|
+
def glyph?(character)
|
|
53
|
+
@glyphs.key?(character.to_s.codepoints.first)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def glyph(character)
|
|
57
|
+
@glyphs[character.to_s.codepoints.first] || @fallback || @glyphs[63] || blank_glyph
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def measure(text)
|
|
61
|
+
lines = Layout.lines(text, self)
|
|
62
|
+
[lines.map { |line| Layout.width(line, self) }.max.to_i, lines.length * line_height]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def text_width(text)
|
|
66
|
+
measure(text)[0]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def kerning(_left, _right)
|
|
70
|
+
0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def draw(target, x, y, text, color: [255, 255, 255, 255], baseline: false, **options)
|
|
74
|
+
Renderer.draw(self, target, x, y, text, color: color, baseline: baseline, **options)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def draw_text_box(target, x:, y:, width:, text:, color: [255, 255, 255, 255], align: :left, wrap: :word, line_spacing: 1.0, **options)
|
|
78
|
+
Renderer.draw_box(self, target, x: x, y: y, width: width, text: text, color: color, align: align, wrap: wrap, line_spacing: line_spacing, **options)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render(text, color: [255, 255, 255, 255], padding: 0)
|
|
82
|
+
width, height = measure(text)
|
|
83
|
+
image = Tessel::Image.new(width + padding * 2, height + padding * 2)
|
|
84
|
+
draw(image, padding, padding, text, color: color)
|
|
85
|
+
image
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def blank_glyph
|
|
91
|
+
@blank_glyph ||= Glyph.new(advance: line_height / 2, bearing_x: 0, bearing_y: ascent, width: 0, height: 0, alpha: "".b)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
module BDF
|
|
96
|
+
module_function
|
|
97
|
+
|
|
98
|
+
def load(source)
|
|
99
|
+
text = if source.respond_to?(:read)
|
|
100
|
+
source.read
|
|
101
|
+
elsif source.is_a?(String) && source.lstrip.start_with?("STARTFONT")
|
|
102
|
+
source
|
|
103
|
+
else
|
|
104
|
+
File.read(source)
|
|
105
|
+
end
|
|
106
|
+
lines = text.lines.map(&:strip)
|
|
107
|
+
raise ArgumentError, "not a BDF font" unless lines.first == "STARTFONT 2.1" || lines.first&.start_with?("STARTFONT")
|
|
108
|
+
|
|
109
|
+
ascent = lines.find { |line| line.start_with?("FONT_ASCENT") }&.split&.last.to_i
|
|
110
|
+
descent = lines.find { |line| line.start_with?("FONT_DESCENT") }&.split&.last.to_i
|
|
111
|
+
registry = lines.find { |line| line.start_with?("CHARSET_REGISTRY") }&.split(" ", 2)&.last.to_s.delete('"')
|
|
112
|
+
glyphs = {}
|
|
113
|
+
skipped = 0
|
|
114
|
+
index = 0
|
|
115
|
+
while index < lines.length
|
|
116
|
+
unless lines[index].start_with?("STARTCHAR")
|
|
117
|
+
index += 1
|
|
118
|
+
next
|
|
119
|
+
end
|
|
120
|
+
block = []
|
|
121
|
+
index += 1
|
|
122
|
+
while index < lines.length && lines[index] != "ENDCHAR"
|
|
123
|
+
block << lines[index]
|
|
124
|
+
index += 1
|
|
125
|
+
end
|
|
126
|
+
encoded = block.find { |line| line.start_with?("ENCODING") }&.split&.last
|
|
127
|
+
code = encoded&.to_i
|
|
128
|
+
if code && code >= 0
|
|
129
|
+
code = jis_to_unicode(code) if registry.start_with?("JISX0208")
|
|
130
|
+
if code
|
|
131
|
+
glyph = parse_glyph(block, advance: block.find { |line| line.start_with?("DWIDTH") }&.split&.last.to_i)
|
|
132
|
+
glyphs[code] = glyph
|
|
133
|
+
else
|
|
134
|
+
skipped += 1
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
skipped += 1
|
|
138
|
+
end
|
|
139
|
+
index += 1
|
|
140
|
+
end
|
|
141
|
+
Font.new(glyphs, line_height: ascent + descent, ascent: ascent, descent: descent, skipped_glyphs: skipped)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def parse_glyph(block, advance:)
|
|
145
|
+
bbx = block.find { |line| line.start_with?("BBX") }.split.drop(1).map(&:to_i)
|
|
146
|
+
width, height, offset_x, offset_y = bbx
|
|
147
|
+
bitmap_start = block.index("BITMAP")
|
|
148
|
+
rows = block[(bitmap_start + 1), height].to_a
|
|
149
|
+
alpha = rows.map do |line|
|
|
150
|
+
value = line.to_i(16)
|
|
151
|
+
(0...width).map { |x| (value & (1 << ([line.length * 4, width].max - x - 1))).positive? ? 255 : 0 }
|
|
152
|
+
end.flatten.pack("C*")
|
|
153
|
+
Glyph.new(advance: advance, bearing_x: offset_x, bearing_y: offset_y + height, width: width, height: height, alpha: alpha)
|
|
154
|
+
end
|
|
155
|
+
private_class_method :parse_glyph
|
|
156
|
+
|
|
157
|
+
def jis_to_unicode(code)
|
|
158
|
+
bytes = [(code >> 8) + 0x80, (code & 0xff) + 0x80].pack("C2")
|
|
159
|
+
bytes.force_encoding("EUC-JP").encode("UTF-8").codepoints.first
|
|
160
|
+
rescue EncodingError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
private_class_method :jis_to_unicode
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
module TrueType
|
|
167
|
+
module_function
|
|
168
|
+
|
|
169
|
+
class Reader
|
|
170
|
+
attr_reader :bytes, :tables
|
|
171
|
+
|
|
172
|
+
def initialize(bytes)
|
|
173
|
+
@bytes = bytes.b
|
|
174
|
+
@tables = {}
|
|
175
|
+
count = @bytes.byteslice(4, 2).unpack1("n")
|
|
176
|
+
count.times do |index|
|
|
177
|
+
tag, _checksum, offset, length = @bytes.byteslice(12 + index * 16, 16).unpack("a4N3")
|
|
178
|
+
@tables[tag] = @bytes.byteslice(offset, length)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def u16(data, offset) = data.byteslice(offset, 2).unpack1("n")
|
|
183
|
+
def i16(data, offset) = data.byteslice(offset, 2).unpack1("s>")
|
|
184
|
+
def u32(data, offset) = data.byteslice(offset, 4).unpack1("N")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class Font < Glyphic::Font
|
|
188
|
+
def initialize(reader, size)
|
|
189
|
+
@reader = reader
|
|
190
|
+
@size = size.to_f
|
|
191
|
+
@head = reader.tables.fetch("head")
|
|
192
|
+
@hhea = reader.tables.fetch("hhea")
|
|
193
|
+
@maxp = reader.tables.fetch("maxp")
|
|
194
|
+
@hmtx = reader.tables.fetch("hmtx")
|
|
195
|
+
@loca = reader.tables.fetch("loca")
|
|
196
|
+
@glyf = reader.tables.fetch("glyf")
|
|
197
|
+
@units_per_em = reader.u16(@head, 18)
|
|
198
|
+
@loca_format = reader.i16(@head, 50)
|
|
199
|
+
@glyph_count = reader.u16(@maxp, 4)
|
|
200
|
+
@metric_count = reader.u16(@hhea, 34)
|
|
201
|
+
@cmap = build_cmap(reader.tables.fetch("cmap"))
|
|
202
|
+
@kern = build_kern(reader.tables["kern"])
|
|
203
|
+
ascent = reader.i16(@hhea, 4) * @size / @units_per_em
|
|
204
|
+
descent = -reader.i16(@hhea, 6) * @size / @units_per_em
|
|
205
|
+
super({}, line_height: (ascent + descent).ceil, ascent: ascent.ceil, descent: descent.ceil)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def glyph?(character)
|
|
209
|
+
@cmap.key?(character.to_s.codepoints.first)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def glyph(character)
|
|
213
|
+
codepoint = character.to_s.codepoints.first
|
|
214
|
+
index = @cmap[codepoint]
|
|
215
|
+
return super unless index
|
|
216
|
+
@glyph_cache ||= {}
|
|
217
|
+
@glyph_cache_order ||= []
|
|
218
|
+
if @glyph_cache.key?(codepoint)
|
|
219
|
+
@glyph_cache_order.delete(codepoint)
|
|
220
|
+
@glyph_cache_order << codepoint
|
|
221
|
+
return @glyph_cache[codepoint]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
@glyph_cache[codepoint] = build_glyph(index)
|
|
225
|
+
@glyph_cache_order << codepoint
|
|
226
|
+
@glyph_cache.delete(@glyph_cache_order.shift) if @glyph_cache_order.length > 2048
|
|
227
|
+
@glyph_cache[codepoint]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def kerning(left, right)
|
|
231
|
+
return 0 unless @kern
|
|
232
|
+
left_index = @cmap[left.to_s.codepoints.first]
|
|
233
|
+
right_index = @cmap[right.to_s.codepoints.first]
|
|
234
|
+
(@kern[[left_index, right_index]] || 0) * @size / @units_per_em
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def build_cmap(data)
|
|
240
|
+
records = @reader.u16(data, 2).times.map do |index|
|
|
241
|
+
platform, encoding, offset = data.byteslice(4 + index * 8, 8).unpack("n2N")
|
|
242
|
+
[platform, encoding, offset]
|
|
243
|
+
end
|
|
244
|
+
selected = records.sort_by { |platform, encoding, _| platform == 3 && encoding == 10 ? 0 : platform == 3 && encoding == 1 ? 1 : platform.zero? ? 2 : 3 }.find { |_, _, offset| [4, 12].include?(@reader.u16(data, offset)) }
|
|
245
|
+
return {} unless selected
|
|
246
|
+
format = @reader.u16(data, selected[2])
|
|
247
|
+
format == 12 ? cmap12(data.byteslice(selected[2]..)) : cmap4(data.byteslice(selected[2]..))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def build_kern(data)
|
|
251
|
+
return if data.nil? || @reader.u16(data, 0) != 0
|
|
252
|
+
|
|
253
|
+
pairs = {}
|
|
254
|
+
offset = 4
|
|
255
|
+
@reader.u16(data, 2).times do
|
|
256
|
+
length = @reader.u16(data, offset + 2)
|
|
257
|
+
coverage = @reader.u16(data, offset + 4)
|
|
258
|
+
if coverage & 0xff == 0
|
|
259
|
+
base = offset + 6
|
|
260
|
+
@reader.u16(data, base).times do |index|
|
|
261
|
+
left, right, value = data.byteslice(base + 8 + index * 6, 6).unpack("n2s>")
|
|
262
|
+
pairs[[left, right]] = value
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
offset += length
|
|
266
|
+
end
|
|
267
|
+
pairs
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def cmap12(data)
|
|
271
|
+
result = {}
|
|
272
|
+
@reader.u32(data, 12).times do |index|
|
|
273
|
+
start_code, end_code, start_glyph = data.byteslice(16 + index * 12, 12).unpack("N3")
|
|
274
|
+
(start_code..end_code).each { |code| result[code] = start_glyph + code - start_code }
|
|
275
|
+
end
|
|
276
|
+
result
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def cmap4(data)
|
|
280
|
+
segments = @reader.u16(data, 6) / 2
|
|
281
|
+
end_codes = data.byteslice(14, segments * 2).unpack("n*")
|
|
282
|
+
start_offset = 16 + segments * 2
|
|
283
|
+
start_codes = data.byteslice(start_offset, segments * 2).unpack("n*")
|
|
284
|
+
delta_offset = start_offset + segments * 2
|
|
285
|
+
deltas = data.byteslice(delta_offset, segments * 2).unpack("s>*")
|
|
286
|
+
range_offset = delta_offset + segments * 2
|
|
287
|
+
offsets = data.byteslice(range_offset, segments * 2).unpack("n*")
|
|
288
|
+
result = {}
|
|
289
|
+
end_codes.each_index do |segment|
|
|
290
|
+
(start_codes[segment]..end_codes[segment]).each do |code|
|
|
291
|
+
next if code == 0xffff
|
|
292
|
+
glyph = if offsets[segment].zero?
|
|
293
|
+
(code + deltas[segment]) & 0xffff
|
|
294
|
+
else
|
|
295
|
+
address = range_offset + segment * 2 + offsets[segment] + (code - start_codes[segment]) * 2
|
|
296
|
+
value = @reader.u16(data, address)
|
|
297
|
+
value.zero? ? 0 : (value + deltas[segment]) & 0xffff
|
|
298
|
+
end
|
|
299
|
+
result[code] = glyph if glyph.positive?
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
result
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def build_glyph(index)
|
|
306
|
+
advance = if index < @metric_count
|
|
307
|
+
@reader.u16(@hmtx, index * 4)
|
|
308
|
+
else
|
|
309
|
+
@reader.u16(@hmtx, (@metric_count - 1) * 4)
|
|
310
|
+
end
|
|
311
|
+
scale = @size / @units_per_em
|
|
312
|
+
points, end_points, x_min, y_min, x_max, y_max, = outline(index, [])
|
|
313
|
+
return Glyph.new(advance: (advance * scale).round, bearing_x: 0, bearing_y: 0, width: 0, height: 0, alpha: "".b) if points.empty?
|
|
314
|
+
width = [(x_max - x_min) * scale, 1].max.ceil
|
|
315
|
+
height = [(y_max - y_min) * scale, 1].max.ceil
|
|
316
|
+
alpha = rasterize(points, end_points, x_min, y_min, x_max, y_max, width, height, scale)
|
|
317
|
+
Glyph.new(advance: (advance * scale).round, bearing_x: (x_min * scale).floor, bearing_y: (y_max * scale).ceil, width: width, height: height, alpha: alpha)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def outline(index, stack)
|
|
321
|
+
raise UnsupportedError, "cyclic TrueType composite glyph" if stack.include?(index)
|
|
322
|
+
|
|
323
|
+
offset = glyph_offset(index)
|
|
324
|
+
finish = glyph_offset(index + 1)
|
|
325
|
+
return [[], [], 0, 0, 0, 0, []] if finish <= offset
|
|
326
|
+
|
|
327
|
+
glyph = @glyf.byteslice(offset, finish - offset)
|
|
328
|
+
contours = @reader.i16(glyph, 0)
|
|
329
|
+
return composite_outline(index, glyph, stack) if contours.negative?
|
|
330
|
+
|
|
331
|
+
end_points = glyph.byteslice(10, contours * 2).unpack("n*")
|
|
332
|
+
point_count = end_points.last.to_i + 1
|
|
333
|
+
cursor = 10 + contours * 2
|
|
334
|
+
instruction_length = @reader.u16(glyph, cursor)
|
|
335
|
+
cursor += 2 + instruction_length
|
|
336
|
+
flags = []
|
|
337
|
+
while flags.length < point_count
|
|
338
|
+
flag = glyph.getbyte(cursor)
|
|
339
|
+
cursor += 1
|
|
340
|
+
repeat = flag.anybits?(8) ? glyph.getbyte(cursor).tap { cursor += 1 } : 0
|
|
341
|
+
flags.concat([flag] * (repeat + 1))
|
|
342
|
+
end
|
|
343
|
+
xs = read_coordinates(glyph, flags, cursor, point_count, horizontal: true)
|
|
344
|
+
ys = read_coordinates(glyph, flags, xs[1], point_count, horizontal: false)
|
|
345
|
+
raw_points = xs[0].zip(ys[0]).map { |x, y| [x, y] }
|
|
346
|
+
x_min, y_min, x_max, y_max = glyph.byteslice(2, 8).unpack("s>4")
|
|
347
|
+
points, flattened_end_points = flatten_contours(raw_points, flags, end_points)
|
|
348
|
+
[points, flattened_end_points, x_min, y_min, x_max, y_max, raw_points]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def glyph_offset(index)
|
|
352
|
+
if @loca_format.zero?
|
|
353
|
+
@reader.u16(@loca, index * 2) * 2
|
|
354
|
+
else
|
|
355
|
+
@reader.u32(@loca, index * 4)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def composite_outline(index, glyph, stack)
|
|
360
|
+
flags = 0
|
|
361
|
+
cursor = 10
|
|
362
|
+
points = []
|
|
363
|
+
reference_points = []
|
|
364
|
+
end_points = []
|
|
365
|
+
loop do
|
|
366
|
+
flags = @reader.u16(glyph, cursor)
|
|
367
|
+
component_index = @reader.u16(glyph, cursor + 2)
|
|
368
|
+
cursor += 4
|
|
369
|
+
word_args = flags.anybits?(1)
|
|
370
|
+
arg1, arg2 = if word_args
|
|
371
|
+
values = glyph.byteslice(cursor, 4).unpack("s>2")
|
|
372
|
+
cursor += 4
|
|
373
|
+
values
|
|
374
|
+
else
|
|
375
|
+
values = glyph.byteslice(cursor, 2).unpack("c2")
|
|
376
|
+
cursor += 2
|
|
377
|
+
values
|
|
378
|
+
end
|
|
379
|
+
dx, dy = flags.anybits?(2) ? [arg1, arg2] : [0, 0]
|
|
380
|
+
a, b, c, d = component_transform(glyph, flags, cursor)
|
|
381
|
+
cursor += transform_size(flags)
|
|
382
|
+
component = outline(component_index, stack + [index])
|
|
383
|
+
component_points = component[0]
|
|
384
|
+
transformed = component_points.map { |x, y| [x * a + y * c, x * b + y * d] }
|
|
385
|
+
transformed_reference = component[6].map { |x, y| [x * a + y * c, x * b + y * d] }
|
|
386
|
+
if flags.anybits?(2)
|
|
387
|
+
transformed.map! { |x, y| [x + dx, y + dy] }
|
|
388
|
+
transformed_reference.map! { |x, y| [x + dx, y + dy] }
|
|
389
|
+
else
|
|
390
|
+
parent = reference_points[arg2]
|
|
391
|
+
child = transformed_reference[arg1]
|
|
392
|
+
raise UnsupportedError, "invalid TrueType composite point index" unless parent && child
|
|
393
|
+
offset_x = parent[0] - child[0]
|
|
394
|
+
offset_y = parent[1] - child[1]
|
|
395
|
+
transformed.map! { |x, y| [x + offset_x, y + offset_y] }
|
|
396
|
+
transformed_reference.map! { |x, y| [x + offset_x, y + offset_y] }
|
|
397
|
+
end
|
|
398
|
+
point_offset = points.length
|
|
399
|
+
points.concat(transformed)
|
|
400
|
+
reference_points.concat(transformed_reference)
|
|
401
|
+
end_points.concat(component[1].map { |endpoint| endpoint + point_offset })
|
|
402
|
+
break unless flags.anybits?(32)
|
|
403
|
+
end
|
|
404
|
+
x_values = points.map(&:first)
|
|
405
|
+
y_values = points.map(&:last)
|
|
406
|
+
[points, end_points, x_values.min.to_i, y_values.min.to_i, x_values.max.to_i, y_values.max.to_i, reference_points]
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def flatten_contours(points, flags, end_points)
|
|
410
|
+
flattened = []
|
|
411
|
+
flattened_end_points = []
|
|
412
|
+
start = 0
|
|
413
|
+
end_points.each do |finish|
|
|
414
|
+
contour = points[start..finish]
|
|
415
|
+
contour_flags = flags[start..finish]
|
|
416
|
+
count = contour.length
|
|
417
|
+
if contour_flags.first.anybits?(1)
|
|
418
|
+
current = contour.first
|
|
419
|
+
index = 1 % count
|
|
420
|
+
processed = 1
|
|
421
|
+
elsif contour_flags.last.anybits?(1)
|
|
422
|
+
current = contour.last
|
|
423
|
+
index = 0
|
|
424
|
+
processed = 1
|
|
425
|
+
else
|
|
426
|
+
current = midpoint(contour.last, contour.first)
|
|
427
|
+
index = 0
|
|
428
|
+
processed = 0
|
|
429
|
+
end
|
|
430
|
+
outline = [current]
|
|
431
|
+
loop do
|
|
432
|
+
point = contour[index]
|
|
433
|
+
if contour_flags[index].anybits?(1)
|
|
434
|
+
outline << point unless point == outline.last
|
|
435
|
+
current = point
|
|
436
|
+
index = (index + 1) % count
|
|
437
|
+
processed += 1
|
|
438
|
+
else
|
|
439
|
+
following_index = (index + 1) % count
|
|
440
|
+
following = contour[following_index]
|
|
441
|
+
if contour_flags[following_index].anybits?(1)
|
|
442
|
+
append_quadratic(outline, current, point, following)
|
|
443
|
+
current = following
|
|
444
|
+
index = (index + 2) % count
|
|
445
|
+
processed += 2
|
|
446
|
+
else
|
|
447
|
+
implied = midpoint(point, following)
|
|
448
|
+
append_quadratic(outline, current, point, implied)
|
|
449
|
+
current = implied
|
|
450
|
+
index = following_index
|
|
451
|
+
processed += 1
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
break if processed >= count
|
|
455
|
+
end
|
|
456
|
+
flattened.concat(outline)
|
|
457
|
+
flattened_end_points << flattened.length - 1
|
|
458
|
+
start = finish + 1
|
|
459
|
+
end
|
|
460
|
+
[flattened, flattened_end_points]
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def append_quadratic(outline, first, control, last)
|
|
464
|
+
length = Math.hypot(control[0] - first[0], control[1] - first[1]) + Math.hypot(last[0] - control[0], last[1] - control[1])
|
|
465
|
+
steps = [(length * @size / @units_per_em / 0.25).ceil, 1].max
|
|
466
|
+
(1..steps).each do |step|
|
|
467
|
+
amount = step.to_f / steps
|
|
468
|
+
inverse = 1 - amount
|
|
469
|
+
outline << [inverse * inverse * first[0] + 2 * inverse * amount * control[0] + amount * amount * last[0], inverse * inverse * first[1] + 2 * inverse * amount * control[1] + amount * amount * last[1]]
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def midpoint(first, second)
|
|
474
|
+
[(first[0] + second[0]) / 2.0, (first[1] + second[1]) / 2.0]
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def component_transform(glyph, flags, cursor)
|
|
478
|
+
if flags.anybits?(128)
|
|
479
|
+
glyph.byteslice(cursor, 8).unpack("s>4").map { |value| value / 16_384.0 }
|
|
480
|
+
elsif flags.anybits?(64)
|
|
481
|
+
x, y = glyph.byteslice(cursor, 4).unpack("s>2").map { |value| value / 16_384.0 }
|
|
482
|
+
[x, 0, 0, y]
|
|
483
|
+
elsif flags.anybits?(8)
|
|
484
|
+
scale = @reader.i16(glyph, cursor) / 16_384.0
|
|
485
|
+
[scale, 0, 0, scale]
|
|
486
|
+
else
|
|
487
|
+
[1, 0, 0, 1]
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def transform_size(flags)
|
|
492
|
+
return 8 if flags.anybits?(128)
|
|
493
|
+
return 4 if flags.anybits?(64)
|
|
494
|
+
flags.anybits?(8) ? 2 : 0
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def read_coordinates(glyph, flags, cursor, count, horizontal:)
|
|
498
|
+
values = []
|
|
499
|
+
previous = 0
|
|
500
|
+
count.times do |index|
|
|
501
|
+
flag = flags[index]
|
|
502
|
+
short = horizontal ? flag.anybits?(2) : flag.anybits?(4)
|
|
503
|
+
same = horizontal ? flag.anybits?(16) : flag.anybits?(32)
|
|
504
|
+
if short
|
|
505
|
+
delta = glyph.getbyte(cursor)
|
|
506
|
+
cursor += 1
|
|
507
|
+
delta = -delta if !same
|
|
508
|
+
elsif same
|
|
509
|
+
delta = 0
|
|
510
|
+
else
|
|
511
|
+
delta = glyph.byteslice(cursor, 2).unpack1("s>")
|
|
512
|
+
cursor += 2
|
|
513
|
+
end
|
|
514
|
+
previous += delta
|
|
515
|
+
values << previous
|
|
516
|
+
end
|
|
517
|
+
[values, cursor]
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def rasterize(points, end_points, x_min, y_min, x_max, y_max, width, height, scale)
|
|
521
|
+
alpha = Array.new(width * height, 0)
|
|
522
|
+
(0...height).each do |row|
|
|
523
|
+
(0...width).each do |column|
|
|
524
|
+
x = x_min + (column + 0.5) / scale
|
|
525
|
+
y = y_max - (row + 0.5) / scale
|
|
526
|
+
winding = 0
|
|
527
|
+
start = 0
|
|
528
|
+
end_points.each do |finish|
|
|
529
|
+
contour = points[start..finish]
|
|
530
|
+
contour.each_with_index do |first, index|
|
|
531
|
+
second = contour[(index + 1) % contour.length]
|
|
532
|
+
next unless (first[1] > y) != (second[1] > y)
|
|
533
|
+
crossing = (second[0] - first[0]) * (y - first[1]) / (second[1] - first[1]) + first[0]
|
|
534
|
+
winding += second[1] > first[1] ? 1 : -1 if crossing > x
|
|
535
|
+
end
|
|
536
|
+
start = finish + 1
|
|
537
|
+
end
|
|
538
|
+
alpha[row * width + column] = 255 if winding != 0
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
alpha.pack("C*")
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def load(path, size: 16)
|
|
546
|
+
Font.new(Reader.new(File.binread(path)), size)
|
|
547
|
+
rescue KeyError => error
|
|
548
|
+
raise UnsupportedError, "TrueType font is missing required table: #{error.message}"
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
module Layout
|
|
553
|
+
module_function
|
|
554
|
+
|
|
555
|
+
def width(text, font)
|
|
556
|
+
previous = nil
|
|
557
|
+
text.each_char.sum do |character|
|
|
558
|
+
advance = font.glyph(character).advance + (previous ? font.kerning(previous, character) : 0)
|
|
559
|
+
previous = character
|
|
560
|
+
advance
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def lines(text, font, width: nil, wrap: nil, tab_width: 4)
|
|
565
|
+
result = []
|
|
566
|
+
text.to_s.split("\n", -1).each do |raw|
|
|
567
|
+
expanded = raw.gsub("\t", " " * tab_width)
|
|
568
|
+
if width && wrap.to_sym == :word
|
|
569
|
+
current = +""
|
|
570
|
+
expanded.split(/(?<=\s)|(?=\s)/).each do |chunk|
|
|
571
|
+
if !current.empty? && Layout.width(current, font) + Layout.width(chunk, font) > width
|
|
572
|
+
result << current.rstrip
|
|
573
|
+
current = chunk.lstrip
|
|
574
|
+
else
|
|
575
|
+
current << chunk
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
result << current
|
|
579
|
+
elsif width && wrap
|
|
580
|
+
current = +""
|
|
581
|
+
expanded.each_char do |character|
|
|
582
|
+
if !current.empty? && Layout.width(current, font) + Layout.width(character, font) > width
|
|
583
|
+
result << current
|
|
584
|
+
current = ""
|
|
585
|
+
end
|
|
586
|
+
current << character
|
|
587
|
+
end
|
|
588
|
+
result << current
|
|
589
|
+
else
|
|
590
|
+
result << expanded
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
result
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
module Renderer
|
|
598
|
+
module_function
|
|
599
|
+
|
|
600
|
+
def draw(font, target, x, y, text, color:, baseline: false, line_spacing: 1.0, **_options)
|
|
601
|
+
x = Integer(x)
|
|
602
|
+
origin_y = baseline ? Integer(y) - font.ascent : Integer(y)
|
|
603
|
+
Layout.lines(text, font).each_with_index do |line, line_index|
|
|
604
|
+
cursor = x
|
|
605
|
+
line_y = origin_y + (font.line_height * line_spacing * line_index).round
|
|
606
|
+
previous = nil
|
|
607
|
+
line.each_char do |character|
|
|
608
|
+
glyph = font.glyph(character)
|
|
609
|
+
cursor += font.kerning(previous, character) if previous
|
|
610
|
+
draw_glyph(target, glyph, cursor + glyph.bearing_x, line_y + font.ascent - glyph.bearing_y, color)
|
|
611
|
+
cursor += glyph.advance
|
|
612
|
+
previous = character
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
target
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def draw_box(font, target, x:, y:, width:, text:, color:, align:, wrap:, line_spacing:, **options)
|
|
619
|
+
lines = Layout.lines(text, font, width: width, wrap: wrap)
|
|
620
|
+
lines.each_with_index do |line, index|
|
|
621
|
+
line_width = Layout.width(line, font)
|
|
622
|
+
offset = case align
|
|
623
|
+
when :center then (width - line_width) / 2
|
|
624
|
+
when :right then width - line_width
|
|
625
|
+
else 0
|
|
626
|
+
end
|
|
627
|
+
draw(font, target, x + offset, y + (font.line_height * line_spacing * index).round, line, color: color, **options)
|
|
628
|
+
end
|
|
629
|
+
target
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def draw_glyph(target, glyph, x, y, color)
|
|
633
|
+
if target.is_a?(Tessel::Image)
|
|
634
|
+
target.blit_mask(glyph, x, y, color)
|
|
635
|
+
elsif target.respond_to?(:get_pixel) && target.respond_to?(:set_pixel)
|
|
636
|
+
packed = Tessel::Color.pack(color).unpack("C4")
|
|
637
|
+
glyph.height.times do |row|
|
|
638
|
+
glyph.width.times do |column|
|
|
639
|
+
alpha = glyph.alpha.getbyte(row * glyph.width + column)
|
|
640
|
+
next if alpha.zero?
|
|
641
|
+
target.set_pixel(x + column, y + row, packed)
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
else
|
|
645
|
+
raise TypeError, "target must be a Tessel::Image or pixel surface"
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
private_class_method :draw_glyph
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
class Chain
|
|
652
|
+
def initialize(*fonts)
|
|
653
|
+
raise ArgumentError, "at least one font is required" if fonts.empty?
|
|
654
|
+
|
|
655
|
+
@fonts = fonts
|
|
656
|
+
@line_height = fonts.map(&:line_height).max
|
|
657
|
+
@ascent = fonts.map(&:ascent).max
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
attr_reader :line_height, :ascent
|
|
661
|
+
|
|
662
|
+
def glyph?(character)
|
|
663
|
+
@fonts.any? { |font| font.glyph?(character) }
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def glyph(character)
|
|
667
|
+
@fonts.find { |font| font.glyph?(character) }&.glyph(character) || @fonts.first.glyph(character)
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def kerning(_left, _right)
|
|
671
|
+
0
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def draw(target, x, y, text, **options)
|
|
675
|
+
Renderer.draw(self, target, x, y, text, **options)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def measure(text)
|
|
679
|
+
lines = Layout.lines(text, self)
|
|
680
|
+
[lines.map { |line| Layout.width(line, self) }.max.to_i, lines.length * line_height]
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def draw_text_box(target, x:, y:, width:, text:, color: [255, 255, 255, 255], align: :left, wrap: :word, line_spacing: 1.0, **options)
|
|
684
|
+
Renderer.draw_box(self, target, x: x, y: y, width: width, text: text, color: color, align: align, wrap: wrap, line_spacing: line_spacing, **options)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def render(text, color: [255, 255, 255, 255], padding: 0)
|
|
688
|
+
width, height = measure(text)
|
|
689
|
+
image = Tessel::Image.new(width + padding * 2, height + padding * 2)
|
|
690
|
+
draw(image, padding, padding, text, color: color)
|
|
691
|
+
image
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
module_function
|
|
696
|
+
|
|
697
|
+
def load(path, size: nil, **_options)
|
|
698
|
+
bytes = File.binread(path)
|
|
699
|
+
if bytes.start_with?("STARTFONT")
|
|
700
|
+
BDF.load(path)
|
|
701
|
+
elsif bytes.start_with?("\x00\x01\x00\x00")
|
|
702
|
+
TrueType.load(path, size: size || 16)
|
|
703
|
+
elsif bytes.start_with?("OTTO")
|
|
704
|
+
raise UnsupportedError, "CFF fonts are not supported"
|
|
705
|
+
else
|
|
706
|
+
raise UnsupportedError, "unknown font format"
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def default
|
|
711
|
+
@default ||= Builtin.font
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
module Builtin
|
|
715
|
+
module_function
|
|
716
|
+
|
|
717
|
+
def font
|
|
718
|
+
patterns = {
|
|
719
|
+
" " => ["00000", "00000", "00000", "00000", "00000", "00000", "00000"],
|
|
720
|
+
"?" => ["11110", "00001", "00010", "00100", "00100", "00000", "00100"],
|
|
721
|
+
"!" => ["00100", "00100", "00100", "00100", "00100", "00000", "00100"],
|
|
722
|
+
"." => ["00000", "00000", "00000", "00000", "00000", "00110", "00110"],
|
|
723
|
+
":" => ["00000", "00110", "00110", "00000", "00110", "00110", "00000"],
|
|
724
|
+
"-" => ["00000", "00000", "00000", "11111", "00000", "00000", "00000"],
|
|
725
|
+
"_" => ["00000", "00000", "00000", "00000", "00000", "00000", "11111"]
|
|
726
|
+
}
|
|
727
|
+
patterns.merge!(
|
|
728
|
+
"A" => %w[01110 10001 10001 11111 10001 10001 10001],
|
|
729
|
+
"B" => %w[11110 10001 10001 11110 10001 10001 11110],
|
|
730
|
+
"C" => %w[01111 10000 10000 10000 10000 10000 01111],
|
|
731
|
+
"D" => %w[11110 10001 10001 10001 10001 10001 11110],
|
|
732
|
+
"E" => %w[11111 10000 10000 11110 10000 10000 11111],
|
|
733
|
+
"F" => %w[11111 10000 10000 11110 10000 10000 10000],
|
|
734
|
+
"G" => %w[01111 10000 10000 10111 10001 10001 01111],
|
|
735
|
+
"H" => %w[10001 10001 10001 11111 10001 10001 10001],
|
|
736
|
+
"I" => %w[11111 00100 00100 00100 00100 00100 11111],
|
|
737
|
+
"J" => %w[00111 00010 00010 00010 00010 10010 01100],
|
|
738
|
+
"K" => %w[10001 10010 10100 11000 10100 10010 10001],
|
|
739
|
+
"L" => %w[10000 10000 10000 10000 10000 10000 11111],
|
|
740
|
+
"M" => %w[10001 11011 10101 10101 10001 10001 10001],
|
|
741
|
+
"N" => %w[10001 11001 10101 10011 10001 10001 10001],
|
|
742
|
+
"O" => %w[01110 10001 10001 10001 10001 10001 01110],
|
|
743
|
+
"P" => %w[11110 10001 10001 11110 10000 10000 10000],
|
|
744
|
+
"Q" => %w[01110 10001 10001 10001 10101 10010 01101],
|
|
745
|
+
"R" => %w[11110 10001 10001 11110 10100 10010 10001],
|
|
746
|
+
"S" => %w[01111 10000 10000 01110 00001 00001 11110],
|
|
747
|
+
"T" => %w[11111 00100 00100 00100 00100 00100 00100],
|
|
748
|
+
"U" => %w[10001 10001 10001 10001 10001 10001 01110],
|
|
749
|
+
"V" => %w[10001 10001 10001 10001 10001 01010 00100],
|
|
750
|
+
"W" => %w[10001 10001 10001 10101 10101 10101 01010],
|
|
751
|
+
"X" => %w[10001 10001 01010 00100 01010 10001 10001],
|
|
752
|
+
"Y" => %w[10001 10001 01010 00100 00100 00100 00100],
|
|
753
|
+
"Z" => %w[11111 00001 00010 00100 01000 10000 11111]
|
|
754
|
+
)
|
|
755
|
+
("a".."z").each { |letter| patterns[letter] = patterns[letter.upcase] }
|
|
756
|
+
patterns.merge!(
|
|
757
|
+
"0" => %w[01110 10001 10011 10101 11001 10001 01110],
|
|
758
|
+
"1" => %w[00100 01100 00100 00100 00100 00100 01110],
|
|
759
|
+
"2" => %w[01110 10001 00001 00010 00100 01000 11111],
|
|
760
|
+
"3" => %w[11110 00001 00001 01110 00001 00001 11110],
|
|
761
|
+
"4" => %w[00010 00110 01010 10010 11111 00010 00010],
|
|
762
|
+
"5" => %w[11111 10000 10000 11110 00001 00001 11110],
|
|
763
|
+
"6" => %w[01110 10000 10000 11110 10001 10001 01110],
|
|
764
|
+
"7" => %w[11111 00001 00010 00100 01000 01000 01000],
|
|
765
|
+
"8" => %w[01110 10001 10001 01110 10001 10001 01110],
|
|
766
|
+
"9" => %w[01110 10001 10001 01111 00001 00001 01110]
|
|
767
|
+
)
|
|
768
|
+
glyphs = patterns.transform_keys(&:ord).transform_values do |rows|
|
|
769
|
+
alpha = rows.join.chars.map { |bit| bit == "1" ? 255 : 0 }.pack("C*")
|
|
770
|
+
Glyph.new(advance: 6, bearing_x: 0, bearing_y: 7, width: 5, height: 7, alpha: alpha)
|
|
771
|
+
end
|
|
772
|
+
Font.new(glyphs, line_height: 9, ascent: 7)
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
def letter_pattern(letter)
|
|
776
|
+
seed = letter.ord
|
|
777
|
+
Array.new(7) { |row| (0...5).map { |column| ((seed + row * 3 + column * 5) % 7).between?(0, 2) ? "1" : "0" }.join }
|
|
778
|
+
end
|
|
779
|
+
private_class_method :letter_pattern
|
|
780
|
+
|
|
781
|
+
def digit_pattern(digit)
|
|
782
|
+
["01110", "10001", "10011", "10101", "11001", "10001", "01110"].rotate(digit.to_i % 7)
|
|
783
|
+
end
|
|
784
|
+
private_class_method :digit_pattern
|
|
785
|
+
end
|
|
786
|
+
end
|
data/sig/glyphic.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: glyphic
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ydah
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: tessel
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.1.0
|
|
26
|
+
description: BDF text rendering and layout on Tessel RGBA8 images.
|
|
27
|
+
email:
|
|
28
|
+
- t.yudai92@gmail.com
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- CHANGELOG.md
|
|
34
|
+
- LICENSE.txt
|
|
35
|
+
- README.md
|
|
36
|
+
- Rakefile
|
|
37
|
+
- lib/glyphic.rb
|
|
38
|
+
- lib/glyphic/version.rb
|
|
39
|
+
- sig/glyphic.rbs
|
|
40
|
+
homepage: https://github.com/rbgfx/glyphic
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata:
|
|
44
|
+
homepage_uri: https://github.com/rbgfx/glyphic
|
|
45
|
+
source_code_uri: https://github.com/rbgfx/glyphic/tree/main
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 3.1.0
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 4.0.16
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Bitmap and outline font rendering for Ruby graphics
|
|
63
|
+
test_files: []
|