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 +4 -4
- data/.rubocop.yml +23 -2
- data/Gemfile +2 -0
- data/Gemfile.lock +10 -1
- data/README.md +97 -9
- data/Rakefile +4 -1
- data/ext/fast_thumbhash/fast_thumbhash.c +502 -4
- data/ext/fast_thumbhash/fast_thumbhash.h +36 -1
- data/fast_thumbhash.gemspec +3 -3
- data/lib/fast_thumbhash/version.rb +1 -1
- data/lib/fast_thumbhash.rb +142 -3
- metadata +8 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d73528e540800828f98914de9bdbeb637eb6a1ea2564e2b48656e151ae0521a5
|
4
|
+
data.tar.gz: 0e8ee28b8872286957d0cc4afb894ccfa87a233391b2e1fcbcc89e603123d561
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
18
|
+
Enabled: false
|
16
19
|
|
17
|
-
|
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
data/Gemfile.lock
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
fast_thumbhash (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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
64
|
+
puts [w, h].inspect # => [10, 32]
|
65
|
+
```
|
24
66
|
|
25
|
-
|
67
|
+
##### `homogeneous_transform` and `size`
|
26
68
|
|
27
|
-
|
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/
|
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/
|
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
|
-
|
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 <
|
1
|
+
#include <fast_thumbhash.h>
|
2
|
+
#include <stdlib.h>
|
3
|
+
#include <math.h>
|
4
|
+
#include <assert.h>
|
5
|
+
#include <stdbool.h>
|
2
6
|
|
3
|
-
|
4
|
-
|
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
|
-
|
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
|
data/fast_thumbhash.gemspec
CHANGED
@@ -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
|
12
|
-
spec.description = "
|
13
|
-
spec.homepage = "https://github.com/
|
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
|
|
data/lib/fast_thumbhash.rb
CHANGED
@@ -4,13 +4,152 @@ require "ffi"
|
|
4
4
|
require_relative "fast_thumbhash/version"
|
5
5
|
|
6
6
|
module FastThumbhash
|
7
|
-
def self.
|
8
|
-
|
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
|
-
|
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.
|
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-
|
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:
|
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/
|
49
|
+
homepage: https://github.com/datocms/fast_thumbhash
|
49
50
|
licenses:
|
50
51
|
- MIT
|
51
52
|
metadata:
|
52
|
-
homepage_uri: https://github.com/
|
53
|
-
source_code_uri: https://github.com/
|
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
|
74
|
+
summary: Ruby C extension to encode/decode ThumbHash
|
74
75
|
test_files: []
|