fast_thumbhash 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79a84ccfbbffef86e7fa3657229d099c27acaeccb99f233a1eefec4147febb3c
4
- data.tar.gz: a374ac8c3a50c7ba7fa189309612a5d41ba483e4e0d3ffcedb4eccf1250a9ba8
3
+ metadata.gz: d73528e540800828f98914de9bdbeb637eb6a1ea2564e2b48656e151ae0521a5
4
+ data.tar.gz: 0e8ee28b8872286957d0cc4afb894ccfa87a233391b2e1fcbcc89e603123d561
5
5
  SHA512:
6
- metadata.gz: 7431add34523be78b8517c38f488e1548957ff0306af213bfca1ed679a4fb4f94378933f01a1ac3b24f6d651e18a8ad9ce3d3b80c7235dc5ee0f3aa5d1110cb9
7
- data.tar.gz: 5d4f12735da45c54e055695cfae03de4d9122b9fc09dbf20f1329d17fcaaa5c435f6880ec83d2d001523dcf09ef668746bc66c6ef4b5a69e12dc66fd6f59be97
6
+ metadata.gz: 1d0964354d31b621773df344c005f6c190c7037a58f46396703c2c7f2805c54f3afe0244cbbdecbeb195f3d0b3c9b6cdf6453265103ebf95e4411dd7eaad159e
7
+ data.tar.gz: e104ab2a5e23f1e2a496647d83da5f6bc1e08e6f7692348ba3c4ff7fd6754d49bacdd5862b90281ec73b54bf629509a27cbdfbd2fc4467a2b1ae330115771236
data/.rubocop.yml CHANGED
@@ -11,8 +11,29 @@ Style/StringLiteralsInInterpolation:
11
11
  Enabled: true
12
12
  EnforcedStyle: double_quotes
13
13
 
14
+ Style/Documentation:
15
+ Enabled: false
16
+
14
17
  Layout/LineLength:
15
- Max: 120
18
+ Enabled: false
16
19
 
17
- Style/Documentation:
20
+ Metrics/MethodLength:
21
+ Enabled: false
22
+
23
+ Metrics/BlockLength:
24
+ Enabled: false
25
+
26
+ Metrics/ParameterLists:
27
+ Enabled: false
28
+
29
+ Metrics/AbcSize:
30
+ Enabled: false
31
+
32
+ Metrics/PerceivedComplexity:
33
+ Enabled: false
34
+
35
+ Metrics/CyclomaticComplexity:
18
36
  Enabled: false
37
+
38
+ Metrics/ModuleLength:
39
+ Enabled: false
data/Gemfile CHANGED
@@ -6,7 +6,9 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "bundler", "~> 2.0"
9
+ gem "chunky_png", "~> 1.4"
9
10
  gem "rake", "~> 13.0"
10
11
  gem "rake-compiler", "~> 1.2"
11
12
  gem "rspec", "~> 3.0"
12
13
  gem "rubocop", "~> 1.21"
14
+ gem "thumbhash", "0.0.1"
data/Gemfile.lock CHANGED
@@ -1,13 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fast_thumbhash (0.1.0)
4
+ fast_thumbhash (0.2.0)
5
5
  ffi
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
10
  ast (2.4.2)
11
+ chunky_png (1.4.0)
11
12
  diff-lcs (1.5.0)
12
13
  ffi (1.15.5)
13
14
  json (2.6.3)
@@ -46,18 +47,26 @@ GEM
46
47
  rubocop-ast (1.28.0)
47
48
  parser (>= 3.2.1.0)
48
49
  ruby-progressbar (1.13.0)
50
+ thumbhash (0.0.1)
49
51
  unicode-display_width (2.4.2)
50
52
 
51
53
  PLATFORMS
54
+ arm64-darwin-21
55
+ x86_64-darwin-18
52
56
  x86_64-darwin-20
57
+ x86_64-darwin-21
58
+ x86_64-darwin-22
59
+ x86_64-linux
53
60
 
54
61
  DEPENDENCIES
55
62
  bundler (~> 2.0)
63
+ chunky_png (~> 1.4)
56
64
  fast_thumbhash!
57
65
  rake (~> 13.0)
58
66
  rake-compiler (~> 1.2)
59
67
  rspec (~> 3.0)
60
68
  rubocop (~> 1.21)
69
+ thumbhash (= 0.0.1)
61
70
 
62
71
  BUNDLED WITH
