191 lines
5.5 KiB
Perl
191 lines
5.5 KiB
Perl
#!/usr/bin/perl
|
|
#
|
|
# DW::Controller::PalImg
|
|
#
|
|
# Serves palette-modified images. Replaces Apache::LiveJournal::PalImg for use
|
|
# under both Plack and mod_perl via DW::Routing.
|
|
#
|
|
# URLs of form /palimg/somedir/file.gif[/pSPEC] where SPEC can be:
|
|
# gFFCOLORTTCOLOR - gradient from palette index FF to TT
|
|
# tCOLOR[DARK] - tint towards COLOR (optional dark tint)
|
|
# IRRGGBB... - set specific palette indices (I=index, RRGGBB=color)
|
|
#
|
|
# Authors:
|
|
# Mark Smith <mark@dreamwidth.org>
|
|
#
|
|
# Copyright (c) 2026 by Dreamwidth Studios, LLC.
|
|
#
|
|
# This program is free software; you may redistribute it and/or modify it under
|
|
# the same terms as Perl itself. For a copy of the license, please reference
|
|
# 'perldoc perlartistic' or 'perldoc perlgpl'.
|
|
#
|
|
|
|
package DW::Controller::PalImg;
|
|
|
|
use strict;
|
|
use v5.10;
|
|
use Log::Log4perl;
|
|
my $log = Log::Log4perl->get_logger(__PACKAGE__);
|
|
|
|
use DW::Request;
|
|
use DW::Routing;
|
|
use PaletteModify;
|
|
|
|
DW::Routing->register_regex(
|
|
qr!^/palimg/(.+)$!, \&palimg_handler,
|
|
app => 1,
|
|
formats => 1,
|
|
);
|
|
|
|
sub palimg_handler {
|
|
my ($opts) = @_;
|
|
my $r = DW::Request->get;
|
|
|
|
# DW::Routing strips file extensions as "formats" when the URL ends with .ext.
|
|
# For plain images like /palimg/solid.png, we get subpattern="solid" and format="png".
|
|
# For palette URLs like /palimg/solid.png/pSPEC, the extension is NOT stripped
|
|
# because the URL doesn't end with .ext, so we parse it from the subpattern.
|
|
my ($rest) = @{ $opts->subpatterns };
|
|
my ( $base, $ext, $extra );
|
|
|
|
my $format = $opts->format;
|
|
if ( $format && $format =~ /^(gif|png)$/ ) {
|
|
|
|
# Extension was stripped by routing
|
|
$ext = $format;
|
|
$base = $rest;
|
|
$extra = '';
|
|
}
|
|
else {
|
|
# Extension still in the path — parse it out
|
|
( $base, $ext, $extra ) = $rest =~ m!^(.+)\.(gif|png)(.*)$!;
|
|
}
|
|
|
|
return $r->NOT_FOUND unless $base && $ext && $base !~ m!\.\.!;
|
|
|
|
my $disk_file = "$LJ::HTDOCS/palimg/$base.$ext";
|
|
return $r->NOT_FOUND 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};
|
|
return $r->NOT_FOUND unless $mime;
|
|
|
|
my %pal_colors;
|
|
if ($extra) {
|
|
return $r->NOT_FOUND unless $extra =~ m!^/p(.+)$!;
|
|
my $pals = $1;
|
|
|
|
return $r->NOT_FOUND
|
|
unless _parse_palspec( $pals, \%pal_colors, \$etag );
|
|
}
|
|
|
|
$etag = qq{"$etag"};
|
|
my $ifnonematch = $r->header_in('If-None-Match');
|
|
if ( defined $ifnonematch && $etag eq $ifnonematch ) {
|
|
$r->status(304);
|
|
return $r->OK;
|
|
}
|
|
|
|
$r->content_type($mime);
|
|
$r->header_out( 'Content-Length' => $size );
|
|
$r->header_out( 'ETag' => $etag );
|
|
$r->header_out( 'Last-Modified' => LJ::time_to_http($modtime) );
|
|
|
|
return $r->OK if $r->method eq 'HEAD';
|
|
|
|
open my $fh, '<', $disk_file or return $r->NOT_FOUND;
|
|
binmode $fh;
|
|
|
|
my $palette;
|
|
if (%pal_colors) {
|
|
if ( $mime eq 'image/gif' ) {
|
|
$palette = PaletteModify::new_gif_palette( $fh, \%pal_colors );
|
|
}
|
|
elsif ( $mime eq 'image/png' ) {
|
|
$palette = PaletteModify::new_png_palette( $fh, \%pal_colors );
|
|
}
|
|
unless ($palette) {
|
|
close $fh;
|
|
return $r->NOT_FOUND;
|
|
}
|
|
}
|
|
|
|
my $body = '';
|
|
$body .= $palette if $palette;
|
|
read $fh, my $buf, 1024 * 1024;
|
|
$body .= $buf if $buf;
|
|
close $fh;
|
|
|
|
$r->header_out( 'Content-Length' => length $body );
|
|
$r->print($body);
|
|
|
|
return $r->OK;
|
|
}
|
|
|
|
# Parse a palette spec string into the %pal_colors hash.
|
|
# Returns true on success, false on invalid spec.
|
|
sub _parse_palspec {
|
|
my ( $pals, $pal_colors, $etag_ref ) = @_;
|
|
|
|
my $hx = '[0-9a-f]';
|
|
|
|
# Gradient: gFFCOLORTTCOLOR
|
|
if ( $pals =~ /^g($hx{2})($hx{6})($hx{2})($hx{6})$/ ) {
|
|
my $from = hex($1);
|
|
my $to = hex($3);
|
|
my $fcolor = _parse_hex_color($2);
|
|
my $tcolor = _parse_hex_color($4);
|
|
return 0 if $from == $to;
|
|
|
|
( $from, $to, $fcolor, $tcolor ) = ( $to, $from, $tcolor, $fcolor )
|
|
if $to < $from;
|
|
|
|
$$etag_ref .= ":pg$pals";
|
|
for ( my $i = $from ; $i <= $to ; $i++ ) {
|
|
$pal_colors->{$i} = [
|
|
map {
|
|
int( $fcolor->[$_] +
|
|
( $tcolor->[$_] - $fcolor->[$_] ) * ( $i - $from ) / ( $to - $from ) )
|
|
} ( 0 .. 2 )
|
|
];
|
|
}
|
|
}
|
|
|
|
# Tint: tCOLOR or tCOLORDARK
|
|
elsif ( $pals =~ /^t($hx{6})($hx{6})?$/ ) {
|
|
my ( $t, $td ) = ( $1, $2 );
|
|
$pal_colors->{tint} = _parse_hex_color($t);
|
|
$pal_colors->{tint_dark} = $td ? _parse_hex_color($td) : [ 0, 0, 0 ];
|
|
}
|
|
|
|
# Direct palette index colors: IRRGGBB repeated
|
|
elsif ( length($pals) <= 42 && $pals !~ /[^0-9a-f]/ ) {
|
|
my $len = length($pals);
|
|
return 0 if $len % 7;
|
|
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_ref .= ":p$_($pal_colors->{$_}->[3])" for sort keys %$pal_colors;
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub _parse_hex_color {
|
|
return [ map { hex( substr( $_[0], $_, 2 ) ) } ( 0, 2, 4 ) ];
|
|
}
|
|
|
|
1;
|