fast_thumbhash 0.1.0 → 0.2.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 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: []