63
72
  2.3.25
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # FastThumbhash
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/fast_thumbhash`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ [![Ruby](https://github.com/datocms/fast_thumbhash/actions/workflows/main.yml/badge.svg)](https://github.com/datocms/fast_thumbhash/actions/workflows/main.yml)
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ FastThumbhash is a Ruby gem that provides a highly optimized implementation of the [ThumbHash algorithm](https://evanw.github.io/thumbhash/), a compact representation of an image placeholder for a smoother loading experience.
6
+
7
+ To achieve these benefits, FastThumbhash is implemented as a Ruby C extension, which means that the core functionality of the ThumbHash algorithm is written in C for faster execution times compared to a pure Ruby implementation. This makes FastThumbhash ideal for applications or websites that require high-performance image processing.
6
8
 
7
9
  ## Installation
8
10
 
@@ -16,21 +18,107 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
18
 
17
19
  ## Usage
18
20
 
19
- TODO: Write usage instructions here
21
+ Using the ChunkyPNG library to process the image, this example shows how to generate a thumbhash that can be stored alongside an image as well as the method to convert the hash into a data string for the image placeholder.
20
22
 
21
- ## Development
23
+ ```ruby
24
+ require "fast_thumbhash"
25
+ require "chunky_png"
26
+ require "base64"
27
+
28
+ # load an image
29
+ image = ChunkyPNG::Image.from_file("image.png")
30
+
31
+ # convert the image into a base64-encoded thumbhash
32
+ thumbhash = FastThumbhash.rgba_to_thumbhash(image.width, image.height, image.to_rgba_stream.unpack("C*"))
33
+
34
+ puts thumbhash # => "rsYJLJZ4d4iAeIiAh5togIk3+A=="
35
+
36
+ # convert the thumbhash back to an RGBA stream
37
+ width, height, rgba = FastThumbhash.thumbhash_to_rgba(thumbhash, max_size: 32)
38
+
39
+ # generate a placeholder image based on thumbhash
40
+ placeholder = ChunkyPNG::Image.new(width, height, rgba.pack("C*").unpack("N*"))
41
+
42
+ # save placeholder to file
43
+ options = { compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_PAETH, interlace: false }
44
+ placeholder.save("placeholder.png", options)
45
+
46
+ # generate a DataURL string for the pleaceholder
47
+ thumbhash_image_blob = "data:image/png;base64,#{Base64.strict_encode64(thumbhash_image.to_blob(options))}"
48
+ ```
49
+
50
+ ### Additional Options
51
+
52
+ This section covers additional options available for the `.thumbhash_to_rgba` method.
53
+
54
+ ##### `max_size`
55
+
56
+ The max_size option allows you to request a thumbnail up to the specified size. This is the suggested option for most use cases.
57
+
58
+ ```ruby
59
+ w, h, rgba = described_class.thumbhash_to_rgba(
60
+ thumbhash,
61
+ max_size: 32
62
+ )
22
63
 
