commit 5496c4cbab514db9862708e0deec65f1cf15a476
Author: u-## <u.numbernumber@gmail.com>
Date:   Tue Mar 22 23:22:22 2011 -0500

    Add ShiPainter animation support
    
    Adds support for saving and viewing Shi Painter PCH animation files in
    the oekaki extra. The PCH files are saved in the same dir as images,
    and can be viewed with the pchview.html. The PCH viewer requires
    JavaScript, because otherwise we'd have to go through CGI for every
    viewer request (to set the PCH filename applet parameter), which seems
    ridiculous.
    
    This adds three directives to oekaki_config.pl:
    
     - OEKAKI_ENABLE_ANIM: Set to a true or false value to enable or
       disable the PCH animation functionality entirely.
    
     - OEKAKI_ANIM_MAX: Set to the maxiumum file size (in KiB) that you
       want the PCH files to grow to. This is handled by a parameter
       passed to Shi Painter, but is also checked by wakaba in getpic.pl
       when the animation data is uploaded.
    
     - OEKAKI_ANIM_DIR: Set to the directory where the resultant .pch
       animation files are to be stored. This can just be set to IMG_DIR
       if you really want to, but this is not recommended. If it is set to
       IMG_DIR, and someone uploads some regular .pch file, and the
       filename does not get renamed, any other user can potentially
       delete that .pch file by uploading a similarly-named .png file and
       then deleting it. You must create the OEKAKI_ANIM_DIR if
       OEKAKI_ENABLE_ANIM is on; the default directory 'ani/' probably
       does not already exist.
    
    You also need a few more files for the viewer. Namely: PCHViewer.jar,
    res.zip, and tt.zip. These go in the same directory as
    spainter_all.jar (typically the board root), and you can get them from
    the same site as Shi Painter (they're in the "viewer" zip file).
    
    You also need to rebuild caches on any Oekaki-enabled board, in order
    to get the "animation" tickybox.

diff --git a/extras/oekaki/finish.pl b/extras/oekaki/finish.pl
index 97a0cb4..4e695af 100755
--- a/extras/oekaki/finish.pl
+++ b/extras/oekaki/finish.pl
@@ -19,6 +19,7 @@ BEGIN { require "strings_en.pl"; }
 BEGIN { require "oekaki_style.pl"; }
 BEGIN { require "oekaki_config.pl"; }
 BEGIN { require "oekaki_strings_en.pl"; }
+BEGIN { require "oekaki_util.pl"; }
 BEGIN { require "wakautils.pl"; }
 
 
@@ -51,11 +52,15 @@ $oek_ip=$ip unless($oek_ip);
 die unless($oek_ip=~/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/);
 
 my $tmpname=TMP_DIR.$oek_ip.'.png';
+my $tmpname_pch=TMP_DIR.$oek_ip.'.pch';
 
 if(!$task)
 {
 	my $oek_parent=$query->param("oek_parent");
 	my $srcinfo=$query->param("srcinfo");
+	my $animated=$query->param("animated");
+	if (!OEKAKI_ENABLE_ANIM) { $animated = undef; }
+	$animated = $tmpname_pch if ($animated);
 
 	make_http_header();
 
@@ -64,7 +69,8 @@ if(!$task)
 		oek_parent=>clean_string($oek_parent),
 		oek_ip=>$oek_ip,
 		srcinfo=>clean_string($srcinfo),
-		decodedinfo=>OEKAKI_INFO_TEMPLATE->(decode_srcinfo($srcinfo)),
+		decodedinfo=>OEKAKI_INFO_TEMPLATE->(decode_srcinfo($srcinfo, $animated)),
+		animated=>$animated,
 	);
 }
 elsif($task eq "post")
@@ -79,13 +85,27 @@ elsif($task eq "post")
 	my $password=$query->param("password");
 	my $captcha=$query->param("captcha");
 	my $srcinfo=$query->param("srcinfo");
+	my $animated=$query->param("animated");
+	if (!OEKAKI_ENABLE_ANIM) { $animated = undef; }
 
 	$ENV{SCRIPT_NAME}=~s/\w+\.pl$/wakaba.pl/;
 
 	open TMPFILE,$tmpname or die "Can't read uploaded file";
 
