thumbhash 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/thumbhash.rb +321 -0
  3. metadata +43 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8a3aead19626a9f7edcc71fb0fb42d4be8d4536ea82bbefdb1ca619edd284b29
4
+ data.tar.gz: ccd866a6a98dd8bc26b2c419186996f8b44a186333feda099b44fd5b9db90874
5
+ SHA512:
6
+ metadata.gz: d1f6a165487dee2dc21451f1103c3af660bc25dd570396129121137b882eb7f1186acdfa83ccf585f6352d5f3fb5bf05b1dbd3cd875f26078c1609f7ea0f3b0a
7
+ data.tar.gz: 57c8d8b6e489861d11be492a8c7f1b1c2026ea4818c10ce4e986c3dc0ec084f87b52d030b1c92f3d038818e2d0e81ed3abe0206eca561b0fbbcce48848a450f2
data/lib/thumbhash.rb ADDED
@@ -0,0 +1,321 @@
1
+ require "base64"
2
+
3
+ class ThumbHash
4
+
5
+ # Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A.
6
+ def self.rgba_to_thumb_hash(w, h, rgba)
7
+ # Encoding an image larger than 100x100 is slow with no benefit
8
+ raise "#{w}x#{h} doesn't fit in 100x100" if w > 100 || h > 100
9
+
10
+ # Determine the average color
11
+ avg_r, avg_g, avg_b, avg_a = 0, 0, 0, 0
12
+ (0...(w * h)).each do |i|
13
+ j = i * 4
14
+ alpha = rgba[j + 3] / 255.0
15
+ avg_r += alpha / 255.0 * rgba[j]
16
+ avg_g += alpha / 255.0 * rgba[j + 1]
17
+ avg_b += alpha / 255.0 * rgba[j + 2]
18
+ avg_a += alpha
19
+ end
20
+
21
+ if avg_a > 0
22
+ avg_r /= avg_a
23
+ avg_g /= avg_a
24
+ avg_b /= avg_a
25
+ end
26
+
27
+ has_alpha = avg_a < w * h
28
+ l_limit = has_alpha ? 5 : 7 # Use fewer luminance bits if there's alpha
29
+ lx = [1, (l_limit * w / [w, h].max).round].max
30
+ ly = [1, (l_limit * h / [w, h].max).round].max
31
+ l = [] # luminance
32
+ p = [] # yellow - blue
33
+ q = [] # red - green
34
+ a = [] # alpha
35
+
36
+ # Convert the image from RGBA to LPQA (composite atop the average color)
37
+ (0...(w * h)).each do |i|
38
+ j = i * 4
39
+ alpha = rgba[j + 3] / 255.0
40
+ r = avg_r * (1 - alpha) + alpha / 255.0 * rgba[j]
41
+ g = avg_g * (1 - alpha) + alpha / 255.0 * rgba[j + 1]
42
+ b = avg_b * (1 - alpha) + alpha / 255.0 * rgba[j + 2]
43
+ l[i] = (r + g + b) / 3
44
+ p[i] = (r + g) / 2 - b
45
+ q[i] = r - g
46
+ a[i] = alpha
47
+ end
48
+
49
+ encode_channel = ->(channel, nx, ny) do
50
+ dc, ac, scale, fx = 0, [], 0, Array.new(w)
51
+
52
+ (0...ny).each do |cy|
53
+ (0...((nx * (ny - cy)).to_f / ny).ceil).each do |cx|
54
+ f = 0
55
+
56
+ w.times do |x|
57
+ fx[x] = Math.cos(Math::PI / w * cx * (x + 0.5))
58
+ end
59
+
60
+ h.times do |y|
61
+ h_scale = Math.cos(Math::PI / h * cy * (y + 0.5))
62
+ w.times do |x|
63
+ f += channel[x + y * w] * fx[x] * h_scale
64
+ end
65
+ end
66
+
67
+ f /= w * h
68
+
69
+ if cx.zero? && cy.zero?
70
+ dc = f
71
+ else
72
+ ac.push(f)
73
+ scale = [scale, f.abs].max
74
+ end
75
+ end
76
+ end
77
+
78
+ ac.map! { |x| 0.5 + 0.5 / scale * x } if scale > 0
79
+
80
+ [dc, ac, scale]
81
+ end
82
+
83
+ l_dc, l_ac, l_scale = encode_channel.(l, [3, lx].max, [3, ly].max)
84
+ p_dc, p_ac, p_scale = encode_channel.(p, 3, 3)
85
+ q_dc, q_ac, q_scale = encode_channel.(q, 3, 3)
86
+ a_dc, a_ac, a_scale = has_alpha ? encode_channel.(a, 5, 5) : []
87
+
88
+ # Write the constants
89
+ is_landscape = w > h
90
+ header24 = (63 * l_dc).round | ((31.5 + 31.5 * p_dc).round << 6) | ((31.5 + 31.5 * q_dc).round << 12) | ((31 * l_scale).round << 18) | (has_alpha ? (1 << 23) : 0)
91
+ header16 = (is_landscape ? ly : lx) | ((63 * p_scale).round << 3) | ((63 * q_scale).round << 9) | (is_landscape ? (1 << 15) : 0)
92
+ hash = [header24 & 255, (header24 >> 8) & 255, header24 >> 16, header16 & 255, header16 >> 8]
93
+ ac_start = has_alpha ? 6 : 5
94
+ ac_index = 0
95
+ hash.push((15 * a_dc).round | ((15 * a_scale).round << 4)) if has_alpha
96
+
97
+ # Write the varying factors
98
+ ac = has_alpha ? [l_ac, p_ac, q_ac, a_ac] : [l_ac, p_ac, q_ac]
99
+
100
+ ac.each do |a|
101
+ a.each do |f|
102
+ index = ac_start + (ac_index >> 1)
103
+ result = (15 * f).round << ((ac_index & 1) * 4)
104
+ hash[index] = hash[index].nil? ? result : hash[index] | result
105
+ ac_index += 1
106
+ end
107
+ end
108
+
109
+ hash.pack("C*")
110
+ end
111
+
112
+ # Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
113
+ def self.thumb_hash_to_rgba(hash)
114
+ hash = hash.unpack("C*")
115
+
116
+ # Read the constants
117
+ header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16)
118
+ header16 = hash[3] | (hash[4] << 8)
119
+ l_dc = (header24 & 63) / 63.0
120
+ p_dc = ((header24 >> 6) & 63) / 31.5 - 1
121
+ q_dc = ((header24 >> 12) & 63) / 31.5 - 1
122
+ l_scale = ((header24 >> 18) & 31) / 31.0
123
+ has_alpha = header24 >> 23
124
+ p_scale = ((header16 >> 3) & 63) / 63.0
125
+ q_scale = ((header16 >> 9) & 63) / 63.0
126
+ is_landscape = header16 >> 15
127
+ lx = [3, is_landscape.zero? ? header16 & 7 : (has_alpha.zero? ? 7 : 5)].max
128
+ ly = [3, is_landscape.zero? ? has_alpha.zero? ? 7 : 5 : header16 & 7].max
129
+ a_dc = has_alpha.zero? ? 1 : (hash[5] & 15) / 15
130
+ a_scale = (hash[5] >> 4) / 15.0
131
+
132
+ # Read the varying factors (boost saturation by 1.25x to compensate for quantization)
133
+ ac_start = has_alpha.zero? ? 5 : 6
134
+ ac_index = 0
135
+
136
+ decode_channel = ->(nx, ny, scale) do
137
+ ac = []
138
+ (0...ny).each do |cy|
139
+ cx = cy.zero? ? 1 : 0
140
+ while cx * ny < nx * (ny - cy)
141
+ ac.push(
142
+ (((hash[ac_start + (ac_index >> 1)] >> ((ac_index & 1) << 2)) & 15) / 7.5 - 1) * scale
143
+ )
144
+ ac_index += 1
145
+ cx += 1
146
+ end
147
+ end
148
+ ac
149
+ end
150
+ l_ac = decode_channel.(lx, ly, l_scale)
151
+ p_ac = decode_channel.(3, 3, p_scale * 1.25)
152
+ q_ac = decode_channel.(3, 3, q_scale * 1.25)
153
+ a_ac = !has_alpha.zero? && decode_channel.(5, 5, a_scale)
154
+
155
+ # Decode using the DCT into RGB
156
+ ratio = thumb_hash_to_approximate_aspect_ratio(hash)
157
+ w = (ratio > 1 ? 32 : 32 * ratio).round
158
+ h = (ratio > 1 ? 32 / ratio : 32).round
159
+ fx, fy, rgba = [], [], Array.new(w * h * 4, 0)
160
+
161
+ (0...h).each do |y|
162
+ i = y * w * 4
163
+ (0...w).each do |x|
164
+ l, p, q, a = l_dc, p_dc, q_dc, a_dc
165
+
166
+ # Precompute the coefficients
167
+ (0...[lx, has_alpha ? 5 : 3].max).each do |cx|
168
+ fx[cx] = Math.cos((Math::PI / w) * (x + 0.5) * cx)
169
+ end
170
+ (0...[ly, has_alpha ? 5 : 3].max).each do |cy|
171
+ fy[cy] = Math.cos((Math::PI / h) * (y + 0.5) * cy)
172
+ end
173
+
174
+ # Decode L
175
+ j = 0
176
+ (0...ly).each do |cy|
177
+ fy2 = fy[cy] * 2
178
+ cx = cy.zero? ? 1 : 0
179
+
180
+ while cx * ly < lx * (ly - cy)
181
+ l += l_ac[j] * fx[cx] * fy2
182
+ cx += 1
183
+ j += 1
184
+ end
185
+ end
186
+
187
+ # Decode P and Q
188
+ j = 0
189
+ (0...3).each do |cy|
190
+ fy2 = fy[cy] * 2
191
+ cx = cy.zero? ? 1 : 0
192
+ while cx < 3 - cy
193
+ f = fx[cx] * fy2
194
+ p += p_ac[j] * f
195
+ q += q_ac[j] * f
196
+ cx += 1
197
+ j += 1
198
+ end
199
+ end
200
+
201
+ # Decode A
202
+ if !has_alpha.zero?
203
+ j = 0
204
+ (0...5).each do |cy|
205
+ fy2 = fy[cy] * 2
206
+ cx = cy.zero? ? 1 : 0
207
+ while cx < 5 - cy
208
+ a += a_ac[j] * fx[cx] * fy2
209
+ cx += 1
210
+ j += 1
211
+ end
212
+ end
213
+ end
214
+
215
+ # Convert to RGB
216
+ b = l - (2 / 3.0) * p
217
+ r = (3 * l - b + q) / 2.0
218
+ g = r - q
219
+ rgba[i] = [0, 255 * [1, r].min].max.to_i
220
+ rgba[i + 1] = [0, 255 * [1, g].min].max.to_i
221
+ rgba[i + 2] = [0, 255 * [1, b].min].max.to_i
222
+ rgba[i + 3] = [0, 255 * [1, a].min].max.to_i
223
+
224
+ i += 4
225
+ end
226
+ end
227
+
228
+ [w, h, rgba]
229
+ end
230
+
231
+ # Extracts the approximate aspect ratio of the original image.
232
+ def self.thumb_hash_to_approximate_aspect_ratio(hash)
233
+ header = hash[3]
234
+ has_alpha = (hash[2] & 0x80) != 0
235
+ is_landscape = (hash[4] & 0x80) != 0
236
+ lx = is_landscape ? (has_alpha ? 5 : 7) : header & 7
237
+ ly = is_landscape ? header & 7 : (has_alpha ? 5 : 7)
238
+ lx.to_f / ly.to_f
239
+ end
240
+
241
+ # Decodes a ThumbHash to a PNG data URL. This is a convenience function that
242
+ # just calls "thumb_hash_to_rgba" followed by "rgba_to_data_url".
243
+ def self.thumb_hash_to_data_url(hash)
244
+ width, height, rgba = thumb_hash_to_rgba(hash)
245
+ rgba_to_data_url(width, height, rgba)
246
+ end
247
+
248
+ # Encodes an RGBA image to a PNG data URL. RGB should not be premultiplied by
249
+ # A. This is optimized for speed and simplicity and does not optimize for size
250
+ # at all. This doesn't do any compression (all values are stored uncompressed).
251
+ def self.rgba_to_data_url(w, h, rgba)
252
+ row = w * 4 + 1
253
+ idat = 6 + h * (5 + row)
254
+
255
+ unsigned_right_shift = ->(value, amount) do
256
+ mask = (1 << (32 - amount)) - 1
257
+ (value >> amount) & mask
258
+ end
259
+
260
+ bytes = [
261
+ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0,
262
+ w >> 8, w & 255, 0, 0, h >> 8, h & 255, 8, 6, 0, 0, 0, 0, 0, 0, 0,
263
+ unsigned_right_shift.(idat, 24), (idat >> 16) & 255, (idat >> 8) & 255, idat & 255,
264
+ 73, 68, 65, 84, 120, 1,
265
+ ]
266
+ table = [
267
+ 0, 498536548, 997073096, 651767980, 1994146192, 1802195444, 1303535960,
268
+ 1342533948, -306674912, -267414716, -690576408, -882789492, -1687895376,
269
+ -2032938284, -1609899400, -1111625188,
270
+ ]
271
+
272
+ a = 1
273
+ b = 0
274
+ i = 0
275
+ end_index = row - 1
276
+ for y in 0...h
277
+ bytes.push(
278
+ y + 1 < h ? 0 : 1,
279
+ row & 255,
280
+ row >> 8,
281
+ ~row & 255,
282
+ (row >> 8) ^ 255,
283
+ 0
284
+ )
285
+ b = (b + a) % 65521
286
+ while i < end_index
287
+ u = rgba[i] & 255
288
+ bytes.push(u)
289
+ a = (a + u) % 65521
290
+ b = (b + a) % 65521
291
+ i += 1
292
+ end
293
+
294
+ end_index += row - 1
295
+ end
296
+
297
+ bytes.push(b >> 8, b & 255, a >> 8, a & 255, 0, 0, 0, 0,
298
+ 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130)
299
+
300
+ [[12, 29], [37, 41 + idat]].each do |start_index, end_index|
301
+ c = ~0
302
+ for i in (start_index...end_index)
303
+ c ^= bytes[i]
304
+ c = unsigned_right_shift.(c, 4) ^ table[c & 15]
305
+ c = unsigned_right_shift.(c, 4) ^ table[c & 15]
306
+ end
307
+ c = ~c
308
+
309
+ bytes[end_index] = unsigned_right_shift.(c, 24)
310
+ end_index += 1
311
+ bytes[end_index] = (c >> 16) & 255
312
+ end_index += 1
313
+ bytes[end_index] = (c >> 8) & 255
314
+ end_index += 1
315
+ bytes[end_index] = c & 255
316
+ end_index += 1
317
+ end
318
+
319
+ "data:image/png;base64," + Base64.encode64(bytes.map(&:chr).join).delete("\n")
320
+ end
321
+ end
metadata ADDED
@@ -0,0 +1,43 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thumbhash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - David Newell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: david@newellis.cool
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/thumbhash.rb
20
+ homepage: https://github.com/daibhin/thumbhash
21
+ licenses:
22
+ - MIT
23
+ metadata: {}
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ requirements: []
39
+ rubygems_version: 3.1.4
40
+ signing_key:
41
+ specification_version: 4
42
+ summary: A Ruby implememntation of ThumbHash
43
+ test_files: []