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
data/lib/alhena/color.rb
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alhena
|
|
4
|
+
# Straight (not premultiplied) sRGB RGBA8 pixels.
|
|
5
|
+
class ColorBitmap
|
|
6
|
+
attr_reader :width, :height, :left, :top, :rgba
|
|
7
|
+
|
|
8
|
+
def initialize(width:, height:, rgba:, left: 0, top: 0)
|
|
9
|
+
unless [width, height].all? { |n| n.is_a?(Integer) && n >= 0 && n <= Rasterizer::MAX_PIXELS } && width * height <= Rasterizer::MAX_PIXELS && rgba.bytesize == width * height * 4
|
|
10
|
+
raise ArgumentError, "invalid color bitmap dimensions"
|
|
11
|
+
end
|
|
12
|
+
@width, @height, @left, @top, @rgba = width, height, left, top, rgba.b.freeze
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_bitmap
|
|
17
|
+
alpha = String.new(capacity: width * height, encoding: Encoding::BINARY)
|
|
18
|
+
(width * height).times { |i| alpha << rgba.getbyte(i * 4 + 3) }
|
|
19
|
+
Bitmap.new(width: width, height: height, left: left, top: top, coverage: alpha)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Bilinear resampling in premultiplied-alpha space prevents dark fringes.
|
|
23
|
+
def resize(factor)
|
|
24
|
+
return self if factor == 1
|
|
25
|
+
raise ArgumentError, "scale must be positive and finite" unless factor.is_a?(Numeric) && factor.finite? && factor > 0
|
|
26
|
+
target_width, target_height = (width * factor).round, (height * factor).round
|
|
27
|
+
raise ArgumentError, "color bitmap exceeds size limit" if target_width * target_height > Rasterizer::MAX_PIXELS
|
|
28
|
+
pixels = +"".b
|
|
29
|
+
target_height.times do |y|
|
|
30
|
+
sy = [[(y + 0.5) / factor - 0.5, 0].max, height - 1].min
|
|
31
|
+
y0, y1, fy = sy.floor, [sy.floor + 1, height - 1].min, sy % 1
|
|
32
|
+
target_width.times do |x|
|
|
33
|
+
sx = [[(x + 0.5) / factor - 0.5, 0].max, width - 1].min
|
|
34
|
+
x0, x1, fx = sx.floor, [sx.floor + 1, width - 1].min, sx % 1
|
|
35
|
+
values = [0.0, 0.0, 0.0, 0.0]
|
|
36
|
+
[[x0, y0, (1 - fx) * (1 - fy)], [x1, y0, fx * (1 - fy)], [x0, y1, (1 - fx) * fy], [x1, y1, fx * fy]].each do |px, py, weight|
|
|
37
|
+
index = (py * width + px) * 4
|
|
38
|
+
alpha = rgba.getbyte(index + 3)
|
|
39
|
+
values[3] += alpha * weight
|
|
40
|
+
3.times { |c| values[c] += rgba.getbyte(index + c) * alpha * weight }
|
|
41
|
+
end
|
|
42
|
+
3.times { |c| pixels << (values[3].zero? ? 0 : values[c] / values[3]).round.clamp(0, 255) }
|
|
43
|
+
pixels << values[3].round.clamp(0, 255)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
self.class.new(width: target_width, height: target_height, left: (left * factor).round, top: (top * factor).round, rgba: pixels)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
EmbeddedBitmap = Data.define(:format, :data, :ppem, :left, :top, :width, :height)
|
|
51
|
+
|
|
52
|
+
class Font
|
|
53
|
+
alias rasterize_outline rasterize
|
|
54
|
+
|
|
55
|
+
def rasterize(glyph, size:, **options)
|
|
56
|
+
if @tables.key?("COLR") || @tables.key?("sbix") || @tables.key?("CBDT")
|
|
57
|
+
bitmap = color_bitmap(glyph, size: size, subpixel_x: options.fetch(:subpixel_x, 0))
|
|
58
|
+
return bitmap.to_bitmap if bitmap
|
|
59
|
+
end
|
|
60
|
+
rasterize_outline(glyph, size: size, **options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param glyph [Integer] glyph ID, not a Unicode scalar
|
|
64
|
+
# @param size [Numeric] requested pixels per em
|
|
65
|
+
# @return [ColorBitmap, nil] straight RGBA8 or nil without color data
|
|
66
|
+
def color_bitmap(glyph, size:, palette: 0, foreground: [0, 0, 0, 255], subpixel_x: 0)
|
|
67
|
+
validate_glyph(glyph)
|
|
68
|
+
scale_factor(size)
|
|
69
|
+
raise ArgumentError, "palette must be a nonnegative integer" unless palette.is_a?(Integer) && palette >= 0
|
|
70
|
+
raise ArgumentError, "foreground must be RGBA8" unless foreground.is_a?(Array) && foreground.length == 4 && foreground.all? { |n| n.is_a?(Integer) && (0..255).cover?(n) }
|
|
71
|
+
if (layers = color_layers(glyph))
|
|
72
|
+
colors = palettes.fetch(palette) { raise ArgumentError, "palette index out of range" }
|
|
73
|
+
rendered = layers.map do |layer, color|
|
|
74
|
+
rgba = color == 0xffff ? foreground : colors.fetch(color) { raise InvalidFont, "COLR palette index out of range" }
|
|
75
|
+
[rasterize_outline(layer, size: size, subpixel_x: subpixel_x), rgba]
|
|
76
|
+
end
|
|
77
|
+
return compose_layers(rendered)
|
|
78
|
+
end
|
|
79
|
+
embedded = embedded_bitmap(glyph, size: size)
|
|
80
|
+
return nil unless embedded
|
|
81
|
+
render_embedded_bitmap(embedded, size)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def palettes
|
|
85
|
+
return [] unless @tables.key?("CPAL")
|
|
86
|
+
@palettes ||= begin
|
|
87
|
+
data = table("CPAL")
|
|
88
|
+
raise UnsupportedFont, "unsupported CPAL version" unless [0, 1].include?(data.u16(0))
|
|
89
|
+
entries, count, records, offset = data.u16(2), data.u16(4), data.u16(6), data.u32(8)
|
|
90
|
+
data.validate_bounds(12, count * 2)
|
|
91
|
+
data.validate_bounds(offset, records * 4)
|
|
92
|
+
count.times.map do |i|
|
|
93
|
+
first = data.u16(12 + i * 2)
|
|
94
|
+
raise InvalidFont, "palette exceeds color records" if first + entries > records
|
|
95
|
+
entries.times.map do |j|
|
|
96
|
+
blue, green, red, alpha = data.bytes(offset + (first + j) * 4, 4).bytes
|
|
97
|
+
[red, green, blue, alpha].freeze
|
|
98
|
+
end.freeze
|
|
99
|
+
end.freeze
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def color_layers(glyph)
|
|
104
|
+
validate_glyph(glyph)
|
|
105
|
+
return nil unless @tables.key?("COLR")
|
|
106
|
+
data = table("COLR")
|
|
107
|
+
version = data.u16(0)
|
|
108
|
+
raise UnsupportedFont, "unsupported COLR version" unless [0, 1].include?(version)
|
|
109
|
+
count, base, layers, total = data.u16(2), data.u32(4), data.u32(8), data.u16(12)
|
|
110
|
+
data.validate_bounds(base, count * 6)
|
|
111
|
+
data.validate_bounds(layers, total * 4)
|
|
112
|
+
lo, hi = 0, count
|
|
113
|
+
while lo < hi
|
|
114
|
+
mid = (lo + hi) / 2
|
|
115
|
+
data.u16(base + mid * 6) < glyph ? lo = mid + 1 : hi = mid
|
|
116
|
+
end
|
|
117
|
+
return nil if lo == count || data.u16(base + lo * 6) != glyph
|
|
118
|
+
first, length = data.u16(base + lo * 6 + 2), data.u16(base + lo * 6 + 4)
|
|
119
|
+
raise InvalidFont, "COLR layer range exceeds table" if first + length > total
|
|
120
|
+
length.times.map { |i| [data.u16(layers + (first + i) * 4), data.u16(layers + (first + i) * 4 + 2)] }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def embedded_bitmap(glyph, size:)
|
|
124
|
+
validate_glyph(glyph)
|
|
125
|
+
scale_factor(size)
|
|
126
|
+
return sbix_bitmap(glyph, size) if @tables.key?("sbix")
|
|
127
|
+
return cbdt_bitmap(glyph, size) if @tables.key?("CBDT") && @tables.key?("CBLC")
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def render_embedded_bitmap(embedded, size)
|
|
134
|
+
raise UnsupportedFont, "embedded #{embedded.format.inspect} requires an image decoder; use embedded_bitmap to obtain the original bytes" unless embedded.format == :png || embedded.format == :rgba
|
|
135
|
+
width, height, pixels = embedded.format == :png ? PNG.decode(embedded.data) : [embedded.width, embedded.height, embedded.data]
|
|
136
|
+
if embedded.width && (embedded.width != width || embedded.height != height)
|
|
137
|
+
raise InvalidFont, "embedded PNG and glyph metrics dimensions differ"
|
|
138
|
+
end
|
|
139
|
+
top = embedded.top || height
|
|
140
|
+
bitmap = ColorBitmap.new(width: width, height: height, left: embedded.left, top: top, rgba: pixels)
|
|
141
|
+
bitmap.resize(size.to_f / embedded.ppem)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def compose_layers(layers)
|
|
145
|
+
layers = layers.reject { |bitmap, _| bitmap.width.zero? || bitmap.height.zero? }
|
|
146
|
+
return ColorBitmap.new(width: 0, height: 0, rgba: "") if layers.empty?
|
|
147
|
+
left = layers.map { |bitmap, _| bitmap.left }.min
|
|
148
|
+
top = layers.map { |bitmap, _| bitmap.top }.max
|
|
149
|
+
width = layers.map { |bitmap, _| bitmap.left + bitmap.width }.max - left
|
|
150
|
+
height = top - layers.map { |bitmap, _| bitmap.top - bitmap.height }.min
|
|
151
|
+
raise InvalidFont, "color layers exceed bitmap limit" if width * height > Rasterizer::MAX_PIXELS
|
|
152
|
+
pixels = "\0".b * (width * height * 4)
|
|
153
|
+
layers.each do |bitmap, color|
|
|
154
|
+
bitmap.coverage.each_byte.with_index do |coverage, i|
|
|
155
|
+
next if coverage.zero? || color[3].zero?
|
|
156
|
+
index = ((top - bitmap.top + i / bitmap.width) * width + bitmap.left - left + i % bitmap.width) * 4
|
|
157
|
+
source_alpha = coverage * color[3] / (255.0 * 255)
|
|
158
|
+
destination_alpha = pixels.getbyte(index + 3) / 255.0
|
|
159
|
+
alpha = source_alpha + destination_alpha * (1 - source_alpha)
|
|
160
|
+
3.times do |channel|
|
|
161
|
+
value = (color[channel] * source_alpha + pixels.getbyte(index + channel) * destination_alpha * (1 - source_alpha)) / alpha
|
|
162
|
+
pixels.setbyte(index + channel, value.round.clamp(0, 255))
|
|
163
|
+
end
|
|
164
|
+
pixels.setbyte(index + 3, (alpha * 255).round.clamp(0, 255))
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
ColorBitmap.new(width: width, height: height, left: left, top: top, rgba: pixels)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def sbix_bitmap(glyph, size)
|
|
171
|
+
data = table("sbix")
|
|
172
|
+
raise InvalidFont, "invalid sbix version" unless data.u16(0) == 1
|
|
173
|
+
count = data.u32(4)
|
|
174
|
+
data.validate_bounds(8, count * 4)
|
|
175
|
+
strikes = count.times.map { |i| at = data.u32(8 + i * 4); [data.u16(at), at] }
|
|
176
|
+
strikes.sort_by! { |ppem, _| [ppem >= size ? 0 : 1, (ppem - size).abs] }
|
|
177
|
+
strikes.each do |ppem, strike|
|
|
178
|
+
raise InvalidFont, "invalid bitmap strike size" if ppem.zero?
|
|
179
|
+
target, visited = glyph, []
|
|
180
|
+
loop do
|
|
181
|
+
raise InvalidFont, "cyclic sbix duplicate" if visited.include?(target)
|
|
182
|
+
visited << target
|
|
183
|
+
raise InvalidFont, "sbix duplicate glyph out of range" unless target >= 0 && target < glyph_count
|
|
184
|
+
first, last = data.u32(strike + 4 + target * 4), data.u32(strike + 8 + target * 4)
|
|
185
|
+
break if first == last
|
|
186
|
+
entry = Binary.new(data.bytes(strike + first, last - first))
|
|
187
|
+
left, bottom, format = entry.i16(0), entry.i16(2), entry.bytes(4, 4)
|
|
188
|
+
if format == "dupe"
|
|
189
|
+
target = entry.u16(8)
|
|
190
|
+
next
|
|
191
|
+
end
|
|
192
|
+
payload = entry.bytes(8, entry.size - 8).freeze
|
|
193
|
+
width = height = nil
|
|
194
|
+
if format == "png "
|
|
195
|
+
png = Binary.new(payload)
|
|
196
|
+
raise InvalidFont, "invalid embedded PNG" unless png.bytes(0, 8) == PNG::SIGNATURE
|
|
197
|
+
width, height = png.u32(16), png.u32(20)
|
|
198
|
+
end
|
|
199
|
+
# sbix origin is relative to glyf's lower-left box when contours exist.
|
|
200
|
+
if @tables.key?("glyf")
|
|
201
|
+
path = outline(glyph)
|
|
202
|
+
unless path.empty?
|
|
203
|
+
bounds = path.bounds
|
|
204
|
+
left += (bounds[0] * ppem.to_f / units_per_em).round
|
|
205
|
+
bottom += (bounds[1] * ppem.to_f / units_per_em).round
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
return EmbeddedBitmap.new(format: format.strip.to_sym, data: payload, ppem: ppem, left: left, top: height && bottom + height, width: width, height: height)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def cbdt_bitmap(glyph, size, ancestors = [])
|
|
215
|
+
raise InvalidFont, "cyclic or deeply nested CBDT composite" if ancestors.include?(glyph) || ancestors.length > 5
|
|
216
|
+
locations, bitmaps = table("CBLC"), table("CBDT")
|
|
217
|
+
count = locations.u32(4)
|
|
218
|
+
locations.validate_bounds(8, count * 48)
|
|
219
|
+
strikes = count.times.map { |i| at = 8 + i * 48; [locations.u8(at + 45), at] }
|
|
220
|
+
strikes.sort_by! { |ppem, _| [ppem >= size ? 0 : 1, (ppem - size).abs] }
|
|
221
|
+
strikes.each do |ppem, strike|
|
|
222
|
+
raise InvalidFont, "invalid CBDT strike size" if ppem.zero?
|
|
223
|
+
next unless glyph >= locations.u16(strike + 40) && glyph <= locations.u16(strike + 42)
|
|
224
|
+
list, count = locations.u32(strike), locations.u32(strike + 8)
|
|
225
|
+
locations.validate_bounds(list, count * 8)
|
|
226
|
+
count.times do |i|
|
|
227
|
+
record = list + i * 8
|
|
228
|
+
first, last = locations.u16(record), locations.u16(record + 2)
|
|
229
|
+
next unless glyph >= first && glyph <= last
|
|
230
|
+
at = list + locations.u32(record + 4)
|
|
231
|
+
index_format, image_format, image_base = locations.u16(at), locations.u16(at + 2), locations.u32(at + 4)
|
|
232
|
+
offset, length, metrics = cbdt_bitmap_location(locations, at, index_format, glyph, first, last)
|
|
233
|
+
next unless offset && length.positive?
|
|
234
|
+
image = Binary.new(bitmaps.bytes(image_base + offset, length))
|
|
235
|
+
if [1, 2, 8, 17].include?(image_format)
|
|
236
|
+
metrics = image.bytes(0, 5)
|
|
237
|
+
image.position = 5
|
|
238
|
+
elsif [6, 7, 9, 18].include?(image_format)
|
|
239
|
+
metrics = image.bytes(0, 8)
|
|
240
|
+
image.position = 8
|
|
241
|
+
end
|
|
242
|
+
raise InvalidFont, "CBDT bitmap has no metrics" unless metrics
|
|
243
|
+
height, width, left, top = metrics.unpack("CCcc")
|
|
244
|
+
if [17, 18, 19].include?(image_format)
|
|
245
|
+
length = image.u32
|
|
246
|
+
payload = image.bytes(image.position, length).freeze
|
|
247
|
+
format = :png
|
|
248
|
+
elsif [1, 2, 5, 6, 7].include?(image_format)
|
|
249
|
+
payload = unpack_cbdt_bitmap(image, width, height, locations.u8(strike + 46), [1, 6].include?(image_format)).freeze
|
|
250
|
+
format = :rgba
|
|
251
|
+
elsif [8, 9].include?(image_format)
|
|
252
|
+
image.u8 if image_format == 8 # padding after small metrics
|
|
253
|
+
payload = compose_cbdt_pixels(image, width: width, height: height, ppem: ppem, glyph: glyph, ancestors: ancestors)
|
|
254
|
+
format = :rgba
|
|
255
|
+
else
|
|
256
|
+
raise UnsupportedFont, "unsupported CBDT image format #{image_format}"
|
|
257
|
+
end
|
|
258
|
+
return EmbeddedBitmap.new(format: format, data: payload, ppem: ppem, left: left, top: top, width: width, height: height)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
nil
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def compose_cbdt_pixels(image, width:, height:, ppem:, glyph:, ancestors:)
|
|
265
|
+
components = image.u16
|
|
266
|
+
image.validate_bounds(image.position, components * 4)
|
|
267
|
+
payload = "\0".b * (width * height * 4)
|
|
268
|
+
components.times do
|
|
269
|
+
component, dx, dy = image.u16, image.i8, image.i8
|
|
270
|
+
raise InvalidFont, "CBDT composite glyph out of range" if component >= glyph_count
|
|
271
|
+
child = cbdt_bitmap(component, ppem, ancestors + [glyph])
|
|
272
|
+
raise InvalidFont, "missing CBDT component bitmap" unless child
|
|
273
|
+
cw, ch, pixels = child.format == :png ? PNG.decode(child.data) : [child.width, child.height, child.data]
|
|
274
|
+
ch.times do |row|
|
|
275
|
+
next unless row + dy >= 0 && row + dy < height
|
|
276
|
+
cw.times do |col|
|
|
277
|
+
next unless col + dx >= 0 && col + dx < width
|
|
278
|
+
source = (row * cw + col) * 4
|
|
279
|
+
target = ((row + dy) * width + col + dx) * 4
|
|
280
|
+
sa, da = pixels.getbyte(source + 3) / 255.0, payload.getbyte(target + 3) / 255.0
|
|
281
|
+
alpha = sa + da * (1 - sa)
|
|
282
|
+
next if alpha.zero?
|
|
283
|
+
3.times { |channel| payload.setbyte(target + channel, ((pixels.getbyte(source + channel) * sa + payload.getbyte(target + channel) * da * (1 - sa)) / alpha).round.clamp(0, 255)) }
|
|
284
|
+
payload.setbyte(target + 3, (alpha * 255).round.clamp(0, 255))
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
payload.freeze
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def cbdt_bitmap_location(data, at, format, glyph, first, last)
|
|
292
|
+
metrics = nil
|
|
293
|
+
case format
|
|
294
|
+
when 1, 3
|
|
295
|
+
width = format == 1 ? 4 : 2
|
|
296
|
+
data.validate_bounds(at + 8, (last - first + 2) * width)
|
|
297
|
+
offset = at + 8 + (glyph - first) * width
|
|
298
|
+
a, b = format == 1 ? [data.u32(offset), data.u32(offset + 4)] : [data.u16(offset), data.u16(offset + 2)]
|
|
299
|
+
when 2, 5
|
|
300
|
+
size = data.u32(at + 8)
|
|
301
|
+
metrics = data.bytes(at + 12, 8)
|
|
302
|
+
index = glyph - first
|
|
303
|
+
if format == 5
|
|
304
|
+
count = data.u32(at + 20)
|
|
305
|
+
data.validate_bounds(at + 24, count * 2)
|
|
306
|
+
index = count.times.find { |i| data.u16(at + 24 + i * 2) == glyph }
|
|
307
|
+
return [nil, nil, nil] unless index
|
|
308
|
+
end
|
|
309
|
+
a, b = index * size, (index + 1) * size
|
|
310
|
+
when 4
|
|
311
|
+
count = data.u32(at + 8)
|
|
312
|
+
data.validate_bounds(at + 12, (count + 1) * 4)
|
|
313
|
+
index = count.times.find { |i| data.u16(at + 12 + i * 4) == glyph }
|
|
314
|
+
return [nil, nil, nil] unless index
|
|
315
|
+
a, b = data.u16(at + 14 + index * 4), data.u16(at + 18 + index * 4)
|
|
316
|
+
else
|
|
317
|
+
raise UnsupportedFont, "unsupported CBLC index format #{format}"
|
|
318
|
+
end
|
|
319
|
+
raise InvalidFont, "unordered CBDT offsets" if b < a
|
|
320
|
+
[a, b - a, metrics]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def unpack_cbdt_bitmap(data, width, height, depth, row_aligned)
|
|
324
|
+
raise UnsupportedFont, "unsupported CBDT bit depth" unless [1, 2, 4, 8, 32].include?(depth)
|
|
325
|
+
stride = row_aligned ? ((width * depth + 7) / 8) * 8 : width * depth
|
|
326
|
+
bytes = data.bytes(data.position, (stride * height + 7) / 8)
|
|
327
|
+
output = +"".b
|
|
328
|
+
(width * height).times do |i|
|
|
329
|
+
bit = (i / width) * stride + (i % width) * depth
|
|
330
|
+
if depth == 32
|
|
331
|
+
blue, green, red, alpha = bytes.byteslice(bit / 8, 4).bytes
|
|
332
|
+
[red, green, blue].each { |c| output << (alpha.zero? ? 0 : (c * 255.0 / alpha).round.clamp(0, 255)) }
|
|
333
|
+
output << alpha
|
|
334
|
+
else
|
|
335
|
+
value = (bytes.getbyte(bit / 8) >> (8 - depth - bit % 8)) & ((1 << depth) - 1)
|
|
336
|
+
output << 0 << 0 << 0 << (value * 255.0 / ((1 << depth) - 1)).round
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
output
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby 3.2 added Data; Struct provides the same record operations used here.
|
|
4
|
+
unless defined?(Data)
|
|
5
|
+
Data = Struct
|
|
6
|
+
def Data.define(*members, &block)
|
|
7
|
+
Struct.new(*members) do
|
|
8
|
+
members.each { |member| undef_method :"#{member}=" }
|
|
9
|
+
define_method(:initialize) do |*values, **keywords|
|
|
10
|
+
if keywords.any?
|
|
11
|
+
raise ArgumentError, "expected either positional or keyword members" unless values.empty? && keywords.keys.sort == members.sort
|
|
12
|
+
values = members.map { |member| keywords.fetch(member) }
|
|
13
|
+
end
|
|
14
|
+
raise ArgumentError, "wrong number of members" unless values.length == members.length
|
|
15
|
+
super(*values)
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
define_method(:with) do |**changes|
|
|
19
|
+
changes.empty? ? self : self.class.new(**to_h.merge(changes))
|
|
20
|
+
end
|
|
21
|
+
class_eval(&block) if block
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|