+	my $postfix;
+
+	if ($animated) {
+		$postfix = sub {
+			my $filename = shift;
+			my $pch_filename = oek_pch_mv($filename, $tmpname_pch);
+			return OEKAKI_INFO_TEMPLATE->(decode_srcinfo($srcinfo,$pch_filename));
+		};
+	} else {
+		$postfix = OEKAKI_INFO_TEMPLATE->(decode_srcinfo($srcinfo, $animated));
+	}
+
 	post_stuff($parent,$name,$email,$subject,$comment,\*TMPFILE,$tmpname,$password,
-	0,$captcha,ADMIN_PASS,0,0,OEKAKI_INFO_TEMPLATE->(decode_srcinfo($srcinfo)));
+	0,$captcha,ADMIN_PASS,0,0,$postfix);
 
 	unlink $tmpname;
 }
@@ -111,9 +131,9 @@ sub expand_filename($)
 	return $self_path.$filename;
 }
 
-sub decode_srcinfo($)
+sub decode_srcinfo($;$)
 {
-	my ($srcinfo)=@_;
+	my ($srcinfo,$animated)=@_;
 	my @info=split /,/,$srcinfo;
 	my @stat=stat $tmpname;
 	my $fileage=$stat[9];
@@ -123,6 +143,7 @@ sub decode_srcinfo($)
 		time=>clean_string(pretty_age($fileage-$info[0])),
 		painter=>clean_string($$painter{name}),
 		source=>clean_string($info[2]),
+		anim=>$animated,
 	);
 }
 
diff --git a/extras/oekaki/getpic.pl b/extras/oekaki/getpic.pl
old mode 100644
new mode 100755
index e5e0b5d..015ee02
--- a/extras/oekaki/getpic.pl
+++ b/extras/oekaki/getpic.pl
@@ -5,12 +5,20 @@ use CGI::Carp qw(fatalsToBrowser);
 use strict;
 
 use lib '.';
+BEGIN { require 'config.pl'; }
+BEGIN { require 'config_defaults.pl'; }
 BEGIN { require 'oekaki_config.pl'; }
 
 my $metadata=<STDIN>;
 my $oek_ip;
+my $image_len = -1;
 
-if($metadata=~/^S[0-9]{8}(.*)[0-9]{8}$/) { $oek_ip=$1; }
+if($metadata=~/^S[0-9]{8}(.*)([0-9]{8})\r?$/) {
+	$oek_ip = $1;
+	$image_len = $2;
+	die("Invalid image len") unless ($image_len =~ /^[0-9]+$/);
+	$image_len = int($image_len);
+}
 else { $oek_ip=$ENV{REMOTE_ADDR}; }
 
 die unless($oek_ip=~/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/);
@@ -22,11 +30,50 @@ open FILE,">$tmpname" or die("Couldn't write to directory");
 binmode FILE;
 binmode STDIN;
 
-my $buffer;
-while(read(STDIN,$buffer,1024)) { print FILE $buffer; }
+writedata(\*STDIN, \*FILE, $image_len);
 
 close FILE;
 
+my $thumb_len;
+
+if (OEKAKI_ENABLE_ANIM and read(STDIN, $thumb_len, 8)) {
+	die("Invalid thumb len") unless ($thumb_len =~ /^[0-9]+$/);
+	$thumb_len = int($thumb_len);
+
+	if ((OEKAKI_ANIM_MAX && $thumb_len > OEKAKI_ANIM_MAX*1024) ||
+	    $thumb_len > MAX_KB*1024) {
+		die("Animation data too large");
+	}
+
+	$tmpname = TMP_DIR.$oek_ip.'.pch';
+	open FILE,">$tmpname" or die("Couldn't write to directory for pch");
+	binmode FILE;
+
+	writedata(\*STDIN, \*FILE, $thumb_len);
+
+	close FILE;
+}
+
 print "Content-Type: text/plain\n";
 print "\n";
 print "ok";
+
+sub writedata($$$) {
+	# read $total bytes from $inf, and write it to $outf
+	# if $total == -1, just read until EOF
+	my ($inf,$outf,$total) = @_;
+	while ($total != 0) {
+		my $buffer;
+		my $len = $total;
+		if ($len > 4096 || $len < 0) {
+			$len = 4096;
+		}
+		if (read($inf, $buffer, $len)) {
+			print $outf $buffer;
+			$total -= $len if ($total > 0);
+		} else {
+			# error or EOF on read
+			return;
+		}
+	}
+}
diff --git a/extras/oekaki/oekaki_config.pl b/extras/oekaki/oekaki_config.pl
index 85361e9..77cd7e9 100644
--- a/extras/oekaki/oekaki_config.pl
+++ b/extras/oekaki/oekaki_config.pl
@@ -1,6 +1,7 @@
 use constant TMP_DIR => 'tmp/';						# Temp file directory (needs to be writeable by the script)
 
 use constant OEKAKI_ENABLE_MODIFY => 1;				# Enable image modification
