#!/usr/bin/perl
#
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.

package Apache::LiveJournal::PalImg;

use strict;
use Apache2::Const qw(:common REDIRECT HTTP_NOT_MODIFIED);
use PaletteModify;

# for callers to 'ping' as a class method for Class::Autouse to lazily load
sub load { 1 }

# URLs of form /palimg/somedir/file.gif[extra]
# where extras can be:
#   /p...    - palette modify

sub handler {
    my $apache_r = shift;
    my $uri      = $apache_r->uri;

    my ( $base, $ext, $extra ) = $uri =~ m!^/palimg/(.+)\.(\w+)(.*)$!;
    return 404 unless $base && $base !~ m!\.\.!;

    my $disk_file = "$LJ::HTDOCS/palimg/$base.$ext";
    return 404 unless -e $disk_file;

    my @st      = stat(_);
    my $size    = $st[7];
    my $modtime = $st[9];
    my $etag    = "$modtime-$size";

    my $mime = {
        'gif' => 'image/gif',
        'png' => 'image/png',
    }->{$ext};

    my $palspec;
    if ($extra) {
        if ( $extra =~ m!^/p(.+)$! ) {
            $palspec = $1;
        }
        else {
            return 404;
        }
    }

    return send_file(
        $apache_r,
        $disk_file,
        {
            'mime'    => $mime,
            'etag'    => $etag,
            'palspec' => $palspec,
            'size'    => $size,
            'modtime' => $modtime,
        }
    );
}

sub parse_hex_color {
    my $color = shift;
    return [ map { hex( substr( $color, $_, 2 ) ) } ( 0, 2, 4 ) ];
}

sub send_file {
    my ( $apache_r, $disk_file, $opts ) = @_;

    my $etag = $opts->{'etag'};

    # palette altering
    my %pal_colors;
    if ( my $pals = $opts->{'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 );
            }
            $etag .= ":pg$pals";
            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 ),
                ];
            }
            $etag .= ":p$_($pal_colors{$_}->[3])" for ( sort keys %pal_colors );
        }
    }

    $etag = '"' . $etag . '"';
    my $ifnonematch = $apache_r->headers_in->{'If-None-Match'};
    return HTTP_NOT_MODIFIED
        if defined $ifnonematch && $etag eq $ifnonematch;

    # send the file
    $apache_r->content_type( $opts->{'mime'} );
    $apache_r->headers_out->{'Content-length'} = $opts->{'size'};
    $apache_r->headers_out->{ETag}             = $etag;
    if ( $opts->{'modtime'} ) {
        $apache_r->update_mtime( $opts->{'modtime'} );
        $apache_r->set_last_modified();
    }

    # HEAD request?
    return OK if $apache_r->method eq "HEAD";

    open my ($fh), $disk_file;
    return 404 unless $fh;

    binmode($fh);

    my $palette;
    if (%pal_colors) {
        if ( $opts->{'mime'} eq "image/gif" ) {
            $palette = PaletteModify::new_gif_palette( $fh, \%pal_colors );
        }
        elsif ( $opts->{'mime'} == "image/png" ) {
            $palette = PaletteModify::new_png_palette( $fh, \%pal_colors );
        }
        unless ($palette) {
            return 404;    # image isn't palette changeable?
        }
    }

    $apache_r->print($palette) if $palette;    # when palette modified.

    # now read the rest of the file, but let's max on a 1MB image for sanity
    read $fh, my $buf, 1024 * 1024;
    $apache_r->print($buf) if $buf;

    $fh->close();
    return OK;
}

1;

