File Coverage

blib/lib/Image/ThumbHash/PP.pm
Criterion Covered Total %
statement 155 221 70.1
branch 37 60 61.6
condition 4 15 26.6
subroutine 17 19 89.4
pod 0 7 0.0
total 213 322 66.1


line stmt bran cond sub pod time code
1             package Image::ThumbHash::PP;
2 1     1   13 use v5.10.0; # //
  1         3  
3 1     1   5 use strict;
  1         2  
  1         23  
4 1     1   5 use warnings qw(all FATAL uninitialized);
  1         1  
  1         69  
5              
6 1     1   6 use Exporter 5.57 qw(import);
  1         12  
  1         39  
7 1     1   5 use Carp qw(croak);
  1         13  
  1         49  
8 1     1   10 use List::Util qw(min max);
  1         2  
  1         101  
9 1     1   523 use MIME::Base64 ();
  1         728  
  1         45  
10              
11             use constant {
12 1         3209 PI => 4 * atan2(1, 1),
13 1     1   6 };
  1         2  
14              
15             our $VERSION = '0.01';
16              
17             our @EXPORT_OK = qw(
18             rgba_to_thumb_hash
19             rgba_to_png
20             rgba_to_data_url
21             thumb_hash_to_rgba
22             thumb_hash_to_average_rgba
23             thumb_hash_to_approximate_aspect_ratio
24             thumb_hash_to_data_url
25             );
26              
27             sub _assert_w_h_rgba {
28 15     15   39 my ($width, $height, $rgba, $sub) = @_;
29 15   33     172 $sub //= (caller 1)[3];
30              
31 15 50 33     70 0 <= $width && $width <= 100
32             or croak "$sub: width is not in range [0, 100]: $width";
33 15 50 33     59 0 <= $height && $height <= 100
34             or croak "$sub: height is not in range [0, 100]: $height";
35 15 50       45 length($rgba) == $width * $height * 4
36             or croak "$sub: rgba length does not match " . ($width * $height * 4) . ": " . length($rgba);
37             }
38              
39             sub _assert_thumb_hash {
40 19     19   53 my ($hash, $sub) = @_;
41 19   33     209 $sub //= (caller 1)[3];
42              
43 19 100       341 length($hash) >= 5
44             or croak "$sub: thumb hash length is less than 5: " . length($hash);
45             }
46              
47             sub rgba_to_thumb_hash {
48 0     0 0 0 my ($width, $height, $rgba) = @_;
49              
50             # Encoding an image larger than 100x100 is slow with no benefit
51 0         0 _assert_w_h_rgba $width, $height, $rgba;
52              
53             # Determine the average color
54 0         0 my ($avg_r, $avg_g, $avg_b, $avg_a) = (0, 0, 0, 0);
55 0         0 for my $pixel (unpack '(a4)*', $rgba) {
56 0         0 my ($pr, $pg, $pb, $pa) = unpack 'C*', $pixel;
57 0         0 my $alpha = $pa / 255;
58 0         0 my $alpha_f = $alpha / 255;
59 0         0 $avg_r += $alpha_f * $pr;
60 0         0 $avg_g += $alpha_f * $pg;
61 0         0 $avg_b += $alpha_f * $pb;
62 0         0 $avg_a += $alpha;
63             }
64 0 0       0 if ($avg_a > 0) {
65 0         0 $_ /= $avg_a for $avg_r, $avg_g, $avg_b;
66             }
67              
68 0         0 my $has_alpha = $avg_a < $width * $height;
69 0 0       0 my $l_limit = $has_alpha ? 5 : 7; # Use fewer luminance bits if there's alpha
70 0         0 my $max_w_h = max $width, $height;
71 0         0 my $lx = max 1, int($l_limit * $width / $max_w_h + 0.5);
72 0         0 my $ly = max 1, int($l_limit * $height / $max_w_h + 0.5);
73             my (
74 0         0 @l, # luminance
75             @p, # yellow - blue
76             @q, # red - green
77             @a, # alpha
78             );
79              
80             # Convert the image from RGBA to LPQA (composite atop the average color)
81 0         0 for my $pixel (unpack '(a4)*', $rgba) {
82 0         0 my ($pr, $pg, $pb, $pa) = unpack 'C*', $pixel;
83 0         0 my $alpha = $pa / 255;
84 0         0 my $alpha_f = $alpha / 255;
85 0         0 my $r = $avg_r * (1 - $alpha) + $alpha_f * $pr;
86 0         0 my $g = $avg_g * (1 - $alpha) + $alpha_f * $pg;
87 0         0 my $b = $avg_b * (1 - $alpha) + $alpha_f * $pb;
88 0         0 push @l, ($r + $g + $b) / 3;
89 0         0 push @p, ($r + $g) / 2 - $b;
90 0         0 push @q, $r - $g;
91 0         0 push @a, $alpha;
92             }
93              
94             # Encode using the DCT into DC (constant) and normalized AC (varying) terms
95             my $encode_channel = sub {
96 0     0   0 my ($channel, $nx, $ny) = @_;
97 0         0 my $dc = 0;
98 0         0 my @ac;
99 0         0 my $scale = 0;
100 0         0 for my $cy (0 .. $ny - 1) {
101 0         0 for (my $cx = 0; $cx * $ny < $nx * ($ny - $cy); $cx++) {
102 0         0 my @fx = map cos(PI / $width * $cx * ($_ + 0.5)), 0 .. $width - 1;
103 0         0 my $f = 0;
104 0         0 for my $y (0 .. $height - 1) {
105 0         0 my $fy = cos(PI / $height * $cy * ($y + 0.5));
106 0         0 for my $x (0 .. $width - 1) {
107 0         0 $f += $channel->[$x + $y * $width] * $fx[$x] * $fy;
108             }
109             }
110 0         0 $f /= $width * $height;
111 0 0 0     0 if ($cx || $cy) {
112 0         0 push @ac, $f;
113 0         0 $scale = max $scale, abs $f;
114             } else {
115 0         0 $dc = $f;
116             }
117             }
118             }
119 0 0       0 if ($scale) {
120 0         0 for my $ac (@ac) {
121 0         0 $ac = 0.5 + 0.5 / $scale * $ac;
122             }
123             }
124 0         0 ($dc, \@ac, $scale)
125 0         0 };
126 0         0 my ($l_dc, $l_ac, $l_scale) = $encode_channel->(\@l, max(3, $lx), max(3, $ly));
127 0         0 my ($p_dc, $p_ac, $p_scale) = $encode_channel->(\@p, 3, 3);
128 0         0 my ($q_dc, $q_ac, $q_scale) = $encode_channel->(\@q, 3, 3);
129 0 0       0 my ($a_dc, $a_ac, $a_scale) = $has_alpha ? $encode_channel->(\@a, 5, 5) : (1, [], 1);
130              
131             # Write the constants
132 0         0 my $is_landscape = $width > $height;
133 0 0       0 my $header24 = int(0.5 + 63 * $l_dc)
134             | int(0.5 + 31.5 + 31.5 * $p_dc) << 6
135             | int(0.5 + 31.5 + 31.5 * $q_dc) << 12
136             | int(0.5 + 31 * $l_scale) << 18
137             | ($has_alpha ? 1 << 23 : 0);
138 0 0       0 my $header16 = ($is_landscape ? $ly : $lx)
    0          
139             | int(0.5 + 63 * $p_scale) << 3
140             | int(0.5 + 63 * $q_scale) << 9
141             | ($is_landscape ? 1 << 15 : 0);
142 0 0       0 my $hash_const = pack 'C*', (
143             $header24 & 0xff,
144             $header24 >> 8 & 0xff,
145             $header24 >> 16,
146             $header16 & 0xff,
147             $header16 >> 8,
148             $has_alpha
149             ? int(0.5 + 15 * $a_dc) | int(0.5 + 15 * $a_scale) << 4
150             : (),
151             );
152              
153             # Write the varying factors
154 0         0 my $ac_index = 0;
155 0         0 my $hash_vary = '';
156 0 0       0 for my $ac ($l_ac, $p_ac, $q_ac, $has_alpha ? $a_ac : ()) {
157 0         0 for my $f (@$ac) {
158 0         0 vec($hash_vary, $ac_index++, 4) = int(0.5 + 15 * $f);
159             }
160             }
161              
162 0         0 $hash_const . $hash_vary
163             }
164              
165             sub rgba_to_png {
166 9     9 0 38 my ($width, $height, $rgba) = @_;
167 9         28 _assert_w_h_rgba $width, $height, $rgba;
168              
169 9         19 my $row = $width * 4 + 1;
170 9         20 my $idat = 6 + $height * (5 + $row);
171 9         83 my @bytes = (
172             137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0,
173             $width >> 8 & 0xff, $width & 0xff, 0, 0, $height >> 8 & 0xff, $height & 0xff, 8, 6, 0, 0, 0, 0, 0, 0, 0,
174             $idat >> 24 & 0xff, $idat >> 16 & 0xff, $idat >> 8 & 0xff, $idat & 0xff,
175             73, 68, 65, 84, 120, 1,
176             );
177 9         21 my $a = 1;
178 9         23 my $b = 0;
179 9         32 for my $y (0 .. $height - 1) {
180 219 100       604 push @bytes, (
181             $y == $height - 1 ? 1 : 0,
182             $row & 0xff,
183             $row >> 8 & 0xff,
184             $row & 0xff ^ 0xff,
185             $row >> 8 & 0xff ^ 0xff,
186             0,
187             );
188 219         293 $b = ($b + $a) % 65521;
189 219         306 my $slice = ($row - 1) * $y;
190 219         380 for my $i ($slice .. $slice + $row - 2) {
191 28032         35426 my $u = vec $rgba, $i, 8;
192 28032         36262 push @bytes, $u;
193 28032         35465 $a = ($a + $u) % 65521;
194 28032         39255 $b = ($b + $a) % 65521;
195             }
196             }
197 9         122 push @bytes, (
198             $b >> 8, $b & 0xff, $a >> 8, $a & 0xff, 0, 0, 0, 0,
199             0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
200             );
201 9         38 my @table = (
202             0, 498536548, 997073096, 651767980, 1994146192, 1802195444, 1303535960,
203             1342533948, 3988292384, 4027552580, 3604390888, 3412177804, 2607071920,
204             2262029012, 2685067896, 3183342108,
205             );
206 9         40 for my $range ([12, 28], [37, 40 + $idat]) {
207 18         37 my ($start, $end) = @$range;
208 18         42 my $c = 0xffff_ffff;
209 18         41 for my $i ($start .. $end) {
210 29589         35201 $c ^= $bytes[$i];
211 29589         39418 $c = $c >> 4 ^ $table[$c & 0xf];
212 29589         42161 $c = $c >> 4 ^ $table[$c & 0xf];
213             }
214 18         27 $c ^= 0xffff_ffff;
215 18         46 $bytes[$end + 1] = $c >> 24 & 0xff;
216 18         41 $bytes[$end + 2] = $c >> 16 & 0xff;
217 18         29 $bytes[$end + 3] = $c >> 8 & 0xff;
218 18         35 $bytes[$end + 4] = $c & 0xff;
219             }
220 9         1262 pack 'C*', @bytes
221             }
222              
223             sub rgba_to_data_url {
224 6     6 0 1967 my ($width, $height, $rgba) = @_;
225 6         26 _assert_w_h_rgba $width, $height, $rgba;
226 6         20 'data:image/png;base64,' . MIME::Base64::encode(rgba_to_png($width, $height, $rgba), '')
227             }
228              
229             sub thumb_hash_to_rgba {
230 8     8 0 7512 my ($hash) = @_;
231 8         38 _assert_thumb_hash $hash;
232 7 100       108 wantarray or croak "thumb_hash_to_rgba: must be called in list context";
233            
234             # Read the constants
235 6         26 my $header24 = vec($hash, 0, 8) | vec($hash, 1, 8) << 8 | vec($hash, 2, 8) << 16;
236 6         17 my $header16 = vec($hash, 3, 8) | vec($hash, 4, 8) << 8;
237 6         17 my $l_dc = ($header24 & 63) / 63;
238 6         13 my $p_dc = ($header24 >> 6 & 63) / 31.5 - 1;
239 6         13 my $q_dc = ($header24 >> 12 & 63) / 31.5 - 1;
240 6         13 my $l_scale = ($header24 >> 18 & 31) / 31;
241 6         11 my $has_alpha = $header24 >> 23;
242 6         14 my $p_scale = ($header16 >> 3 & 63) / 63;
243 6         16 my $q_scale = ($header16 >> 9 & 63) / 63;
244 6         11 my $is_landscape = $header16 >> 15;
245 6 100       28 my $l_max = $has_alpha ? 5 : 7;
246 6         23 my $l_min = max(3, $header16 & 7);
247 6 100       20 my ($lx, $ly) = $is_landscape
248             ? ($l_max, $l_min)
249             : ($l_min, $l_max);
250 6 100       27 my ($a_dc, $a_scale) = $has_alpha
251             ? (map vec($hash, $_, 4) / 15,
252             10, 11)
253             : (1, 1);
254              
255             # Read the varying factors (boost saturation by 1.25x to compensate for quantization)
256 6 100       17 my $ac_index = $has_alpha ? 12 : 10;
257             my $decode_channel = sub {
258 20     20   39 my ($nx, $ny, $scale) = @_;
259 20         33 my @ac;
260 20         47 for my $cy (0 .. $ny - 1) {
261 74         162 for (my $cx = !$cy; $cx * $ny < $nx * ($ny - $cy); $cx++) {
262 196         508 push @ac, (vec($hash, $ac_index++, 4) / 7.5 - 1) * $scale;
263             }
264             }
265             \@ac
266 6         45 };
  20         61  
267 6         27 my $l_ac = $decode_channel->($lx, $ly, $l_scale);
268 6         32 my $p_ac = $decode_channel->(3, 3, $p_scale * 1.25);
269 6         25 my $q_ac = $decode_channel->(3, 3, $q_scale * 1.25);
270 6 100       30 my $a_ac = $has_alpha ? $decode_channel->(5, 5, $a_scale) : [];
271              
272             # Decode using the DCT into RGB
273 6 100       18 my $ratio = $is_landscape
274             ? $l_max / ($header16 & 7)
275             : ($header16 & 7) / $l_max;
276 6 100       33 my ($width, $height) = $ratio > 1
277             ? (32, int(0.5 + 32 / $ratio))
278             : (int(0.5 + 32 * $ratio), 32);
279 6         14 my $rgba = '';
280 6         11 my (@fx, @fy);
281 6         13 for my $y (0 .. $height - 1) {
282 146         317 for my $x (0 .. $width - 1) {
283 4672         7038 my $l = $l_dc;
284 4672         5727 my $p = $p_dc;
285 4672         5905 my $q = $q_dc;
286 4672         5728 my $a = $a_dc;
287              
288             # Precompute the coefficients
289 4672 100       26116 my @fx = map cos(PI / $width * ($x + 0.5) * $_), 0 .. max($lx, $has_alpha ? 5 : 3) - 1;
290 4672 100       22534 my @fy = map cos(PI / $height * ($y + 0.5) * $_), 0 .. max($ly, $has_alpha ? 5 : 3) - 1;
291              
292             # Decode L
293             {
294 4672         5910 my $j = 0;
295 4672         7454 for my $cy (0 .. $ly - 1) {
296 22208         30234 my $fy2 = $fy[$cy] * 2;
297 22208         42774 for (my $cx = !$cy; $cx * $ly < $lx * ($ly - $cy); $cx++) {
298 81792         165105 $l += $l_ac->[$j++] * $fx[$cx] * $fy2;
299             }
300             }
301             }
302              
303             # Decode P and Q
304             {
305 4672         7392 my $j = 0;
  4672         5805  
  4672         5767  
306 4672         6859 for my $cy (0 .. 2) {
307 14016         18774 my $fy2 = $fy[$cy] * 2;
308 14016         20659 for my $cx (!$cy .. 2 - $cy) {
309 23360         31998 my $f = $fx[$cx] * $fy2;
310 23360         30450 $p += $p_ac->[$j] * $f;
311 23360         29586 $q += $q_ac->[$j] * $f;
312 23360         34566 $j++;
313             }
314             }
315             }
316              
317             # Decode A
318 4672 100       8020 if ($has_alpha) {
319 2048         2502 my $j = 0;
320 2048         3010 for my $cy (0 .. 4) {
321 10240         13376 my $fy2 = $fy[$cy] * 2;
322 10240         14511 for my $cx (!$cy .. 4 - $cy) {
323 28672         42734 $a += $a_ac->[$j++] * $fx[$cx] * $fy2;
324             }
325             }
326             }
327              
328             # Convert to RGB
329 4672         6595 my $b = $l - 2 / 3 * $p;
330 4672         7012 my $r = (3 * $l - $b + $q) / 2;
331 4672         6232 my $g = $r - $q;
332 4672         27836 $rgba .= pack 'C*', map max(0, 255 * min(1, $_)), $r, $g, $b, $a;
333             }
334             }
335              
336 6         181 $width, $height, $rgba
337             }
338              
339             sub thumb_hash_to_average_rgba {
340 5     5 0 2926 my ($hash) = @_;
341 5         17 _assert_thumb_hash $hash;
342 4 100       87 wantarray or croak "thumb_hash_to_average_rgba: must be called in list context";
343 3         14 my $header = vec($hash, 0, 8) | vec($hash, 1, 8) << 8 | vec($hash, 2, 8) << 16;
344 3         10 my $l = ($header & 63) / 63;
345 3         11 my $p = ($header >> 6 & 63) / 31.5 - 1;
346 3         8 my $q = ($header >> 12 & 63) / 31.5 - 1;
347 3         6 my $has_alpha = $header >> 23;
348 3 100       11 my $a = $has_alpha ? (vec($hash, 5, 8) & 15) / 15 : 1;
349 3         9 my $b = $l - 2 / 3 * $p;
350 3         10 my $r = (3 * $l - $b + $q) / 2;
351 3         7 my $g = $r - $q;
352              
353 3         26 max(0, min(1, $r)),
354             max(0, min(1, $g)),
355             max(0, min(1, $b)),
356             $a
357             }
358              
359             sub thumb_hash_to_approximate_aspect_ratio {
360 3     3 0 3356 my ($hash) = @_;
361 3         10 _assert_thumb_hash $hash;
362 3         13 my $has_alpha = vec($hash, 2, 8) & 0x80;
363 3         7 my $is_landscape = vec($hash, 4, 8) & 0x80;
364 3 100       9 my $l_max = $has_alpha ? 5 : 7;
365 3         6 my $l_min = vec($hash, 3, 8) & 0x7;
366 3 100       14 $is_landscape
367             ? $l_max / $l_min
368             : $l_min / $l_max
369             }
370              
371             sub thumb_hash_to_data_url {
372 3     3 0 1556 my ($hash) = @_;
373 3         13 _assert_thumb_hash $hash;
374 3         11 rgba_to_data_url thumb_hash_to_rgba $hash
375             }
376              
377             1
378             __END__