+use constant OEKAKI_ENABLE_ANIM => 1;				# Enable image animation (recording the image creation process)
 
 use constant OEKAKI_DEFAULT_X => 300;               # Default X dimension for oekaki drawings
 use constant OEKAKI_DEFAULT_Y => 300;               # Default Y dimension for oekaki drawings
@@ -9,6 +10,11 @@ use constant OEKAKI_MAX_Y => 800;					# Max Y dimension allowed for oekaki drawi
 use constant OEKAKI_MIN_X => 100;   	            # Min X dimension allowed for oekaki drawings
 use constant OEKAKI_MIN_Y => 100;   	            # Min Y dimension allowed for oekaki drawings
 
+use constant OEKAKI_ANIM_MAX => 0;         # Max file size (in KiB) for animation recording
+use constant OEKAKI_ANIM_DIR => 'ani/';    # Directory to store .pch animation files
+                                           # this can be the same as IMG_DIR, but that
+                                           # is mildly not recommended. Must be writeable
+
 use constant OEKAKI_DEFAULT_PAINTER => "shi_norm";	# Default painter selection
 
 1;
diff --git a/extras/oekaki/oekaki_style.pl b/extras/oekaki/oekaki_style.pl
index 9cf04a5..172c351 100644
--- a/extras/oekaki/oekaki_style.pl
+++ b/extras/oekaki/oekaki_style.pl
@@ -4,6 +4,7 @@ use strict;
 
 BEGIN { require 'oekaki_config.pl'; }
 BEGIN { require 'oekaki_strings_en.pl'; }
+BEGIN { require 'oekaki_util.pl'; }
 BEGIN { require 'futaba_style.pl'; }
 BEGIN { require 'wakautils.pl'; }
 
@@ -53,6 +54,11 @@ use constant PAGE_TEMPLATE => compile_template(NORMAL_HEAD_INCLUDE.q{
 	</select>
 </if>
 
+<if OEKAKI_ENABLE_ANIM>
+	<br />
+	<input type="checkbox" name="oek_anim" checked />Animation
+</if>
+
 <input type="submit" value="<const S_OEKSUBMIT>" />
 </form>
 </div><hr />
@@ -255,8 +261,12 @@ table.nospace tr td { margin:0px; }
 <param name="tools" value="<var $mode>" />
 <param name="layer_count" value="3" />
 <param name="url_save" value="getpic.pl" />
-<param name="url_exit" value="finish.pl?oek_parent=<var $oek_parent>&amp;oek_ip=<var $ip>&amp;srcinfo=<var $time>,<var $oek_painter>,<var $oek_src>" />
+<param name="url_exit" value="finish.pl?animated=<var $oek_anim>&amp;oek_parent=<var $oek_parent>&amp;oek_ip=<var $ip>&amp;srcinfo=<var $time>,<var $oek_painter>,<var $oek_src>" />
 <param name="send_header" value="<var $ip>" />
+<if $oek_anim>
+	<param name="thumbnail_type" value="animation" />
+	<param name="animation_max" value="<const OEKAKI_ANIM_MAX>" />
+</if>
 </applet>
 </td>
 <if $selfy>
@@ -269,10 +279,9 @@ table.nospace tr td { margin:0px; }
 </html>
 });
 
-
 use constant OEKAKI_INFO_TEMPLATE => compile_template(q{
 <p><small><strong>
-Oekaki post</strong> (Time: <var $time>, Painter: <var $painter><if $source>, Source: <a href="<var $path><var $source>"><var $source></a></if>)
+Oekaki post</strong> (Time: <var $time>, Painter: <var $painter><if $source>, Source: <a href="<var $path><var $source>"><var $source></a></if><if $anim>, Animation: <a href="<var $path>pchview.html?pchfile=<var $anim>">View</a>, <a href="<var $path><var $anim>">Download</a></if>)
 </small></p>
 });
 
