########################################################################### # Palimg plugin that allows Perlbal to serve palette altered images ########################################################################### package Perlbal::Plugin::Palimg; use strict; use warnings; # called when we're being added to a service sub register { my ($class, $svc) = @_; # verify that an incoming request is a palimg request $svc->register_hook('Palimg', 'start_serve_request', sub { my Perlbal::ClientHTTPBase $obj = $_[0]; return 0 unless $obj; my Perlbal::HTTPHeaders $hd = $obj->{req_headers}; my $uriref = $_[1]; return 0 unless $uriref; # if this is palimg, peel off the requested modifications and put in headers return 0 unless $$uriref =~ m!^/palimg/(.+)\.(\w+)(.*)$!; my ($fn, $ext, $extra) = ($1, $2, $3); return 0 unless $extra; my ($palspec) = $extra =~ m!^/p(.+)$!; return 0 unless $fn && $palspec; # must be ok, setup for it $$uriref = "/palimg/$fn.$ext"; $obj->{scratch}->{palimg} = [ $ext, $palspec ]; return 0; }); # actually serve a palimg $svc->register_hook('Palimg', 'start_send_file', sub { my Perlbal::ClientHTTPBase $obj = $_[0]; return 0 unless $obj && (my $palimginfo = $obj->{scratch}->{palimg}); # turn off writes $obj->watch_write(0); # create filehandle for reading my $data = ''; Perlbal::AIO::aio_read($obj->reproxy_fh, 0, 2048, $data, sub { # got data? undef is error return $obj->_simple_response(500) unless $_[0] > 0; # pass down to handler my Perlbal::HTTPHeaders $hd = $obj->{req_headers}; my $res = PalImg::modify_file(\$data, $palimginfo->[0], $palimginfo->[1]); return $obj->_simple_response(500) unless defined $res; return $obj->_simple_response($res) if $res; # seek into the file now so sendfile starts further in my $ld = length $data; sysseek($obj->{reproxy_fh}, $ld, &POSIX::SEEK_SET); $obj->{reproxy_file_offset} = $ld; # reenable writes after we get data $obj->tcp_cork(1); # by setting reproxy_file_offset above, it won't cork, so we cork it $obj->write($data); $obj->watch_write(1); }); return 1; }); return 1; } # called when we're no longer active on a service sub unregister { my ($class, $svc) = @_; # clean up time $svc->unregister_hooks('Palimg'); return 1; } # called when we are loaded/unloaded ... someday add some stats viewing # commands here? sub load { return 1; } sub unload { return 1; } ####### PALIMG START ########################################################################### package PalImg; sub parse_hex_color { my $color = shift; return [ map { hex(substr($color, $_, 2)) } (0,2,4) ]; } sub modify_file { my ($data, $type, $palspec) = @_; # palette altering my %pal_colors; if (my $pals = $palspec) { my $hx = "[0-9a-f]"; if ($pals =~ /^g($hx{2,2})($hx{6,6})($hx{2,2})($hx{6,6})$/) { # gradient from index $1, color $2, to index $3, color $4 my $from = hex($1); my $to = hex($3); return 404 if $from == $to; my $fcolor = parse_hex_color($2); my $tcolor = parse_hex_color($4); if ($to < $from) { ($from, $to, $fcolor, $tcolor) = ($to, $from, $tcolor, $fcolor); } for (my $i=$from; $i<=$to; $i++) { $pal_colors{$i} = [ map { int($fcolor->[$_] + ($tcolor->[$_] - $fcolor->[$_]) * ($i-$from) / ($to-$from)) } (0..2) ]; } } elsif ($pals =~ /^t($hx{6,6})($hx{6,6})?$/) { # tint everything towards color my ($t, $td) = ($1, $2); $pal_colors{'tint'} = parse_hex_color($t); $pal_colors{'tint_dark'} = $td ? parse_hex_color($td) : [0,0,0]; } elsif (length($pals) > 42 || $pals =~ /[^0-9a-f]/) { return 404; } else { my $len = length($pals); return 404 if $len % 7; # must be multiple of 7 chars for (my $i = 0; $i < $len/7; $i++) { my $palindex = hex(substr($pals, $i*7, 1)); $pal_colors{$palindex} = [ hex(substr($pals, $i*7+1, 2)), hex(substr($pals, $i*7+3, 2)), hex(substr($pals, $i*7+5, 2)), substr($pals, $i*7+1, 6), ]; } } } if (%pal_colors) { if ($type eq 'gif') { return 404 unless PaletteModify::new_gif_palette($data, \%pal_colors); } elsif ($type eq 'png') { return 404 unless PaletteModify::new_png_palette($data, \%pal_colors); } } # success return 0; } ####### PALIMG END ############################################################################# ####### PALETTEMODIFY START #################################################################### package PaletteModify; BEGIN { $PaletteModify::HAVE_CRC = eval "use String::CRC32 (); 1;"; } sub common_alter { my ($palref, $table) = @_; my $length = length $table; my $pal_size = $length / 3; # tinting image? if so, we're remaking the whole palette if (my $tint = $palref->{'tint'}) { my $dark = $palref->{'tint_dark'}; my $diff = [ map { $tint->[$_] - $dark->[$_] } (0..2) ]; $palref = {}; for (my $idx=0; $idx<$pal_size; $idx++) { for my $c (0..2) { my $curr = ord(substr($table, $idx*3+$c)); my $p = \$palref->{$idx}->[$c]; $$p = int($dark->[$c] + $diff->[$c] * $curr / 255); } } } while (my ($idx, $c) = each %$palref) { next if $idx >= $pal_size; substr($table, $idx*3+$_, 1) = chr($c->[$_]) for (0..2); } return $table; } sub new_gif_palette { my ($data, $palref) = @_; # make sure we have data to operate on, or the substrs below die return unless $$data; # 13 bytes for magic + image info (size, color depth, etc) # and then the global palette table (3*256) my $header = substr($$data, 0, 13+3*256); # figure out how big global color table is (don't want to overwrite it) my $pf = ord substr($header, 10, 1); my $gct = 2 ** (($pf & 7) + 1); # last 3 bits of packaged fields # final sanity check for size so the substr below doesn't die return unless length $header >= 13 + 3 * $gct; substr($header, 13, 3*$gct) = common_alter($palref, substr($header, 13, 3*$gct)); $$data = $header; return 1; } sub new_png_palette { my ($data, $palref) = @_; # subroutine for reading data my ($curidx, $maxlen) = (0, length $$data); my $read = sub { # put $_[1] data into scalar reference $_[0] return undef if $_[1] + $curidx > $maxlen; ${$_[0]} = substr($$data, $curidx, $_[1]); $curidx += $_[1]; return length ${$_[0]}; }; # without this module, we can't proceed. return 0 unless $PaletteModify::HAVE_CRC; my $imgdata; # Validate PNG signature my $png_sig = pack("H16", "89504E470D0A1A0A"); my $sig; $read->(\$sig, 8); return 0 unless $sig eq $png_sig; $imgdata .= $sig; # Start reading in chunks my ($length, $type) = (0, ''); while ($read->(\$length, 4)) { $imgdata .= $length; $length = unpack("N", $length); return 0 unless $read->(\$type, 4) == 4; $imgdata .= $type; if ($type eq 'IHDR') { my $header; $read->(\$header, $length+4); my ($width,$height,$depth,$color,$compression, $filter,$interlace, $CRC) = unpack("NNCCCCCN", $header); return 0 unless $color == 3; # unpaletted image $imgdata .= $header; } elsif ($type eq 'PLTE') { # Finally, we can go to work my $palettedata; $read->(\$palettedata, $length); $palettedata = common_alter($palref, $palettedata); $imgdata .= $palettedata; # Skip old CRC my $skip; $read->(\$skip, 4); # Generate new CRC my $crc = String::CRC32::crc32($type . $palettedata); $crc = pack("N", $crc); $imgdata .= $crc; $$data = $imgdata; return 1; } else { my $skip; # Skip rest of chunk and add to imgdata # Number of bytes is +4 becauses of CRC # for (my $count=0; $count < $length + 4; $count++) { $read->(\$skip, 1); $imgdata .= $skip; } } } return 0; } ####### PALETTEMODIFY END ###################################################################### 1;