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   15 use v5.10.0; # //
  1         3  
3 1     1   5 use strict;
  1         2  
  1         24  
4 1     1   4 use warnings qw(all FATAL uninitialized);
  1         2  
  1         61  
5              
6 1     1   9 use Exporter 5.57 qw(import);
  1         14  
  1         28  
7 1     1   4 use Carp qw(croak);
  1         13  
  1         49  
8 1     1   15 use List::Util qw(min max);
  1         2  
  1         91  
9 1     1   471 use MIME::Base64 ();
  1         758  
  1         48  
10              
11             use constant {
12 1         2647 PI => 4 * atan2(1, 1),
13 1     1   7 };
  1         3  
14              
15             our $VERSION = '0.02';
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   42 my ($width, $height, $rgba, $sub) = @_;
29 15   33     178 $sub //= (caller 1)[3];
30              
31 15 50 33     90 0 <= $width && $width <= 100
32             or croak "$sub: width is not in range [0, 100]: $width";
33 15 50 33     58 0 <= $height && $height <= 100
34             or croak "$sub: height is not in range [0, 100]: $height";
35 15 50       43 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   52 my ($hash, $sub) = @_;
41 19   33     204 $sub //= (caller 1)[3];
42              
43 19 100       364 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 41 my ($width, $height, $rgba) = @_;
167 9         29 _assert_w_h_rgba $width, $height, $rgba;
168              
169 9         20 my $row = $width * 4 + 1;
170 9         26 my $idat = 6 + $height * (5 + $row);
171 9         51 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         13 my $a = 1;
178 9         23 my $b = 0;
179 9         25 for my $y (0 .. $height - 1) {
180 219 100       519 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         281 $b = ($b + $a) % 65521;
189 219         308 my $slice = ($row - 1) * $y;
190 219         341 for my $i ($slice .. $slice + $row - 2) {
191 28032         35211 my $u = vec $rgba, $i, 8;
192 28032         34585 push @bytes, $u;
193 28032         35465 $a = ($a + $u) % 65521;
194 28032         38180 $b = ($b + $a) % 65521;
195             }
196             }
197 9         89 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         29 my @table = (
202             0, 498536548, 997073096, 651767980, 1994146192, 1802195444, 1303535960,
203             1342533948, 3988292384, 4027552580, 3604390888, 3412177804, 2607071920,
204             2262029012, 2685067896, 3183342108,
205             );
206 9         42 for my $range ([12, 28], [37, 40 + $idat]) {
207 18         37 my ($start, $end) = @$range;
208 18         63 my $c = 0xffff_ffff;
209 18         42 for my $i ($start .. $end) {
210 29589         35845 $c ^= $bytes[$i];
211 29589         39571 $c = $c >> 4 ^ $table[$c & 0xf];
212 29589         42720 $c = $c >> 4 ^ $table[$c & 0xf];
213             }
214 18         24 $c ^= 0xffff_ffff;
215 18         44 $bytes[$end + 1] = $c >> 24 & 0xff;
216 18         29 $bytes[$end + 2] = $c >> 16 & 0xff;
217 18         26 $bytes[$end + 3] = $c >> 8 & 0xff;
218 18         36 $bytes[$end + 4] = $c & 0xff;
219             }
220 9         1505 pack 'C*', @bytes
221             }
222              
223             sub rgba_to_data_url {
224 6     6 0 2231 my ($width, $height, $rgba) = @_;
225 6         24 _assert_w_h_rgba $width, $height, $rgba;
226 6         44 '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 7729 my ($hash) = @_;
231 8         29 _assert_thumb_hash $hash;
232 7 100       107 wantarray or croak "thumb_hash_to_rgba: must be called in list context";
233            
234             # Read the constants
235 6         27 my $header24 = vec($hash, 0, 8) | vec($hash, 1, 8) << 8 | vec($hash, 2, 8) << 16;
236 6         23 my $header16 = vec($hash, 3, 8) | vec($hash, 4, 8) << 8;
237 6         16 my $l_dc = ($header24 & 63) / 63;
238 6         14 my $p_dc = ($header24 >> 6 & 63) / 31.5 - 1;
239 6         13 my $q_dc = ($header24 >> 12 & 63) / 31.5 - 1;
240 6         14 my $l_scale = ($header24 >> 18 & 31) / 31;
241 6         13 my $has_alpha = $header24 >> 23;
242 6         15 my $p_scale = ($header16 >> 3 & 63) / 63;
243 6         17 my $q_scale = ($header16 >> 9 & 63) / 63;
244 6         12 my $is_landscape = $header16 >> 15;
245 6 100       15 my $l_max = $has_alpha ? 5 : 7;
246 6         33 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       29 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       19 my $ac_index = $has_alpha ? 12 : 10;
257             my $decode_channel = sub {
258 20     20   36 my ($nx, $ny, $scale) = @_;
259 20         29 my @ac;
260 20         42 for my $cy (0 .. $ny - 1) {
261 74         158 for (my $cx = !$cy; $cx * $ny < $nx * ($ny - $cy); $cx++) {
262 196         452 push @ac, (vec($hash, $ac_index++, 4) / 7.5 - 1) * $scale;
263             }
264             }
265             \@ac
266 6         56 };
  20         34  
267 6         18 my $l_ac = $decode_channel->($lx, $ly, $l_scale);
268 6         18 my $p_ac = $decode_channel->(3, 3, $p_scale * 1.25);
269 6         13 my $q_ac = $decode_channel->(3, 3, $q_scale * 1.25);
270 6 100       22 my $a_ac = $has_alpha ? $decode_channel->(5, 5, $a_scale) : [];
271              
272             # Decode using the DCT into RGB
273 6 100       17 my $ratio = $is_landscape
274             ? $l_max / ($header16 & 7)
275             : ($header16 & 7) / $l_max;
276 6 100       26 my ($width, $height) = $ratio > 1
277             ? (32, int(0.5 + 32 / $ratio))
278             : (int(0.5 + 32 * $ratio), 32);
279 6         10 my $rgba = '';
280 6         22 my (@fx, @fy);
281 6         15 for my $y (0 .. $height - 1) {
282 146         270 for my $x (0 .. $width - 1) {
283 4672         7185 my $l = $l_dc;
284 4672         5673 my $p = $p_dc;
285 4672         5740 my $q = $q_dc;
286 4672         5848 my $a = $a_dc;
287              
288             # Precompute the coefficients
289 4672 100       26203 my @fx = map cos(PI / $width * ($x + 0.5) * $_), 0 .. max($lx, $has_alpha ? 5 : 3) - 1;
290 4672 100       23161 my @fy = map cos(PI / $height * ($y + 0.5) * $_), 0 .. max($ly, $has_alpha ? 5 : 3) - 1;
291              
292             # Decode L
293             {
294 4672         5960 my $j = 0;
295 4672         7740 for my $cy (0 .. $ly - 1) {
296 22208         30456 my $fy2 = $fy[$cy] * 2;
297 22208         42058 for (my $cx = !$cy; $cx * $ly < $lx * ($ly - $cy); $cx++) {
298 81792         159618 $l += $l_ac->[$j++] * $fx[$cx] * $fy2;
299             }
300             }
301             }
302              
303             # Decode P and Q
304             {
305 4672         7386 my $j = 0;
  4672         5870  
  4672         5988  
306 4672         6806 for my $cy (0 .. 2) {
307 14016         18623 my $fy2 = $fy[$cy] * 2;
308 14016         21021 for my $cx (!$cy .. 2 - $cy) {
309 23360         31798 my $f = $fx[$cx] * $fy2;
310 23360         31315 $p += $p_ac->[$j] * $f;
311 23360         30590 $q += $q_ac->[$j] * $f;
312 23360         33664 $j++;
313             }
314             }
315             }
316              
317             # Decode A
318 4672 100       7556 if ($has_alpha) {
319 2048         2537 my $j = 0;
320 2048         2881 for my $cy (0 .. 4) {
321 10240         13801 my $fy2 = $fy[$cy] * 2;
322 10240         14841 for my $cx (!$cy .. 4 - $cy) {
323 28672         42665 $a += $a_ac->[$j++] * $fx[$cx] * $fy2;
324             }
325             }
326             }
327              
328             # Convert to RGB
329 4672         6666 my $b = $l - 2 / 3 * $p;
330 4672         6984 my $r = (3 * $l - $b + $q) / 2;
331 4672         6040 my $g = $r - $q;
332 4672         27954 $rgba .= pack 'C*', map max(0, 255 * min(1, $_)), $r, $g, $b, $a;
333             }
334             }
335              
336 6         199 $width, $height, $rgba
337             }
338              
339             sub thumb_hash_to_average_rgba {
340 5     5 0 2855 my ($hash) = @_;
341 5         17 _assert_thumb_hash $hash;
342 4 100       93 wantarray or croak "thumb_hash_to_average_rgba: must be called in list context";
343 3         16 my $header = vec($hash, 0, 8) | vec($hash, 1, 8) << 8 | vec($hash, 2, 8) << 16;
344 3         13 my $l = ($header & 63) / 63;
345 3         10 my $p = ($header >> 6 & 63) / 31.5 - 1;
346 3         9 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         19 my $b = $l - 2 / 3 * $p;
350 3         12 my $r = (3 * $l - $b + $q) / 2;
351 3         6 my $g = $r - $q;
352              
353 3         35 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 3500 my ($hash) = @_;
361 3         10 _assert_thumb_hash $hash;
362 3         10 my $has_alpha = vec($hash, 2, 8) & 0x80;
363 3         10 my $is_landscape = vec($hash, 4, 8) & 0x80;
364 3 100       11 my $l_max = $has_alpha ? 5 : 7;
365 3         7 my $l_min = vec($hash, 3, 8) & 0x7;
366 3 100       13 $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 1542 my ($hash) = @_;
373 3         16 _assert_thumb_hash $hash;
374 3         11 rgba_to_data_url thumb_hash_to_rgba $hash
375             }
376              
377             1
378             __END__