@@ -287,6 +296,7 @@ use constant OEKAKI_FINISH_TEMPLATE => compile_template(NORMAL_HEAD_INCLUDE.q{
 <input type="hidden" name="task" value="post" />
 <input type="hidden" name="oek_ip" value="<var $oek_ip>" />
 <input type="hidden" name="srcinfo" value="<var $srcinfo>" />
+<if $animated><input type="hidden" name="animated" value="1" /></if>
 <table><tbody>
 <tr><td class="postblock"><const S_NAME></td><td><input type="text" name="field1" size="28" /></td></tr>
 <tr><td class="postblock"><const S_EMAIL></td><td><input type="text" name="field2" size="28" /></td></tr>
diff --git a/extras/oekaki/oekaki_util.pl b/extras/oekaki/oekaki_util.pl
new file mode 100644
index 0000000..ac1186a
--- /dev/null
+++ b/extras/oekaki/oekaki_util.pl
@@ -0,0 +1,47 @@
+BEGIN { require "config.pl"; }
+BEGIN { require "config_defaults.pl"; }
+
+sub oek_pch_filename($) {
+	my ($filename) = @_;
+	my $pch_filename = $filename;
+	# trim dir
+	$pch_filename =~ s:^.*/([^/]+)$:$1:;
+	# trim extension
+	$pch_filename =~ s/[.][^.]+$//;
+	# put in the anim dir, and give a .pch extension
+	$pch_filename = OEKAKI_ANIM_DIR.$pch_filename.".pch";
+
+	return $pch_filename;
+}
+
+sub oek_pch_mv($$) {
+	my ($img_filename, $tmpname) = @_;
+	my $pch_filename;
+	my $buffer;
+
+	open PCHTMP,$tmpname or die "Can't read temporary PCH file";
+	binmode PCHTMP;
+
+	$pch_filename = oek_pch_filename($img_filename);
+	if (-e $pch_filename) {
+		unlink $img_filename;
+		make_error(S_DUPENAME);
+	}
+
+	if (not open (OUTFILE,">>$pch_filename")) {
+		unlink $img_filename;
+		make_error(S_NOTWRITE);
+	}
+	binmode OUTFILE;
+	while (read(PCHTMP,$buffer,4096)) {
+		print OUTFILE $buffer;
+	}
+	close PCHTMP;
+	close OUTFILE;
+
+	unlink $tmpname;
+
+	return $pch_filename;
+}
+
+1;
diff --git a/extras/oekaki/paint.pl b/extras/oekaki/paint.pl
old mode 100644
new mode 100755
index c0f3def..7c2637e
--- a/extras/oekaki/paint.pl
+++ b/extras/oekaki/paint.pl
@@ -23,10 +23,12 @@ my $oek_x=$query->param("oek_x");
 my $oek_y=$query->param("oek_y");
 my $oek_parent=$query->param("oek_parent");
 my $oek_src=$query->param("oek_src");
+my $oek_anim=$query->param("oek_anim");
 my $ip=$ENV{REMOTE_ADDR};
 
 make_error(S_HAXORING) if($oek_x=~/[^0-9]/ or $oek_y=~/[^0-9]/ or $oek_parent=~/[^0-9]/);
 make_error(S_HAXORING) if($oek_src and !OEKAKI_ENABLE_MODIFY);
+make_error(S_HAXORING) if($oek_anim and !OEKAKI_ENABLE_ANIM);
 make_error(S_HAXORING) if($oek_src=~m![^0-9a-zA-Z/\.]!);
 make_error(S_OEKTOOBIG) if($oek_x>OEKAKI_MAX_X or $oek_y>OEKAKI_MAX_Y);
 make_error(S_OEKTOOSMALL) if($oek_x<OEKAKI_MIN_X or $oek_y<OEKAKI_MIN_Y);
@@ -50,6 +52,7 @@ if($oek_painter=~/shi/)
 		oek_y=>clean_string($oek_y),
 		oek_parent=>clean_string($oek_parent),
 		oek_src=>clean_string($oek_src),
+		oek_anim=>clean_string($oek_anim),
 		ip=>$ip,
 		time=>$time,
 		mode=>$mode,
diff --git a/extras/oekaki/pchview.html b/extras/oekaki/pchview.html
new file mode 100755
index 0000000..5970c50
--- /dev/null
+++ b/extras/oekaki/pchview.html
@@ -0,0 +1,73 @@
+
+<html>
+<head>
+<style type="text/css">
+body { background: #9999BB; font-family: sans-serif; }
+input,textarea { background-color:#CFCFFF; font-size: small; }
+table.nospace { border-collapse:collapse; }
+table.nospace tr td { margin:0px; }
+.menu { background-color:#CFCFFF; border: 1px solid #666666; padding: 2px; margin-bottom: 2px; }
+</style>
+</head>
+<body>
+<noscript>Javascript is required to use this PCH animation viewer. Sorry! You
+can still get the PCH file directly, if you can tell where it is.</noscript>
+<script language="javascript">
+<!--
+function err(str) {
+	document.write("<p>"+str+"</p>\n");
+}
+
+function onLoad() {
+
+	var varstr = "?pchfile=";
+	var pch = document.pch;
+	var href = window.location.href;
+
+var start = href.indexOf(varstr);
+if (start < 0) {
+	err("No PCH file specified?");
+	return;
+}
+start += varstr.length;
+
+var amp = href.indexOf("&", start);
+var hash = href.indexOf("#", start);
+if (hash < 0) {
+	hash = amp;
+}
+if (amp < 0) {
+	amp = hash;
+}
+var end = amp;
+if (hash < amp) {
+	end = hash;
+}
+var filename;
+if (end < 0) {
+	filename = href.slice(start);
+} else {
+	filename = href.slice(start, end);
+}
+if (filename.length < 1) {
+	err("No PCH file specified?");
+	return;
+}
+document.title = "PCHViewer: Playing " + filename;
+
+document.write('<table class="nospace" width="100%" height="100%"><tbody><tr>\n' +
+               '<td width="100%">\n' +
+               '<applet name="pch" code="pch2.PCHViewer.class" codebase="./" archive="PCHViewer.jar" width="100%" height="100%">\n' +
+               '<param name="pch_file" value="'+filename+'">\n' +
+               '<param name="speed" value="1">\n' +
+               '<param name="run" value="true">\n' +
+               '<param name="res.zip" value="res.zip">\n' +
+               '</applet></td></tr></tbody></table>\n');
+}
+onLoad();
+
+//-->
+</script>
+
+</body>
+</html>
diff --git a/wakaba.pl b/wakaba.pl
index 8c6892e..86dd00d 100755
--- a/wakaba.pl
+++ b/wakaba.pl
@@ -527,15 +527,20 @@ sub post_stuff($$$$$$$$$$$$$$)
 	# fix up the email/link
 	$email="mailto:$email" if $email and $email!~/^$protocol_re:/;
 
+	# $postfix can be a function reference, which is run with the filename of
+	# the posted file to generate the actual postfix contents
+	my $postfix_code = 0;
+	$postfix_code = 1 if (ref($postfix) eq 'CODE');
+
 	# format comment
 	$comment=format_comment(clean_string(decode_string($comment,CHARSET))) unless $no_format;
-	$comment.=$postfix;
+	$comment.=$postfix unless ($postfix_code);
 
 	# insert default values for empty fields
 	$parent=0 unless $parent;
 	$name=make_anonymous($ip,$time) unless $name or $trip;
 	$subject=S_ANOTITLE unless $subject;
-	$comment=S_ANOTEXT unless $comment;
+	$comment=S_ANOTEXT unless ($comment or $postfix_code);
 
 	# flood protection - must happen after inputs have been cleaned up
 	flood_check($numip,$time,$comment,$file);
@@ -551,6 +556,8 @@ sub post_stuff($$$$$$$$$$$$$$)
 	# copy file, do checksums, make thumbnail, etc
 	my ($filename,$md5,$width,$height,$thumbnail,$tn_width,$tn_height)=process_file($file,$uploadname,$time) if($file);
 
+	$comment .= &$postfix($filename) if ($postfix_code);
+
 	# finally, write to the database
 	my $sth=$dbh->prepare("INSERT INTO ".SQL_TABLE." VALUES(null,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);") or make_error(S_SQLFAIL);
 	$sth->execute($parent,$time,$lasthit,$numip,
@@ -1163,6 +1170,12 @@ sub delete_post($$$$)
 					unlink $$res{image};
 					unlink $$res{thumbnail} if($$res{thumbnail}=~/^$thumb/);
 				}
+
+				if (defined &OEKAKI_ENABLE_ANIM and eval 'OEKAKI_ENABLE_ANIM') {
+					my $pch_filename = eval 'oek_pch_filename($$res{image})';
+					die $@ if $@;
+					unlink $pch_filename;
+				}
 			}
 
 			# remove post and possible replies
@@ -1179,6 +1192,12 @@ sub delete_post($$$$)
 				unlink $$row{image};
 				unlink $$row{thumbnail} if($$row{thumbnail}=~/^$thumb/);
 
+				if (defined &OEKAKI_ENABLE_ANIM and eval 'OEKAKI_ENABLE_ANIM') {
+					my $pch_filename = eval 'oek_pch_filename($$row{image})';
+					die $@ if $@;
+					unlink $pch_filename;
+				}
+
 				$sth=$dbh->prepare("UPDATE ".SQL_TABLE." SET size=0,md5=null,thumbnail=null WHERE num=?;") or make_error(S_SQLFAIL);
 				$sth->execute($post) or make_error(S_SQLFAIL);
 			}