23
- After checking out the repo, run `bin/setup` to install dependencies.
64
+ puts [w, h].inspect # => [10, 32]
65
+ ```
24
66
 
25
- Then, run `rake compile:fast_thumbhash` to compile the extensions.
67
+ ##### `homogeneous_transform` and `size`
26
68
 
27
- Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
69
+ The `homogeneous_transform` option allows you to apply a homogeneous transformation matrix when creating a thumbnail. The transformation matrix is a 3x3 array. This can be useful for applying roto-transformations to the thumbnail. The `size` option is useful to explicitly specify the width/height of the thumbnail when combined with the `homogeneous_transform` option to create a thumbnail with specific dimensions and transformations applied to it.
70
+
71
+
72
+ ```ruby
73
+ w, h, rgba = described_class.thumbhash_to_rgba(
74
+ thumbhash,
75
+ size: [32, 32],
76
+ homogeneous_transform: [
77
+ [0.5, 0.0, 0.5],
78
+ [0.0, 1.0, 0.0],
79
+ [0.0, 0.0, 1.0]
80
+ ]
81
+ )
82
+ ```
83
+
84
+ ##### `saturation`
85
+
86
+ The `saturation` option adjusts the saturation of the thumbnail's colors. It accepts a value in the range of -100 to +100. A value of -100 will result in a grayscale image.
87
+
88
+ ```ruby
89
+ w, h, rgba = described_class.thumbhash_to_rgba(
90
+ thumbhash,
91
+ max_size: 32,
92
+ saturation: -100
93
+ )
94
+ ```
95
+
96
+ ##### `fill_mode`
97
+
98
+ The `fill_mode` option specifies how to fill in any transparent areas in your image with a color of your choice. When `fill_mode` is `:solid`, you need to pass an additional `fill_color` option to specify the actual RGBA color to use. When `fill_mode` is `:blur`, the excess space will be filled with a blurred version of the original image itself.
99
+
100
+ ```ruby
101
+ w, h, rgba = described_class.thumbhash_to_rgba(
102
+ thumbhash,
103
+ max_size: 32,
104
+ fill_mode: :solid,
105
+ fill_color: [255, 0, 0, 100],
106
+ )
107
+ ```
108
+
109
+ ## Development
110
+
111
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
112
 
29
113
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
114
 
115
+ ## Releasing a new version
116
+
117
+ First update the FastThumbhash::VERSION, then run `rake release`.
118
+
31
119
  ## Contributing
32
120
 
33
- Bug reports and pull requests are welcome on GitHub at https://github.com/stefanoverna/thumbhash. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/stefanoverna/thumbhash/blob/master/CODE_OF_CONDUCT.md).
121
+ Bug reports and pull requests are welcome on GitHub at https://github.com/datocms/fast_thumbhash. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/datocms/fast_thumbhash/blob/master/CODE_OF_CONDUCT.md).
34
122
 
35
123
  ## License
36
124
 
@@ -38,4 +126,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
38
126
 
39
127
  ## Code of Conduct
40
128
 
41
- Everyone interacting in the FastThumbhash project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/stefanoverna/thumbhash/blob/master/CODE_OF_CONDUCT.md).
129
+ Everyone interacting in the FastThumbhash project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/datocms/fast_thumbhash/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -8,8 +8,11 @@ require "rubocop/rake_task"
8
8
  RSpec::Core::RakeTask.new(:spec)
9
9
  RuboCop::RakeTask.new
10
10
 
11
- task default: %i[spec rubocop]
11
+ Rake::Task[:build].enhance %i[spec rubocop]
12
+ Rake::Task[:spec].enhance %i[compile:fast_thumbhash]
12
13
 
13
14
  Rake::ExtensionTask.new "fast_thumbhash" do |ext|
14
15
  ext.lib_dir = "lib"
15
16
  end
17
+
18
+ task default: %i[spec rubocop]
@@ -1,5 +1,503 @@
1
- #include <stdio.h>
1
+ #include <fast_thumbhash.h>
2
+ #include <stdlib.h>
3
+ #include <math.h>
4
+ #include <assert.h>
5
+ #include <stdbool.h>
2
6
 
3
- void myfunc() {
4
- printf("Hello from C!\n");
5
- }
7
+ typedef struct encoded_channel
8
+ {
9
+ double dc;
10
+ double *ac;
11
+ int ac_size;
12
+ double scale;
13
+ } encoded_channel;
14
+
15
+ encoded_channel encode_channel(double *channel, uint8_t nx, uint8_t ny, uint8_t w, uint8_t h)
16
+ {
17
+ double dc = 0, scale = 0;
18
+ double *ac = (double *)malloc(nx * ny * sizeof(double));
19
+ double *fx = (double *)malloc(w * sizeof(double));
20
+ int ac_length = 0;
21
+
22
+ for (int cy = 0; cy < ny; cy++)
23
+ {
24
+ for (int cx = 0; cx * ny < nx * (ny - cy); cx++)
25
+ {
26
+ double f = 0;
27
+
28
+ for (int x = 0; x < w; x++)
29
+ {
30
+ fx[x] = cos(M_PI / w * cx * (x + 0.5));
31
+ }
32
+
33
+ for (int y = 0; y < h; y++)
34
+ {
35
+ double fy = cos(M_PI / h * cy * (y + 0.5));
36
+ for (int x = 0; x < w; x++)
37
+ {
38
+ f += channel[x + y * w] * fx[x] * fy;
39
+ }
40
+ }
41
+
42
+ f /= w * h;
43
+
44
+ if (cx || cy)
45
+ {
46
+ ac[ac_length++] = f;
47
+ scale = fmax(scale, fabsl(f));
48
+ }
49
+ else
50
+ {
51
+ dc = f;
52
+ }
53
+ }
54
+ }
55
+
56
+ if (scale)
57
+ {
58
+ for (int i = 0; i < ac_length; i++)
59
+ {
60
+ ac[i] = 0.5 + 0.5 / scale * ac[i];
61
+ }
62
+ }
63
+
64
+ free(fx);
65
+
66
+ return (encoded_channel){dc, ac, ac_length, scale};
67
+ }
68
+
69
+ void write_varying_factors(double *ac, int ac_size, uint8_t *thumbhash, uint8_t *ac_index) {
70
+ for (int i = 0; i < ac_size; i++) {
71
+ uint8_t index = (*ac_index) >> 1;
72
+ thumbhash[index] |= (uint8_t) roundf(15.0 * ac[i]) << (((*ac_index)++ & 1) << 2);
73
+ }
74
+ }
75
+
76
+ void rgba_to_thumbhash(uint8_t w, uint8_t h, uint8_t *rgba, uint8_t *thumbhash)
77
+ {
78
+ assert(w <= 100);
79
+ assert(h <= 100);
80
+
81
+ // Determine the average color
82
+ double avg_r = 0, avg_g = 0, avg_b = 0, avg_a = 0;
83
+
84
+ for (int i = 0, j = 0; i < w * h; i++, j += 4)
85
+ {
86
+ double alpha = (double)rgba[j + 3] / 255.0;
87
+ avg_r += alpha / 255.0 * rgba[j];
88
+ avg_g += alpha / 255.0 * rgba[j + 1];
89
+ avg_b += alpha / 255.0 * rgba[j + 2];
90
+ avg_a += alpha;
91
+ }
92
+
93
+ if (avg_a)
94
+ {
95
+ avg_r /= avg_a;
96
+ avg_g /= avg_a;
97
+ avg_b /= avg_a;
98
+ }
99
+
100
+ bool has_alpha = avg_a < w * h;
101
+ uint8_t l_limit = has_alpha ? 5 : 7; // Use fewer luminance bits if there's alpha
102
+ uint8_t lx = fmax(1, roundf((double)l_limit * w / fmax(w, h)));
103
+ uint8_t ly = fmax(1, roundf((double)l_limit * h / fmax(w, h)));
104
+
105
+ double *l = (double *)malloc(w * h * sizeof(double)); // luminance
106
+ double *p = (double *)malloc(w * h * sizeof(double)); // yellow - blue
107
+ double *q = (double *)malloc(w * h * sizeof(double)); // red - green
108
+ double *a = (double *)malloc(w * h * sizeof(double)); // alpha
109
+
110
+ // Convert the image from RGBA to LPQA (composite atop the average color)
111
+ for (int i = 0, j = 0; i < w * h; i++, j += 4)
112
+ {
113
+ double alpha = (double)rgba[j + 3] / 255;
114
+ double r = avg_r * (1.0 - alpha) + alpha / 255.0 * (double)rgba[j];
115
+ double g = avg_g * (1.0 - alpha) + alpha / 255.0 * (double)rgba[j + 1];
116
+ double b = avg_b * (1.0 - alpha) + alpha / 255.0 * (double)rgba[j + 2];
117
+ l[i] = (r + g + b) / 3;
118
+ p[i] = (r + g) / 2 - b;
119
+ q[i] = r - g;
120
+ a[i] = alpha;
121
+ }
122
+
123
+ // Encode using the DCT into DC (constant) and normalized AC (varying) terms
124
+ encoded_channel el = encode_channel(l, fmax(3, lx), fmax(3, ly), w, h);
125
+ encoded_channel ep = encode_channel(p, 3, 3, w, h);
126
+ encoded_channel eq = encode_channel(q, 3, 3, w, h);
127
+ encoded_channel ea = has_alpha ? encode_channel(a, 5, 5, w, h) : (encoded_channel){1.0, NULL, 0, 1.0};
128
+
129
+ free(l);
130
+ free(p);
131
+ free(q);
132
+ free(a);
133
+
134
+ // Write the constants
135
+ bool is_landscape = w > h;
136
+ u_int32_t header24 = (u_int32_t)roundf(63.0 * el.dc) | ((u_int32_t)roundf(31.5 + 31.5 * ep.dc) << 6) | ((u_int32_t)roundf(31.5 + 31.5 * eq.dc) << 12) | ((u_int32_t)roundf(31.0 * el.scale) << 18) | (has_alpha << 23);
137
+ u_int32_t header16 = (is_landscape ? ly : lx) | ((u_int16_t)roundf(63.0 * ep.scale) << 3) | ((u_int16_t)roundf(63.0 * eq.scale) << 9) | (is_landscape << 15);
138
+
139
+ thumbhash[0] = (uint8_t)(header24 & 0xff);
140
+ thumbhash[1] = (uint8_t)((header24 >> 8) & 0xff);
141
+ thumbhash[2] = (uint8_t)(header24 >> 16);
142
+ thumbhash[3] = (uint8_t)(header16 & 0xff);
143
+ thumbhash[4] = (uint8_t)(header16 >> 8);
144
+
145
+ if (has_alpha) {
146
+ thumbhash[5] = (uint8_t) roundf(15.0 * ea.dc) | ((uint8_t) roundf(15.0 * ea.scale) << 4);
147
+ }
148
+
149
+ uint8_t ac_start = has_alpha ? 6 : 5;
150
+ uint8_t ac_index = 0;
151
+
152
+ write_varying_factors(el.ac, el.ac_size, thumbhash + ac_start, &ac_index);
153
+ write_varying_factors(ep.ac, ep.ac_size, thumbhash + ac_start, &ac_index);
154
+ write_varying_factors(eq.ac, eq.ac_size, thumbhash + ac_start, &ac_index);
155
+
156
+ if (has_alpha) {
157
+ write_varying_factors(ea.ac, ea.ac_size, thumbhash + ac_start, &ac_index);
158
+ }
159
+
160
+ free(el.ac);
161
+ free(ep.ac);
162
+ free(eq.ac);
163
+
164
+ if (has_alpha) {
165
+ free(ea.ac);
166
+ }
167
+ }
168
+
169
+ double *decode_channel(uint8_t nx, uint8_t ny, double scale, uint8_t *hash, uint8_t ac_start, uint8_t *ac_index)
170
+ {
171
+ double *ac = (double *)malloc(nx * ny * sizeof(double));
172
+ int i = 0;
173
+ for (int cy = 0; cy < ny; cy++)
174
+ {
175
+ for (int cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++)
176
+ {
177
+ double bit = (hash[ac_start + (*ac_index >> 1)] >> (((*ac_index)++ & 1) << 2)) & 15;
178
+ ac[i++] = (bit / 7.5 - 1) * scale;
179
+ }
180
+ }
181
+
182
+ return ac;
183
+ }
184
+
185
+ double thumbhash_to_approximate_aspect_ratio(uint8_t *hash)
186
+ {
187
+ uint8_t has_alpha = (hash[2] & 0x80) != 0;
188
+ uint8_t l_max = has_alpha ? 5 : 7;
189
+ uint8_t l_min = hash[3] & 7;
190
+ uint8_t is_landscape = (hash[4] & 0x80) != 0;
191
+ uint8_t lx = is_landscape ? l_max : l_min;
192
+ uint8_t ly = is_landscape ? l_min : l_max;
193
+
194
+ return (double)lx / (double)ly;
195
+ }
196
+
197
+ void thumb_size(uint8_t *hash, uint8_t max_size, uint8_t *size)
198
+ {
199
+ double ratio = thumbhash_to_approximate_aspect_ratio(hash);
200
+
201
+ size[0] = roundf(ratio > 1 ? max_size : max_size * ratio);
202
+ size[1] = roundf(ratio > 1 ? max_size / ratio : max_size);
203
+ }
204
+
205
+ void rgb2hsv(uint8_t *rgb, float *hsv)
206
+ {
207
+ float min, max, delta;
208
+
209
+ float r = rgb[0] / 255.0;
210
+ float g = rgb[1] / 255.0;
211
+ float b = rgb[2] / 255.0;
212
+
213
+ min = r < g ? r : g;
214
+ min = min < b ? min : b;
215
+
216
+ max = r > g ? r : g;
217
+ max = max > b ? max : b;
218
+
219
+ hsv[2] = max;
220
+
221
+ delta = max - min;
222
+
223
+ if (delta < 0.00001)
224
+ {
225
+ hsv[1] = 0;
226
+ hsv[0] = 0; // undefined, maybe nan?
227
+ return;
228
+ }
229
+ if (max > 0.0)
230
+ { // NOTE: if Max is == 0, this divide would cause a crash
231
+ hsv[1] = (delta / max); // s
232
+ }
233
+ else
234
+ {
235
+ // if max is 0, then r = g = b = 0
236
+ // s = 0, h is undefined
237
+ hsv[1] = 0.0;
238
+ hsv[0] = NAN; // its now undefined
239
+ return;
240
+ }
241
+
242
+ if (r >= max) // > is bogus, just keeps compilor happy
243
+ hsv[0] = (g - b) / delta; // between yellow & magenta
244
+ else if (g >= max)
245
+ hsv[0] = 2.0 + (b - r) / delta; // between cyan & yellow
246
+ else
247
+ hsv[0] = 4.0 + (r - g) / delta; // between magenta & cyan
248
+
249
+ hsv[0] *= 60.0; // degrees
250
+
251
+ if (hsv[0] < 0.0)
252
+ hsv[0] += 360.0;
253
+ }
254
+
255
+ void hsv2rgb(float *hsv, uint8_t *rgb)
256
+ {
257
+ float hh, p, q, t, ff;
258
+ long i;
259
+
260
+ if (hsv[1] <= 0.0)
261
+ { // < is bogus, just shuts up warnings
262
+ rgb[0] = hsv[2] * 255.0;
263
+ rgb[1] = hsv[2] * 255.0;
264
+ rgb[2] = hsv[2] * 255.0;
265
+ return;
266
+ }
267
+
268
+ hh = hsv[0];
269
+
270
+ if (hh >= 360.0)
271
+ hh = 0.0;
272
+
273
+ hh /= 60.0;
274
+
275
+ i = (long)hh;
276
+
277
+ ff = hh - i;
278
+ p = hsv[2] * (1.0 - hsv[1]);
279
+ q = hsv[2] * (1.0 - (hsv[1] * ff));
280
+ t = hsv[2] * (1.0 - (hsv[1] * (1.0 - ff)));
281
+
282
+ switch (i)
283
+ {
284
+ case 0:
285
+ rgb[0] = hsv[2] * 255.0;
286
+ rgb[1] = t * 255.0;
287
+ rgb[2] = p * 255.0;
288
+ return;
289
+ case 1:
290
+ rgb[0] = q * 255.0;
291
+ rgb[1] = hsv[2] * 255.0;
292
+ rgb[2] = p * 255.0;
293
+ return;
294
+ case 2:
295
+ rgb[0] = p * 255.0;
296
+ rgb[1] = hsv[2] * 255.0;
297
+ rgb[2] = t * 255.0;
298
+ return;
299
+
300
+ case 3:
301
+ rgb[0] = p * 255.0;
302
+ rgb[1] = q * 255.0;
303
+ rgb[2] = hsv[2] * 255.0;
304
+ return;
305
+ case 4:
306
+ rgb[0] = t * 255.0;
307
+ rgb[1] = p * 255.0;
308
+ rgb[2] = hsv[2] * 255.0;
309
+ return;
310
+ case 5:
311
+ default:
312
+ rgb[0] = hsv[2] * 255.0;
313
+ rgb[1] = p * 255.0;
314
+ rgb[2] = q * 255.0;
315
+ return;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
321
+ */
322
+ void thumbhash_to_rgba(
323
+ uint8_t *hash,
324
+ uint8_t w,
325
+ uint8_t h,
326
+ enum FillMode fill_mode,
327
+ uint8_t *fill_color,
328
+ double *homogeneous_transform,
329
+ int saturation,
330
+ uint8_t *rgba
331
+ )
332
+ {
333
+ // Read the constants
334
+ u_int32_t header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16);
335
+ u_int32_t header16 = hash[3] | (hash[4] << 8);
336
+ double l_dc = (double)(header24 & 63) / 63;
337
+ double p_dc = (double)((header24 >> 6) & 63) / 31.5 - 1;
338
+ double q_dc = (double)((header24 >> 12) & 63) / 31.5 - 1;
339
+ double l_scale = (double)((header24 >> 18) & 31) / 31;
340
+ bool has_alpha = (header24 >> 23) != 0;
341
+ double p_scale = (double)((header16 >> 3) & 63) / 63;
342
+ double q_scale = (double)((header16 >> 9) & 63) / 63;
343
+ bool is_landscape = (header16 >> 15) != 0;
344
+ uint8_t lx = fmax(3, is_landscape ? has_alpha ? 5 : 7 : header16 & 7);
345
+ uint8_t ly = fmax(3, is_landscape ? header16 & 7 : has_alpha ? 5 : 7);
346
+ double a_dc = (double)has_alpha ? (hash[5] & 15) / 15 : 1;
347
+ double a_scale = (double)(hash[5] >> 4) / 15;
348
+
349
+ // Read the varying factors (boost saturation by 1.25x to compensate for quantization)
350
+ uint8_t ac_start = has_alpha ? 6 : 5;
351
+ uint8_t ac_index = 0;
352
+
353
+ double *l_ac = decode_channel(lx, ly, l_scale, hash, ac_start, &ac_index);
354
+ double *p_ac = decode_channel(3, 3, p_scale * 1.25, hash, ac_start, &ac_index);
355
+ double *q_ac = decode_channel(3, 3, q_scale * 1.25, hash, ac_start, &ac_index);
356
+ double *a_ac = has_alpha ? decode_channel(5, 5, a_scale, hash, ac_start, &ac_index) : NULL;
357
+
358
+ // Decode using the DCT into RGB
359
+ double fx[7];
360
+ double fy[7];
361
+ u_int32_t i = 0;
362
+
363
+ for (uint8_t ry = 0; ry < h; ry++)
364
+ {
365
+ for (uint8_t rx = 0; rx < w; rx++, i += 4)
366
+ {
367
+
368
+ double px = ((double)rx + 0.5) / w;
369
+ double py = ((double)ry + 0.5) / h;
370
+
371
+ double x = px;
372
+ double y = py;
373
+
374
+ if (homogeneous_transform)
375
+ {
376
+ x = homogeneous_transform[0] * px + homogeneous_transform[1] * py + homogeneous_transform[2];
377
+ y = homogeneous_transform[3] * px + homogeneous_transform[4] * py + homogeneous_transform[5];
378
+ }
379
+
380
+ double r, g, b, a;
381
+
382
+ if (fill_mode == BLUR) {
383
+ if (x < 0)
384
+ {
385
+ x = 0;
386
+ }
387
+ else if (x >= 1)
388
+ {
389
+ x = 1;
390
+ }
391
+ else if (y < 0)
392
+ {
393
+ y = 0;
394
+ }
395
+ else if (y >= 1)
396
+ {
397
+ y = 1;
398
+ }
399
+ }
400
+
401
+ if (x >= 0 && x <= 1.0 && y >= 0 && y <= 1.0) {
402
+ double l = l_dc, p = p_dc, q = q_dc;
403
+ a = a_dc;
404
+
405
+ // Precompute the coefficients
406
+ for (int cx = 0, n = fmax(lx, has_alpha ? 5 : 3); cx < n; cx++)
407
+ {
408
+ fx[cx] = cos(M_PI * x * cx);
409
+ }
410
+ for (int cy = 0, n = fmax(ly, has_alpha ? 5 : 3); cy < n; cy++)
411
+ {
412
+ fy[cy] = cos(M_PI * y * cy);
413
+ }
414
+
415
+ // Decode L
416
+ double fy2;
417
+ for (int cy = 0, j = 0; cy < ly; cy++)
418
+ {
419
+ fy2 = fy[cy] * 2;
420
+ for (int cx = cy ? 0 : 1; cx * ly < lx * (ly - cy); cx++, j++)
421
+ {
422
+ l += l_ac[j] * fx[cx] * fy2;
423
+ }
424
+ }
425
+
426
+ // Decode P and Q
427
+ for (int cy = 0, j = 0; cy < 3; cy++)
428
+ {
429
+ fy2 = fy[cy] * 2;
430
+ for (int cx = cy ? 0 : 1; cx < 3 - cy; cx++, j++)
431
+ {
432
+ double f = fx[cx] * fy2;
433
+ p += p_ac[j] * f;
434
+ q += q_ac[j] * f;
435
+ }
436
+ }
437
+
438
+ // Decode A
439
+ if (has_alpha)
440
+ {
441
+ for (int cy = 0, j = 0; cy < 5; cy++)
442
+ {
443
+ fy2 = fy[cy] * 2;
444
+ for (int cx = cy ? 0 : 1; cx < 5 - cy; cx++, j++)
445
+ {
446
+ a += a_ac[j] * fx[cx] * fy2;
447
+ }
448
+ }
449
+ }
450
+
451
+ // Convert to RGB
452
+ b = l - 2.0 / 3.0 * p;
453
+ r = (3.0 * l - b + q) / 2.0;
454
+ g = r - q;
455
+ } else {
456
+ r = 0;
457
+ g = 0;
458
+ b = 0;
459
+ a = 0;
460
+ }
461
+
462
+ uint_fast8_t top[4] = {
463
+ fmax(0, 255 * fmin(1, r)),
464
+ fmax(0, 255 * fmin(1, g)),
465
+ fmax(0, 255 * fmin(1, b)),
466
+ fmax(0, 255 * fmin(1, a))
467
+ };
468
+
469
+ if (fill_mode == SOLID) {
470
+ // Alpha-blending
471
+ double top_alpha = (double) top[3] / 255.0;
472
+
473
+ rgba[i] = roundf(top[0] * top_alpha + fill_color[0] * (1.0 - top_alpha));
474
+ rgba[i+1] = roundf(top[1] * top_alpha + fill_color[1] * (1.0 - top_alpha));
475
+ rgba[i+2] = roundf(top[2] * top_alpha + fill_color[2] * (1.0 - top_alpha));
476
+ rgba[i+3] = roundf(top[3] * top_alpha + fill_color[3] * (1.0 - top_alpha));
477
+ } else {
478
+ rgba[i] = top[0];
479
+ rgba[i+1] = top[1];
480
+ rgba[i+2] = top[2];
481
+ rgba[i+3] = top[3];
482
+ }
483
+
484
+ if (saturation)
485
+ {
486
+ float hsv[3] = {0};
487
+ rgb2hsv(rgba + i, hsv);
488
+ float mult = ((float) saturation + 100.0f) / 200.0f * 1.4f;
489
+ hsv[1] = fminf(fmaxf(hsv[1] * mult, 0), 1.0f);
490
+ hsv2rgb(hsv, rgba + i);
491
+ }
492
+ }
493
+ }
494
+
495
+ free(l_ac);
496
+ free(p_ac);
497
+ free(q_ac);
498
+
499
+ if (has_alpha)
500
+ {
501
+ free(a_ac);
502
+ }
503
+ }
@@ -1 +1,36 @@
1
- void myfunc();
1
+ #ifndef __FAST_THUMBHASH_H__
2
+ #define __FAST_THUMBHASH_H__
3
+
4
+ #include <stdint.h>
5
+
6
+ enum FillMode {
7
+ NO_FILL = 0,
8
+ SOLID = 1,
9
+ BLUR = 2,
10
+ };
11
+
12
+ void rgba_to_thumbhash(
13
+ uint8_t w,
14
+ uint8_t h,
15
+ uint8_t *rgba,
16
+ uint8_t *thumbhash
17
+ );
18
+
19
+ void thumb_size(
20
+ uint8_t *hash,
21
+ uint8_t max_size,
22
+ uint8_t *size
23
+ );
24
+
25
+ void thumbhash_to_rgba(
26
+ uint8_t *hash,
27
+ uint8_t w,
28
+ uint8_t h,
29
+ enum FillMode fill_mode,
30
+ uint8_t *fill_color,
31
+ double *homogeneous_transform,
32
+ int saturation,
33
+ uint8_t *rgba
34
+ );
35
+
36
+ #endif
@@ -8,9 +8,9 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Stefano Verna"]
9
9
  spec.email = ["s.verna@datocms.com"]
10
10
 
11
- spec.summary = "Ruby bindings for encoding/decoding thumbhash"
12
- spec.description = "Ruby bindings for encoding/decoding thumbhash"
13
- spec.homepage = "https://github.com/stefanoverna/thumbhash"
11
+ spec.summary = "Ruby C extension to encode/decode ThumbHash"
12
+ spec.description = "Provides a highly optimized implementation of the ThumbHash algorithm, a compact representation of an image placeholder for a smoother loading experience"
13
+ spec.homepage = "https://github.com/datocms/fast_thumbhash"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 2.6.0"
16
16
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastThumbhash
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -4,13 +4,152 @@ require "ffi"
4
4
  require_relative "fast_thumbhash/version"
5
5
 
6
6
  module FastThumbhash
7
- def self.myfunc
8
- Library.myfunc
7
+ def self.thumbhash_to_rgba(
8
+ thumbhash,
9
+ max_size: nil,
10
+ size: nil,
11
+ fill_mode: :no_fill,
12
+ fill_color: nil,
13
+ homogeneous_transform: nil,
14
+ saturation: 0
15
+ )
16
+ binary_thumbhash_to_rgba(
17
+ Base64.decode64(thumbhash),
18
+ max_size: max_size,
19
+ size: size,
20
+ fill_mode: fill_mode,
21
+ fill_color: fill_color,
22
+ homogeneous_transform: homogeneous_transform,
23
+ saturation: saturation
24
+ )
25
+ end
26
+
27
+ def self.binary_thumbhash_to_rgba(
28
+ binary_thumbhash,
29
+ max_size: nil,
30
+ size: nil,
31
+ fill_mode: :no_fill,
32
+ fill_color: nil,
33
+ homogeneous_transform: nil,
34
+ saturation: 0
35
+ )
36
+ !max_size.nil? ^ !size.nil? or
37
+ raise ArgumentError, "Pass either the `max_size` option, or an explicit `size`"
38
+
39
+ %i[solid blur no_fill].include?(fill_mode) or
40
+ raise ArgumentError, "Invalid `fill_mode` option"
41
+
42
+ fill_color_pointer =
43
+ if fill_mode == :solid
44
+ fill_color or
45
+ raise ArgumentError, "`fill_color` is required if fill_mode = :solid"
46
+
47
+ fill_color.length == 4 or
48
+ raise ArgumentError, "You need to pass [r, g, b, a] to the `fill_color` option"
49
+
50
+ FFI::MemoryPointer.new(:uint8, 4).tap do |p|
51
+ p.write_array_of_uint8(fill_color)
52
+ end
53
+ end
54
+
55
+ transform_pointer =
56
+ if homogeneous_transform
57
+ (homogeneous_transform.size == 3 && homogeneous_transform.all? { |row| row.size == 3 }) or
58
+ raise ArgumentError, "`homogeneous_transform` option must be a 3x3 matrix"
59
+
60
+ FFI::MemoryPointer.new(:double, 6).tap do |p|
61
+ p.write_array_of_double(
62
+ [
63
+ homogeneous_transform[0][0],
64
+ homogeneous_transform[0][1],
65
+ homogeneous_transform[0][2],
66
+ homogeneous_transform[1][0],
67
+ homogeneous_transform[1][1],
68
+ homogeneous_transform[1][2]
69
+ ]
70
+ )
71
+ end
72
+ end
73
+
74
+ thumbhash_pointer = FFI::MemoryPointer.new(:uint8, binary_thumbhash.size)
75
+ thumbhash_pointer.put_array_of_uint8(0, binary_thumbhash.unpack("C*"))
76
+
77
+ width, height =
78
+ if size
79
+ size.length == 2 or
80
+ raise ArgumentError, "You need to pass [width, height] to the `size` option"
81
+
82
+ size.all? { |dimension| dimension < 100 } or
83
+ raise ArgumentError, "Cannot generate images bigger then 100 pixels"
84
+
85
+ size
86
+ else
87
+ max_size <= 100 or
88
+ raise ArgumentError, "Cannot generate images bigger then 100 pixels"
89
+
90
+ thumb_size_pointer = FFI::MemoryPointer.new(:uint8, 2)
91
+ Library.thumb_size(thumbhash_pointer, max_size, thumb_size_pointer)
92
+ thumb_size_pointer.read_array_of_uint8(2)
93
+ end
94
+
95
+ rgba_size = width * height * 4
96
+ rgba_pointer = FFI::MemoryPointer.new(:uint8, rgba_size)
97
+ Library.thumbhash_to_rgba(
98
+ thumbhash_pointer,
99
+ width,
100
+ height,
101
+ fill_mode.to_sym,
102
+ fill_color_pointer,
103
+ transform_pointer,
104
+ saturation,
105
+ rgba_pointer
106
+ )
107
+
108
+ [width, height, rgba_pointer.read_array_of_uint8(rgba_size)]
109
+ ensure
110
+ fill_color_pointer&.free
111
+ transform_pointer&.free
112
+ thumbhash_pointer&.free
113
+ thumb_size_pointer&.free
114
+ rgba_pointer&.free
115
+ end
116
+
117
+ def self.rgba_to_thumbhash(width, height, rgba)
118
+ Base64.strict_encode64(rgba_to_binary_thumbhash(width, height, rgba))
119
+ end
120
+
121
+ def self.rgba_to_binary_thumbhash(width, height, rgba)
122
+ (width <= 100 && height <= 100) or
123
+ raise ArgumentError, "Encoding an image larger than 100x100 is slow with no benefit"
124
+
125
+ rgba_pointer = FFI::MemoryPointer.new(:uint8, rgba.size)
126
+ rgba_pointer.put_array_of_uint8(0, rgba)
127
+
128
+ thumbhash_pointer = FFI::MemoryPointer.new(:uint8, 25)
129
+
130
+ Library.rgba_to_thumbhash(width, height, rgba_pointer, thumbhash_pointer)
131
+
132
+ result = thumbhash_pointer.read_array_of_uint8(25)
133
+ result.pop while result.last.zero?
134
+
135
+ result.pack("C*")
136
+ ensure
137
+ rgba_pointer&.free
138
+ thumbhash_pointer&.free
9
139
  end
10
140
 
11
141
  module Library
12
142
  extend FFI::Library
13
143
  ffi_lib File.join(File.expand_path(__dir__), "fast_thumbhash.#{RbConfig::CONFIG["DLEXT"]}")
14
- attach_function :myfunc, [], :void
144
+
145
+ enum :fill_mode, [
146
+ :no_fill, 0,
147
+ :solid,
148
+ :blur
149
+ ]
150
+
151
+ attach_function :thumb_size, %i[pointer uint8 pointer], :size_t
152
+ attach_function :thumbhash_to_rgba, %i[pointer uint8 uint8 fill_mode pointer pointer int pointer], :void
153
+ attach_function :rgba_to_thumbhash, %i[uint8 uint8 pointer pointer], :void
15
154
  end
16
155
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_thumbhash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefano Verna
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-05 00:00:00.000000000 Z
11
+ date: 2023-04-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -24,7 +24,8 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- description: Ruby bindings for encoding/decoding thumbhash
27
+ description: Provides a highly optimized implementation of the ThumbHash algorithm,
28
+ a compact representation of an image placeholder for a smoother loading experience
28
29
  email:
29
30
  - s.verna@datocms.com
30
31
  executables: []
@@ -45,12 +46,12 @@ files:
45
46
  - fast_thumbhash.gemspec
46
47
  - lib/fast_thumbhash.rb
47
48
  - lib/fast_thumbhash/version.rb
48
- homepage: https://github.com/stefanoverna/thumbhash
49
+ homepage: https://github.com/datocms/fast_thumbhash
49
50
  licenses:
50
51
  - MIT
51
52
  metadata:
52
- homepage_uri: https://github.com/stefanoverna/thumbhash
53
- source_code_uri: https://github.com/stefanoverna/thumbhash
53
+ homepage_uri: https://github.com/datocms/fast_thumbhash
54
+ source_code_uri: https://github.com/datocms/fast_thumbhash
54
55
  rubygems_mfa_required: 'true'
55
56
  post_install_message:
56
57
  rdoc_options: []
@@ -70,5 +71,5 @@ requirements: []
70
71
  rubygems_version: 3.1.6
71
72
  signing_key:
72
73
  specification_version: 4
73
- summary: Ruby bindings for encoding/decoding thumbhash
74
+ summary: Ruby C extension to encode/decode ThumbHash
74
75
  test_files: []