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 +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
|
+
[](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: []
|