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.
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alhena
4
+ # Analytic signed-area scan conversion; no supersampling for grayscale.
5
+ class Rasterizer
6
+ MAX_PIXELS = 16_777_216
7
+
8
+ def initialize(width:, height:, tolerance: 0.25)
9
+ unless [width, height].all? { |n| n.is_a?(Integer) && n >= 0 && n <= MAX_PIXELS } && width * height <= MAX_PIXELS
10
+ raise ArgumentError, "invalid or excessively large raster dimensions"
11
+ end
12
+ raise ArgumentError, "tolerance must be positive" unless tolerance.is_a?(Numeric) && tolerance.finite? && tolerance > 0
13
+ @width, @height, @tolerance = width, height, tolerance.to_f
14
+ end
15
+
16
+ # Coordinates increase right and down. Open subpaths are implicitly closed.
17
+ # darkening is a coverage-space gain (0..1); gamma is a positive exponent.
18
+ def fill(outline, transform: nil, left: 0, top: 0, gamma: 1.0, darkening: 0.0, lcd: nil)
19
+ raise ArgumentError, "gamma must be positive" unless gamma.is_a?(Numeric) && gamma.finite? && gamma > 0
20
+ raise ArgumentError, "darkening must be between 0 and 1" unless darkening.is_a?(Numeric) && darkening.finite? && (0..1).cover?(darkening)
21
+ raise ArgumentError, "LCD order must be :rgb or :bgr" unless [nil, :rgb, :bgr].include?(lcd)
22
+ if @width.zero? || @height.zero?
23
+ return Bitmap.new(width: @width, height: @height, left: left, top: top, coverage: "", channels: lcd ? 3 : 1)
24
+ end
25
+ outline = outline.transform(transform) if transform
26
+ return lcd_bitmap(outline, left: left, top: top, gamma: gamma, darkening: darkening, order: lcd) if lcd
27
+ @area = Array.new((@width + 1) * @height, 0.0)
28
+ draw_outline(outline)
29
+ Bitmap.new(width: @width, height: @height, left: left, top: top, coverage: grayscale_coverage(gamma, darkening))
30
+ ensure
31
+ @area = nil
32
+ end
33
+
34
+ private
35
+
36
+ def lcd_bitmap(outline, left:, top:, gamma:, darkening:, order:)
37
+ high = self.class.new(width: @width * 3 + 4, height: @height, tolerance: @tolerance)
38
+ coverage = high.fill(outline, transform: [3, 0, 0, 1, 2, 0]).coverage
39
+ output = String.new(capacity: @width * @height * 3, encoding: Encoding::BINARY)
40
+ @height.times do |y|
41
+ @width.times do |x|
42
+ values = 3.times.map do |channel|
43
+ at = y * (@width * 3 + 4) + x * 3 + channel
44
+ value = (coverage.getbyte(at) + 2 * coverage.getbyte(at + 1) + 3 * coverage.getbyte(at + 2) +
45
+ 2 * coverage.getbyte(at + 3) + coverage.getbyte(at + 4)) / (9.0 * 255)
46
+ encode_coverage(value, gamma, darkening)
47
+ end
48
+ values.reverse! if order == :bgr
49
+ values.each { |value| output << value }
50
+ end
51
+ end
52
+ Bitmap.new(width: @width, height: @height, left: left, top: top, coverage: output, channels: 3)
53
+ end
54
+
55
+ def draw_outline(outline)
56
+ x = y = sx = sy = 0.0
57
+ opened = false
58
+ outline.each do |command, *args|
59
+ case command
60
+ when :move_to
61
+ draw_line(x, y, sx, sy) if opened
62
+ x, y = args
63
+ sx, sy = x, y
64
+ opened = true
65
+ when :line_to
66
+ draw_line(x, y, *args)
67
+ x, y = args
68
+ when :quad_to
69
+ flatten_quad(x, y, *args)
70
+ x, y = args[-2, 2]
71
+ when :cubic_to
72
+ flatten_cubic(x, y, *args)
73
+ x, y = args[-2, 2]
74
+ when :close
75
+ draw_line(x, y, sx, sy) if opened
76
+ x, y, opened = sx, sy, false
77
+ end
78
+ end
79
+ draw_line(x, y, sx, sy) if opened
80
+ end
81
+
82
+ def grayscale_coverage(gamma, darkening)
83
+ output = String.new(capacity: @width * @height, encoding: Encoding::BINARY)
84
+ @height.times do |row|
85
+ acc = 0.0
86
+ @width.times do |col|
87
+ acc += @area[row * (@width + 1) + col]
88
+ output << encode_coverage([acc.abs, 1.0].min, gamma, darkening)
89
+ end
90
+ end
91
+ output
92
+ end
93
+
94
+ def encode_coverage(value, gamma, darkening)
95
+ value = [value * (1.0 + darkening), 1.0].min
96
+ ((gamma == 1.0 ? value : value**(1.0 / gamma)) * 255).round
97
+ end
98
+
99
+ def flatten_quad(x0, y0, cx, cy, x1, y1)
100
+ deviation = [(x0 - 2 * cx + x1).abs, (y0 - 2 * cy + y1).abs].max
101
+ needed = [Math.sqrt(deviation / @tolerance).ceil, 1].max
102
+ count = [1 << (needed - 1).bit_length, 4096].min
103
+ px, py = x0, y0
104
+ 1.upto(count) do |i|
105
+ t = i.to_f / count
106
+ s = 1.0 - t
107
+ x = s * s * x0 + 2 * s * t * cx + t * t * x1
108
+ y = s * s * y0 + 2 * s * t * cy + t * t * y1
109
+ draw_line(px, py, x, y)
110
+ px, py = x, y
111
+ end
112
+ end
113
+
114
+ def flatten_cubic(x0, y0, c1x, c1y, c2x, c2y, x1, y1, depth = 0)
115
+ deviation = [(2 * x1 - 3 * c2x + x0).abs, (2 * y1 - 3 * c2y + y0).abs,
116
+ (x1 - 3 * c1x + 2 * x0).abs, (y1 - 3 * c1y + 2 * y0).abs].max
117
+ if deviation <= @tolerance * 2 || depth >= 16
118
+ draw_line(x0, y0, x1, y1)
119
+ else
120
+ ax, ay = (x0 + c1x) / 2.0, (y0 + c1y) / 2.0
121
+ bx, by = (c1x + c2x) / 2.0, (c1y + c2y) / 2.0
122
+ cx, cy = (c2x + x1) / 2.0, (c2y + y1) / 2.0
123
+ dx, dy = (ax + bx) / 2.0, (ay + by) / 2.0
124
+ ex, ey = (bx + cx) / 2.0, (by + cy) / 2.0
125
+ mx, my = (dx + ex) / 2.0, (dy + ey) / 2.0
126
+ flatten_cubic(x0, y0, ax, ay, dx, dy, mx, my, depth + 1)
127
+ flatten_cubic(mx, my, ex, ey, cx, cy, x1, y1, depth + 1)
128
+ end
129
+ end
130
+
131
+ # Integral of clamp(x, 0, 1), used to integrate an edge's cell coverage.
132
+ def coverage_integral(x)
133
+ x <= 0 ? 0.0 : x >= 1 ? x - 0.5 : x * x * 0.5
134
+ end
135
+
136
+ def draw_line(x0, y0, x1, y1)
137
+ return if y0 == y1 || @width.zero? || @height.zero?
138
+ sign = 1.0
139
+ if y0 > y1
140
+ x0, y0, x1, y1 = x1, y1, x0, y0
141
+ sign = -1.0
142
+ end
143
+ first, last = [y0.floor, 0].max, [y1.ceil, @height].min
144
+ slope = (x1 - x0) / (y1 - y0)
145
+ first.upto(last - 1) do |row|
146
+ ya, yb = [y0, row].max, [y1, row + 1].min
147
+ xa, xb = x0 + (ya - y0) * slope, x0 + (yb - y0) * slope
148
+ xa, xb = xb, xa if xa > xb
149
+ height = (yb - ya) * sign
150
+ at = row * (@width + 1)
151
+ if xb <= 0
152
+ @area[at] += height
153
+ next
154
+ end
155
+ next if xa >= @width
156
+ start, finish = [xa.floor, 0].max, [xb.floor, @width - 1].min
157
+ previous = 0.0
158
+ start.upto(finish) do |col|
159
+ fraction = if xb - xa < 1e-12
160
+ [[col + 1 - xa, 0].max, 1].min
161
+ else
162
+ (coverage_integral(col + 1 - xa) - coverage_integral(col + 1 - xb)) / (xb - xa)
163
+ end
164
+ value = height * fraction
165
+ @area[at + col] += value - previous
166
+ previous = value
167
+ end
168
+ @area[at + finish + 1] += height - previous
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alhena
4
+ # Shared ItemVariationStore interpolation used by CFF2, HVAR and VVAR.
5
+ class VariationStore
6
+ def initialize(data, offset, coordinates)
7
+ @data, @offset, @coordinates = data, offset, coordinates
8
+ raise InvalidFont, "invalid item variation store" unless data.u16(offset) == 1
9
+ region_offset = offset + data.u32(offset + 2)
10
+ count = data.u16(offset + 6)
11
+ data.validate_bounds(offset + 8, count * 4)
12
+ @items = count.times.map { |i| offset + data.u32(offset + 8 + i * 4) }
13
+ axes, regions = data.u16(region_offset), data.u16(region_offset + 2)
14
+ raise InvalidFont, "variation axis count mismatch" unless axes == coordinates.length
15
+ data.validate_bounds(region_offset + 4, axes * regions * 6)
16
+ @scalars = regions.times.map do |region|
17
+ scalar = 1.0
18
+ axes.times do |axis|
19
+ at = region_offset + 4 + (region * axes + axis) * 6
20
+ start, peak, finish = 3.times.map { |i| data.i16(at + i * 2) / 16_384.0 }
21
+ next if peak.zero? || start > peak || peak > finish || (start < 0 && finish > 0)
22
+ coordinate = coordinates[axis]
23
+ scalar *= if coordinate == peak
24
+ 1.0
25
+ elsif coordinate <= start || coordinate >= finish
26
+ 0.0
27
+ elsif coordinate < peak
28
+ (coordinate - start) / (peak - start)
29
+ else
30
+ (finish - coordinate) / (finish - peak)
31
+ end
32
+ end
33
+ scalar
34
+ end
35
+ end
36
+
37
+ def region_scalars(index)
38
+ at = @items.fetch(index) { raise InvalidFont, "variation outer index out of range" }
39
+ count = @data.u16(at + 4)
40
+ @data.validate_bounds(at + 6, count * 2)
41
+ count.times.map { |i| @scalars.fetch(@data.u16(at + 6 + i * 2)) { raise InvalidFont, "variation region out of range" } }
42
+ end
43
+
44
+ def delta(outer, inner)
45
+ return 0.0 if outer == 0xffff && inner == 0xffff
46
+ at = @items.fetch(outer) { raise InvalidFont, "variation outer index out of range" }
47
+ items, words, count = @data.u16(at), @data.u16(at + 2), @data.u16(at + 4)
48
+ raise InvalidFont, "variation inner index out of range" unless inner >= 0 && inner < items
49
+ long = words & 0x8000 != 0
50
+ words &= 0x7fff
51
+ raise InvalidFont, "invalid variation word count" if words > count
52
+ stride = long ? words * 4 + (count - words) * 2 : words * 2 + count - words
53
+ position = at + 6 + count * 2 + inner * stride
54
+ scalars = region_scalars(outer)
55
+ @data.validate_bounds(position, stride)
56
+ @data.position = position
57
+ scalars.each_with_index.sum do |scalar, i|
58
+ value = long ? (i < words ? @data.i32 : @data.i16) : (i < words ? @data.i16 : @data.i8)
59
+ value * scalar
60
+ end
61
+ end
62
+
63
+ def self.index(data, at, glyph)
64
+ return [0, glyph] if at.zero?
65
+ format, entry_format = data.u8(at), data.u8(at + 1)
66
+ raise InvalidFont, "invalid variation index map" unless [0, 1].include?(format)
67
+ count = format.zero? ? data.u16(at + 2) : data.u32(at + 2)
68
+ return [0xffff, 0xffff] if count.zero?
69
+ width = ((entry_format >> 4) & 3) + 1
70
+ bits = (entry_format & 15) + 1
71
+ start = at + (format.zero? ? 4 : 6) + [glyph, count - 1].min * width
72
+ value = 0
73
+ width.times { |i| value = (value << 8) | data.u8(start + i) }
74
+ [value >> bits, value & ((1 << bits) - 1)]
75
+ end
76
+ end
77
+
78
+ class Font
79
+ # Axis descriptors are in fvar order; coordinates use user-space values.
80
+ def axes
81
+ @axes ||= begin
82
+ if @tables.key?("fvar")
83
+ data = table("fvar")
84
+ offset, count, stride = data.u16(4), data.u16(8), data.u16(10)
85
+ raise InvalidFont, "invalid fvar axis record size" if stride < 20
86
+ data.validate_bounds(offset, count * stride)
87
+ count.times.to_h do |i|
88
+ at = offset + i * stride
89
+ tag = data.bytes(at, 4)
90
+ minimum, default, maximum = data.fixed(at + 4), data.fixed(at + 8), data.fixed(at + 12)
91
+ raise InvalidFont, "invalid fvar axis range" unless minimum <= default && default <= maximum
92
+ [tag, {min: minimum, default: default, max: maximum, hidden: data.u16(at + 16) & 1 != 0, name: names[data.u16(at + 18)]}.freeze]
93
+ end.freeze
94
+ else
95
+ {}.freeze
96
+ end
97
+ end
98
+ end
99
+
100
+ def variation(values)
101
+ self.class.new(@binary.data, index: @index, axes: @axis_values.merge(values.transform_keys(&:to_s)))
102
+ end
103
+
104
+ def normalized_coordinates
105
+ @normalized_coordinates ||= begin
106
+ unknown = @axis_values.keys - axes.keys
107
+ raise ArgumentError, "unknown variation axes: #{unknown.join(', ')}" unless unknown.empty?
108
+ values = axes.map do |tag, axis|
109
+ value = @axis_values.fetch(tag, axis[:default])
110
+ raise ArgumentError, "axis values must be finite" unless value.is_a?(Numeric) && value.finite?
111
+ value = [[value, axis[:min]].max, axis[:max]].min
112
+ value == axis[:default] ? 0.0 : (value - axis[:default]) / (value < axis[:default] ? axis[:default] - axis[:min] : axis[:max] - axis[:default])
113
+ end
114
+ if @tables.key?("avar")
115
+ data = table("avar")
116
+ raise UnsupportedFont, "avar version 2 is unsupported" unless data.u16(0) == 1
117
+ raise InvalidFont, "avar axis count mismatch" unless data.u16(6) == values.length
118
+ data.position = 8
119
+ values.map! do |value|
120
+ count = data.u16
121
+ maps = count.times.map { [data.i16 / 16_384.0, data.i16 / 16_384.0] }
122
+ raise InvalidFont, "invalid avar segment map" unless maps.length >= 3 && maps.each_cons(2).all? { |a, b| a[0] < b[0] }
123
+ match = maps.find { |from, _| from == value }
124
+ if match
125
+ match[1]
126
+ else
127
+ pair = maps.each_cons(2).find { |a, b| value > a[0] && value < b[0] }
128
+ raise InvalidFont, "avar map does not cover coordinate" unless pair
129
+ a, b = pair
130
+ a[1] + (b[1] - a[1]) * (value - a[0]) / (b[0] - a[0])
131
+ end
132
+ end
133
+ end
134
+ values.freeze
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def variable? = @tables.key?("fvar") && normalized_coordinates.any? { |n| n != 0 }
141
+
142
+ def metric(glyph, vertical: false)
143
+ result = raw_metric(glyph, vertical: vertical)
144
+ return result unless variable?
145
+ tag = vertical ? "VVAR" : "HVAR"
146
+ if @tables.key?(tag)
147
+ data = table(tag)
148
+ @metric_stores ||= {}
149
+ store = @metric_stores[tag] ||= VariationStore.new(data, data.u32(4), normalized_coordinates)
150
+ result[0] += store.delta(*VariationStore.index(data, data.u32(8), glyph))
151
+ mapping = data.u32(12)
152
+ result[1] += store.delta(*VariationStore.index(data, mapping, glyph)) unless mapping.zero?
153
+ elsif @tables.key?("gvar")
154
+ @metric_deltas ||= {}
155
+ truetype_glyph(glyph, []) unless @metric_deltas.key?(glyph)
156
+ deltas = @metric_deltas.fetch(glyph, [0, 0])
157
+ result[0] += deltas[vertical ? 1 : 0]
158
+ end
159
+ result
160
+ end
161
+
162
+ def vary_points(glyph, points, ends, glyph_data)
163
+ return points unless variable? && @tables.key?("gvar")
164
+ # Phantom points let gvar supply advances when HVAR/VVAR are absent.
165
+ advance, bearing = raw_metric(glyph)
166
+ xmin = glyph_data.size >= 10 ? glyph_data.i16(2) : 0
167
+ ymax = glyph_data.size >= 10 ? glyph_data.i16(8) : 0
168
+ left = xmin - bearing
169
+ vadvance, vbearing = @tables.key?("vmtx") ? raw_metric(glyph, vertical: true) : [ascent - descent, ascent - ymax]
170
+ top = ymax + vbearing
171
+ all = points + [[left, 0], [left + advance, 0], [0, top], [0, top - vadvance]]
172
+ deltas = glyph_deltas(glyph, all, ends)
173
+ @metric_deltas ||= {}
174
+ @metric_deltas[glyph] = [deltas[-3][0] - deltas[-4][0], deltas[-2][1] - deltas[-1][1]]
175
+ points.each_with_index.map { |(x, y), i| [x + deltas[i][0], y + deltas[i][1]] }
176
+ end
177
+
178
+ def glyph_deltas(glyph, points, ends)
179
+ data = table("gvar")
180
+ axis_count = data.u16(4)
181
+ raise InvalidFont, "gvar axis count mismatch" unless axis_count == normalized_coordinates.length
182
+ raise InvalidFont, "gvar glyph count mismatch" unless data.u16(12) == glyph_count
183
+ long = data.u16(14) & 1 != 0
184
+ base = data.u32(16)
185
+ first = long ? data.u32(20 + glyph * 4) : data.u16(20 + glyph * 2) * 2
186
+ last = long ? data.u32(24 + glyph * 4) : data.u16(22 + glyph * 2) * 2
187
+ result = Array.new(points.length) { [0.0, 0.0] }
188
+ return result if first == last
189
+ record = Binary.new(data.bytes(base + first, last - first))
190
+ flags, serialized = record.u16, record.u16
191
+ headers = (flags & 0xfff).times.map do
192
+ size, index = record.u16, record.u16
193
+ peak = if index & 0x8000 != 0
194
+ axis_count.times.map { record.i16 / 16_384.0 }
195
+ else
196
+ shared = index & 0xfff
197
+ raise InvalidFont, "gvar shared tuple out of range" if shared >= data.u16(6)
198
+ location = data.u32(8) + shared * axis_count * 2
199
+ axis_count.times.map { |i| data.i16(location + i * 2) / 16_384.0 }
200
+ end
201
+ start, finish = if index & 0x4000 != 0
202
+ [axis_count.times.map { record.i16 / 16_384.0 }, axis_count.times.map { record.i16 / 16_384.0 }]
203
+ end
204
+ [size, index, peak, start, finish]
205
+ end
206
+ raise InvalidFont, "gvar expansion exceeds work limit" if points.length * headers.length > 4_000_000
207
+ raise InvalidFont, "overlapping gvar header and data" if record.position > serialized
208
+ record.position = serialized
209
+ shared_points = flags & 0x8000 != 0 ? packed_points(record, points.length) : nil
210
+ headers.each do |size, index, peak, start, finish|
211
+ tuple = Binary.new(record.bytes(record.position, size))
212
+ record.position += size
213
+ scalar = tuple_scalar(peak, start, finish)
214
+ next if scalar.zero?
215
+ selected = index & 0x2000 != 0 ? packed_points(tuple, points.length) : shared_points
216
+ selected ||= (0...points.length).to_a
217
+ dx, dy = packed_deltas(tuple, selected.length), packed_deltas(tuple, selected.length)
218
+ offsets = Array.new(points.length)
219
+ selected.each_with_index do |point, i|
220
+ offsets[point] ||= [0.0, 0.0]
221
+ offsets[point][0] += dx[i]
222
+ offsets[point][1] += dy[i]
223
+ end
224
+ interpolate_untouched(points, offsets, ends)
225
+ offsets.each_with_index do |delta, i|
226
+ next unless delta
227
+ result[i][0] += delta[0] * scalar
228
+ result[i][1] += delta[1] * scalar
229
+ end
230
+ end
231
+ result
232
+ end
233
+
234
+ def tuple_scalar(peak, start, finish)
235
+ scalar = 1.0
236
+ peak.each_with_index do |p, i|
237
+ next if p.zero?
238
+ value = normalized_coordinates[i]
239
+ if start
240
+ return 0.0 if value < start[i] || value > finish[i]
241
+ next if value == p
242
+ scalar *= value < p ? (value - start[i]) / (p - start[i]) : (finish[i] - value) / (finish[i] - p)
243
+ else
244
+ return 0.0 if value.zero? || value * p < 0
245
+ scalar *= value / p if value.abs < p.abs
246
+ end
247
+ end
248
+ scalar
249
+ end
250
+
251
+ def packed_points(data, maximum)
252
+ count = data.u8
253
+ return nil if count.zero?
254
+ count = ((count & 0x7f) << 8) | data.u8 if count & 0x80 != 0
255
+ points, previous = [], 0
256
+ while points.length < count
257
+ control = data.u8
258
+ run = (control & 0x7f) + 1
259
+ raise InvalidFont, "gvar point run overflow" if points.length + run > count
260
+ run.times do
261
+ previous += control & 0x80 != 0 ? data.u16 : data.u8
262
+ raise InvalidFont, "gvar point out of bounds" if previous >= maximum
263
+ points << previous
264
+ end
265
+ end
266
+ points
267
+ end
268
+
269
+ def packed_deltas(data, count)
270
+ result = []
271
+ while result.length < count
272
+ control = data.u8
273
+ length = (control & 0x3f) + 1
274
+ raise InvalidFont, "gvar delta run overflow" if result.length + length > count
275
+ length.times { result << (control & 0x80 != 0 ? 0 : control & 0x40 != 0 ? data.i16 : data.i8) }
276
+ end
277
+ result
278
+ end
279
+
280
+ def interpolate_untouched(points, deltas, ends)
281
+ first = 0
282
+ ends.each do |last|
283
+ touched = (first..last).select { |i| deltas[i] }
284
+ if touched.length == 1
285
+ (first..last).each { |i| deltas[i] ||= deltas[touched[0]].dup }
286
+ elsif touched.length > 1
287
+ touched.each_with_index do |a, index|
288
+ b = touched[(index + 1) % touched.length]
289
+ cursor = a == last ? first : a + 1
290
+ while cursor != b
291
+ deltas[cursor] = 2.times.map do |axis|
292
+ c1, c2 = points[a][axis], points[b][axis]
293
+ d1, d2 = deltas[a][axis], deltas[b][axis]
294
+ c1, c2, d1, d2 = c2, c1, d2, d1 if c1 > c2
295
+ value = points[cursor][axis]
296
+ if c1 == c2
297
+ d1 == d2 ? d1 : 0.0
298
+ elsif value <= c1
299
+ d1
300
+ elsif value >= c2
301
+ d2
302
+ else
303
+ d1 + (d2 - d1) * (value - c1).to_f / (c2 - c1)
304
+ end
305
+ end
306
+ cursor = cursor == last ? first : cursor + 1
307
+ end
308
+ end
309
+ end
310
+ first = last + 1
311
+ end
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alhena
4
+ VERSION = "0.1.0"
5
+ end
data/lib/alhena.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "alhena/version"
4
+ require_relative "alhena/data_compat"
5
+
6
+ module Alhena
7
+ class Error < StandardError; end
8
+ class InvalidFont < Error; end
9
+ class UnsupportedFont < Error; end
10
+ end
11
+
12
+ require_relative "alhena/bitmap"
13
+ require_relative "alhena/outline"
14
+ require_relative "alhena/binary"
15
+ require_relative "alhena/rasterizer"
16
+ require_relative "alhena/cff"
17
+ require_relative "alhena/font"
18
+ require_relative "alhena/variation"
19
+ require_relative "alhena/png"
20
+ require_relative "alhena/color"
21
+ require_relative "alhena/cache"
data/sig/alhena.rbs ADDED
@@ -0,0 +1,117 @@
1
+ module Alhena
2
+ VERSION: String
3
+ type matrix = Array[Numeric]
4
+ class Error < StandardError
5
+ end
6
+ class InvalidFont < Error
7
+ end
8
+ class UnsupportedFont < Error
9
+ end
10
+ class Binary
11
+ attr_reader data: String
12
+ attr_accessor position: Integer
13
+ def initialize: (String data) -> void
14
+ def size: () -> Integer
15
+ def bytes: (Integer offset, Integer length) -> String
16
+ def validate_bounds: (Integer offset, Integer length) -> void
17
+ def check: (Integer offset, Integer length) -> void
18
+ def u8: (?Integer? offset) -> Integer
19
+ def i8: (?Integer? offset) -> Integer
20
+ def u16: (?Integer? offset) -> Integer
21
+ def i16: (?Integer? offset) -> Integer
22
+ def u24: (?Integer? offset) -> Integer
23
+ def u32: (?Integer? offset) -> Integer
24
+ def i32: (?Integer? offset) -> Integer
25
+ def fixed: (?Integer? offset) -> Float
26
+ end
27
+ class Outline
28
+ attr_reader commands: Array[Symbol]
29
+ attr_reader coordinates: Array[Float]
30
+ def initialize: () -> void
31
+ def move_to: (Numeric x, Numeric y) -> self
32
+ def line_to: (Numeric x, Numeric y) -> self
33
+ def quad_to: (Numeric cx, Numeric cy, Numeric x, Numeric y) -> self
34
+ def cubic_to: (Numeric c1x, Numeric c1y, Numeric c2x, Numeric c2y, Numeric x, Numeric y) -> self
35
+ def close: () -> self
36
+ def empty?: () -> bool
37
+ def each: () { (Symbol, *Float) -> void } -> self
38
+ | () -> Enumerator[untyped, self]
39
+ def transform: (matrix) -> Outline
40
+ def append: (Outline) -> self
41
+ def bounds: () -> [Numeric, Numeric, Numeric, Numeric]
42
+ def to_quadratic: (?tolerance: Numeric) -> Outline
43
+ end
44
+ class Bitmap
45
+ attr_reader width: Integer
46
+ attr_reader height: Integer
47
+ attr_reader left: Integer
48
+ attr_reader top: Integer
49
+ attr_reader coverage: String
50
+ attr_reader channels: Integer
51
+ def initialize: (width: Integer, height: Integer, coverage: String, ?left: Integer, ?top: Integer, ?channels: Integer) -> void
52
+ def to_ascii: (?ramp: String) -> String
53
+ end
54
+ class Font
55
+ attr_reader index: Integer
56
+ attr_reader tables: Hash[String, [Integer, Integer]]
57
+ attr_reader axis_values: Hash[String, Numeric]
58
+ def data: () -> String
59
+ def self.open: (String path, ?index: Integer, ?axes: Hash[String | Symbol, Numeric]) -> Font
60
+ def initialize: (String data, ?index: Integer, ?axes: Hash[String | Symbol, Numeric]) -> void
61
+ def table: (String tag) -> Binary
62
+ def units_per_em: () -> Integer
63
+ def glyph_count: () -> Integer
64
+ def ascent: () -> Integer
65
+ def descent: () -> Integer
66
+ def line_gap: () -> Integer
67
+ def names: () -> Hash[Integer, String]
68
+ def family: () -> String?
69
+ def os2: () -> Hash[Symbol, Integer]
70
+ def post: () -> Hash[Symbol, Numeric | bool]
71
+ def glyph_id: (String | Integer character, ?variation_selector: String | Integer | nil) -> Integer
72
+ def advance: (Integer glyph, ?size: Numeric, ?vertical: bool) -> Float
73
+ def bearing: (Integer glyph, ?size: Numeric, ?vertical: bool) -> Float
74
+ def outline: (Integer glyph) -> Outline
75
+ def axes: () -> Hash[String, Hash[Symbol, Numeric | bool | String | nil]]
76
+ def variation: (Hash[String | Symbol, Numeric]) -> Font
77
+ def normalized_coordinates: () -> Array[Float]
78
+ def palettes: () -> Array[Array[Array[Integer]]]
79
+ def color_layers: (Integer glyph) -> Array[[Integer, Integer]]?
80
+ def color_bitmap: (Integer glyph, size: Numeric, ?palette: Integer, ?foreground: Array[Integer], ?subpixel_x: Numeric) -> ColorBitmap?
81
+ def embedded_bitmap: (Integer glyph, size: Numeric) -> EmbeddedBitmap?
82
+ def rasterize: (Integer glyph, size: Numeric, ?subpixel_x: Numeric, ?tolerance: Numeric, ?gamma: Numeric, ?darkening: Numeric, ?lcd: :rgb | :bgr | nil) -> Bitmap
83
+ end
84
+ class ColorBitmap
85
+ attr_reader width: Integer
86
+ attr_reader height: Integer
87
+ attr_reader left: Integer
88
+ attr_reader top: Integer
89
+ attr_reader rgba: String
90
+ def initialize: (width: Integer, height: Integer, rgba: String, ?left: Integer, ?top: Integer) -> void
91
+ def to_bitmap: () -> Bitmap
92
+ def resize: (Numeric factor) -> ColorBitmap
93
+ end
94
+ class EmbeddedBitmap < Data
95
+ attr_reader format: Symbol
96
+ attr_reader data: String
97
+ attr_reader ppem: Integer
98
+ attr_reader left: Integer
99
+ attr_reader top: Integer?
100
+ attr_reader width: Integer?
101
+ attr_reader height: Integer?
102
+ end
103
+ class Rasterizer
104
+ MAX_PIXELS: Integer
105
+ def initialize: (width: Integer, height: Integer, ?tolerance: Numeric) -> void
106
+ def fill: (Outline, ?transform: matrix?, ?left: Integer, ?top: Integer, ?gamma: Numeric, ?darkening: Numeric, ?lcd: :rgb | :bgr | nil) -> Bitmap
107
+ end
108
+ class Cache
109
+ attr_reader capacity: Integer
110
+ attr_reader bytesize: Integer
111
+ def initialize: (?capacity: Integer, ?max_bytes: Integer?) -> void
112
+ def size: () -> Integer
113
+ def rasterize: (Font font, Integer glyph, size: Numeric, ?subpixel_x: Numeric, **untyped options) -> Bitmap
114
+ def prewarm: (Font font, String text, ?Numeric? size, **untyped options) -> self
115
+ def clear: () -> self
116
+ end
117
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alhena
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Read sfnt and TTC fonts, extract outlines, and produce antialiased grayscale
13
+ or LCD glyph bitmaps without native dependencies.
14
+ email:
15
+ - t.yudai92@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - LICENSE.txt
22
+ - README.md
23
+ - examples/color.rb
24
+ - examples/render.rb
25
+ - lib/alhena.rb
26
+ - lib/alhena/binary.rb
27
+ - lib/alhena/bitmap.rb
28
+ - lib/alhena/cache.rb
29
+ - lib/alhena/cff.rb
30
+ - lib/alhena/color.rb
31
+ - lib/alhena/data_compat.rb
32
+ - lib/alhena/font.rb
33
+ - lib/alhena/outline.rb
34
+ - lib/alhena/png.rb
35
+ - lib/alhena/rasterizer.rb
36
+ - lib/alhena/variation.rb
37
+ - lib/alhena/version.rb
38
+ - sig/alhena.rbs
39
+ homepage: https://github.com/noxdea/alhena
40
+ licenses:
41
+ - MIT
42
+ metadata:
43
+ allowed_push_host: https://rubygems.org
44
+ source_code_uri: https://github.com/noxdea/alhena
45
+ changelog_uri: https://github.com/noxdea/alhena/blob/main/CHANGELOG.md
46
+ rubygems_mfa_required: 'true'
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 4.0.19
62
+ specification_version: 4
63
+ summary: Pure Ruby TrueType and CFF font rasterization
64
+ test_files: []