File Coverage

blib/lib/Perlbal/Plugin/Palimg.pm
Criterion Covered Total %
statement 11 154 7.1
branch 0 66 0.0
condition 0 9 0.0
subroutine 5 17 29.4
pod 0 4 0.0
total 16 250 6.4


line stmt bran cond sub pod time code
1             ###########################################################################
2             # Palimg plugin that allows Perlbal to serve palette altered images
3             ###########################################################################
4              
5             package Perlbal::Plugin::Palimg;
6              
7 1     1   2200 use strict;
  1         3  
  1         360  
8 1     1   7 use warnings;
  1         3  
  1         34  
9 1     1   7 no warnings qw(deprecated);
  1         2  
  1         1574  
10              
11             # called when we're being added to a service
12             sub register {
13 0     0 0   my ($class, $svc) = @_;
14              
15             # verify that an incoming request is a palimg request
16             $svc->register_hook('Palimg', 'start_serve_request', sub {
17 0     0     my Perlbal::ClientHTTPBase $obj = $_[0];
18 0 0         return 0 unless $obj;
19 0           my Perlbal::HTTPHeaders $hd = $obj->{req_headers};
20 0           my $uriref = $_[1];
21 0 0         return 0 unless $uriref;
22              
23             # if this is palimg, peel off the requested modifications and put in headers
24 0 0         return 0 unless $$uriref =~ m!^/palimg/(.+)\.(\w+)(.*)$!;
25 0           my ($fn, $ext, $extra) = ($1, $2, $3);
26 0 0         return 0 unless $extra;
27 0           my ($palspec) = $extra =~ m!^/p(.+)$!;
28 0 0 0       return 0 unless $fn && $palspec;
29              
30             # must be ok, setup for it
31 0           $$uriref = "/palimg/$fn.$ext";
32 0           $obj->{scratch}->{palimg} = [ $ext, $palspec ];
33 0           return 0;
34 0           });
35              
36             # actually serve a palimg
37             $svc->register_hook('Palimg', 'start_send_file', sub {
38 0     0     my Perlbal::ClientHTTPBase $obj = $_[0];
39 0 0 0       return 0 unless $obj &&
40             (my $palimginfo = $obj->{scratch}->{palimg});
41              
42             # turn off writes
43 0           $obj->watch_write(0);
44              
45             # create filehandle for reading
46 0           my $data = '';
47             Perlbal::AIO::aio_read($obj->reproxy_fh, 0, 2048, $data, sub {
48             # got data? undef is error
49 0 0         return $obj->_simple_response(500) unless $_[0] > 0;
50              
51             # pass down to handler
52 0           my Perlbal::HTTPHeaders $hd = $obj->{req_headers};
53 0           my $res = PalImg::modify_file(\$data, $palimginfo->[0], $palimginfo->[1]);
54 0 0         return $obj->_simple_response(500) unless defined $res;
55 0 0         return $obj->_simple_response($res) if $res;
56              
57             # seek into the file now so sendfile starts further in
58 0           my $ld = length $data;
59 0           sysseek($obj->{reproxy_fh}, $ld, &POSIX::SEEK_SET);
60 0           $obj->{reproxy_file_offset} = $ld;
61              
62             # re-enable writes after we get data
63 0           $obj->tcp_cork(1); # by setting reproxy_file_offset above, it won't cork, so we cork it
64 0           $obj->write($data);
65 0           $obj->watch_write(1);
66 0           });
67              
68 0           return 1;
69 0           });
70              
71 0           return 1;
72             }
73              
74             # called when we're no longer active on a service
75             sub unregister {
76 0     0 0   my ($class, $svc) = @_;
77              
78             # clean up time
79 0           $svc->unregister_hooks('Palimg');
80 0           return 1;
81             }
82              
83             # called when we are loaded/unloaded ... someday add some stats viewing
84             # commands here?
85 0     0 0   sub load { return 1; }
86 0     0 0   sub unload { return 1; }
87              
88             ####### PALIMG START ###########################################################################
89             package PalImg;
90              
91             sub parse_hex_color
92             {
93 0     0     my $color = shift;
94 0           return [ map { hex(substr($color, $_, 2)) } (0,2,4) ];
  0            
95             }
96              
97             sub modify_file
98             {
99 0     0     my ($data, $type, $palspec) = @_;
100              
101             # palette altering
102 0           my %pal_colors;
103 0 0         if (my $pals = $palspec) {
104 0           my $hx = "[0-9a-f]";
105 0 0 0       if ($pals =~ /^g($hx{2,2})($hx{6,6})($hx{2,2})($hx{6,6})$/) {
    0          
    0          
106             # gradient from index $1, color $2, to index $3, color $4
107 0           my $from = hex($1);
108 0           my $to = hex($3);
109 0 0         return 404 if $from == $to;
110 0           my $fcolor = parse_hex_color($2);
111 0           my $tcolor = parse_hex_color($4);
112 0 0         if ($to < $from) {
113 0           ($from, $to, $fcolor, $tcolor) =
114             ($to, $from, $tcolor, $fcolor);
115             }
116 0           for (my $i=$from; $i<=$to; $i++) {
117 0           $pal_colors{$i} = [ map {
118 0           int($fcolor->[$_] +
119             ($tcolor->[$_] - $fcolor->[$_]) *
120             ($i-$from) / ($to-$from))
121             } (0..2) ];
122             }
123             } elsif ($pals =~ /^t($hx{6,6})($hx{6,6})?$/) {
124             # tint everything towards color
125 0           my ($t, $td) = ($1, $2);
126 0           $pal_colors{'tint'} = parse_hex_color($t);
127 0 0         $pal_colors{'tint_dark'} = $td ? parse_hex_color($td) : [0,0,0];
128             } elsif (length($pals) > 42 || $pals =~ /[^0-9a-f]/) {
129 0           return 404;
130             } else {
131 0           my $len = length($pals);
132 0 0         return 404 if $len % 7; # must be multiple of 7 chars
133 0           for (my $i = 0; $i < $len/7; $i++) {
134 0           my $palindex = hex(substr($pals, $i*7, 1));
135 0           $pal_colors{$palindex} = [
136             hex(substr($pals, $i*7+1, 2)),
137             hex(substr($pals, $i*7+3, 2)),
138             hex(substr($pals, $i*7+5, 2)),
139             substr($pals, $i*7+1, 6),
140             ];
141             }
142             }
143             }
144              
145 0 0         if (%pal_colors) {
146 0 0         if ($type eq 'gif') {
    0          
147 0 0         return 404 unless PaletteModify::new_gif_palette($data, \%pal_colors);
148             } elsif ($type eq 'png') {
149 0 0         return 404 unless PaletteModify::new_png_palette($data, \%pal_colors);
150             }
151             }
152              
153             # success
154 0           return 0;
155             }
156             ####### PALIMG END #############################################################################
157              
158             ####### PALETTEMODIFY START ####################################################################
159             package PaletteModify;
160              
161             BEGIN {
162 1     1   320 $PaletteModify::HAVE_CRC = eval "use String::CRC32 (); 1;";
  1     1   1628  
  0            
  0            
163             }
164              
165             sub common_alter
166             {
167 0     0     my ($palref, $table) = @_;
168 0           my $length = length $table;
169              
170 0           my $pal_size = $length / 3;
171              
172             # tinting image? if so, we're remaking the whole palette
173 0 0         if (my $tint = $palref->{'tint'}) {
174 0           my $dark = $palref->{'tint_dark'};
175 0           my $diff = [ map { $tint->[$_] - $dark->[$_] } (0..2) ];
  0            
176 0           $palref = {};
177 0           for (my $idx=0; $idx<$pal_size; $idx++) {
178 0           for my $c (0..2) {
179 0           my $curr = ord(substr($table, $idx*3+$c));
180 0           my $p = \$palref->{$idx}->[$c];
181 0           $$p = int($dark->[$c] + $diff->[$c] * $curr / 255);
182             }
183             }
184             }
185              
186 0           while (my ($idx, $c) = each %$palref) {
187 0 0         next if $idx >= $pal_size;
188 0           substr($table, $idx*3+$_, 1) = chr($c->[$_]) for (0..2);
189             }
190              
191 0           return $table;
192             }
193              
194             sub new_gif_palette
195             {
196 0     0     my ($data, $palref) = @_;
197              
198             # make sure we have data to operate on, or the substrs below die
199 0 0         return unless $$data;
200              
201             # 13 bytes for magic + image info (size, color depth, etc)
202             # and then the global palette table (3*256)
203 0           my $header = substr($$data, 0, 13+3*256);
204              
205             # figure out how big global color table is (don't want to overwrite it)
206 0           my $pf = ord substr($header, 10, 1);
207 0           my $gct = 2 ** (($pf & 7) + 1); # last 3 bits of packaged fields
208              
209             # final sanity check for size so the substr below doesn't die
210 0 0         return unless length $header >= 13 + 3 * $gct;
211              
212 0           substr($header, 13, 3*$gct) = common_alter($palref, substr($header, 13, 3*$gct));
213 0           $$data = $header;
214 0           return 1;
215             }
216              
217             sub new_png_palette
218             {
219 0     0     my ($data, $palref) = @_;
220              
221             # subroutine for reading data
222 0           my ($curidx, $maxlen) = (0, length $$data);
223             my $read = sub {
224             # put $_[1] data into scalar reference $_[0]
225 0 0   0     return undef if $_[1] + $curidx > $maxlen;
226 0           ${$_[0]} = substr($$data, $curidx, $_[1]);
  0            
227 0           $curidx += $_[1];
228 0           return length ${$_[0]};
  0            
229 0           };
230              
231             # without this module, we can't proceed.
232 0 0         return 0 unless $PaletteModify::HAVE_CRC;
233              
234 0           my $imgdata;
235              
236             # Validate PNG signature
237 0           my $png_sig = pack("H16", "89504E470D0A1A0A");
238 0           my $sig;
239 0           $read->(\$sig, 8);
240 0 0         return 0 unless $sig eq $png_sig;
241 0           $imgdata .= $sig;
242              
243             # Start reading in chunks
244 0           my ($length, $type) = (0, '');
245 0           while ($read->(\$length, 4)) {
246              
247 0           $imgdata .= $length;
248 0           $length = unpack("N", $length);
249 0 0         return 0 unless $read->(\$type, 4) == 4;
250 0           $imgdata .= $type;
251              
252 0 0         if ($type eq 'IHDR') {
    0          
253 0           my $header;
254 0           $read->(\$header, $length+4);
255 0           my ($width,$height,$depth,$color,$compression,
256             $filter,$interlace, $CRC)
257             = unpack("NNCCCCCN", $header);
258 0 0         return 0 unless $color == 3; # unpaletted image
259 0           $imgdata .= $header;
260             } elsif ($type eq 'PLTE') {
261             # Finally, we can go to work
262 0           my $palettedata;
263 0           $read->(\$palettedata, $length);
264 0           $palettedata = common_alter($palref, $palettedata);
265 0           $imgdata .= $palettedata;
266              
267             # Skip old CRC
268 0           my $skip;
269 0           $read->(\$skip, 4);
270              
271             # Generate new CRC
272 0           my $crc = String::CRC32::crc32($type . $palettedata);
273 0           $crc = pack("N", $crc);
274              
275 0           $imgdata .= $crc;
276 0           $$data = $imgdata;
277 0           return 1;
278             } else {
279 0           my $skip;
280             # Skip rest of chunk and add to imgdata
281             # Number of bytes is +4 because of CRC
282             #
283 0           for (my $count=0; $count < $length + 4; $count++) {
284 0           $read->(\$skip, 1);
285 0           $imgdata .= $skip;
286             }
287             }
288             }
289              
290 0           return 0;
291             }
292             ####### PALETTEMODIFY END ######################################################################
293              
294             1;
295              
296             __END__